NodeJS Puppeteer 从 XPath 获取子元素的内部文本

NodeJS Puppeteer Get InnerText of Child Elements from XPath

我有一个项目可以从内部 CRM 中抓取某些客户购买的产品。此 CRM 使用大量动态加载的磁贴,因此没有多少一致的 class 名称(许多在每次页面加载时随机附加一个 ID),并且页面上也有许多不同的 reports/elements相同的 class 名称,所以我无法在整个页面中查询元素选择器。

我已经通过 xpath 确定了我想要的 "parent" 元素。然后我想深入了解并仅获取与查询选择器匹配的子项的 innerText(我看到的大多数线程都有人在整个页面上执行查询选择器,这将从我不想要的菜单中获取结果)。

我可以在浏览器控制台中以常规 Javascript 执行此操作,但我不知道如何在 Node/Puppeteer 中执行此操作。这是我目前所拥有的:

//Getting xpath of the "box" that contains all of the product tiles that a customer has
const productsBox = await page.$x("/html/body/blah/blah/blah");

这就是它崩溃的地方。我不太熟悉某些语法或理解 Puppeteer 的文档,但我尝试了几种不同的方法(我也不太习惯使用 => 格式的函数。Puppeteer 文档有一个 example 我正在尝试做的事情,但我尝试使用相同的结构,但它也没有返回任何内容):

//Tried using the elementHandle.$$eval approach on the zero index of my xpath results, 
//but doesn't return anything when I console.log(productsList)
    const productsList = await productsBox[0].$$eval('.title-heading', function parseAndText (products) {
      productsList=[];
      for (i=0; i<products.length; i++) {
        productsList.push(products[i].innerText.trim());
      }
      return productsList;
      }
    );

//Tried doing the page.$$eval approach with selector, passing in the zero index of my xpath
      const productsList = await page.$$eval('.title-heading', function parseAndText (products) {
      productsList=[];
      for (i=0; i<products.length; i++) {
        productsList.push(products[i].innerText.trim());
      }
      return productsList;
      }, productsBox[0]

//Tried the page.evaluate and then page.evaluateHandle approach on the zero index of my xpath, 
//doing the query selection inside the evaluation and then doing something with that.
    let productsList= await page.evaluateHandle(function parseAndText(productsBoxZero) {
      productsInnerList = productsBoxZero.querySelectorAll(".title-heading");
      productsList=[];
      for (i=0; i<productsInnerList.length; i++) {
        productsList.push(productsInnerList[i].innerText.trim());
        //Threw a console log here to see if it does anything, 
        //But nothing is logged
        console.log("Pushed product " + i + " into the product list");
      }
      return productsList;
    }, productsBox[0]);

就输出而言,我在控制台记录了一些变量,我得到了这个:

productsBox is JSHandle@node
productsBox[0] is JSHandle@node
productList is

为了进行比较,我在控制台中通过 Javascript 并行执行此操作,以确保我正确地逐步执行逻辑并得到我期望的结果:

>productsBox=$x("/html/body/blah/blah/blah");
>productsInnerList=productsBox[0].querySelectorAll(".title-heading");
>productsInnerList.length;
//2, and this customer has 2 products
>productsList=[];
>for (i=0; i<productsInnerList.length; i++) {
    productsList.push(productsInnerList[i].innerText.trim());
};
>console.log(productsList)
>["Product 1", "Product 2"]

感谢您阅读到这里,感谢您的帮助!

[编辑]

对于一些额外的研究,我尝试使用 page.evaluateHandle 并尝试记录我的变量:

productsBox is JSHandle@node
productsBox[0] is JSHandle@node
productList is JSHandle@array

这是进步。我试着做: let productsText=await productsList.jsonValue();

但是当我尝试输出时我什么也没得到:

await console.log("productsText is " + productsText);

productsBox is JSHandle@node
productsBox[0] is JSHandle@node
productList is JSHandle@array
productsText is

我建议在尝试每个功能之前仔细阅读文档。 $$eval 在选择器上求值,在这种情况下传递元素是没有意义的。 evaluateHandle 用于返回页内元素,因为您要返回一个文本数组并且它是可序列化的,所以您不需要它。您只需将元素传递给 page.evaluate 或在 puppeteer 上下文中执行所有操作。

要能够看到页内 console.log,您需要:

page.on('console', msg => console.log(msg.text()));
  1. 使用page.evaluate
let productsList= await page.evaluate((element) => {
    const productsInnerList = element.querySelectorAll(".title-heading");
    const productsList=[];
    for (const el of productsInnerList) {
        productsList.push(el.innerText.trim());
        console.log("Pushed product " + el.innerText.trim() + " into the product list");
    }
    return productsList;
}, productsBox[0]);
  1. 使用elementHandle.$$
const productList = [];
const productsInnerList = await productsBox[0].$$('.title-heading');
for (const element of productsInnerList){
    const innerText = await (await element.getProperty('innerText')).jsonValue();
    productList.push(innerText);
}

根据 @mbit 的回答,我能够让它工作。我首先在另一个与我的结构相似的网站上进行了测试。将代码复制到我的原始站点,它仍然无法正常工作,只得到一个空输出。事实证明,虽然我有一个 await page.$x(full/xpath) 用于父元素,但包含 innerText 的子元素仍未加载。所以我做了两件事:

1) 添加了另一个等待页面。$x(full/xpath) 用于列表中作为我的目标之一的第一个元素 2) 实现了 mbit 提供的 page.evaluate 方法。 2a) 显式写出函数(仍然围绕着 => 结构)

下面的最终代码(一些变量名称因测试而改变):

let productsTextList= await page.evaluate(function list(list) {
  const productsInnerList = list.querySelectorAll(".title-heading");
  productsTextList =[];
  for (n=0; n<productsInnerList.length; n++) {
      product=productsInnerList[n].innerText.trim();
      productsTextList.push(product);
  }
  return productsTextList;
}, productsBox[0]);

console.log(productsTextList);

我选择 page.evaluate 方法是因为它更符合我在浏览器控制台中所做的事情,因此易于测试。正如 mbit 提到的,elementHandle.$$ 方法的技巧是使用 await element.getProperty('innerText') 而不是 .innerText。在整个故障排除和学习过程中,我还偶然发现了 this thread on GitHub,它也谈到了如何提取它(与上面 mbit 的方法相同)。对于 运行 遇到类似问题的任何人,您并不孤单!