Nodejs-cli 填坑记

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

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

命令行翻译小工具Nodejs实现

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

[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工具就能制作完毕了。真的是无波折,不开心呐。

总结

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

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

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 157,012评论 4 359
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 66,589评论 1 290
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 106,819评论 0 237
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,652评论 0 202
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 51,954评论 3 285
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,381评论 1 210
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,687评论 2 310
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,404评论 0 194
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,082评论 1 238
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,355评论 2 241
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 31,880评论 1 255
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,249评论 2 250
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 32,864评论 3 232
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,007评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,760评论 0 192
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,394评论 2 269
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,281评论 2 259

推荐阅读更多精彩内容