从2小时到3秒:我为什么弃用 Puppeteer 转投这个命令行爬取工具

上周五下午四点半,产品经理走过来拍我肩膀:”嘿,帮我把 Hacker News 首页的标题抓下来,发我个 Excel。”

我心想:就这?五分钟搞定。

结果两小时后,我还在调试 CSS 选择器。

一个”小需求”是怎么失控的

第一步:初始化项目

# 创建项目目录并进入
mkdir hacker-news-scraper && cd hacker-news-scraper
npm init -y
# 安装 Puppeteer,它会自动下载超过 200MB 的 Chromium 浏览器
npm install puppeteer

按下回车,盯着进度条看了三分钟。我开始怀疑人生。

第二步:写代码

“不就是 document.querySelectorAll 吗?”

const puppeteer = require('puppeteer');

(async () => {
  // 启动浏览器实例,headless 表示无头模式,args 是沙箱相关参数
  const browser = await puppeteer.launch({
    headless: true,
    args: ['--no-sandbox', '--disable-setuid-sandbox']
  });
  const page = await browser.newPage();

  try {
    // 打开 Hacker News 首页,等待网络空闲
    await page.goto('https://news.ycombinator.com', {
      waitUntil: 'networkidle2',
      timeout: 30000
    });

    // 等待标题链接元素出现
    await page.waitForSelector('.titleline > a', {
      timeout: 10000
    });

    // 在页面上下文中执行 JS,提取所有标题和链接
    const titles = await page.evaluate(() => {
      const items = document.querySelectorAll('.titleline > a');
      return Array.from(items).map(el => ({
        title: el.textContent,
        url: el.href
      }));
    });

    console.log(JSON.stringify(titles, null, 2));
  } catch (err) {
    // 抓取失败时打印错误信息
    console.error('Scraping failed:', err.message);
  } finally {
    // 无论成功失败都关闭浏览器
    await browser.close();
  }
})();

整整 27 行。而且这还是最简版本,没加 User-Agent 伪装、重试逻辑、代理和并发控制,加上去轻松破 50 行。

第三步:运行

node index.js

报错:Navigation timeout of 30000 ms exceeded

改成 domcontentloaded 后,又卡在 waitForSelector——因为 Hacker News 悄悄把 .storylink 改成了 .titleline,没人通知我。

第四步:调试

设置 headless: false,看着浏览器窗口弹出来,才发现选择器变了。修完再跑,终于拿到结果。

第五步:交付

格式化成 CSV 发给产品经理。然后默默删掉项目目录——因为我知道下次换个网站,这些代码一个字符都用不上。

总共花了两小时,就为了 30 个标题。

为什么”简单”的浏览器爬取这么复杂?

咱们冷静分析下,复杂度到底从哪来。

框架本身太重

Puppeteer 和 Playwright 本质上是浏览器测试框架。它们是为复杂的端到端测试设计的:模拟登录、填表、验证页面状态。抓个标题只用得上它们 1% 的能力,却要承受另外 99% 的代价。装个 Puppeteer 等于在你电脑上装一整个 Chromium,就像为了开罐汤而先搭一间厨房。

每次都要从零开始

今天写给 Hacker News 的脚本,明天能直接用在 Reddit 上吗?不能。选择器不同、加载策略不同、反爬机制不同。每个网站都是全新的冒险,没有记忆,没有通用策略,页面一改就抓瞎。

异步地狱

看看任何一段 Puppeteer 脚本,满眼都是 await

// 以下每一步都是异步操作,都需要 await
await browser.launch()
await browser.newPage()
await page.goto()
await page.waitForSelector()
await page.evaluate()
await browser.close()

浏览器操作确实需要异步,但一个”打开网页、拿点数据”的任务,要承受这么高的认知成本,实在有点冤。

错误处理爆炸

超时、元素找不到、网络错误、重定向、SSL 错误……每一步都可能失败,每一步都要 try-catch。一个健壮的爬取脚本里,错误处理代码往往比业务逻辑还多。

try {
  // 尝试打开页面并设置 30 秒超时
  await page.goto(url, { timeout: 30000 });
} catch (e) {
  if (e.name === 'TimeoutError') {
    // 判断是超时,还是其他网络问题
  } else {
    // 页面真的打不开了?
  }
}

try {
  // 等待选择器出现
  await page.waitForSelector(sel, { timeout: 10000 });
} catch (e) {
  // 选择器变了?页面没加载完?还是被反爬了?
}

你以为是来爬数据的,其实是来写错误处理框架的。

如果浏览器操作像 curl 一样简单就好了

curl 多优雅:

# 直接请求 API 并提取 login 字段
curl https://api.github.com/users/octocat | jq '.login'

一行搞定。但 curl 有个致命缺陷:它不会执行 JavaScript

都 2026 年了,大量网站是客户端渲染的。curl 过去只能拿到一个空壳 HTML 和一堆 <script> 标签,真正的数据要等浏览器执行完 JS 才出现。

所以我们需要的是会执行 JavaScript 的 curl

不是测试框架,不是自动化库,就是一个命令行工具。你输入命令,它给你数据。完事。

一行命令能做什么?

回到 Hacker News 的场景:

# 直接抓取 Hacker News 首页内容,以 Markdown 格式输出到终端
xbrowser scrape https://news.ycombinator.com

只要这一行。

只想提取标题?加个选择器:

# 访问页面,并通过 CSS 选择器提取文本
xbrowser goto https://news.ycombinator.com , text --selector ".titleline"

想要 JSON 格式?

# 在上一命令基础上,追加 --json 参数输出结构化数据
xbrowser goto https://news.ycombinator.com , text --selector ".titleline" --json

没有 npm init,没有 async/await,没有 try-catch。一条命令,结果直接出来。

查搜索引擎排名

产品经理说:”查查咱们公司在 Google 搜 ‘AI agent’ 排第几。”

老方法?打开 Puppeteer,模拟搜索,解析 SERP,处理动态加载,又是 50 行。

现在:

# 使用 Google 引擎搜索关键词,返回前 10 条结果的标题、链接和摘要
xbrowser search "AI agent" --engine google --limit 10 --full

自带 Google、Bing、Baidu、DuckDuckGo 支持。

截图

“给这个页面截个图。”

# 访问页面并生成全页截图
xbrowser goto https://news.ycombinator.com , screenshot --full-page

不用管窗口大小、懒加载图片或视口设置,一行搞定。

填表并提交

“测试下注册流程。”

# 链式命令:访问注册页、填写邮箱密码、点击提交、最后截图
xbrowser goto https://example.com/signup , fill "#email" "test@example.com" , fill "#password" "123456" , click "#submit" , screenshot

用逗号分隔命令链,像写 Shell 管道一样自然。

监控页面变化

“价格低于 500 的时候通知我。”

# 每小时检查一次价格,当匹配到 400-499 时发送桌面通知
while true; do
  xbrowser text --selector ".price" | grep -q "^4[0-9][0-9]$" && notify-send "Price dropped!"
  sleep 3600
done

因为是命令行工具,天然就能和 cron、CI/CD、Shell 脚本无缝集成。

不只是”简单”

你可能会想:这不就是把 Puppeteer 包了个 CLI 壳吗?

不完全是。背后是完全不同的设计理念

瀑布 vs 水龙头

Puppeteer 像瀑布——威力巨大,但你得站在下面接水,还得承受水流冲击。你得管理异步、处理生命周期、写一堆样板代码。

CLI 工具应该像水龙头——拧开就有水,关上就停。简单、直接、按需使用。

框架 vs 工具

框架要求你按它的方式思考。Browser → Page → Frame → Element,每一步都是异步,每一步都可能失败。

工具应该按你的方式思考。你想”打开这个页面”——goto;”获取这段文字”——text;”截个图”——screenshot。就这么简单。

编程接口 vs 命令接口

API 的灵活性无可替代,复杂自动化场景确实需要细粒度控制。但对于 80% 的”打开页面、拿点数据”场景,CLI 的效率是 API 的 10 倍。

就像 Git:你可以用 libgit2 写程序操作仓库,但大多数时候一句 git commit -m "xxx" 就够了。

该用什么,心里要有数

这里不是踩 Puppeteer 或 Playwright,它们在各自领域非常强大。问题是不能把大炮当苍蝇拍

  • 如果只是抓取单个页面、提取搜索结果、快速截图、或者要集成到 Shell 脚本里,选 CLI 工具。
  • 如果是复杂的端到端测试,需要细粒度控制浏览器行为,选 Playwright 或 Puppeteer。
  • 如果是性能测试,用 Lighthouse 或 k6。
  • 如果是大规模分布式爬取,用 Scrapy 或自研系统。

工具要匹配场景。用榔头敲钉子不是榔头的错,但用榔头缝衣服就是你的不对了。

回到那个周五下午

如果当时有这个工具,我的下午会是这样:

# 3 秒钟抓取首页内容并保存到 hn.md
xbrowser scrape https://news.ycombinator.com > hn.md

三秒。然后把 Markdown 文件扔给产品经理,回去干正事。

不是因为技术有多革命性,而是工具的尺寸终于和问题的尺寸匹配了。抓个页面标题,本不该搭一整个项目。


直达网址:https://github.com/dyyz1993/xbrowser

类似文章