Puppeteer 迭代 table 个单元格并单击特定单元格

Puppeteer iterate table cells and click specific cells

我希望在 Puppeteer 中遍历 table(日历 table)并单击特定的单元格(日期)以切换其状态(至“离开”)。

我在下面包含了 table 的一个片段。每个 td 单元格包含两个 child divs,一个带有日期编号 (<div class="day_num">),另一个带有标记为“AWAY”(<div class="day_content">)。

到目前为止,我已经能够抓取 table,但这不允许我单击实际的单元格,因为抓取只是将 table 内容抓取到一个数组中。

如何遍历所有单元格并根据 child "day_num" div 中包含的天数单击特定单元格?例如,我希望在下面的示例中单击第 8 天的 td,以切换它的状态。

<table class="calendar">
<tr class="days">

<td class="day">
<div class="day_num">7</div>
<div class="day_content"></div>
</td>
<td class="day">
<div class="day_num">8</div>
<div class="day_content"></div>
</td>
<td class="day">
<div class="day_num">9</div>
<div class="day_content">AWAY</div>
</td>

我目前的抓取代码是:

 const result = await page.evaluate(() => {
    const rows = document.querySelectorAll('.calendar tr td div');
    return Array.from(rows, (row) => {
      const columns = row.querySelectorAll('div');
      return Array.from(columns, (column) => column.innerHTML);
    });
  });

  console.log(result);

结果是:

[
  [],           [ '1', '' ],  [ '2', 'AWAY' ],
  [ '3', '' ],  [ '4', '' ],  [ '5', '' ],
  [ '6', '' ],  [ '7', '' ],  [ '8', '' ],
  [ '9', 'AWAY' ],  [ '10', '' ], [ '11', '' ],
  [ '12', '' ], [ '13', '' ], [ '14', '' ],
  [ '15', '' ], [ '16', '' ], [ '17', '' ],
  [ '18', '' ], [ '19', '' ], [ '20', '' ],
  [ '21', '' ], [ '22', '' ], [ '23', '' ],
  [ '24', '' ], [ '25', '' ], [ '26', '' ],
  [ '27', '' ], [ '28', '' ], [ '29', '' ],
  [ '30', '' ], [],           [],
  [],           []
]

虽然你没有提供实时页面(所以我无法验证任意 JS、可见性和时间不会导致此失败),但我会尝试一下,看看以下是否有效,假设您的 HTML 几乎是静态的:

const puppeteer = require("puppeteer"); // ^13.0.1

let browser;
(async () => {
  const html = `
    <body>
    <table class="calendar">
      <tr class="days">
        <td class="day">
          <div class="day_num">7</div>
          <div class="day_content"></div>
        </td>
        <td class="day">
          <div class="day_num">8</div>
          <div class="day_content"></div>
        </td>
        <td class="day">
          <div class="day_num">9</div>
          <div class="day_content">AWAY</div>
        </td>
      </tr>
    </table>
    <script>
      [...document.querySelectorAll(".day_content")][1]
        .addEventListener("click", e => {
          e.target.textContent = "CLICKED";
        })
      ;
    </script>
    </body>
  `;
  browser = await puppeteer.launch({headless: true});
  const [page] = await browser.pages();
  await page.setContent(html);
  const xp = '//div[contains(@class, "day_num") and text()="8"]';
  const [dayEl] = await page.$x(xp);
  const dayContent = await dayEl.evaluate(el => {
    const dayContent = el.closest(".day").querySelector(".day_content");
    dayContent.click();
    return dayContent.textContent;
  });
  console.log(dayContent); // => CLICKED
})()
  .catch(err => console.error(err))
  .finally(() => browser?.close())
;

方法是在 class 和文本上使用 XPath 找到您感兴趣的 .day_num 元素,然后弹出树到 .day 元素并向下再次转到关联的 .day_content 元素以单击它。我添加了一个侦听器以在单击时更改文本以验证它确实被单击了。

您也可以在 .day_num 上使用 nextElementSibling 而不是 closest/querySelector 组合,但这更多地假设了 [=12= 之间的关系] 和 .day_content 元素,并且可能会更脆。

此外,如果文本内容 "8" 可能有空格,您可以在 XPath 中使用 将其放宽一点。 '//div[contains(@class, "day_num") and contains(text(), "8")]',冒着误报和选择 "18""28" 的风险。在这种情况下,正则表达式或树遍历和 trim 可能更合适。断章取义 HTML 的这段摘录很难提出建议。


更进一步,听起来您需要在循环中单击多个元素并且正在努力做到这一点。这是一个适用于 mocked-up 版本网站的尝试:

const puppeteer = require("puppeteer"); // ^13.0.1

let browser;
(async () => {
  const html = `
    <body>
    <table class="calendar">
      <tr class="days"></tr>
    </table>
    <script>
      for (let i = 0; i < 30; i++) {
        document.querySelector(".days").innerHTML += 
          \`<td class="day">
            <div class="day_num">${i + 1}</div>
            <div class="day_content"></div>
          </td>\`
        ;
      }

      [...document.querySelectorAll(".day_content")].forEach(e =>
        e.addEventListener("click", e => {
          e.target.textContent = "AWAY";
        })
      );
    </script>
    </body>
  `;
  browser = await puppeteer.launch({headless: true});
  const [page] = await browser.pages();
  await page.setContent(html);
  const awayDatesInMonth = [5, 12, 18, 20];

  for (const day of awayDatesInMonth) {
    const xp = `//div[contains(@class, "day_num") and text()="${day}"]`;
    const [dayEl] = await page.$x(xp);
    const dayContent = await dayEl.evaluate(el =>
      el.closest(".day").querySelector(".day_content").click()
    );
  }

  /* or if you can assume the elements are correctly indexed */
  const days = await page.$$(".day_content");

  for (const day of awayDatesInMonth) {
    await days[day-1].evaluate(el => el.click());
  }
  /* --- */

  console.log(await page.content());
})()
  .catch(err => console.error(err))
  .finally(() => browser?.close())
;

如果这不起作用,请提供您自己的 mock-up,它能更好地代表您正在使用的原始站点,这样我就可以确定我正在解决相关问题。