
本文介绍如何利用 playwright 的 locator 机制与语义化 css 选择器,稳定、高效地批量提取漫画网站的全部章节标题与链接,避免硬编码脆弱选择器,并通过禁用 js 和资源拦截提升性能与可靠性。
本文介绍如何利用 playwright 的 locator 机制与语义化 css 选择器,稳定、高效地批量提取漫画网站的全部章节标题与链接,避免硬编码脆弱选择器,并通过禁用 js 和资源拦截提升性能与可靠性。
在 Web 爬虫开发中,针对漫画类网站(如 Battwo 或 Anime-Planet)批量抓取章节列表是一项常见需求。但许多初学者容易陷入“逐个元素手动定位”的误区——例如依赖 div:nth-child(3) > astro-island:nth-child(1) 这类由浏览器开发者工具自动生成、极易断裂的嵌套选择器。这类选择器一旦页面结构微调(如新增广告位、重构组件),整个脚本就会失效。
✅ 正确做法是:聚焦语义化、稳定且具有业务含义的选择器。观察目标页面源码可发现,章节容器通常带有明确标识属性,例如
[name="chapter-list"] [class="space-x-1"]
该选择器具备强语义性与低耦合性,远比深度嵌套的 > 和 nth-child() 更健壮。
以下是优化后的完整实现(兼容 Playwright v1.38+):
import { firefox } from "playwright";
(async () => {
const url = "https://battwo.com/title/75019-blue-lock";
const userAgent =
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/58.0.3029.110 Safari/537.36";
const browser = await firefox.launch();
const context = await browser.newContext({
userAgent,
javaScriptEnabled: false, // ✅ 关键:禁用 JS,加速加载并规避动态渲染干扰
});
// ? 拦截非目标请求,仅允许访问目标 URL(减少干扰、提升稳定性)
await context.route("**/*", (route) =>
route.request().url() === url ? route.continue() : route.abort()
);
const page = await context.newPage();
await page.goto(url, { waitUntil: "domcontentloaded" });
// ✅ 使用 locator + evaluateAll —— 推荐范式,比 $$/$$eval 更安全、更可读
const chapterSelector = '[name="chapter-list"] [class="space-x-1"]';
const chapters = await page.locator(chapterSelector).evaluateAll((elements) =>
elements.map((el) => {
const linkEl = el.querySelector("a");
const spanEl = el.querySelector("span");
// 注意:span 内容可能含换行或空格,childNodes[2] 是常见文本节点位置(需根据实际 DOM 调整)
const nameText = spanEl?.childNodes[2]?.textContent?.trim() || "";
return {
chapter: linkEl?.textContent?.trim() || "",
url: linkEl?.getAttribute("href") || "",
name: nameText,
};
})
);
console.log(`共获取 ${chapters.length} 章节:`);
console.log(chapters.slice(0, 5)); // 打印前 5 条示例
await context.close();
await browser.close();
})();
? 关键优化点说明:
- 禁用 JavaScript:javaScriptEnabled: false 可显著加快页面加载(跳过 JS 解析与执行),尤其适用于静态 HTML 渲染的漫画目录页;
- 精准资源路由拦截:通过 context.route() 阻止所有非目标请求,避免因第三方资源加载失败或超时导致 goto 异常;
- 优先使用 locator.evaluateAll():相比 $$eval,它更契合 Playwright 的现代 API 设计,支持自动重试、内置等待逻辑,且避免手动管理元素句柄(ElementHandle)带来的内存与生命周期风险;
- 避免 nth-child 和 > 链式选择器:它们对 DOM 结构变动极度敏感;应转向基于 id、name、class(如 space-x-1)、data-* 属性等语义化标识;
- 合理处理文本节点:span 中的章节名常位于 childNodes[2](因前后可能有换行符或空格节点),建议结合 textContent.trim() 并做空值防护;若结构复杂,可改用 Array.from(spanEl?.childNodes).find(n => n.nodeType === Node.TEXT_NODE)?.textContent?.trim() 提升鲁棒性。
? 延伸建议:
- 若目标站点存在反爬机制(如验证码、频率限制),应在 context 中配置代理或添加随机延迟(使用 node:timers/promises 的 setTimeout);
- 对于分页章节列表,可结合 page.locator("button:has-text('Next')") 实现自动翻页;
- 最终数据建议导出为 JSON 或 CSV,便于后续分析或前端展示。
遵循以上原则,即可构建出高稳定性、易维护的漫画章节爬取脚本——不再被页面微调“牵着鼻子走”,而是真正面向数据语义编程。











