从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 文件扔给产品经理,回去干正事。
不是因为技术有多革命性,而是工具的尺寸终于和问题的尺寸匹配了。抓个页面标题,本不该搭一整个项目。
