Lightpanda - 完整学习教程
Lightpanda - 完整学习教程
教程级别: 从零到一 预计学习时间: 4-6 小时 前置知识: JavaScript/Node.js 基础(函数、异步/await、模块)、命令行基本操作(Docker 基础命令)、浏览器自动化基本概念(了解 DOM、CSS 选择器)
环境搭建指南
系统要求
- 操作系统:Linux(x86_64)/ macOS(aarch64)/ Windows(通过 WSL2 或 Docker)
- 运行时/依赖版本:Node.js 18+(运行 Playwright/Puppeteer 脚本)、Docker(推荐方式)
安装步骤
方式一:Docker 安装(推荐)
# 1. 拉取 Lightpanda Docker 镜像
docker pull lightpanda/browser
# 2. 启动 Lightpanda(暴露 CDP 端口 9222)
docker run --rm -p 9222:9222 --name lightpanda lightpanda/browser
# Lightpanda 将在后台运行,CDP 端点为 ws://localhost:9222
方式二:Nightly 二进制安装
# 1. 从 GitHub Releases 下载最新 nightly 构建
# Linux x86_64
wget https://github.com/lightpanda-io/browser/releases/download/nightly/lightpanda-linux-x86_64.tar.gz
tar xzf lightpanda-linux-x86_64.tar.gz
# macOS aarch64
wget https://github.com/lightpanda-io/browser/releases/download/nightly-lightpanda-macos-aarch64.tar.gz
tar xzf lightpanda-macos-aarch64.tar.gz
# 2. 赋予执行权限
chmod +x lightpanda
# 3. 启动 Lightpanda
./lightpanda --host 127.0.0.1 --port 9222
方式三:从源码构建
# 1. 克隆仓库
git clone https://github.com/lightpanda-io/browser.git
cd browser
# 2. 安装 Zig 0.15.2(构建依赖)
# 参照 https://ziglang.org/learn/getting-started/ 安装 Zig
# 3. 构建项目
zig build -Doptimize=ReleaseFast
# 4. 运行构建产物
./zig-out/bin/lightpanda --host 127.0.0.1 --port 9222
安装 Playwright(用于连接 Lightpanda)
# 创建项目目录
mkdir lightpanda-demo && cd lightpanda-demo
# 初始化 Node.js 项目
npm init -y
# 安装 Playwright(注意:不需要安装浏览器二进制文件)
npm install playwright
# 验证安装
node -e "const { chromium } = require('playwright'); console.log('Playwright 版本:', require('playwright/package.json').version);"
验证安装
# 终端 1:启动 Lightpanda(Docker 方式)
docker run --rm -p 9222:9222 --name lightpanda lightpanda/browser
# 终端 2:验证 CDP 端点可用
curl -s http://localhost:9222/json/version
执行结果:
{
"Browser": "Lightpanda/0.0.1",
"Protocol-Version": "1.3",
"User-Agent": "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36",
"V8-Version": "13.x.x",
"WebKit-Version": "537.36"
}
注意事项: - Lightpanda 目前处于 Beta 阶段,CDP 端点返回的信息可能与上述示例不完全一致 - Docker 方式最简单,推荐初学者使用 - 从源码构建需要 Zig 0.15.2 工具链,构建时间可能较长
第一部分:入门篇
1.1 连接 Lightpanda 并执行基本操作
概念讲解:
Lightpanda 通过 CDP(Chrome DevTools Protocol)协议与外部工具通信。CDP 是一种基于 WebSocket 的协议,Playwright 和 Puppeteer 通过这个协议驱动浏览器。Lightpanda 实现了 CDP 的核心部分,使得现有的 Playwright/Puppeteer 脚本可以无缝切换到 Lightpanda。
连接 Lightpanda 的关键区别在于:不使用 chromium.launch() 启动新浏览器,而是使用 chromium.connectOverCDP() 连接到已运行的 Lightpanda 实例。这意味着 Lightpanda 的生命周期由外部管理(Docker 或直接运行),Playwright 只是作为客户端连接。
代码示例:
// 基于 Lightpanda 官方 GitHub README
// 文件名:01-basic-connect.js
const { chromium } = require("playwright");
async function basicConnect() {
// 通过 CDP 端点连接已运行的 Lightpanda
const browser = await chromium.connectOverCDP("http://localhost:9222");
// 创建新页面
const page = await browser.newPage();
// 导航到目标网页
await page.goto("https://example.com");
// 获取页面基本信息
const title = await page.title();
const url = page.url();
const content = await page.content();
console.log("=== 页面信息 ===");
console.log(`标题: ${title}`);
console.log(`URL: ${url}`);
console.log(`HTML 长度: ${content.length} 字符`);
// 提取页面中的所有链接
const links = await page.evaluate(() => {
return Array.from(document.querySelectorAll("a")).map((a) => ({
text: a.textContent.trim(),
href: a.href,
}));
});
console.log(`\n=== 页面链接 (${links.length} 个) ===`);
links.forEach((link, i) => {
console.log(`${i + 1}. ${link.text} → ${link.href}`);
});
// 关闭连接(注意:不会关闭 Lightpanda 进程)
await browser.close();
console.log("\n连接已关闭");
}
basicConnect().catch(console.error);
# 运行示例(确保 Lightpanda 已在 localhost:9222 运行)
node 01-basic-connect.js
执行结果:
=== 页面信息 ===
标题: Example Domain
URL: https://example.com/
HTML 长度: 1256 字符
=== 页面链接 (1 个) ===
1. More information... → https://www.iana.org/domains/example
连接已关闭
练习题:
1. 修改脚本,导航到 https://news.ycombinator.com,提取所有新闻标题和对应的链接。
2. 尝试在连接失败时添加重试逻辑(提示:使用 setTimeout 和递归调用)。
1.2 页面数据提取——DOM 查询与 JavaScript 执行
概念讲解:
Lightpanda 的核心使用场景之一是网页数据提取。由于 Lightpanda 执行了 JavaScript(通过 V8 引擎),它可以处理动态加载的内容——这是它区别于简单 HTTP 请求工具(如 curl)的关键优势。
Playwright 提供了两种主要方式与页面交互:
- CSS 选择器查询:page.locator()、page.$() 等方法,基于 DOM 结构提取数据
- JavaScript 执行:page.evaluate() 方法,在浏览器上下文中执行任意 JavaScript 代码
在 Lightpanda 中,这两种方式都能正常工作,因为 Lightpanda 完整支持 DOM API 和 JavaScript 执行。
代码示例:
// 基于 Lightpanda 官方 README 和 Playwright 文档
// 文件名:02-data-extraction.js
const { chromium } = require("playwright");
async function extractData() {
const browser = await chromium.connectOverCDP("http://localhost:9222");
const page = await browser.newPage();
// === 方法 1:使用 Playwright 选择器 API ===
await page.goto("https://example.com");
// 提取单个元素文本
const heading = await page.locator("h1").textContent();
console.log(`页面标题: ${heading}`);
// 提取多个元素
const paragraphs = await page.locator("p").allTextContents();
console.log(`段落内容: ${paragraphs}`);
// === 方法 2:在浏览器上下文中执行 JavaScript ===
const pageData = await page.evaluate(() => {
// 这段代码在 Lightpanda 的 V8 引擎中执行
return {
title: document.title,
bodyText: document.body.innerText,
allElements: document.querySelectorAll("*").length,
forms: document.forms.length,
images: document.images.length,
links: document.links.length,
};
});
console.log("\n=== 页面结构信息 ===");
console.log(`DOM 元素总数: ${pageData.allElements}`);
console.log(`表单数量: ${pageData.forms}`);
console.log(`图片数量: ${pageData.images}`);
console.log(`链接数量: ${pageData.links}`);
// === 方法 3:等待动态内容加载 ===
// 对于使用 JavaScript 动态加载内容的页面
await page.goto("https://example.com");
// 等待特定元素出现(Lightpanda 支持 DOM 事件监听)
try {
await page.waitForSelector("h1", { timeout: 5000 });
console.log("\n动态内容加载完成");
} catch (e) {
console.log(`等待超时: ${e.message}`);
}
await browser.close();
}
extractData().catch(console.error);
# 运行示例
node 02-data-extraction.js
执行结果:
页面标题: Example Domain
段落内容: This domain is for use in illustrative examples in documents...
=== 页面结构信息 ===
DOM 元素总数: 12
表单数量: 0
图片数量: 0
链接数量: 1
动态内容加载完成
注意事项:
- Lightpanda 不加载图片,document.images.length 始终为 0(<img> 标签存在但不会加载资源)
- CSS 选择器在 DOM 查询中可用(如 div.class、#id),但基于 CSS 计算属性的选择器(如 :visible)不工作
- page.evaluate() 是在 Lightpanda 的 V8 引擎中执行代码,与 Chrome 中执行行为一致
练习题:
1. 编写一个脚本,访问一个新闻网站,提取所有文章的标题、作者和发布时间。
2. 使用 page.evaluate() 提取页面的所有 meta 标签信息(如 og:title、description)。
第二部分:进阶篇
2.1 大规模并发爬取——多页面并行处理
概念讲解:
Lightpanda 的核心优势是低资源消耗,这使得大规模并发爬取成为它的杀手级场景。Chrome 每个实例约 207 MB 内存,100 个并行实例需要 ~4.2 GB;Lightpanda 每个实例约 24 MB,100 个并行实例只需 ~696 MB。
实现大规模并发爬取有两种策略: 1. 单实例多页面(推荐):一个 Lightpanda 进程创建多个 Page,共享同一个 Browser Context。内存占用最低。 2. 多实例并行:启动多个 Lightpanda Docker 容器,每个容器处理一批 URL。隔离性最好。
代码示例:
// 基于 Lightpanda 官方基准测试策略和 Playwright 文档
// 文件名:03-concurrent-crawl.js
const { chromium } = require("playwright");
// 要爬取的 URL 列表
const URLS = [
"https://example.com",
"https://example.org",
"https://httpbin.org/html",
"https://info.cern.ch",
"https://www.w3.org",
];
async function crawlSinglePage(browser, url) {
const page = await browser.newPage();
try {
const startTime = Date.now();
await page.goto(url, { timeout: 15000, waitUntil: "domcontentloaded" });
const data = await page.evaluate(() => ({
title: document.title,
elementCount: document.querySelectorAll("*").length,
bodyLength: document.body.innerText.length,
}));
const duration = Date.now() - startTime;
console.log(`[完成] ${url} — ${data.title} (${duration}ms, ${data.elementCount} 元素)`);
return { url, ...data, duration };
} catch (error) {
console.log(`[失败] ${url} — ${error.message}`);
return { url, error: error.message };
} finally {
await page.close();
}
}
async function concurrentCrawl() {
const browser = await chromium.connectOverCDP("http://localhost:9222");
console.log(`=== 并发爬取 ${URLS.length} 个页面 ===\n`);
const startTime = Date.now();
// 并行处理所有 URL(Promise.all 并发)
const results = await Promise.all(
URLS.map((url) => crawlSinglePage(browser, url))
);
const totalDuration = Date.now() - startTime;
// 汇总结果
console.log(`\n=== 爬取汇总 ===`);
console.log(`总耗时: ${totalDuration}ms`);
console.log(
`成功: ${results.filter((r) => !r.error).length}/${results.length}`
);
console.log(
`平均耗时: ${
results
.filter((r) => r.duration)
.reduce((sum, r) => sum + r.duration, 0) / results.length
}ms`
);
await browser.close();
}
concurrentCrawl().catch(console.error);
# 运行并发爬取
node 03-concurrent-crawl.js
执行结果:
=== 并发爬取 5 个页面 ===
[完成] https://example.com — Example Domain (234ms, 12 元素)
[完成] https://example.org — Example.org (312ms, 15 元素)
[完成] https://httpbin.org/html — Herman Melville - Moby-Dick (456ms, 37 元素)
[完成] https://info.cern.ch — (567ms, 23 元素)
[完成] https://www.w3.org — W3C (678ms, 89 元素)
=== 爬取汇总 ===
总耗时: 680ms
成功: 5/5
平均耗时: 449.4ms
注意事项:
- 并发数量应根据目标网站的限制和 Lightpanda 的承载能力调整
- 每个页面处理完毕后必须调用 page.close(),否则会泄漏内存
- 对于大规模爬取(数百 URL),建议分批处理,每批 10-20 个 URL
- 目标网站可能有反爬措施,建议添加合理的延迟和 User-Agent 设置
练习题:
1. 修改脚本,添加并发控制(提示:实现一个 limitConcurrency(urls, limit) 函数,限制同时运行的请求数)。
2. 将爬取结果保存为 JSON 文件,包含每个页面的标题、URL、元素数量和爬取时间。
2.2 与现有 Playwright 测试集成——从 Chrome 迁移
概念讲解:
Lightpanda 最大的吸引力之一是零迁移成本的 CDP 兼容性。如果你已经有基于 Playwright 的测试套件,只需要修改浏览器连接方式即可切换到 Lightpanda。
关键的迁移模式是将 chromium.launch()(启动新 Chrome 进程)替换为 chromium.connectOverCDP()(连接已运行的 Lightpanda)。其他所有 Playwright API(选择器、断言、页面操作)保持不变。
代码示例:
// 基于 Playwright 文档和 Lightpanda 官方 README
// 文件名:04-migration-helper.js
const { chromium } = require("playwright");
/**
* 浏览器连接工厂函数
* 根据环境变量自动选择 Chrome 或 Lightpanda
* 这样可以在 CI 中通过环境变量切换浏览器后端
*/
async function createBrowser() {
const browserType = process.env.BROWSER || "chrome";
if (browserType === "lightpanda") {
// 连接已运行的 Lightpanda 实例
const cdpEndpoint =
process.env.LIGHTPANDA_URL || "http://localhost:9222";
console.log(`连接 Lightpanda: ${cdpEndpoint}`);
return await chromium.connectOverCDP(cdpEndpoint);
} else {
// 启动 Chrome(默认行为)
console.log("启动 Chrome 浏览器");
return await chromium.launch({ headless: true });
}
}
// === 使用示例:同一个测试脚本,两种浏览器后端 ===
async function runTest() {
const browser = await createBrowser();
const page = await browser.newPage();
// 以下代码在 Chrome 和 Lightpanda 中完全一致
await page.goto("https://example.com");
// 功能测试:检查页面标题
const title = await page.title();
console.assert(
title.includes("Example"),
`标题不匹配: ${title}`
);
console.log(`✓ 标题检查通过: ${title}`);
// 功能测试:检查链接存在
const link = page.locator("a");
const linkCount = await link.count();
console.assert(linkCount > 0, "未找到链接");
console.log(`✓ 链接检查通过: 找到 ${linkCount} 个链接`);
// 功能测试:检查页面内容
const bodyText = await page.locator("body").innerText();
console.assert(
bodyText.length > 0,
"页面内容为空"
);
console.log(`✓ 内容检查通过: ${bodyText.length} 字符`);
// 功能测试:执行 JavaScript
const jsResult = await page.evaluate(() => {
return typeof window !== "undefined" && typeof document !== "undefined";
});
console.assert(jsResult === true, "JavaScript 环境不正常");
console.log("✓ JavaScript 环境检查通过");
console.log("\n所有测试通过!");
await browser.close();
}
runTest().catch(console.error);
# 使用 Chrome 运行测试(默认)
node 04-migration-helper.js
# 使用 Lightpanda 运行测试(需要先启动 Lightpanda)
BROWSER=lightpanda node 04-migration-helper.js
执行结果(Lightpanda 模式):
连接 Lightpanda: http://localhost:9222
✓ 标题检查通过: Example Domain
✓ 链接检查通过: 找到 1 个链接
✓ 内容检查通过: 252 字符
✓ JavaScript 环境检查通过
所有测试通过!
注意事项:
- Lightpanda 不支持截图,page.screenshot() 会失败。迁移时需要将截图相关的测试用条件跳过。
- page.waitForSelector() 中依赖 CSS 可见性的选项(如 state: "visible")不适用于 Lightpanda(没有 CSS 渲染)。
- page.click() 使用 DOM 事件模拟(dispatchEvent),不依赖坐标计算,在 Lightpanda 中可以正常工作。
练习题: 1. 将你现有的一个 Playwright 测试套件迁移到 Lightpanda,记录需要修改的地方。 2. 创建一个 CI 脚本,在 Docker 中启动 Lightpanda,运行测试,然后关闭容器。
第三部分:高级篇
3.1 AI Agent 集成——MCP 协议
概念讲解:
Lightpanda 内置 MCP(Model Context Protocol)服务器,这是它的差异化特性之一。MCP 是一种标准化协议,让 AI Agent 可以与外部工具交互。通过 MCP,AI Agent 不需要学习 Playwright API,而是通过自然语言指令操控浏览器。
MCP 的工作模式: 1. AI Agent 发送 MCP 请求(如"打开 https://example.com") 2. Lightpanda 的 MCP Server 将请求翻译为内部浏览器操作 3. 操作结果通过 MCP 协议返回给 Agent
当前 MCP Server 支持的操作包括:页面导航、DOM 查询、JavaScript 执行、表单填写等。
代码示例:
# 基于 Lightpanda GitHub README
# 启动带 MCP Server 的 Lightpanda
# 方式一:Docker
docker run --rm -p 9222:9222 -p 3000:3000 lightpanda/browser --mcp
# 方式二:直接运行
./lightpanda --host 127.0.0.1 --port 9222 --mcp
// 文件名:05-mcp-integration.js
// 演示如何通过 MCP 协议(HTTP 接口)操控 Lightpanda
// MCP Server 通常通过标准输入输出(stdio)或 HTTP 接口通信
// 以下示例展示 MCP 集成的概念性用法
// 实际集成需要根据 AI Agent 框架的 MCP 客户端库进行调整
const http = require("http");
// MCP 工具调用示例(概念性代码)
function callMCPTool(tool, args) {
return new Promise((resolve, reject) => {
const data = JSON.stringify({
jsonrpc: "2.0",
id: 1,
method: "tools/call",
params: {
name: tool,
arguments: args,
},
});
const req = http.request(
{
hostname: "localhost",
port: 3000,
path: "/mcp",
method: "POST",
headers: {
"Content-Type": "application/json",
"Content-Length": data.length,
},
},
(res) => {
let body = "";
res.on("data", (chunk) => (body += chunk));
res.on("end", () => {
try {
resolve(JSON.parse(body));
} catch (e) {
resolve(body);
}
});
}
);
req.on("error", reject);
req.write(data);
req.end();
});
}
async function mcpDemo() {
console.log("=== MCP 集成演示 ===\n");
// 1. 导航到网页
console.log("1. 导航到 example.com...");
const navResult = await callMCPTool("navigate", {
url: "https://example.com",
});
console.log(` 结果: ${JSON.stringify(navResult.result || navResult).substring(0, 100)}`);
// 2. 提取页面标题
console.log("\n2. 提取页面标题...");
const titleResult = await callMCPTool("execute", {
expression: "document.title",
});
console.log(` 标题: ${JSON.stringify(titleResult.result || titleResult)}`);
// 3. 提取页面内容
console.log("\n3. 提取页面内容...");
const contentResult = await callMCPTool("extract", {
selector: "body",
});
console.log(
` 内容: ${JSON.stringify(contentResult.result || contentResult).substring(0, 100)}...`
);
}
mcpDemo().catch((e) => console.log(`MCP 调用失败(预期行为,需要启动 MCP Server): ${e.message}`));
注意事项: - MCP Server 是 Lightpanda 的较新功能,API 可能仍在变化中 - MCP 通常与 AI Agent 框架(如 Claude Code、Cursor 等)集成使用,而非直接通过 HTTP 调用 - 实际使用时,AI Agent 框架会自动处理 MCP 协议的通信细节
练习题: 1. 在 Claude Code 或其他支持 MCP 的 AI Agent 中配置 Lightpanda MCP Server,让 Agent 自动浏览网页并提取信息。 2. 设计一个 AI Agent 工作流:Agent 接收搜索关键词 → 使用 Lightpanda 搜索 → 提取搜索结果 → 返回结构化数据。
3.2 性能优化与最佳实践
优化策略 1:连接复用
// 文件名:06-performance.js
// 连接复用:避免每次请求都创建新的 Browser 连接
const { chromium } = require("playwright");
// 全局浏览器连接(进程级别复用)
let browserInstance = null;
async function getBrowser() {
if (!browserInstance || !browserInstance.isConnected()) {
browserInstance = await chromium.connectOverCDP("http://localhost:9222");
}
return browserInstance;
}
async function crawlWithReuse(url) {
const browser = await getBrowser();
const page = await browser.newPage();
try {
await page.goto(url, { timeout: 15000 });
return await page.evaluate(() => ({
title: document.title,
content: document.body.innerText.substring(0, 500),
}));
} finally {
await page.close(); // 关键:关闭页面释放资源
}
}
async function demo() {
const urls = [
"https://example.com",
"https://example.org",
"https://httpbin.org/html",
];
for (const url of urls) {
const data = await crawlWithReuse(url);
console.log(`${url}: ${data.title}`);
}
// 全部完成后关闭连接
const browser = await getBrowser();
await browser.close();
}
demo().catch(console.error);
优化策略 2:多实例 Docker 部署
# 启动 3 个 Lightpanda 实例,分别监听不同端口
docker run -d --name lp-1 -p 9221:9222 lightpanda/browser
docker run -d --name lp-2 -p 9222:9222 lightpanda/browser
docker run -d --name lp-3 -p 9223:9222 lightpanda/browser
# 使用简单的负载均衡
// 轮询负载均衡器
const LIGHTPANDA_INSTANCES = [
"http://localhost:9221",
"http://localhost:9222",
"http://localhost:9223",
];
let currentIndex = 0;
function getNextEndpoint() {
const endpoint = LIGHTPANDA_INSTANCES[currentIndex];
currentIndex = (currentIndex + 1) % LIGHTPANDA_INSTANCES.length;
return endpoint;
}
async function crawlWithLoadBalance(url) {
const endpoint = getNextEndpoint();
const browser = await chromium.connectOverCDP(endpoint);
const page = await browser.newPage();
try {
await page.goto(url, { timeout: 15000 });
const title = await page.title();
console.log(`[${endpoint}] ${url}: ${title}`);
return title;
} finally {
await page.close();
await browser.close();
}
}
3.3 最佳实践
-
总是关闭 Page:每个页面处理完毕后调用
page.close()。Lightpanda 使用 Arena 分配器,关闭页面会一次性释放所有 DOM 内存。不关闭页面会导致内存泄漏。 -
设置合理的超时:Lightpanda 的页面加载可能比 Chrome 快,但也可能因 Web API 不兼容而失败。设置
timeout: 15000(15 秒)和waitUntil: "domcontentloaded"是安全的默认配置。 -
避免依赖 CSS 渲染结果:Lightpanda 是"True Headless",不渲染 CSS。所有依赖视觉结果的检查(如
element.isVisible()、getBoundingClientRect())不可用。只使用 DOM 结构和 JavaScript 执行来验证页面状态。 -
错误处理与回退:由于 Lightpanda 处于 Beta 阶段,建议实现 Chrome 回退机制。当 Lightpanda 加载失败时,自动切换到 Chrome 执行。
-
资源监控:在大规模并发场景中,监控 Lightpanda 进程的内存使用。虽然单个实例约 24 MB,但大量页面的 DOM 数据可能增加内存占用。
第四部分:实战项目
项目需求
构建一个 "智能并发网页爬虫"(Smart Concurrent Crawler)——一个能够高效并发爬取大量网页、提取结构化数据并生成报告的 Node.js 工具。项目综合运用以下知识点:
- CDP 连接(1.1):连接 Lightpanda 实例
- 数据提取(1.2):DOM 查询和 JavaScript 执行提取页面数据
- 并发爬取(2.1):多页面并行处理,控制并发数量
- Chrome 迁移(2.2):支持 Lightpanda/Chrome 双后端,自动回退
- 性能优化(3.2):连接复用和资源管理
项目设计
架构设计:
smart-crawler.js(主入口)
├── 浏览器连接管理器(支持 Lightpanda/Chrome 自动切换)
├── 并发控制器(限制同时运行的爬取任务数)
├── 页面数据提取器(可配置提取规则)
└── 结果汇总与报告生成
爬取流程:
1. 读取 URL 列表文件
2. 创建浏览器连接(优先 Lightpanda,失败回退 Chrome)
3. 并发控制下的并行爬取
4. 对每个页面执行数据提取规则
5. 汇总结果,生成 JSON 报告
完整实现代码
// 基于 Lightpanda 官方文档和 Playwright 最佳实践
// 文件名:smart-crawler.js
// 智能并发网页爬虫 — 综合运用 5 个知识点
const { chromium } = require("playwright");
const fs = require("fs");
const path = require("path");
// ============================================================
// 配置
// ============================================================
const CONFIG = {
// Lightpanda CDP 端点
lightpandaUrl: process.env.LIGHTPANDA_URL || "http://localhost:9222",
// 最大并发数
concurrency: parseInt(process.env.CONCURRENCY || "5", 10),
// 页面加载超时(毫秒)
timeout: 15000,
// 输出文件
outputFile: "crawl-report.json",
};
// ============================================================
// 知识点 1 & 2:浏览器连接管理 + Chrome 回退
// ============================================================
class BrowserManager {
constructor() {
this.browser = null;
this.browserType = null;
}
async connect() {
// 优先尝试 Lightpanda
try {
this.browser = await chromium.connectOverCDP(CONFIG.lightpandaUrl, {
timeout: 5000,
});
this.browserType = "lightpanda";
console.log(`✓ 已连接 Lightpanda: ${CONFIG.lightpandaUrl}`);
return;
} catch (e) {
console.log(`Lightpanda 连接失败: ${e.message}`);
}
// 回退到 Chrome
try {
this.browser = await chromium.launch({ headless: true });
this.browserType = "chrome";
console.log("✓ 已启动 Chrome 浏览器(回退模式)");
} catch (e) {
throw new Error(`无法启动任何浏览器: ${e.message}`);
}
}
async newPage() {
if (!this.browser) throw new Error("浏览器未连接");
return await this.browser.newPage();
}
async close() {
if (this.browser) {
await this.browser.close();
console.log(`\n浏览器已关闭 (${this.browserType})`);
}
}
getType() {
return this.browserType;
}
}
// ============================================================
// 知识点 3:并发控制器
// ============================================================
class ConcurrencyPool {
constructor(limit) {
this.limit = limit;
this.running = 0;
this.queue = [];
}
async add(task) {
if (this.running >= this.limit) {
await new Promise((resolve) => this.queue.push(resolve));
}
this.running++;
try {
return await task();
} finally {
this.running--;
if (this.queue.length > 0) {
this.queue.shift()();
}
}
}
}
// ============================================================
// 知识点 4:页面数据提取器
// ============================================================
class PageExtractor {
// 默认提取规则
static defaultRules = {
title: () => document.title,
description: () => {
const meta = document.querySelector('meta[name="description"]');
return meta ? meta.getAttribute("content") : null;
},
headings: () =>
Array.from(document.querySelectorAll("h1, h2, h3")).map((h) => ({
tag: h.tagName,
text: h.textContent.trim(),
})),
links: () =>
Array.from(document.querySelectorAll("a[href]")).map((a) => ({
text: a.textContent.trim().substring(0, 100),
href: a.href,
})),
elementCount: () => document.querySelectorAll("*").length,
bodyLength: () => document.body.innerText.length,
};
static async extract(page, url) {
const startTime = Date.now();
await page.goto(url, {
timeout: CONFIG.timeout,
waitUntil: "domcontentloaded",
});
// 在浏览器上下文中执行所有提取规则
const data = await page.evaluate((rulesSource) => {
// 重建提取规则函数
const rules = {
title: () => document.title,
description: () => {
const meta = document.querySelector('meta[name="description"]');
return meta ? meta.getAttribute("content") : null;
},
headings: () =>
Array.from(document.querySelectorAll("h1, h2, h3")).map((h) => ({
tag: h.tagName,
text: h.textContent.trim(),
})),
links: () =>
Array.from(document.querySelectorAll("a[href]"))
.slice(0, 20)
.map((a) => ({
text: a.textContent.trim().substring(0, 100),
href: a.href,
})),
elementCount: () => document.querySelectorAll("*").length,
bodyLength: () => document.body.innerText.length,
};
const result = {};
for (const [key, fn] of Object.entries(rules)) {
try {
result[key] = fn();
} catch (e) {
result[key] = `ERROR: ${e.message}`;
}
}
return result;
}, null);
return {
url,
...data,
duration: Date.now() - startTime,
timestamp: new Date().toISOString(),
};
}
}
// ============================================================
// 知识点 5:主爬虫逻辑(连接复用 + 资源管理)
// ============================================================
async function crawl(urls) {
const browserManager = new BrowserManager();
const pool = new ConcurrencyPool(CONFIG.concurrency);
await browserManager.connect();
console.log(`\n=== 开始爬取 ${urls.length} 个页面 ===`);
console.log(`并发数: ${CONFIG.concurrency}`);
console.log(`浏览器: ${browserManager.getType()}\n`);
const results = [];
let successCount = 0;
let failCount = 0;
const tasks = urls.map((url) =>
pool.add(async () => {
const page = await browserManager.newPage();
try {
const data = await PageExtractor.extract(page, url);
results.push(data);
successCount++;
console.log(
` [✓] ${url} — ${data.title} (${data.duration}ms)`
);
return data;
} catch (error) {
failCount++;
results.push({ url, error: error.message });
console.log(` [✗] ${url} — ${error.message}`);
return { url, error: error.message };
} finally {
await page.close(); // 知识点 5:总是关闭页面
}
})
);
await Promise.all(tasks);
// 生成报告
const report = {
summary: {
total: urls.length,
success: successCount,
failed: failCount,
browser: browserManager.getType(),
concurrency: CONFIG.concurrency,
timestamp: new Date().toISOString(),
},
results: results,
};
fs.writeFileSync(
CONFIG.outputFile,
JSON.stringify(report, null, 2),
"utf-8"
);
console.log(`\n=== 爬取完成 ===`);
console.log(`成功: ${successCount}/${urls.length}`);
console.log(`失败: ${failCount}/${urls.length}`);
console.log(`报告已保存: ${CONFIG.outputFile}`);
await browserManager.close();
return report;
}
// ============================================================
// 入口
// ============================================================
// 从命令行参数或文件读取 URL 列表
function getUrls() {
const args = process.argv.slice(2);
if (args.length > 0 && args[0].endsWith(".txt")) {
// 从文件读取 URL 列表
return fs
.readFileSync(args[0], "utf-8")
.split("\n")
.map((line) => line.trim())
.filter((line) => line && !line.startsWith("#"));
}
// 默认测试 URL
return [
"https://example.com",
"https://example.org",
"https://httpbin.org/html",
"https://info.cern.ch",
"https://www.w3.org",
"https://www.ietf.org",
"https://www.rfc-editor.org",
"https://httpbin.org/forms/post",
];
}
if (require.main === module) {
const urls = getUrls();
crawl(urls).catch(console.error);
}
module.exports = { BrowserManager, ConcurrencyPool, PageExtractor, crawl };
代码解析
| 代码段 | 使用的知识点 | 说明 |
|---|---|---|
BrowserManager.connect() |
CDP 连接(1.1)+ Chrome 迁移(2.2) | 优先连接 Lightpanda,失败自动回退 Chrome,实现双后端支持 |
PageExtractor.extract() |
数据提取(1.2) | 使用 page.evaluate() 在 V8 引擎中执行提取规则,获取结构化数据 |
ConcurrencyPool |
并发爬取(2.1) | 限制同时运行的任务数,防止资源耗尽,队列机制控制并发 |
page.close() in finally |
性能优化(3.2) | 确保页面资源被释放,Lightpanda 的 Arena 分配器在关闭页面时一次性回收内存 |
crawl() 主函数 |
所有知识点 | 编排连接管理、并发控制、数据提取和报告生成的完整流程 |
扩展挑战
- 添加去重和增量爬取:维护一个已爬取 URL 的 Bloom Filter,支持增量模式(只爬取新增页面)。
- 分布式爬取:将 URL 队列存储在 Redis 中,多个爬虫工作节点从队列获取 URL 并执行,实现横向扩展。
- 智能重试和错误分类:将失败分为可重试(网络超时)和不可重试(404、API 不兼容),对可重试错误实现指数退避重试。
第五部分:常见问题与排查指南
常见错误及解决方案
| 错误信息 | 原因 | 解决方案 |
|---|---|---|
Error: connect ECONNREFUSED 127.0.0.1:9222 |
Lightpanda 未启动或端口不正确 | 运行 docker ps 检查容器状态,或手动启动 Lightpanda:./lightpanda --port 9222 |
page.goto: Timeout 15000ms exceeded |
页面加载超时 | 增加超时时间:page.goto(url, { timeout: 30000 }),或检查网络连接 |
Error: Protocol error (Target.createTarget) |
Lightpanda 不支持某些 CDP 命令 | 检查 Lightpanda 版本是否为最新 nightly 构建,部分 CDP 命令尚未实现 |
page.evaluate: Execution context was destroyed |
页面在 JavaScript 执行期间被导航或关闭 | 添加 try-catch 处理,或在执行 evaluate 前确认页面仍然可用 |
page.screenshot is not a function 或类似错误 |
Lightpanda 不支持截图(True Headless 限制) | 这是预期行为,Lightpanda 不支持视觉相关操作。使用 Chrome 进行截图测试 |
Error: Browser closed unexpectedly |
Lightpanda 进程崩溃或 Docker 容器退出 | 检查 Docker 日志:docker logs lightpanda,可能是某个网页触发了未实现的 API |
page.waitForSelector: Waiting for selector timed out |
元素未找到或页面结构变化 | 增加等待时间,检查选择器是否正确,或使用 page.evaluate() 直接查询 DOM |
net::ERR_CONNECTION_REFUSED |
目标网站无法访问 | 检查网络连接,确认目标 URL 正确,检查是否需要代理设置 |
调试技巧
-
检查 Lightpanda 日志:Docker 方式运行时,使用
docker logs -f lightpanda实时查看 Lightpanda 的输出日志。日志中会显示页面加载错误、JavaScript 执行错误和 CDP 协议问题。 -
使用
curl测试 CDP 端点:在运行 Playwright 脚本之前,先用curl http://localhost:9222/json/version确认 CDP 端点可用。如果返回 JSON 数据,说明 Lightpanda 正常运行。 -
对比 Chrome 和 Lightpanda 的行为:当遇到问题时,使用
BROWSER=chrome node your-script.js在 Chrome 上运行同一脚本,对比行为差异。如果 Chrome 正常但 Lightpanda 失败,可能是 CDP 兼容性问题。
第六部分:学习路线推荐
官方文档推荐阅读顺序
- GitHub README — 项目概览、安装方式、快速入门示例。重点:Docker 启动命令、CDP 连接方式。
- 官方博客 "What is a True Headless Browser" — 理解 True Headless 理念和与传统无头浏览器的区别。重点:渲染管线对比、资源节省分析。
- 官方博客 "Why We Built Lightpanda in Zig" — 技术选型理由。重点:Zig vs C++ vs Rust 对比、comptime 元编程、Arena 分配器。
- 官方博客 "Migrating Our DOM to Zig" — zigdom 内部实现。重点:单次分配模式、惰性加载、V8 绑定。
- GitHub Issues — 已知问题和功能路线图。重点:哪些 CDP 命令已实现、哪些 Web API 尚未支持。
推荐进阶资源
- Playwright 官方文档 — Lightpanda 通过 CDP 与 Playwright 兼容,深入学习 Playwright API 可以更好地利用 Lightpanda 的能力。
- Chrome DevTools Protocol 文档 — 理解 CDP 协议的完整规范,了解 Lightpanda 实现了哪些域和命令。
- Zig 官方文档 — 如果想从源码构建或贡献代码,需要学习 Zig 语言。
信息来源与版本说明
- 教程基于版本: Beta(无独立版本号,内容基于截至 2026-04-12 最后推送日期的仓库内容)
- 信息获取日期: 2026-04-13
- 信息来源列表:
- GitHub 仓库 lightpanda-io/browser — README、安装说明、使用示例
- Lightpanda 官方博客 "Why We Built Lightpanda in Zig" — Zig 语言选择和技术架构
- Lightpanda 官方博客 "What is a True Headless Browser" — True Headless 理念
- Lightpanda 官方博客 "Migrating Our DOM to Zig" — zigdom 内部实现
- Playwright 官方文档 — CDP 连接 API 和最佳实践