用 nodejs 开发属于自己的命令行工具
前言
在写毕业论文的时候用的是 latex,参考文献通过 bib 文件导入后编译,所以每一篇论文都要转成 BibTex 格式 ,而在加参考文献时个人感觉最麻烦的就是去搜 BibTex ,不管是通过知网、谷歌或者百度学术都可以获取,但是一篇大论文的参考文献起码要几十篇,例如作者本人就有将近 90 篇,所以感觉手动获取好像不太方便(虽然当时是手动获取的,后来想着做一个工具,单纯就是不想写论文 🤪)。
其实,如果是专业搞论文的,应该会有一个文献管理软件,比如 Mac 可以用 Zotero,这个软件可以导出 bib 文件,但是大部分人应该都是只是想毕个业,应该也不会专门去下一个软件。或者你之前写过小论文,说不定也能有几篇文献已经有 BibTex 格式的,但是大部分应该还是没有的吧?
哈哈,说了这么久废话只是想说明一点,我这个工具或许有那么一丢丢的价值 🧐。
🥶 需要友情提示一下,如果在一段时间内频繁获取 BibTex,会被判定为 🤖,然后可能会被暂时 🚫 访问资源。
功能演示
为了更直观的感受一下本工具的具体功能,接下来通过一个视频来做演示,也让大家可以有兴趣读完我的文章 🥲。
好像上面的 GIF 图片比较模糊,如果耐心看完的话,大家应该能看出几个关键的步骤:
- 通过
bib set google-cookie
命令设置 cookie; - 通过
bib create
命令批量获取 bibTex; - 最后只消耗了
15.93 秒
就完成了对90个文献
的获取,个人觉得效率还是不错的 🥳,同时也会提示将结果保存在哪个文件里了。
看完演示视频,有没有产生一点点 🤏 的兴趣呢?接下来会从 安装使用 、功能介绍 、功能实现 、遇到的问题以及解决方案 以及 注意事项 这几个方面进行讲解。如果对哪一部分有兴趣,可以自行跳转一下。
安装使用
🌟 推荐全局安装:
shell
npm install bib-quick -g
如果安装失败,可以使用
sudo
权限。
然后在终端中输入 bib -V
,如果出现版本号 0.0.0
,即表示安装成功。
功能介绍
命令帮助
为什么把这个功能在第一个介绍呢?因为希望大家在不会用的时候可以使用 bib help
命令来查看使用说明,或者有问题(BUG )也可以留言(私信)问问,我看见的话会回复一下,然后放一张效果截图吧。
下面是用的 bib help
命令的效果图:
如果是查看某个具体命令,可以用 bib help create
命令:
✅ 总结一下就是:如果查看全部的命令帮助信息就用 bib help
,如果查看具体某个命令就用 bib help [具体命令名字]
就行了。
查看版本号
这个就比较简单了,用 bib --version
或者 bib -V
可以查看当前工具的版本信息,在终端会显示 0.0.0
。
获取配置信息
这里需要说明一下,这个工具会提供以下参数的配置:
json
{
"google": "谷歌学术的网站地址",
"baidu": "百度学术的网站地址",
"output": "bibTex 输出文件路径",
"proxy": "本地 🪜 的 IP + port",
"timeout": "每个任务最长时间",
"google-cookie": "使用谷歌学术获取时需要的 Cookie",
"baidu-cookie": "使用百度学术获取时需要的 Cookie(大部分时候不会用到,空着即可)",
"bibTex-process": "自定义 bibTex 操作的 js 文件绝对路径"
}
了解上述配置参数后,可以使用 bib get [key]
来获取指定 key
值所对应的具体配置,例如使用 bib get google
可以获取谷歌学术网站的具体地址:
如果不指定 key
则获取的是全部信息,具体如下:
💡 如果获取的 key
不存在,那么会提示 ✖ no such key
;
🌟
key
为非必选参数,如果不传,默认获取全部配置信息。
设置配置信息
与获取相反的,当然就会有设置配置项信息,具体命令为 bib set <key> <value>
,具体操作如下所示:
💡 如果设置的 key
不存在,那么会提示 ✖ no such key
;
🌟 其中
key
和value
是必选项,如果不传,则会报错error: missing required argument 'key'
。
批量获取 BibTex
🎉 现在将介绍本工具的核心功能 ------ 批量获取 BibTex。
✅ 基本用法 通过 bib create A B C ...
进行批量获取给定的文献名:
接下来将列举一些 options 参数,可以通过一些配置来实现增强功能:
Option 名称 | 简写 | 用法 | 功能描述 |
---|---|---|---|
--title |
-t |
--title [title...] |
[string[]] 你想要获取bibTeX的文章标题,标题也可以是一些关键词,标题中的空格替换为"+"号 (默认[]) |
--start |
-s |
--start [start] |
[int] 搜索范围的起始年份 (默认 0) |
--end |
-e |
--end [end] |
[int] 搜索范围的结束年份 (默认 0) |
--flag |
-f |
--flag [flag] |
[0/1] 搜索结果排序标志。如果为0,则按照相关性排序。 如果为1,则按照时间倒序排序 (默认 0) |
--output |
-o |
--output <path> |
[string] bibTex输出文件绝对路径 (必填) |
--name |
-n |
--name <name> |
[string] bibTex 文件名 (必填) |
--path |
-p |
--path <path> |
[string] 由搜索关键字组成的 txt 文件 的绝对路径。 文件中的每一行都包含一个关键字。 值得注意的是,它的优先级高于 -t (必填) |
--google |
-g |
--google |
[boolean] 使用谷歌学术 (默认 false) |
--baidu |
-b |
--baidu |
[boolean] 使用百度学术 (默认 false) |
--auto |
-a |
--auto |
[boolean] 如果标题包含中文,则使用百度学术,否则使用谷歌学术,且该选项优先级最高 (默认 false) |
--custom |
-c |
--custom |
[boolean] 获取所有BibTex后是否要求进行后续处理 (默认 false) |
--yes |
-y |
--yes |
[boolean] 保留 BibTex 处理的默认选项,否则将显示问卷 (默认 false) |
💌 列举了这么多可选参数,想必大家已经眼花缭乱了,接下来将重点介绍其中的某一些功能,例如
--custom
其中提供的操作主要是以下两个,默认是不会询问的:
- 是否保留标题中的大写字母,这个主要是因为有些场景下需要保留,例如一些专有名词,有些场景可能不需要;
- 是否保留 DOI 参数。
具体操作如下:
功能实现
👨🏻💻 展示完工具的每一个功能后,如果大家好奇具体用到什么包或者什么技术的话,可以认真阅读完以下内容。
使用到的相关 NPM 包
首先将列举一下主要使用到的 NPM 包:
NPM 包名称 | 具体功能 | 链接 |
---|---|---|
chalk |
更改终端打印颜色 | www.npmjs.com/package/cha... |
cheerio |
解析 DOM,爬虫必备 | www.npmjs.com/package/che... |
cli-progress |
终端进度条 | www.npmjs.com/package/cli... |
commander |
解析命令行 | www.npmjs.com/package/com... |
figlet |
生成花体 | www.npmjs.com/package/fig... |
iconv-lite |
纯JS字符编码转换 | www.npmjs.com/package/ico... |
inquirer |
生成终端问卷 | www.npmjs.com/package/inq... |
ora |
生成加载 logo | www.npmjs.com/package/ora |
当然还有一些常见的 NPM 包,比如
request
包等。
打印花体
大家在看到 GIF 动图后,可能会好奇,我是怎么实现每个命令都可以展示出 quick-bib
的花体,那么现在就来解决大家的疑惑 🤨,其实使用到了 figlet
包来生成,具体代码如下:
js
module.exports = function () {
console.log(chalk.cyan(figlet.textSync('quick-bib',
{
font: 'Graffiti', // 设置使用的字体
horizontalLayout: 'full'
}
)));
console.log('\n'); // 空出一行
}
然后在 commander
提供的 addHelpText
函数中设置即可,具体代码如下:
js
const { program } = require('commander');
const printLogo = require('./utils/printLogo'); // 生成花体的函数
program
.addHelpText('before', printLogo())
.showHelpAfterError('(run "bib --help" to list commands)');
生成进度条
这里使用到的是 cli-progress
包,初始化生成进度条示例的代码如下:
js
const cliProgress = require('cli-progress');
const b1 = new cliProgress.SingleBar({
format: ls.info + ' Progress |' + chalk.cyan('{bar}') + '| {percentage}% || {value} / {total} Tasks || Tip: {title}',
barCompleteChar: '\u2588',
barIncompleteChar: '\u2591',
hideCursor: true
});
b1.start(...); // 开始
b1.increment(...); // 步增 1
b1.stop(); // 停止
获取 BibTex
这可能就是核心的实现原理了,主要使用到了 cheerio
和 request
以及 iconv-lite
包。具体实现代码如下:
js
module.exports = function (title, options) {
return new Promise(async (resolve, reject) => {
const start = +new Date();
try {
const atid = await getResult(title, options); // 获取文章的 atids
const href = await getPanel(atid); // 获取 BibTex 链接
const bibTex = await getBibTex(href); // 获取 bibTex
resolve(...);
} catch (e) {
reject(...)
}
})
}
我们首先通过 Network 面板查找出,具体是哪个请求获取文献数据,是通过 XHR 请求还是直接返回 HTML 页面,这个需要确定一下,那么结果如下:
那么通过请求可以看出,是直接通过获取 HTML 文件返回文献的具体信息的。然后分析一下 DOM 结构:
可以看到我们想要的 atid 是在 .gs_rt > a[data-clk-atid]
中,那么我们就需要使用 cheerio
对返回的结果进行 DOM 解析,然后获取到 atid,为什么要获取这个,原因如下图:
点击 cite
按钮后,会发送 XHR 请求得到 Cite 面板的 HTML,那么我们可以从中获取到 BibTex 的生成链接 🔗,具体如下:
最后访问链接就可以得到 BibTex 了,因此获取 BibTex 流程可以总结如下:
🤪 不知道大家有没有看明白
自定义 bibTex 处理函数
虽然本工具中已经提供了两个处理函数(之前已经说明过了,如果跳着看的可以往前翻翻 💖),但是为了提高本工具的可扩展性和灵活性,本工具提供给用户对每条 bibTex 自定义处理的功能,感觉类似于 webpack 的 loader。例如,你想在每个篇文章的 author 后加上你自己的名字,就可以轻松实现啦(开玩笑的)。具体步骤如下:
- 利用
bib set
命令设置"bibTex-process"
的值为你自己写的 js 文件的绝对路径,并提供一个默认导出(一定要有) ,类型是 function[]; - 然后获取 bibTex 时一定要在
bib create
命令后加上--custom(-c)
,这样才能开启自定义功能,如果你不想展示问卷可以使用--yes(-y)
使用默认值,也就是可以使用命令bib create -p [txt文件路径] -g -c -y
自定义函数具体如何书写,会在后续进行详细讲解,现在展示一下代码示例:
js
// custom-process.js
// 对 year 字段的处理
function processYear(key, val) {
if (key === 'year') {
return (+val) % 2 === 0 ? `${val}-偶数` : `${val}-奇数`;
}
return val;
}
// 对 author 字段的处理
function processAuthor(key, val) {
if (key === 'author') {
return val + ', Karl_fang 🎉';
}
return val;
}
module.exports = [processYear, processAuthor];
然后运行结果如下:
可以看出,year
字段中在根据年份奇偶性进行了处理,同时 author
字段中在最后也加上了我自己的名字 ------ Karl Fang 🎉
。
现在来具体说一下,自定义函数的书写规范吧:
-
必须要有一个默认导出 ,而且必须为数组 ,处理时的顺序是从左到右;
-
每个处理函数会被传入两个参数,一个是
key
,一个是对应的val
值,例如要处理的 bibTex 的内容如下:txt@article{graves2012long, title={Long short-term memory}, author={Graves, Alex and Graves, Alex}, journal={Supervised sequence labelling with recurrent neural networks}, pages={37--45}, year={2012}, publisher={Springer} }
那么传入的
key-value
值的内容如下:key value title Long short-term memory author Graves, Alex and Graves, Alex journal Supervised sequence labelling with recurrent neural networks pages 37--45 year 2012 publisher Springer -
我们可以在函数中使用
if-else
或者switch-case
语句对每一种情况进行相应的处理,同时将函数的返回值作为该key
值的最终结果 ,例如return val + ', Karl_fang 🎉';
,所以剩余情况一定别忘记return val
,如果返回值为逻辑值的空
就不会被最终展示,例如processAuthor
函数是以下这样的,那么:
js
function processAuthor(key, val) {
if (key === 'author') {
return val + ', Karl_fang 🎉';
}
}
// 最终处理结果如下:
// @article{graves2012long,
// author={Graves, Alex and Graves, Alex, Karl_fang 🎉}
// }
本地调试自己的命令行
在 package.json
里设置如下信息:
json
{
...
"bin": {
"bib": "./index.js" // 入口文件
},
...
}
然后运行 npm link
就可以啦 🥳。
遇到的问题以及解决方案
在面试中,应该很容易被问到,你这个项目中有遇到什么问题,你怎么解决的呢?那么接下来,我将介绍一下,在开发这个项目中遇到的问题,并列举出解决方案,
被系统判定为机器人 🤖
可能一次性发起太多请求,会被系统判定为机器人,因此很容易刚开始的一些请求能成功,大概到 40~50 个的时候可能会开始请求失败了。因此就可以通过写一个 Schedule
类来控制并发数,这好像是一道经典的面试手写题,我在面试腾讯和字节的时候都被要求写过。
❌ 当然,如果你在某一段时间频繁请求,也是会被判定为 🤖,因此需要手动打开网页,进行人机验证。
解析 DOM 失败
有时候会发现对请求返回的结果不能正确的 DOM 解析,后来发现是因为返回的 content-type
字符集不是 utf-8
导致的,因此使用 iconv-lite
来进行针对性解析,具体代码如下:
js
function promisefyRequest(url, options) {
return new Promise((resolve, reject) => {
request({...}, (error, response, body) => {
if (error) {
reject(error);
}
const contentType = response && response.headers && response.headers['content-type'];
if (contentType) {
const match = contentType.match(/charset=(?<charset>.*?)($|;)/);
let charset = 'utf-8';
if (match) {
charset = match.groups.charset;
}
body = iconv.decode(body, charset.toLowerCase());
}
resolve(body);
})
})
}
请求设置代理
因为要请求谷歌学术,因此需要 🪜,刚开始使用的 axios
包,但是使用网上提供的代理设置后一直无法成功,后来就无意发现 request
包也可以使用代理,就试了一下,没想到成功了,是要设置 option.proxy
即可。
解除链接失败
当时调试完后,想解除 bib 的链接,使用 npm unlink bib
无效,然后直接 which bib
找到 bib
的文件,直接暴力删除了。
设计合适的 bibTex 处理逻辑
因为处理函数类似于管道,因此写了一个 BibParse
类来专门处理解析过程,实现的思路就是将 bibTex 格式转为 Object,通过将预处理操作转化为对 Object 的增删改操作,最后再将 Object 转为 bibTex 的格式即可。主要具体代码如下:
js
class BibParse {
constructor(bib, indent = 2) {
// 解析传入的 bibTex 格式
}
...
forEach(callback) {
const result = this.result;
[...Object.entries(result)].forEach(([key, val]) => {
const res = callback.call(this, key, val);
result[key] = res;
});
}
// 管道处理函数
pipe(processList) {
if (processList && processList.length) {
processList
.filter(fn => typeof fn === 'function')
.reduce((_, fn) => this.forEach(fn), 0);
}
return this;
}
}
module.exports = BibParse;
那么对于每条 BibTex 的处理,代码就可以写作如下:
js
const result = bibTexList.map(bib => {
const bibTex = new BibParse(bib)
// processList: 默认处理函数,customProcessList: 用户自定义处理函数
.pipe([...processList, ...customProcessList]);
return bibTex.bib; // 通过 .bib 获取处理结果
});
其他问题
应该还有,但是想不起来了 🥲。
注意事项
接下来,将详细说一下这个工具在使用时的注意事项:
-
🚦不要在一段时间内频繁获取,否则会被判定为 🤖(这个在之前也说过好几次了,我也在想怎么绕过人机验证,之前尝试过
ez-captcha
,但是好像将获取的 token 发送后好像没有成功绕过,之后就暂时搁置了),如果被判定为 🤖 后,需要手动打开网页,然后进行验证,然后等一会再获取,具体如下: -
🚦如果同一时间无法大量获取 bibTex 可能是 Cookie 过期了,如果使用谷歌学术 则打开 Network 面板找到如下的内容,然后用
bib set
重新设置即可:如果是百度学术,则按下述找到 Cookie:
-
🚦如果使用谷歌学术一般需要本地有 🪜,否则会请求失败。有了 🪜 后需要获取代理的地址,然后使用
bib set proxy [你的 🪜]
进行设置,怎样查看自己的 🪜 嘞,这里简单介绍一下(应该懂了吧 🤪): -
如果使用
bib set
写入失败,需要使用sudo
权限即可(开始使用的时候一定要进行某些参数的配置)。
其他一些内容
项目的结构树
txt
quick-bib
├─ .gitignore
├─ bibProcess
│ ├─ BibParse.js
│ └─ index.js
├─ bibTex
│ ├─ getBaiduBibTex.js
│ ├─ getBibTexAuto.js
│ └─ getGoogleBibTex.js
├─ config
│ └─ index.js
├─ config.json
├─ customProcess.js
├─ http
│ └─ request.js
├─ index.js
├─ inquirer
│ ├─ processBibInquirer.js
│ ├─ question.js
│ └─ term.js
├─ log
│ └─ index.js
├─ package-lock.json
├─ package.json
├─ schedule
│ └─ index.js
└─ utils
├─ createBibTex.js
├─ getDetailInfo.js
├─ getInt.js
├─ getTip.js
├─ getToolInfo.js
├─ printLogo.js
└─ updateCookie.js
相关仓库
📦 NPM 仓库:www.npmjs.com/package/qui...
👨🏻💻 GitHub 仓库:github.com/ox4f5da2/qu...
💖 喜欢的话可以给一颗 🌟,支持一下吗?或者给这篇文章点个赞 + 收藏。目前该工具还处于开发阶段,大家如果有什么好的想法可以评论区讨论,如果有好的建议,我会采纳并落地哦~
总结
在我写大论文那痛苦又折磨的时候,我通过自己写一点小工具来转移注意力,来让自己变得更有活力,也希望自己以后能继续对前端保持热爱吧。之后可能还想做一些小东西,喜欢的可以点歌关注。