如何让 Playwright 返回的字符串数组与网页展开后的评论内容一致?
每次点击后,都应等待对应评论展开完成,再提取文本。await revealLink.click() 会等待点击操作完成,但不保证点击触发的异步内容更新也已完成。
现有代码存在同步风险。由于缺少展开前后的实际 HTML,还需要结合页面结构,确定用什么状态判断评论已展开。
为什么最后一条评论仍带有 ...more
当前循环里的两行代码分别负责点击和计数:
await revealLink.click();
revealsRemaining--;
第一行完成点击后,第二行就把待处理数量减一。计数归零只说明程序执行了相应次数的点击,所有评论的完整正文未必都已写入 DOM。
接下来的 evaluateAll() 读取的是执行当时的 DOM,不会自动等待评论展开。最后一次点击后马上提取文本,最后一条评论就最容易出现这个问题。前面的评论则可能在后续点击过程中已经完成更新。
firstPage.locator('a.reveal').first() 每次操作时还会重新定位。如果上一个展开链接尚未消失,下一轮可能再次点击同一个链接。
点击后,等待同一条评论的展开状态
先检查实际 DOM,找出能表示展开完成的变化,例如:
- 完整正文替换摘要后,
a.reveal被移除或隐藏。 - 评论容器获得表示展开完成的 class 或属性。
- 完整正文容器出现,且已填入内容。
等待的状态必须能说明正文已经就绪。如果点击后链接立即隐藏,正文稍后才加载,仅等待链接隐藏仍然不够。
使用下面的代码,需要满足这些前提:
- 当前页面的评论列表已经加载完成,展开时评论数量和顺序保持不变。
- 每条评论最多有一个
a.reveal。 - 正文完成更新时,该链接被移除或隐藏。
.body-text对应需要提取的评论正文。
const reviews = firstPage.locator('.js-listitem .js-review');
const reviewCount = await reviews.count();
for (let i = 0; i < reviewCount; i++) {
const review = reviews.nth(i);
const reveal = review.locator('a.reveal');
if (await reveal.count() === 0) {
continue;
}
if (!(await reveal.isVisible())) {
continue;
}
await reveal.click();
// 前提:链接消失或隐藏,表示这一条评论的正文已经展开完成。
await reveal.waitFor({
state: 'hidden',
timeout: 10_000,
});
}
const pageOneReviews = (
await reviews.locator('.body-text').allInnerTexts()
).map(text => text.trim());
await context.close();
return { pageOneReviews };
展开链接限定在当前评论内,因此其他评论的链接消失不会影响这里的等待。
state: 'hidden' 会等待元素隐藏或从 DOM 中移除。如果确认链接一定会被移除,也可以使用 state: 'detached'。10_000 是等待上限,条件满足后就会继续执行,不会固定停顿十秒。
如果展开后链接仍然可见,只是变成了收起按钮,就需要等待实际的展开状态,不能套用这里的隐藏条件。
提取网页显示的文本时,注意 textContent
原代码使用:
paragraph.textContent
textContent 会读取节点中的文本,也包括隐藏后代节点的文本。如果页面同时保留折叠摘要和完整正文,只通过 CSS 切换显示状态,提取结果可能包含网页上看不到的摘要或 ...more。
如果希望结果接近网页显示的内容,可以使用示例中的 allInnerTexts(),它读取匹配元素的 innerText。这个方法同样不会自动等待内容加载完成,因此仍需保留前面的展开等待。
还要检查 .body-text 的范围。如果其中包含可见的收起按钮、评分或其他操作文字,这些文字也会被提取。此时应把选择器缩小到实际的正文容器。
如果继续按 <p> 提取,建议用 join('\n\n') 连接段落,避免直接拼接把两段文字连在一起。
排查时需要注意什么
不要通过删除字符串末尾的 ...more 修复结果。这样只能删掉提示文字,无法补回尚未读取的正文。
也不建议依赖固定的 waitForTimeout()。固定延时无法判断正文是否已经更新完成,等待明确的 DOM 状态更可靠。如果等待超时,应检查展开请求是否失败、选择器是否匹配,以及所等待的状态是否确实表示正文已就绪。
count() 读取的是调用当时的数量。如果页面还会懒加载更多评论,应先完成加载,再展开评论并提取文本。上面的代码只处理当前已加载的评论列表。
备注:内容仅供参考。
