🥳 写毕业论文时手动获取 BibTex 太麻烦?用 nodejs 写了批量获取命令行工具

用 nodejs 开发属于自己的命令行工具

前言

在写毕业论文的时候用的是 latex,参考文献通过 bib 文件导入后编译,所以每一篇论文都要转成 BibTex 格式 ,而在加参考文献时个人感觉最麻烦的就是去搜 BibTex ,不管是通过知网、谷歌或者百度学术都可以获取,但是一篇大论文的参考文献起码要几十篇,例如作者本人就有将近 90 篇,所以感觉手动获取好像不太方便(虽然当时是手动获取的,后来想着做一个工具,单纯就是不想写论文 🤪)。

其实,如果是专业搞论文的,应该会有一个文献管理软件,比如 Mac 可以用 Zotero,这个软件可以导出 bib 文件,但是大部分人应该都是只是想毕个业,应该也不会专门去下一个软件。或者你之前写过小论文,说不定也能有几篇文献已经有 BibTex 格式的,但是大部分应该还是没有的吧?

哈哈,说了这么久废话只是想说明一点,我这个工具或许有那么一丢丢的价值 🧐。

🥶 需要友情提示一下,如果在一段时间内频繁获取 BibTex,会被判定为 🤖,然后可能会被暂时 🚫 访问资源。

功能演示

为了更直观的感受一下本工具的具体功能,接下来通过一个视频来做演示,也让大家可以有兴趣读完我的文章 🥲。

好像上面的 GIF 图片比较模糊,如果耐心看完的话,大家应该能看出几个关键的步骤:

  1. 通过 bib set google-cookie 命令设置 cookie;
  2. 通过 bib create 命令批量获取 bibTex;
  3. 最后只消耗了 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

🌟 其中 keyvalue 是必选项,如果不传,则会报错 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

其中提供的操作主要是以下两个,默认是不会询问的

  1. 是否保留标题中的大写字母,这个主要是因为有些场景下需要保留,例如一些专有名词,有些场景可能不需要;
  2. 是否保留 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

这可能就是核心的实现原理了,主要使用到了 cheeriorequest 以及 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 流程可以总结如下:

graph LR 请求关键词 --> 获取到列表第一个\n文章的atid --> |利用 atid|获取Cite面板 --> 得到BibTex链接

🤪 不知道大家有没有看明白

自定义 bibTex 处理函数

虽然本工具中已经提供了两个处理函数(之前已经说明过了,如果跳着看的可以往前翻翻 💖),但是为了提高本工具的可扩展性和灵活性,本工具提供给用户对每条 bibTex 自定义处理的功能,感觉类似于 webpack 的 loader。例如,你想在每个篇文章的 author 后加上你自己的名字,就可以轻松实现啦(开玩笑的)。具体步骤如下:

  1. 利用 bib set 命令设置 "bibTex-process" 的值为你自己写的 js 文件的绝对路径,并提供一个默认导出(一定要有) ,类型是 function[]
  2. 然后获取 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 🎉

现在来具体说一下,自定义函数的书写规范吧:

  1. 必须要有一个默认导出 ,而且必须为数组 ,处理时的顺序是从左到右

  2. 每个处理函数会被传入两个参数,一个是 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
  3. 我们可以在函数中使用 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 获取处理结果
});

其他问题

应该还有,但是想不起来了 🥲。

注意事项

接下来,将详细说一下这个工具在使用时的注意事项:

  1. 🚦不要在一段时间内频繁获取,否则会被判定为 🤖(这个在之前也说过好几次了,我也在想怎么绕过人机验证,之前尝试过 ez-captcha,但是好像将获取的 token 发送后好像没有成功绕过,之后就暂时搁置了),如果被判定为 🤖 后,需要手动打开网页,然后进行验证,然后等一会再获取,具体如下:

  2. 🚦如果同一时间无法大量获取 bibTex 可能是 Cookie 过期了,如果使用谷歌学术 则打开 Network 面板找到如下的内容,然后用 bib set 重新设置即可:

    如果是百度学术,则按下述找到 Cookie:

  3. 🚦如果使用谷歌学术一般需要本地有 🪜,否则会请求失败。有了 🪜 后需要获取代理的地址,然后使用 bib set proxy [你的 🪜] 进行设置,怎样查看自己的 🪜 嘞,这里简单介绍一下(应该懂了吧 🤪):

  4. 如果使用 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...

💖 喜欢的话可以给一颗 🌟,支持一下吗?或者给这篇文章点个赞 + 收藏。目前该工具还处于开发阶段,大家如果有什么好的想法可以评论区讨论,如果有好的建议,我会采纳并落地哦~

总结

在我写大论文那痛苦又折磨的时候,我通过自己写一点小工具来转移注意力,来让自己变得更有活力,也希望自己以后能继续对前端保持热爱吧。之后可能还想做一些小东西,喜欢的可以点歌关注。

相关推荐
也无晴也无风雨1 小时前
深入剖析输入URL按下回车,浏览器做了什么
前端·后端·计算机网络
Martin -Tang2 小时前
Vue 3 中,ref 和 reactive的区别
前端·javascript·vue.js
FakeOccupational3 小时前
nodejs 020: React语法规则 props和state
前端·javascript·react.js
放逐者-保持本心,方可放逐3 小时前
react 组件应用
开发语言·前端·javascript·react.js·前端框架
曹天骄5 小时前
next中服务端组件共享接口数据
前端·javascript·react.js
阮少年、5 小时前
java后台生成模拟聊天截图并返回给前端
java·开发语言·前端
郝晨妤6 小时前
鸿蒙ArkTS和TS有什么区别?
前端·javascript·typescript·鸿蒙
AvatarGiser7 小时前
《ElementPlus 与 ElementUI 差异集合》Icon 图标 More 差异说明
前端·vue.js·elementui
喝旺仔la7 小时前
vue的样式知识点
前端·javascript·vue.js
别忘了微笑_cuicui7 小时前
elementUI中2个日期组件实现开始时间、结束时间(禁用日期面板、控制开始时间不能超过结束时间的时分秒)实现方案
前端·javascript·elementui