真的是玩玩没想到,一个cli竟然坑了我这么久,想当年写Python命令行工具的时候,哪有这么麻烦?随随便便写几下,添加个批处理命令脚本就搞定了。怎么Nodejs写一个就这么不顺利呢?
吐槽归吐槽,当我成功的写出来一个cli版本的工具之后,我才发现,是我错了。nodejs-cli其实真的是很方便,也很简单。
秉承分享知识的原则,在此记录一下。
[toc]
写在前面
这篇文章严格来说不能算是一片技术性的文章,没什么难点。有的只是一些好玩的小工具。对于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工具就能制作完毕了。真的是无波折,不开心呐。
总结
最后, 我想对自己说的就是:
遵守编码规范,不要想当然。