我如何返回一个与它从网页抓取的数据相匹配的字符串数组?

前端开发 2026-07-09

我写了一个Letterboxd的网页爬虫,目标是抓取某一页上的所有评论。我在有头模式下确认它能成功点击每个“...more”标签以揭露页面上被压缩的评论,但在之后查看字符串表示时,离页面底部最近的展开标签总是以未展开的形式呈现。

这个评论的末尾不应该再有一个"...more":

我的实现中是不是出现了某种同步错误?我不知道该怎么做,才能确保在字符串转换时网页上的所有内容都被正确提取。我还附上了在点击它们前后的页面元素状态。

let revealsRemaining = await firstPage.locator('a.reveal').count();

while (revealsRemaining > 0) {
  let revealLink = firstPage.locator('a.reveal').first();
  await revealLink.click();
  revealsRemaining--;
}

const pageOneReviews = await firstPage.locator('.js-listitem .js-review .body-text')
.evaluateAll( els => 
  els.map(el => // This will already isolate each individual instance of the locator tag
    { 
    const nodeOfReview = el.querySelectorAll("p");     // Unpacks every p tag within each review into a NodeList
    let   concatReview = "";

    for (let i = 0; i < nodeOfReview.length; i++)
      {
      let paragraph = nodeOfReview.item(i).textContent; // Get the text content of each paragraph (every item is a DOM element
                                                       //  that can be treated as a standard tag in HTML)

      concatReview += paragraph;                    // Append onto concatReview
      }
    return concatReview;                         // Return the entire text content and then push it onto the reviews list
    }
  )
);

await context.close();
return { pageOneReviews };
展开前的评论正文状态:

展开后的评论正文状态:

解决方案

首先,重要的是把你正在爬取的实际站点分享出来,方便回答者复现问题并确保他们解决的是正确的问题。你截图中的文本经过检索得到了 https://letterboxd.com/itscharlibb/film/erupcja/2/,因此我猜那就是你正在爬取的页面*。

在网页抓取中的一个通用提示:不要想着像人类用户那样操作**。人类用户对编程一无所知,需要点击才能看到完整的评论。但你是程序员,可以直接查看页面源代码并提取信息。像在原始HTML响应中搜索隐藏文本,例如“去看看,它很特别。”,会发现数据在类名为 .js-review-body 的元素中是静态可用的。使用它来提取。

const playwright = require("playwright"); // ^1.58.0

let browser;
(async () => {
  browser = await playwright.chromium.launch({headless: false});
  const page = await browser.newPage();
  const url = "https://letterboxd.com/itscharlibb/film/erupcja/2/";
  await page.goto(url, {waitUntil: "domcontentloaded"});
  console.log(await page.locator(".js-review-body").textContent());
})()
  .catch(err => console.error(err))
  .finally(() => browser?.close());

由于不需要JavaScript和额外的网络资源,你可以完全屏蔽它们,这不仅减轻服务器负担,还能加速你的脚本:

let browser;
let context;
(async () => {
  browser = await playwright.chromium.launch({headless: false});
  context = await browser.newContext({javaScriptEnabled: false});
  const page = await context.newPage();
  const url = "https://letterboxd.com/itscharlibb/film/erupcja/2/";
  await page.route("**", route => {
    if (route.request().url().startsWith(url)) {
      route.continue();
    }
    else {
      route.abort();
    }
  });
  await page.goto(url, {waitUntil: "domcontentloaded"});
  console.log(await page.locator(".js-review-body").textContent());
})()
  .catch(err => console.error(err))
  .finally(async () => {
    await context?.close();
    await browser?.close();
  });

请注意,上述方法使用的是带界面的Playwright。这一点很重要,因为该站点有反机器人保护,因此会拒绝无头浏览器和纯获取请求。

有多种绕过方法,例如使用真实的Chrome浏览器,或者像cuimp-ts这样的浏览器伪装工具:

import * as cheerio from "cheerio"; // ^1.2.0
import {get} from "cuimp"; // ^1.10.0

const url = "https://letterboxd.com/itscharlibb/film/erupcja/2/";
const response = await get(url);
console.log(cheerio.load(response.data)(".js-review-body").text());

上述方法是完全无头的,且比带界面的浏览器更轻量。这在通过API或静态HTML直接找到数据后是一种常见的重构


*: 在写完这个回答后,我意识到你其实很可能在抓取列表页 https://letterboxd.com/film/erupcja/

将上述cuimp方案扩展以抓取所有评论也很直接。在评论列表页,提取每条评论详情的链接并逐个请求:

const url = "https://letterboxd.com/film/erupcja/";
const response = await get(url);
const $ = cheerio.load(response.data);
const reviews = [];

for (const el of [...$("[title^='Read']")]) {
  const base = "https://letterboxd.com/";
  const response = await get(base + $(el).attr("href"));
  reviews.push(cheerio.load(response.data)(".js-review-body").text().trim());
}

console.log(reviews);

或者如果你在抓取 https://letterboxd.com/film/erupcja/reviews/by/activity/,那么需要使用不同的选择器并遍历分页:

const url = "https://letterboxd.com/film/erupcja/reviews/by/activity/page/";
const reviews = [];
const maxPages = 5; // increase to 500 for a full run

for (let page = 1; page < maxPages; page++) {
  const response = await get(url + page);
  const $ = cheerio.load(response.data);
  const reviewLinks = [...$(".attribution-detail a")];

  if (reviewLinks.length === 0) {
    break;
  }

  for (const el of reviewLinks) {
    const base = "https://letterboxd.com/";
    const response = await get(base + $(el).attr("href"));
    reviews.push(cheerio.load(response.data)(".js-review-body").text().trim());
  }
}

console.log(reviews);
console.log(reviews.length);

同时边抓边把评论写入文件也是一个好主意,因为这是一次相当大规模的爬取(约250页,每页12条评论,总请求数超过3k次)。

你可以用队列来加速(pLimit是一个不错的库),但这已经非常快了,所以要么就保持现状,要么通过在请求之间加入一些睡眠来降低速度,并接受运行时间会比较久的现实。

如果你确实想坚持使用Playwright,请记住,点击“more”按钮并不是同步渲染。这会触发一个服务器请求,与Playwright读取文本的过程并发进行,因此你需要等待网络响应,或等待文本变化。

这有点麻烦,也很容易变得不稳定。除非在cuimp上遇到真正的阻塞,且增加睡眠/限流来尊重服务器的方法都无效时,我才会考虑使用Playwright。


*: 注意,如果你是在用Playwright测试你自己的应用,而不是在爬取数据,那么应遵循相反的原则:尽量让你的用户的行为像真实用户一样,这样你就能更有信心地确认你的应用按预期工作。

站内所有文章版权归属LeftHeroAI导航站,无授权禁止任何主体转载、抄袭、复制内容,亦不得私自架设镜像站点。一经侵权,本站将通过法律途径追责。

相关文章