简答:不要(重新)在 Node.js 中构建解析器,而是使用浏览器
如果您仍然使用 puppeteer 进行爬网,我强烈建议您不要在 Node.js 中评估或解析爬网数据。当您使用 puppeteer 时,您已经拥有了一个浏览器,其中包含用于在另一个进程中运行的 JavaScript 代码的出色沙箱。为什么要冒这种隔离风险并在 Node.js 脚本中“重建”解析器?如果您的 Node.js 脚本中断,您的整个脚本都会失败。在最坏的情况下,当您尝试在主线程中运行不受信任的代码时,您甚至可能会使您的机器面临严重的风险。
相反,请尝试在页面上下文中进行尽可能多的解析。你甚至可以在那里打一个 evil eval 电话。有可能发生最坏的情况吗?您的浏览器挂起或崩溃。
示例
想象一下下面的 HTML 页面(非常简化)。您正在尝试读取推入数组的文本。您拥有的唯一信息是有一个附加属性id 设置为target-data。
<html>
<body>
<!--- ... -->
<script>
var arr = [];
// some complex code...
arr.push({
id: 'not-interesting-data',
data: 'some data you do not want to crawl',
});
// more complex code here...
arr.push({
id: 'target-data',
data: 'THIS IS THE DATA YOU WANT TO CRAWL', // <---- You want to get this text
});
// more code...
arr.push({
id: 'some-irrelevant-data',
data: 'again, you do not want to crawl this',
});
</script>
<!--- ... -->
</body>
</html>
错误代码
这是一个简单的示例,您的代码现在可能是什么样子:
await page.goto('http://...');
const crawledJsCode = await page.evaluate(() => document.querySelector('script').innerHTML);
在此示例中,脚本从页面中提取 JavaScript 代码。现在我们有了页面中的 JavaScript 代码,我们“只”需要解析它,对吧?好吧,这是错误的方法。不要尝试在 Node.js 中重建解析器。只需使用浏览器。在你的情况下,基本上有两种方法可以做到这一点。
- 在页面中注入代理函数,伪造一些内置函数(推荐)
- 使用
JSON.parse、正则表达式或 eval 解析客户端 (!) 上的数据(仅在确实需要时进行评估)
选项1:将代理函数注入页面
在这种方法中,您正在用自己的“假函数”替换本机浏览器函数。示例:
const originalPush = Array.prototype.push;
Array.prototype.push = function (item) {
if (item && item.id === 'target-data') {
const data = item.data; // This is the data we are trying to crawl
window.exposedDataFoundFunction(data); // send this data back to Node.js
}
originalPush.apply(this, arguments);
}
这段代码用我们自己的函数替换了原来的Array.prototype.push函数。一切正常,但是当将具有我们目标 id 的项目推入数组时,会触发特殊条件。要将此功能注入页面,您可以使用page.evaluateOnNewDocument。要从 Node.js 接收数据,您必须通过 page.exposeFunction 向浏览器公开一个函数:
// called via window.dataFound from within the fake Array.prototype.push function
await page.exposeFunction('exposedDataFoundFunction', data => {
// handle the data in Node.js
});
现在,页面代码的复杂程度并不重要,它是发生在某个异步处理程序中还是页面是否更改了周围的代码。只要目标数据将数据推入数组中,我们就会得到它。
您可以使用这种方法进行大量抓取。检查数据的处理方式,并将处理数据的低级函数替换为您自己的代理版本。
选项 2:解析数据
让我们假设第一种方法由于某种原因不起作用。数据在某个脚本标签中,但您无法通过伪函数获取。
然后您应该解析数据,但不是在您的 Node.js 环境中。在页面上下文中执行此操作。您可以运行正则表达式或使用JSON.parse。但是在将数据返回到 Node.js 之前执行此操作。这种方法的好处是,如果您的代码由于某种原因使您的环境崩溃,那么崩溃的不是您的主脚本,而是只是您的浏览器。
给出一些示例代码。我们不再运行原始“错误代码”示例中的代码,而是将其更改为:
const crawledJsCode = await page.evaluate(() => {
const code = document.querySelector('script').innerHTML; // instead of returning this
const match = code.match(/some tricky regex which extracts the data you want/); // we run our regex in the browser
return match; // and only return the results
});
这只会返回我们需要的部分代码,然后可以在 Node.js 中进一步处理。
无论您选择哪种方法,这两种方法都比在主线程中运行未知代码更好、更安全。如果您绝对必须在 Node.js 环境中处理数据,请使用正则表达式,如 trincot 的答案所示。您应该从不使用 eval 运行不受信任的代码。