使用 Node.js 递归爬取 VCB-Studio 文档

使用 Node.js 递归爬取 VCB-Studio 文档

在雪飘工作室的 字幕站 可以打包下载一整个目录的内容,然而在 VCB-Studio 开放课程 中却没有提供批下载的按钮。HTTP 文件索引服务是使用 h5ai 搭建的,这个是否可以打包下载应该也是可以配置的。当我尝试套用雪飘的打包下载到 VCB-Studio 后,发现无法奏效。不光是前端没有显示下载按钮,后端也做了限制,很好。于是当时我就放弃了,并没有考虑太多。

最近看到 极客湾 的关于视频编码的一些知识,便想着重新学习一下,每天看一点。文件并不多,但要手动下载还是有些烦。由于 JavaScript 和 JSON 具有良好的相容性,比 Python 的 obj['xxx'] 的方式看着优雅,于是使用 Node.js 编写脚本对这些文档链接进行爬取。

在文件的 API 接口中返回的数据并不是简单的所有文件列表,而是只有当前目录下的文档和同级的内容,并不包含其他路径的上的文件或目录。因此这种多层级的内容,非常适合使用递归来进行遍历,甚至循环还不太好写。

示例数据(格式化后,并对 href 进行了 decodeURI):

{
  "items": [
    {
      "href": "/",
      "time": 1500856587000,
      "size": null,
      "managed": true,
      "fetched": true
    },
    {
      "href": "/Dark Shrine/",
      "time": 1481994949000,
      "size": null,
      "managed": true,
      "fetched": true
    },
    {
      "href": "/Dark Shrine/[VCB-Studio][教程03]基础工具的安装和调试/",
      "time": 1453400906000,
      "size": null,
      "managed": true,
      "fetched": true
    },
    {
      "href": "/Dark Shrine/[VCB-Studio][教程04]BDRip的制作流程/",
      "time": 1453401051000,
      "size": null,
      "managed": true,
      "fetched": false
    },
    {
      "href": "/Dark Shrine/[VCB-Studio][教程05]封装、分离和转封装容器/",
      "time": 1453611394000,
      "size": null,
      "managed": true,
      "fetched": false
    },
    {
      "href": "/Dark Shrine/[VCB-Studio][教程08]章节的处理与BDMV的分割/",
      "time": 1455725204000,
      "size": null,
      "managed": true,
      "fetched": false
    },
    {
      "href": "/Dark Shrine/[VCB-Studio][教程09]x264参数设置/",
      "time": 1455817303000,
      "size": null,
      "managed": true,
      "fetched": false
    },
    {
      "href": "/Dark Shrine/[VCB-Studio][教程10]x265 2.0参数设置/",
      "time": 1481994953000,
      "size": null,
      "managed": true,
      "fetched": false
    },
    {
      "href": "/Dark Shrine/[VCB-Studio][教程11]编码器参数研发方法/",
      "time": 1456939759000,
      "size": null,
      "managed": true,
      "fetched": false
    },
    {
      "href": "/Templar Archive/",
      "time": 1467944455000,
      "size": null,
      "managed": true,
      "fetched": false
    },
    {
      "href": "/Twilight Council/",
      "time": 1466443713000,
      "size": null,
      "managed": true,
      "fetched": false
    },
    {
      "href": "/Dark Shrine/[VCB-Studio][教程03]基础工具的安装和调试/[VCB-Studio][教程03]基础工具的安装和调试.doc",
      "time": 1453400906000,
      "size": 1952768
    },
    {
      "href": "/Dark Shrine/[VCB-Studio][教程03]基础工具的安装和调试/[VCB-Studio][教程03]基础工具的安装和调试.pdf",
      "time": 1453400877000,
      "size": 691289
    },
    {
      "href": "/Menu.xlsx",
      "time": 1464100763000,
      "size": 11266
    },
    {
      "href": "/readme.txt",
      "time": 1452322985000,
      "size": 767
    }
  ]
}

HTTP 请求使用 axios 这个最流行的模块,它有非常优美的使用方法,天然使用 Promise,我很喜欢。向文件输出内容直接使用 fs 这个自带的模块即可完成。

const axios = require('axios');
const fs = require('fs');

axios.defaults.baseURL = 'https://vcb-s.nmm-hd.org';

const visitedLinks = [];
const files = [];

async function getURL(url) {
  if (visitedLinks.includes(url)) return;
  else visitedLinks.push(url);
  console.log('>>>', url);
  const body = {
    action: 'get',
    items: {
      href: url,
      what: 1
    }
  };
  const {
    data: { items }
  } = await axios.post(url + '?', body);
  items
    .filter(i => i.fetched === undefined)
    .map(i => {
      files.push(i.href);
    });
  items.filter(i => i.fetched === false).map(i => getURL(i.href));
}

getURL('/');

setTimeout(() => {
  fs.writeFileSync('url.txt', files.reduce((a, b) => `${a}\n${b}`));
}, 100000);

仅仅 35 行代码不到就实现了文件列表的爬取,使用文本编辑器进行一些正则替换后,使用 aria2c -i url.txt -d documents -j 8 即可将文件下载到本地,工作完成。

不过这里还有一些问题,我将时间设置为 100 秒后将链接输出到文件,但可能 100 秒早就完成了内容的爬取,而程序还没有退出。或者 100 秒还没有完成网络请求,就已经将链接写入到文件。一时之间我也没有想到好的办法,因为 getURL 是进行递归调用的,一次调用 getURL 可能又要发起好几个 getURL 的调用,必须等到被调函数成功得到结果后,调用函数才应该结束。我尝试使用 async 函数解决,最终也没能发现更好的方法。

后来我用 Promise 解决了问题,但我觉得这种方式始终不够好。我并不想使用 Promise.all() 等待所有的 Promise 完成,因为我觉得可能有些性能上的问题,当然也可能是我多虑了。希望我以后能够想出更好的解决方法吧。

第二版在第一版的基础上除了会按时写入文件,还避免了手工删除重复文档链接的步骤。

const axios = require('axios');
const fs = require('fs');

const baseURL = 'https://vcb-s.nmm-hd.org';
axios.defaults.baseURL = baseURL;

const visitedLinks = [];
const files = [];

async function getURL(url, first = false) {
  if (visitedLinks.includes(url)) return;
  else visitedLinks.push(url);
  console.log('>>>', decodeURI(url));
  const body = {
    action: 'get',
    items: {
      href: url,
      what: 1
    }
  };
  const {
    data: { items }
  } = await axios.post(url + '?', body);
  let file = items.filter(i => i.fetched === undefined);
  if (!first)
    file = file.filter(i => !['/Menu.xlsx', '/readme.txt'].includes(i.href));
  file.map(i => {
    files.push(i.href);
  });
  return Promise.all(
    items.filter(i => i.fetched === false).map(i => getURL(i.href))
  );
}

getURL('/', true).then(() => {
  fs.writeFileSync('url.txt', files.map(i => baseURL + i).join('\n'));
});

结果展示(经过 decodeURI 了):

https://vcb-s.nmm-hd.org/Menu.xlsx
https://vcb-s.nmm-hd.org/readme.txt
https://vcb-s.nmm-hd.org/Twilight Council/[VCB-Studio][教程21]后缀表达式求值与转换/[VCB-Studio][教程21]后缀表达式的求值与转换.docx
https://vcb-s.nmm-hd.org/Twilight Council/[VCB-Studio][教程21]后缀表达式求值与转换/[VCB-Studio][教程21]后缀表达式的求值与转换.pdf
https://vcb-s.nmm-hd.org/Twilight Council/[VCB-Studio][教程02]播放器教程背后的知识/[VCB-Studio][教程02]播放器教程背后的知识.pdf
https://vcb-s.nmm-hd.org/Twilight Council/[VCB-Studio][教程00]视频格式基础知识/[VCB-Studio][教程00]视频格式基础知识.docx
https://vcb-s.nmm-hd.org/Twilight Council/[VCB-Studio][教程00]视频格式基础知识/[VCB-Studio][教程00]视频格式基础知识.pdf
https://vcb-s.nmm-hd.org/Twilight Council/[VCB-Studio][教程01]傻瓜式解码包(完美解码)安装设置教程/[VCB-Studio][教程01]傻瓜式解码包(完美解码)安装设置教程.pdf
https://vcb-s.nmm-hd.org/Dark Shrine/[VCB-Studio][教程05]封装、分离和转封装容器/[VCB-Studio][教程05]封装、分离和转封装容器.docx
https://vcb-s.nmm-hd.org/Dark Shrine/[VCB-Studio][教程05]封装、分离和转封装容器/[VCB-Studio][教程05]封装、分离和转封装容器.pdf
https://vcb-s.nmm-hd.org/Dark Shrine/[VCB-Studio][教程03]基础工具的安装和调试/[VCB-Studio][教程03]基础工具的安装和调试.doc
https://vcb-s.nmm-hd.org/Dark Shrine/[VCB-Studio][教程03]基础工具的安装和调试/[VCB-Studio][教程03]基础工具的安装和调试.pdf
https://vcb-s.nmm-hd.org/Dark Shrine/[VCB-Studio][教程04]BDRip的制作流程/[VCB-Studio][教程04]BDRip的制作流程.docx
https://vcb-s.nmm-hd.org/Dark Shrine/[VCB-Studio][教程04]BDRip的制作流程/[VCB-Studio][教程04]BDRip的制作流程.pdf
https://vcb-s.nmm-hd.org/Dark Shrine/[VCB-Studio][教程10]x265 2.0参数设置/[VCB-Studio][教程10]x265 2.0参数设置.docx
https://vcb-s.nmm-hd.org/Dark Shrine/[VCB-Studio][教程10]x265 2.0参数设置/[VCB-Studio][教程10]x265 2.0参数设置.pdf
https://vcb-s.nmm-hd.org/Dark Shrine/[VCB-Studio][教程11]编码器参数研发方法/[VCB-Studio][教程11]编码器参数研发方法.docx
https://vcb-s.nmm-hd.org/Dark Shrine/[VCB-Studio][教程11]编码器参数研发方法/[VCB-Studio][教程11]编码器参数研发方法.pdf
https://vcb-s.nmm-hd.org/Dark Shrine/[VCB-Studio][教程09]x264参数设置/[VCB-Studio][教程09]x264参数设置.docx
https://vcb-s.nmm-hd.org/Dark Shrine/[VCB-Studio][教程09]x264参数设置/[VCB-Studio][教程09]x264参数设置.pdf
https://vcb-s.nmm-hd.org/Dark Shrine/[VCB-Studio][教程08]章节的处理与BDMV的分割/[VCB-Studio][教程08]章节的处理与BDMV的分割.docx
https://vcb-s.nmm-hd.org/Dark Shrine/[VCB-Studio][教程08]章节的处理与BDMV的分割/[VCB-Studio][教程08]章节的处理与BDMV的分割.pdf
https://vcb-s.nmm-hd.org/Templar Archive/[VCB-Studio][教程12]16bit YUV的处理/[VCB-Studio][教程12]16bit YUV的处理.docx
https://vcb-s.nmm-hd.org/Templar Archive/[VCB-Studio][教程12]16bit YUV的处理/[VCB-Studio][教程12]16bit YUV的处理.pdf
https://vcb-s.nmm-hd.org/Templar Archive/[VCB-Studio][教程06]VapourSynth基础与入门/[VCB-Studio][教程06]VapourSynth基础与入门.docx
https://vcb-s.nmm-hd.org/Templar Archive/[VCB-Studio][教程06]VapourSynth基础与入门/[VCB-Studio][教程06]VapourSynth基础与入门.pdf
https://vcb-s.nmm-hd.org/Templar Archive/[VCB-Studio][教程15]Clip加减运算与Unsharp Mask/[VCB-Studio][教程15]Clip加减运算与Unsharp Mask.docx
https://vcb-s.nmm-hd.org/Templar Archive/[VCB-Studio][教程15]Clip加减运算与Unsharp Mask/[VCB-Studio][教程15]Clip加减运算与Unsharp Mask.pdf
https://vcb-s.nmm-hd.org/Templar Archive/[VCB-Studio][教程26]YUV和RGB互转(2)/[VCB-Studio][教程26]YUV和RGB互转(2).docx
https://vcb-s.nmm-hd.org/Templar Archive/[VCB-Studio][教程26]YUV和RGB互转(2)/[VCB-Studio][教程26]YUV和RGB互转(2).pdf
https://vcb-s.nmm-hd.org/Templar Archive/[VCB-Studio][教程18]YUV与RGB的互转(1)/[VCB-Studio][教程18]YUV与RGB的互转(1).docx
https://vcb-s.nmm-hd.org/Templar Archive/[VCB-Studio][教程18]YUV与RGB的互转(1)/[VCB-Studio][教程18]YUV与RGB的互转(1).pdf
https://vcb-s.nmm-hd.org/Templar Archive/[VCB-Studio][教程25]Resizer(2)/[VCB-Studio][教程25]Resizer(2).docx
https://vcb-s.nmm-hd.org/Templar Archive/[VCB-Studio][教程25]Resizer(2)/[VCB-Studio][教程25]Resizer(2).pdf
https://vcb-s.nmm-hd.org/Templar Archive/[VCB-Studio][教程07]AviSynth基础与入门/[VCB-Studio][教程07]AviSynth基础与入门.docx
https://vcb-s.nmm-hd.org/Templar Archive/[VCB-Studio][教程07]AviSynth基础与入门/[VCB-Studio][教程07]AviSynth基础与入门.pdf
https://vcb-s.nmm-hd.org/Templar Archive/[VCB-Studio][教程20]MaskTools (1)/[VCB-Studio][教程20]MaskTools (1).docx
https://vcb-s.nmm-hd.org/Templar Archive/[VCB-Studio][教程20]MaskTools (1)/[VCB-Studio][教程20]MaskTools (1).pdf
https://vcb-s.nmm-hd.org/Templar Archive/[VCB-Studio][教程16]Repair的用法与Contra-Sharp/[VCB-Studio][教程16]Repair的用法与Contra-Sharp.docx
https://vcb-s.nmm-hd.org/Templar Archive/[VCB-Studio][教程16]Repair的用法与Contra-Sharp/[VCB-Studio][教程16]Repair的用法与Contra-Sharp.pdf
https://vcb-s.nmm-hd.org/Templar Archive/[VCB-Studio][教程13]Resizer(1)/[VCB-Studio][教程13]Resizer(1).docx
https://vcb-s.nmm-hd.org/Templar Archive/[VCB-Studio][教程13]Resizer(1)/[VCB-Studio][教程13]Resizer(1).pdf
https://vcb-s.nmm-hd.org/Templar Archive/[VCB-Studio][教程17]LimitDiff的用法与nr-deband/[VCB-Studio][教程17]LimitDiff的用法与nr-deband.docx
https://vcb-s.nmm-hd.org/Templar Archive/[VCB-Studio][教程17]LimitDiff的用法与nr-deband/[VCB-Studio][教程17]LimitDiff的用法与nr-deband.pdf
https://vcb-s.nmm-hd.org/Templar Archive/[VCB-Studio][教程14]Blur/[VCB-Studio][教程14]Blur.docx
https://vcb-s.nmm-hd.org/Templar Archive/[VCB-Studio][教程14]Blur/[VCB-Studio][教程14]Blur.pdf
https://vcb-s.nmm-hd.org/Templar Archive/[VCB-Studio][教程19]AVS的多线程优化-MPP的使用/[VCB-Studio][教程19]AVS的多线程优化-MPP的使用.docx
https://vcb-s.nmm-hd.org/Templar Archive/[VCB-Studio][教程19]AVS的多线程优化-MPP的使用/[VCB-Studio][教程19]AVS的多线程优化-MPP的使用.pdf