Nodejs-cli 填坑记

本文涉及的产品
公共DNS(含HTTPDNS解析),每月1000万次HTTP解析
云解析 DNS,旗舰版 1个月
全局流量管理 GTM,标准版 1个月
简介: 真的是玩玩没想到,一个cli竟然坑了我这么久,想当年写Python命令行工具的时候,哪有这么麻烦?随随便便写几下,添加个批处理命令脚本就搞定了。怎么Nodejs写一个就这么不顺利呢? 吐槽归吐槽,当我成功的写出来一个cli版本的工具之后,我才发现,是我错了。

真的是玩玩没想到,一个cli竟然坑了我这么久,想当年写Python命令行工具的时候,哪有这么麻烦?随随便便写几下,添加个批处理命令脚本就搞定了。怎么Nodejs写一个就这么不顺利呢?

吐槽归吐槽,当我成功的写出来一个cli版本的工具之后,我才发现,是我错了。nodejs-cli其实真的是很方便,也很简单。

命令行翻译小工具Nodejs实现

秉承分享知识的原则,在此记录一下。

写在前面

这篇文章严格来说不能算是一片技术性的文章,没什么难点。有的只是一些好玩的小工具。对于Nodejs新人来说,鉴于没什么难度,倒是可以适当的练练手。因此,如果你是Nodejs高手的话,还是不要在此浪费时间了。

段子手

言简意赅点,就是一个爬取糗事百科的段子的小助手,一个简单的不能再简单的爬虫。姑且称之为爬虫吧。

外部模块

这里需要用到一点点的第三方模块。虽然实现相同的功能,标准模块也可以满足,但是有轮子的话,何必自己再去造一个呢(除非你能做的更好)。

  • superagent: 类似于Python里面的requests,挺好用的。
  • cheerio: 类似于Python里面的BeautifulSoup,但是其使用的是JQuery的选择器语法,所以对前端比较熟悉的话,用起来会非常的顺手。
  • cli-color: 如果你想在命令行里面打印出彩色的字符,那么用它就对了。

events事件发射接收

在Nodejs中events模块可谓是核心了。异步编程要是没有它,那就算是完了。这里为了使用而使用,我也简单的用了一下。

具体的思路是:

  • 每一张网页解析完毕后,发射一个pageover事件,来更新解析列表。
  • 全部内容解析完毕后发射done事件,通知客户端完成代码运行。

完整代码

/**
 * 使用几个比较不错的第三方模块实现糗事百科网站的小爬虫。
 */
const superagent = require('superagent');
const cheerio = require('cheerio');
const color = require('cli-color');
const events = require('events');

/**
 * 定义一个网页总数变量。
 */
const MAX_PAGE_SIZE = 3;

// var website = 'https://www.qiushibaike.com/8hr/page/2/';
// var website = "http://blog.csdn.net/marksinoberg";
// var website = 'https://www.qiushibaike.com/article/119148411';
var total_results = [];
var emitter = new events.EventEmitter();

function crawl_by_page(page, website) {
    var results = []
    superagent.get(website).then((response) => {
        // 获取到网页内容,交给cheerion进行解析即可。
        var $ = cheerio.load(response.text);
        // fs.writeFileSync('text.txt', response.text);

        $("div .untagged").each(function (index, element) {
            console.log("正在处理第:" + (page)+"页第"+(index+1)+" 条数据!");
            var authorage = $(this).find('div[class="author clearfix"]').text();
            var author = '', age = '';
            author = authorage.trim().split("\n")[0];
            age = authorage.match(/\d+/g);
            // console.log("作者:" + author + "\n年龄:" + age + "\n");
            // console.log('---------------------\n笑话内容:\n');
            var temp = $(this).find('div[class="content"]').text().trim();
            // console.log(temp);
            var obj = {
                author: author,
                age: age,
                content: temp
            }

            read_duanzi(obj);

            if (obj)
                results.push(obj);
        });
    }).then(function () {
        console.log('done.');
        // 设置一个最终响应事件
        var counts = (parseInt(page) * 20);
        emitter.emit('pageover', results);
        if (page == MAX_PAGE_SIZE) {
            emitter.emit('done', counts);
        }
    });

}


function read_duanzi(item){
    console.log(color.red('作者:'+item.author));
    console.log(color.green('年龄:' + item.age));
    console.log(color.blue('段子内容:\n')+item.content);
    console.log(color.green.bold("----------------------------------------------\
    ------------------------------------------------------------------------------------------"));
}


function main() {
    for (var page = 1; page <= MAX_PAGE_SIZE; page++) {
        var website = 'https://www.qiushibaike.com/8hr/page/' + page + '/';
        crawl_by_page(page, website);
    }
}




/**
 * 入口函数。
 */
main();
emitter.on('pageover', function (results) {
    total_results.concat(results);
    console.log(color.yellow("page downloading over."));

});
emitter.on('done', (counts) => {
    console.log(color.green("共下载了:" + counts + "个段子!"));
});

实现效果

下面上张图,来演示一下运行效果。
糗事百科段子爬虫运行示例


翻译官

之前用Python写过一个类似的工具,可以方便的读取系统剪切板的待翻译文本,然后以模态弹出框的形式通知用户翻译结果。自认为用起来还是不错的。现在学了点Nodejs,就有点手痒了,于是也来用Nodejs实现一个类似的功能。

当然不能完全的模仿,要有点新意。比如加一个这样的功能。

自判断语言类型(主要是英语和汉语),并进行翻译处理。

外部模块

同样的,因为有网络请求,所以离不开我最喜欢的SuperAgent了。然后为了进一步提升用户体验,我又添加了一个cli-color模块。

这样就可以优雅的在命令行里面显示绚丽的文本了。

部分代码释义

首先为了完成汉语的识别,用到了下面的这个函数。

/**
 * 判断给定的文本是否为中文。
 * @param {*给定的文本内容串} text 
 */
function is_Chinese(text) {
    // [\u4e00-\u9fa5] 是汉语所在的Unicode字符区间。
    return !!text.match(/[\u4e00-\u9fa5]+/gi);
}

代码比较简单,而且一看就差不多能明白这段代码的功能。我们都知道汉语在Unicode字符中的区间为[\u4e00-\u9fa5],所以匹配到这些字符的话,就默认为汉语了。

这里面的最后一句话是双非表达式。作用就是返回一个boolean类型的结果。这个使用技巧是我在浏览别人GitHub代码的时候看到的,当时就觉得这样写很优雅,于是模仿了一下。

完整代码

#!/usr/bin/env node
/**
 * 利用百度翻译接口实现一个小小的翻译软件。
 */
const superagent = require('superagent');
const color = require('cli-color');
const commander = require('commander');


/**
 * 将中文文本编码为安全的URI字符串。
 * @param {*中文文本词} text 
 */
function Chinese_encode(text) {
    return encodeURIComponent(text);
}


/**
 * 判断给定的文本是否为中文。
 * @param {*给定的文本内容串} text 
 */
function is_Chinese(text) {
    // [\u4e00-\u9fa5] 是汉语所在的Unicode字符区间。
    return !!text.match(/[\u4e00-\u9fa5]+/gi);
}

/**
 * 处理可能会出现异常信息的内容。
 * @param {*JSON格式的结果串} json 
 */
function handle_chinese_exception(json) {
    var dict;
    try {
        dict = {
            english_means: json['dict_result']['zdict']['simple']['means'][0]['exp'][0]['des'][0]['main'],
            word_means: json['dict_result']['simple_means']['word_means'],
            chinese_means: json['dict_result']['simple_means']['symbols'][0]['parts'][0]['means'][0]['means']
        }
        return dict;
    } catch (err) {
        return dict = {
            english_means: "未找到",
            word_means: "未找到",
            chinese_means: "未找到"
        };
    }
}

/**
 * 处理中文翻译部分的内容。
 * @param {*JSON格式的结果集串} json 
 */
function handle_Chinese_result(json) {
    // 获取字面意思:literal
    var literal = json['trans_result']['data'][0]['result'][0][1];
    // console.log(literal);
    // 处理字典方面含义
    var dict = handle_chinese_exception(json);
    // console.log(dict);
    return result = {
        literal: literal,
        dict: dict
    };

}

/**
 * 处理英文翻译相关可能出现异常的操作。
 */
function handle_english_exception(json) {
    var dict;
    // console.log(json['dict_result']['edict']['item'][0]['tr_group'][0]['tr']);
    try {
        dict = {
            english_means: json['dict_result']['edict']['item'][0]['tr_group'][0]['tr'],
            word_means: json['dict_result']['simple_means']['word_means'],
            tags: json['dict_result']['simple_means']['tags']['core'],
            chinese_means: json['dict_result']['simple_means']['symbols'][0]['parts'][0]['means']
        }
        return dict;
    } catch (error) {
        return dict = {
            english_means: 'not found.',
            word_means: 'not found.',
            tags: 'not found.',
            chinese_means: 'not found.'
        };
    }
}

/**
 * 处理英文翻译相关可能出现操作异常的情况。
 * @param {*JSON格式的结果串} json 
 */
function handle_english_result(json) {
    // 处理字面意义literal
    var literal = json['trans_result']['data'][0]['result'][0][1];
    // console.log(literal);
    // 处理字典相关含义
    var dict = handle_english_exception(json);
    // console.log("****************************\n", dict);
    return result = {
        literal: literal,
        dict: dict
    }
}


/**
 * 翻译给定的文本。
 * @param {*待翻译的文本内容串} text 
 */
function trans(text) {
    var flag = is_Chinese(text);
    var url_interface;
    var result;
    // 处理中文待翻译串
    if (flag) {
        var encoded_text = Chinese_encode(text);
        url_interface = "http://fanyi.baidu.com/v2transapi?from=zh&to=en&query=" + encoded_text;
        // 开始进行网络请求
        superagent.get(url_interface).then((response) => {
            // console.log(response.text);
            var json = JSON.parse(response.text);
            var result = handle_Chinese_result(json);
            // console.log('-------------------\n');
            // console.log(result)
            // 调用终端打印函数,打印相关内容
            print_in_terminal(result, false);

        });
    } else {
        // url_interface = "http://fanyi.baidu.com/v2transapi?query=" + text;
        url_interface = "http://fanyi.baidu.com/v2transapi?from=zh&to=en&query=" + text;
        // 开始处理英文的翻译请求
        superagent.get(url_interface).then((response) => {
            var json = JSON.parse(response.text);
            var result = handle_english_result(json);
            // console.log('============================\n');
            // console.log(result);
            // 调用终端打印函数,打印相关内容
            print_in_terminal(result, true);
        });
    }

}

/**
 * 根据中英文的不同,在命令行打印相关的内容。
 * @param {*待打印内容对象} data 
 * @param {*是否为英文文本} isEnglish 
 */
function print_in_terminal(data, isEnglish) {
    // 英文模式
    if (isEnglish) {
        console.log("字面意:" + color.red.bold(data.literal) + '\n');
        console.log("英文解释:" + color.green.bold(data.dict.english_means) + '\n');
        console.log("中文解释:" + color.green.bold(data.dict.chinese_means) + '\n');
        console.log("考试标签:" + color.blue(data.dict.tags) + '\n');
    } else { // 中文模式
        console.log("字面意:" + color.red.bold(data.literal) + '\n');
        console.log("英文解释:" + color.green.bold(data.dict.english_means) + '\n');
        console.log("词语解释:" + color.blue.bold(data.dict.word_means) + '\n');
        console.log("中文解释:" + color.yellow(data.dict.chinese_means) + '\n');
    }

}

/**
 * 创建命令行参数解析工具,并予以运用。
 */
function create_commander() {
    commander.command('help').description('提示信息: 如何使用这个小工具.').action(() => { commander.outputHelp(); });
    commander.command('trans [text]').description('翻译给定的文本,中英文都可.').action((text) => {
        trans(text);
    });

    // 开始解析命令行参数
    commander.parse(process.argv);
}


/////////////////////////////////////////////测试部分
function main() {
    // var text = "你好";
    // var flag = is_Chinese(text);
    // console.log("是否为中文:"+flag);
    // text = "hello";
    // flag = is_Chinese(text);
    // console.log("是否为中文:"+flag);

    // trans("软件");
    create_commander();
}
main();

为了尽可能清楚的表达我的逻辑,代码上添加了很多注释。应该是很容易就能读懂了吧。

效果演示

下面来看下运行的效果。
翻译官代码运行示例


cli工具

到了最重要的一部分内容了。被这个cli坑了一个多小时了。不过还好,最终还是解决了它。

标配需求

安装完最新版本的Node之后,默认会自带npm这个包管理工具。为了构建我的cli工具,我需要一个package.json的文件。

比如我的文件目录结构如下:

E:\Code\Nodejs\learn\my-work\translator>tree /f .
卷 文档 的文件夹 PATH 列表
卷序列号为 0000-4823
E:\CODE\NODEJS\LEARN\MY-WORK\TRANSLATOR
│  index.js
│  package.json
└─ test.js

然后在当前目录的命令行中执行下面的命令:

npm init

然后根据命令行中的提示信息进行填写即可。填写完成就会生成一个package.json的文件了。

修改配置

完成上面一步后就会得到类似于下面内容的文件了。

{
  "name": "translator",
  "version": "1.0.0",
  "description": "命令行版本的翻译工具,适用于中英文,无需手动选择语言模式。",
  "preferGlobal": true,
  "bin":{
    "translator": "index.js"
  },
  "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1"
  },
  "keywords": [
    "翻译小工具"
  ],
  "author": "郭璞",
  "license": "MIT"
}

需要注意的就是bin属性,里面填写我们即将被打包的js文件的相对路径即可。比如我的index.js在package.json的同级目录下,所以我这么写了。

如果你的index.js文件在package.json的同级目录下的bin文件夹内,那你就得这么写:

"bin":{
    "translator": "./bin/index.js"
  },

最后记得保存。


再就是执行下面的链接命令了(记得命令行路径在package.json的同级目录下)。

npm link

然后就可以根据bin中声明的属性来调用命令行脚本了。

易错点剖析

下面谈谈我掉进去的坑。按照官网给的使用技巧,本人也测试了一下,发现并不好使。然后我发现通过npm link命令生成的cmd文件有这么个内容:

"%~dp0\node_modules\translator\index.js"   %*

也就是说这个文件本身就是错误的。node是找不到它的。而且在命令行中运行translator命令的时候根本不管用,它总会是用电脑上默认的文本编辑器打开相应的js脚本文件。

一开始我以为是代码有问题,然后修修改改,发现没什么问题啊。前前后后尝试了好几遍,都不得终。场面一度陷入了尴尬的地步。

然后在浏览一些帖子的时候,灵光一闪,为什么找不到???
究其原因就是环境变量呗。于是我就顺藤摸瓜,发现别人家的js脚本的开头都有这么一行代码

#!/usr/bin/env node

一开始没在意,以为是Linux特有的风格,然后我用的是VSCode,也能很好的运行就没在意,然后这次在我的JS文件中,我抱着试一试的态度加了这么一行语句,结果竟然成功了。

这更是验证了我关于环境变量的猜想,原来是这么回事哦。。。


至此,一个简单的node-cli工具就能制作完毕了。真的是无波折,不开心呐。

总结

最后, 我想对自己说的就是:

遵守编码规范,不要想当然。

目录
相关文章
|
2月前
|
JavaScript 关系型数据库 MySQL
nodejs使用初体验
文章介绍了Node.js的基本概念和使用方法,包括Node.js的定义、创建第一个应用、实现HTTP服务器服务和操作数据库的步骤。通过示例代码展示了如何使用Node.js创建服务和连接MySQL数据库,并使用npm安装所需的依赖包。
nodejs使用初体验
|
4月前
|
JavaScript 开发者
入职必会-开发环境搭建30-Vue-cli环境下载和安装
Vue CLI(Vue Command Line Interface)是一个官方发布的用于快速搭建基于 Vue.js 开发环境的工具。它可以帮助开发者快速搭建 Vue 项目、进行项目配置、构建等操作,让开发过程更加高效。
|
6月前
|
JavaScript 前端开发
Nodejs 第六章(npx)
Nodejs 第六章(npx)
104 0
|
JavaScript 前端开发 中间件
Nodejs 学习
Nodejs 学习
71 0
|
Web App开发 JavaScript 前端开发
扛着锄头写代码之nodejs初识
扛着锄头写代码之nodejs初识
76 0
|
Web App开发 JavaScript 前端开发
快速掌握Nodejs安装以及入门
官网:[http://nodejs.cn/](http://nodejs.cn/) Node 是一个让 JavaScript 运行在服务端的开发平台,它让 JavaScript 成为与PHP、Python、Perl、Ruby 等服务端语言平起平坐的脚本语言。 发布于2009年5月,由Ryan Dahl开发,实质是对Chrome V8引擎进行了封装。
252 0
快速掌握Nodejs安装以及入门
|
JavaScript 前端开发 网络协议
Nodejs基础(一)
Nodejs基础(一)
156 0
|
Web App开发 缓存 开发框架
Vue05之ElementUI入门+nodejs环境搭建+运行nodejs项目
Vue05之ElementUI入门+nodejs环境搭建+运行nodejs项目
Vue05之ElementUI入门+nodejs环境搭建+运行nodejs项目
|
JavaScript API
nodejs笔记
process.nextTick() --定时器(setTimeout()和setInterval()与浏览器中的API是一致的,定时器的问题在于,他并非精确的(在容忍范围内)。尽管事件循环十分快,但是如果某一次循环占用的时间较多,那么下次循环时,他也许已经超时很久了。
|
开发者
npm 发包实践教程之 gRPC 怎么使用?(1)
npm 发包实践教程之 gRPC 怎么使用?(1)
123 0
下一篇
无影云桌面