如何在 JavaScript 代码中获取 JavaScript 对象?

How to get JavaScript object in JavaScript code?

TL;DR

我想要 parseParameter 像下面的代码那样解析 JSON。 someCrawledJSCode 被抓取 JavaScript 代码。

const data = parseParameter(someCrawledJSCode);
console.log(data);  // data1: {...}

问题

我正在使用 puppeteer 抓取一些 JavaScript 代码,我想从中提取一个 JSON 对象,但我不知道如何解析给定的 JavaScript 代码.

已抓取 JavaScript 代码示例:

const somecode = 'somevalue';
arr.push({
  data1: {
    prices: [{
      prop1: 'hi',
      prop2: 'hello',
    },
    {
      prop1: 'foo',
      prop2: 'bar',
    }]
  }
});

在这段代码中,我想要获取 prices 数组(或 data1)。

我做了什么

我尝试将代码解析为 JSON,但它不起作用。于是搜索了解析工具,得到了Esprima。但是我觉得对解决这个问题没有帮助。

抓取会很丑陋。对您要解析的字符串做出一些假设,您可以:

  1. 提取推入数组的部分
  2. 将该字符串转换为有效的 JSON:

    • 用双引号替换字符串文字的定界单引号;
    • 用双引号将未加引号的 属性 名称括起来;
    • 删除最后一个 属性
    • 后的尾随逗号

要可靠地做到这一点,您必须编写一个与 JSON 解析器一样复杂的解析器,但根据一些假设,它可能可以简化为:

// Sample data
var someCrawledJSCode = `
const somecode = 'somevalue';
arr.push({
  data1: {
    prices: [{
      prop1: 'hi',
      prop2: 'hello',
    },
    {
      prop1: 'foo',
      prop2: 'bar',
    }]
  }
});`;


var obj;
var notJson = someCrawledJSCode.replace(/\.push\(([^]*?)\)/, (_, notJson) => {
    // Try to turn the string into valid JSON:
    // 1. string literals should not be enclosed in single, but double quotes
    // 2. property names should be enclosed in double quotes
    // 3. there should be no trailing comma after the last property
    var json = notJson.replace(/'((\.|[^\'])*)'/g, '""')
                      .replace(/(\w+):/g, '"":')
                      .replace(/,\s*}/g, "}");
    obj = JSON.parse(json);
});
console.log(obj);

事情仍然可能出错,但至少你没有使用 eval。例如,如果您有一个字符串文字,其内容匹配 (\w+):,那么上面的内容将改变该字符串。当然可以使解析更可靠...

简短回答:不要(重新)在 Node.js 中构建解析器,而是使用浏览器

我强烈建议不要评估或解析 Node.js 中的已爬网数据,如果您无论如何都在使用 puppeteer 进行爬网。当您使用 puppeteer 时,您已经有一个 浏览器,它带有一个很棒的沙箱,用于 JavaScript 代码 运行 在另一个进程 中运行。为什么要在 Node.js 脚本中冒这种隔离和 "rebuild" 解析器的风险?如果您的 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 代码,我们 "only" 需要解析它,对吗?好吧,这是错误的方法。不要试图在 Node.js 中重建解析器。只需使用浏览器。根据您的情况,基本上可以采用两种方法来做到这一点。

  1. 在页面中注入代理函数并伪造一些内置函数(推荐)
  2. 使用 JSON.parse、正则表达式或 eval 在客户端 (!) 解析数据(仅在确实需要时才求值)

选项 1:将代理函数注入页面

在这种方法中,您将用自己的 "fake functions" 替换本机浏览器功能。示例:

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. To receive the data from Node.js you would have to expose a function to the browser via 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
});

现在,页面代码的复杂程度、它是否发生在某个异步处理程序内部或页面是否更改了周围的代码都不再重要了。只要target data是把数据push到一个数组中,我们就get到了。

您可以使用这种方法进行大量抓取。检查数据的处理方式,并将处理数据的低级函数替换为您自己的代理版本。

选项 2:解析数据

让我们假设第一种方法由于某种原因不起作用。数据在一些脚本标签中,但你无法通过使用假函数来获取它。

然后你应该解析数据,但不在你的 Node.js 环境中。在页面上下文中执行此操作。您可以 运行 正则表达式或使用 JSON.parse。但是 在 return 将数据返回 到 Node.js 之前执行此操作。这种方法的好处是,如果您的代码由于某种原因使您的环境崩溃,崩溃的将不是您的主脚本,而是只是您的浏览器。

给出一些示例代码。我们不再使用 运行 原始 "bad code" 示例中的代码,而是将其更改为:

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
});

这只会 return 我们需要的代码部分,然后可以从 Node.js.

中进一步处理

无论您选择哪种方法,这两种方法都比 运行在您的主线程中使用未知代码更好、更安全。如果您绝对必须在 Node.js 环境中处理数据,请使用正则表达式,如 trincot 的答案所示。你应该永远不要对运行不受信任的代码使用eval。

我认为使用 Esprima 等 AST 生成器或其他 AST 工具是阅读和使用源代码的最简单方法。

老实说,如果您了解如何 运行 Esprima,并从源代码生成 "Abstract Syntax Tree",您会发现阅读表示您刚刚解析的代码,您会发现阅读信息并将其转换成您想要的任何内容出奇地容易。

乍一看似乎令人生畏,但老实说,事实并非如此。您会感到惊讶:像 Esprima 这样的 AST 工具正是出于与您正在尝试做的类似的目的而制作的,目的是让工作变得简单。

AST 工具诞生于对如何阅读和操作源代码的多年研究,因此我强烈推荐它们。

试试吧!

为了帮助您了解各种 AST 的外观,您可以查看 https://astexplorer.net。它对于了解来自各种工具的 AST 树结构是什么样子非常有用。

哦,最后一件事!为了遍历 AST 树,你可以使用类似 https://github.com/estools/estraverse 的东西。它会让生活变得轻松。