互联网上有许多可供人类消费的信息。但是如果这些数据不是以专用的 REST API 的形式出现,通常很难以编程方式对其进行访问。使用 jsdom 之类的 Node.js 工具,你可以直接从网页上抓取并解析这些数据,并用于你自己的项目和应用。

让我们以用 MIDI 音乐数据来训练神经网络生成听起来经典的任天堂音乐为例。我们需要一套来自旧任天堂游戏的 MIDI 音乐。通过使用 jsdom 可以从视频游戏音乐档案中抓取这些数据。

入门和依赖项设置

在继续之前,你需要确保自己有 Node.js 和 npm 的最新版本。

切换到你希望此代码存在的目录,并在终端中运行以下命令创建项目的程序包:

npm init --yes

--yes 参数可以忽略所有你必须填写或跳过的提示。现在我们的程序有了 package.json

为了通过发出 HTTP 请求从网页获取数据,我们将使用 Got 库,对于 HTML 的解析,我们将用 Cheerio。

在终端中运行以下命令安装这些库:

npm install got@10.4.0 jsdom@16.2.2

jsdom 是大量 Web 标准的纯 JavaScript 实现,也是许多 JavaScript 开发人员熟悉的工具。让我们深入了解该如何使用它。

用 Got 检索要与 jsdom 一起使用的数据

首先让我们编写一些从网页中获取 HTML 的代码,然后看看如何开始解析。以下代码将向我们想要的网页发送一个 GET 请求,并使用该页面的 HTML 创建一个 jsdom 对象,我们将其命名为 dom

const fs = require('fs');
const got = require('got');
const jsdom = require("jsdom");
const { JSDOM } = jsdom;

const vgmUrl= 'https://www.vgmusic.com/music/console/nintendo/nes';

got(vgmUrl).then(response => {
  const dom = new JSDOM(response.body);
  console.log(dom.window.document.querySelector('title').textContent);
}).catch(err => {
  console.log(err);
});

当向构造函数 JSDOM 传递一个字符串时,将返回一个 JSDOM 对象,你可以从中访问许多可用的属性,例如 window。如该代码所示,你可以用查询选择器(query selector)。

例如 querySelector('title').textContent 将获取页面上 <title> 标记内的文本。如果将此代码保存到名为 index.js 的文件并用命令 node index.js 运行,它会把网页的标题记录到控制台。

通过 jsdom 使用 CSS 选择器

如果你想在查询中获得更具体的信息,可以使用 HTML 解析器进行解析。最常见的两个方法是按 classID 获取。如果要获取 ID 为 “menu” 的div,则可以用 querySelectorAll('#menu'),并且如果要获取 VGM MIDI 表格中的所有标题列,则可以执行 querySelectorAll('td.header')

我们在此页面上想要的是我们需要下载的所有 MIDI 文件的超链接。可以用 querySelectorAll('a')开始获取页面上的每个链接。在 index.js 中的代码中添加以下内容:

got(vgmUrl).then(response => {
  const dom = new JSDOM(response.body);
    dom.window.document.querySelectorAll('a').forEach(link => {
    console.log(link.href);
  });
}).catch(err => {
  console.log(err);
});

此代码记录页面上每个链接的 URL。可以用 forEach 函数浏览给定选择器中的所有元素。遍历页面上的每个链接都很棒,但是如果要下载所有 MIDI 文件,则需要更具体一些。

通过 HTML 元素过滤

在编写更多代码去解析所需的内容之前,先来看一下浏览器渲染出来的 HTML。每个网页都是不同的,有时从其中获取正确的数据需要一些创造力、模式识别和实验。

网页上的MIDI文件

我们的目标是下载许多 MIDI 文件,但是这个网页上有很多重复的曲目以及歌曲的混音。我们只希望下载重复歌曲中的一首,并且因为我们的最终目标是用这些数据来训练神经网络以生成准确的 Nintendo 音乐,所以我们不想在用户创建的混音上对其进行训练。

当你编写代码解析网页时,通常可以用现代浏览器中的开发者工具。如果右键单击你感兴趣的元素,则可以检查该元素后面的 HTML 并获取更多信息。

检查元素

你可以编写过滤器函数来微调所需的选择器数据。这些函数遍历给定选择器的所有元素,并根据是否应将它们包含在集合中而返回 true 或 false。

如果查看了上一步中记录的数据,可能会注意到页面上有很多链接没有 href 属性,因此无处可寻。可以确定它们不是我们要寻找的 MIDI,所以需要写一个简短的函数来过滤掉那些 MIDI,并包含确实能够链接到 .mid 文件的 href 元素:

const isMidi = (link) => {
  // Return false if there is no href attribute.
  if(typeof link.href === 'undefined') { return false }

  return link.href.includes('.mid');
};

现在有一个问题,我们不想下载重复项或用户生成的混音。可以用正则表达式来确保仅获取文本中不带括号的链接,因为只有重复项和混音项包含括号:

const noParens = (link) => {
  // Regular expression to determine if the text has parentheses.
  const parensRegex = /^((?!\().)*$/;
  return parensRegex.test(link.textContent);
};

试着将它们添加到你的 index.js 中的代码中,通过从 querySelectorAll 返回的 HTML 元素节点集合中创建一个数组,然后把过滤器函数应用到其中:

got(vgmUrl).then(response => {
  const dom = new JSDOM(response.body);

  // Create an Array out of the HTML Elements for filtering using spread syntax.
  const nodeList = [...dom.window.document.querySelectorAll('a')];

  nodeList.filter(isMidi).filter(noParens).forEach(link => {
    console.log(link.href);
  });
}).catch(err => {
  console.log(err);
});

再次运行代码,它仅应打印 .mid 文件,而不复制任何特定歌曲。

从网页下载我们想要的 MIDI 文件

现在我们有了遍历所需的每个 MIDI 文件的工作代码,必须编写代码来下载所有这些文件。

在用于遍历所有 MIDI 链接的回调函数中,添加以下代码以将 MIDI 下载流式传输到本地文件,并进行错误检查:

  nodeList.filter(isMidi).filter(noParens).forEach(link => {
    const fileName = link.href;
    got.stream(`${vgmUrl}/${fileName}`)
      .on('error', err => { console.log(err); console.log(`Error on ${vgmUrl}/${fileName}`) })
      .pipe(fs.createWriteStream(`MIDIs/${fileName}`))
      .on('error', err => { console.log(err); console.log(`Error on ${vgmUrl}/${fileName}`) })
      .on('finish', () => console.log(`Downloaded: ${fileName}`));
  });

从要保存 MIDI 文件的目录中运行代码,从终端屏幕上能够看到下载的所有 2230 个 MIDI 文件(在编写此代码时)。这样我们就完成所有需要的 MIDI 文件的抓取了。

Logging the results of the file downloads

现在可以仔细倾听并欣赏任天堂音乐了!

浩瀚的万维网

你可以通过编程的方式从网页上获取内容,无论你需要什么项目,都可以访问大量的数据源。要记住的一件事是,被更改过网页的 HTML 可能会破坏你的代码,所以如果你要在此基础上构建应用程序,请确保所有内容保持最新。

如果你正在寻找与刚刚从视频游戏音乐档案库中获取的数据有关的内容,则可以尝试使用 Python 库,例如 Magenta to train a neural network with it