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:titledescription)。


第二部分:进阶篇

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 最佳实践

  1. 总是关闭 Page:每个页面处理完毕后调用 page.close()。Lightpanda 使用 Arena 分配器,关闭页面会一次性释放所有 DOM 内存。不关闭页面会导致内存泄漏。

  2. 设置合理的超时:Lightpanda 的页面加载可能比 Chrome 快,但也可能因 Web API 不兼容而失败。设置 timeout: 15000(15 秒)和 waitUntil: "domcontentloaded" 是安全的默认配置。

  3. 避免依赖 CSS 渲染结果:Lightpanda 是"True Headless",不渲染 CSS。所有依赖视觉结果的检查(如 element.isVisible()getBoundingClientRect())不可用。只使用 DOM 结构和 JavaScript 执行来验证页面状态。

  4. 错误处理与回退:由于 Lightpanda 处于 Beta 阶段,建议实现 Chrome 回退机制。当 Lightpanda 加载失败时,自动切换到 Chrome 执行。

  5. 资源监控:在大规模并发场景中,监控 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() 主函数 所有知识点 编排连接管理、并发控制、数据提取和报告生成的完整流程

扩展挑战

  1. 添加去重和增量爬取:维护一个已爬取 URL 的 Bloom Filter,支持增量模式(只爬取新增页面)。
  2. 分布式爬取:将 URL 队列存储在 Redis 中,多个爬虫工作节点从队列获取 URL 并执行,实现横向扩展。
  3. 智能重试和错误分类:将失败分为可重试(网络超时)和不可重试(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 正确,检查是否需要代理设置

调试技巧

  1. 检查 Lightpanda 日志:Docker 方式运行时,使用 docker logs -f lightpanda 实时查看 Lightpanda 的输出日志。日志中会显示页面加载错误、JavaScript 执行错误和 CDP 协议问题。

  2. 使用 curl 测试 CDP 端点:在运行 Playwright 脚本之前,先用 curl http://localhost:9222/json/version 确认 CDP 端点可用。如果返回 JSON 数据,说明 Lightpanda 正常运行。

  3. 对比 Chrome 和 Lightpanda 的行为:当遇到问题时,使用 BROWSER=chrome node your-script.js 在 Chrome 上运行同一脚本,对比行为差异。如果 Chrome 正常但 Lightpanda 失败,可能是 CDP 兼容性问题。


第六部分:学习路线推荐

官方文档推荐阅读顺序

  1. GitHub README — 项目概览、安装方式、快速入门示例。重点:Docker 启动命令、CDP 连接方式。
  2. 官方博客 "What is a True Headless Browser" — 理解 True Headless 理念和与传统无头浏览器的区别。重点:渲染管线对比、资源节省分析。
  3. 官方博客 "Why We Built Lightpanda in Zig" — 技术选型理由。重点:Zig vs C++ vs Rust 对比、comptime 元编程、Arena 分配器。
  4. 官方博客 "Migrating Our DOM to Zig" — zigdom 内部实现。重点:单次分配模式、惰性加载、V8 绑定。
  5. GitHub Issues — 已知问题和功能路线图。重点:哪些 CDP 命令已实现、哪些 Web API 尚未支持。

推荐进阶资源

  • Playwright 官方文档 — Lightpanda 通过 CDP 与 Playwright 兼容,深入学习 Playwright API 可以更好地利用 Lightpanda 的能力。
  • Chrome DevTools Protocol 文档 — 理解 CDP 协议的完整规范,了解 Lightpanda 实现了哪些域和命令。
  • Zig 官方文档 — 如果想从源码构建或贡献代码,需要学习 Zig 语言。

信息来源与版本说明