无头浏览器是一种在没有可见窗口的情况下运行的真实浏览器引擎:它加载页面、运行 JavaScript、应用 CSS 并构建与 Chrome 或 Firefox 相同的 DOM,只是在你的脚本控制下在后台运行。对于无头浏览器网络抓取,这一点至关重要,因为现代网络的大部分内容只有在 JavaScript 运行之后才会出现。普通的 HTTP 请求给你返回的是初始 HTML 框架;无头浏览器返回的是人类实际看到的页面。
本指南是一份实用的可运行教程。你将在 Node 中搭建一个现代无头技术栈(Puppeteer,然后是 Playwright),加载一个 JavaScript 密集型页面,等待正确的内容,提取结构化数据,并截取屏幕截图。然后我们会坦诚面对这种方式在规模上的痛点,并展示单次调用的替代方案:通过 Crawlbase Crawling API 带 JavaScript token 在服务器端渲染页面。
无头浏览器究竟是什么
普通浏览器将像素渲染到屏幕上。无头浏览器跳过可见的 UI,但保留其下面的一切:JavaScript 引擎、布局引擎、网络栈、Cookie 以及完整的 DOM。你用代码驱动它而不是点击,因此它非常适合自动化测试、生成屏幕截图,以及抓取在客户端构建内容的页面。
现代无头 Chrome 和 Firefox 与其可见版本使用相同的渲染代码,所以页面的行为与真实访客看到的完全一样。这种保真度正是关键所在:当网站在初始响应后通过 fetch 调用加载其内容时,只有运行该 JavaScript 的东西才能看到数据。
为什么 JavaScript 密集型网站会破坏普通 HTTP 爬虫
如果你用普通 HTTP 客户端请求单页应用或无限滚动列表,你通常会得到状态码 200 和近乎空白的响应体。你想要的标记不在该响应中。它是在浏览器运行页面的脚本、发出 XHR 或 fetch 调用并将结果渲染到 DOM 中之后才被注入的。
Cheerio 或 Beautiful Soup 等工具可以解析你提供给它们的任何 HTML,但它们无法运行 JavaScript,所以它们只能看到那个空框架。无头浏览器填补了这一差距:它像真实访客的浏览器一样执行页面,然后让你读取完成后的 DOM。对于已经包含你数据的静态服务器渲染页面,你不需要这种开销,但对于任何客户端渲染的内容,这是数据与空数组之间的区别。
当你想要的内容只有在脚本运行后才出现,当你需要点击或滚动来显示更多内容,或者当你需要屏幕截图时,才需要使用无头浏览器。对于已经包含你数据的静态 HTML,普通 HTTP 请求加解析器更快、更便宜。根据页面匹配工具,而不是反过来。
设置项目
你需要安装 Node.js(18 或更新版本)和 npm。确认两者,创建一个项目,并安装 Puppeteer。Puppeteer 在安装时会为你下载兼容的 Chrome 版本,所以没有其他配置工作。
node --version npm --version mkdir headless-scraper && cd headless-scraper npm init -y npm install puppeteer
有一件值得提前了解的事:无头浏览器很重。每个实例都是一个完整的 Chrome 进程,有其自己的内存和 CPU 占用。在你的笔记本电脑上一次处理一个页面没问题,而一旦你想并行处理数百个页面,这就成为了核心扩展问题。稍后我们会回到这一点。
启动无头浏览器并加载页面
使用 Puppeteer 的核心循环始终是一样的:启动浏览器,打开一个新页面,导航到 URL,完成工作,然后关闭浏览器以免进程泄漏。以下是加载页面并打印其标题的最小版本。
const puppeteer = require('puppeteer') async function run() { const browser = await puppeteer.launch({ headless: true }) const page = await browser.newPage() await page.goto('https://quotes.toscrape.com/js/', { waitUntil: 'networkidle2', }) console.log(await page.title()) await browser.close() } run().catch((err) => console.error(err))
使用 node index.js 运行它。这里的目标(Quotes to Scrape 的 JavaScript 版本)故意在客户端渲染其引用,所以它是一个干净的测试场地:普通请求返回空列表,而无头浏览器看到真实内容。waitUntil: 'networkidle2' 选项告知 goto 在网络平静一段时间之前不要解析,这是你的第一个也是最粗糙的等待策略。
等待正确的内容,而不是固定的计时器
等待是大多数无头爬虫出问题的地方。固定的 sleep 很脆弱:太短,你在数据到达之前就解析了;太长,每次运行都会拖延。更好的方法是等待你想要的内容确实存在的特定信号。
Puppeteer 为你提供了几个选项,大致按优先顺序排列:
-
waitForSelector阻塞直到特定元素出现在 DOM 中。这是最可靠的信号,因为它将等待与你关心的数据绑定。 -
waitForFunction阻塞直到任意 JavaScript 条件为真,例如列表达到特定长度。当仅凭存在还不够时使用它。 -
goto上的waitUntil(load、domcontentloaded、networkidle2)控制导航何时解析。适合作为基线,但作为唯一保证时较弱。
尽可能优先等待选择器而不是硬计时器。它平均更快,且在网络较慢时更具健壮性。
await page.goto('https://quotes.toscrape.com/js/', { waitUntil: 'domcontentloaded', }) // Block until the first quote is actually in the DOM. await page.waitForSelector('.quote') // Or wait for a richer condition: at least 10 quotes loaded. await page.waitForFunction(() => { return document.querySelectorAll('.quote').length >= 10 })
从渲染后的 DOM 中提取结构化数据
一旦内容出现,page.evaluate 就会在页面自己的上下文中运行一个函数,在那里你拥有完整的 DOM 和标准的浏览器 API。你返回的任何内容都会被序列化回你的 Node 脚本。这使提取保持简单:你就像在浏览器控制台中一样编写普通的 querySelectorAll 代码。
const quotes = await page.evaluate(() => { const cards = document.querySelectorAll('.quote') return Array.from(cards).map((card) => ({ text: card.querySelector('.text').innerText.trim(), author: card.querySelector('.author').innerText.trim(), tags: Array.from(card.querySelectorAll('.tag')).map((t) => t.innerText), })) }) console.log(quotes)
结果是一个干净的对象数组,你可以写入 JSON、推送到数据库或输入管道。经过修剪的输出样本如下所示:
[ { "text": "The world as we have created it is a process of our thinking.", "author": "Albert Einstein", "tags": ["change", "deep-thoughts", "thinking"] }, { "text": "It is our choices that show what we truly are.", "author": "J.K. Rowling", "tags": ["abilities", "choices"] } ]
截取屏幕截图
只有真实的渲染引擎才能为你提供的是可信的屏幕截图,这对视觉质量保证、存档页面状态或调试返回空结果的抓取很有用。Puppeteer 通过单次调用捕获视口或完整可滚动页面。
await page.screenshot({ path: 'quotes.png', fullPage: true, })
如果屏幕截图是你在大量场景下的主要需求,仅为了拍照而运行和维护浏览器集群是过度的。Crawlbase Screenshots API 在服务器端渲染页面并直接返回图像,你无需在自己这边管理任何浏览器。
同样的工作在 Playwright 中完成
Playwright 由 Microsoft 维护,是另一个现代选择。它通过一个 API 驱动 Chromium、Firefox 和 WebKit,其自动等待行为使上述许多显式等待变得不必要:click 和定位器读取等操作默认等待元素准备就绪。其结构与 Puppeteer 非常相似,因此两者之间的移植很简单。
const { chromium } = require('playwright') async function run() { const browser = await chromium.launch({ headless: true }) const page = await browser.newPage() await page.goto('https://quotes.toscrape.com/js/') await page.waitForSelector('.quote') const quotes = await page.$$eval('.quote .text', (els) => els.map((el) => el.innerText.trim()), ) console.log(quotes) await browser.close() } run().catch((err) => console.error(err))
这两个库都很出色。如果想以不同的语言和工具进行更深入的比较,使用 Python 和 Selenium 进行网络抓取在 Selenium 和 Python 构建中介绍了相同的概念。
无头抓取在规模上的痛点
以上所有内容在你的机器上处理单个页面时都能完美运行。麻烦从你需要大量处理时开始,它以两种不同的方式显现。
第一是资源问题。每个无头浏览器实例都是一个占用数百兆字节内存的完整 Chrome 进程。并行运行少数几个没问题;运行足够多的实例以每小时抓取数千个页面,意味着需要搭建集群、管理内存泄漏和僵尸进程、回收崩溃的实例,并为底层服务器买单。原本只需一行 launch 调用的浏览器变成了基础设施。
第二是反爬虫防御。严肃的商业网站不仅仅渲染内容,它们还在积极寻找自动化的迹象。默认无头浏览器会泄露信号(navigator.webdriver 标志、缺失或奇怪的浏览器指纹、数据中心 IP),检测系统会立即识别。你最终不得不添加隐身插件、轮换住宅代理使请求来自真实用户的 IP,以及解决 CAPTCHA,而这些都有各自持续的维护负担。抓取本身不再是困难所在。
关于保持不被封锁的更广泛操作手册,请参阅如何在不被封锁的情况下抓取网站。简而言之:渲染本身你可以自己解决,但从目标信任的 IP 可靠地大规模渲染是一个不同的、大得多的问题。
单次调用替代方案:带 JS token 的 Crawling API
这正是托管 API 所消除的痛点。与其运行和加固自己的浏览器集群,不如向 Crawlbase Crawling API 发送一个带 JavaScript token 的 URL。API 在其一侧的真实浏览器中渲染页面,位于轮换的可信住宅 IP 池之后,并返回完成后的 HTML 供你解析。渲染和 IP 问题在单次请求中折叠解决。
安装客户端并发出一次调用。注册 Crawlbase 账户,从仪表板获取你的 JavaScript token,并将其替换到你看到 YOUR_CRAWLBASE_JS_TOKEN 的位置。
npm install crawlbase cheerio
const { CrawlingAPI } = require('crawlbase') const cheerio = require('cheerio') const api = new CrawlingAPI({ token: 'YOUR_CRAWLBASE_JS_TOKEN' }) const options = { ajax_wait: true, page_wait: 5000, } async function scrape() { const response = await api.get('https://quotes.toscrape.com/js/', options) const $ = cheerio.load(response.body) const quotes = [] $('.quote').each((i, el) => { quotes.push({ text: $(el).find('.text').text().trim(), author: $(el).find('.author').text().trim(), }) }) console.log(quotes) } scrape().catch((err) => console.error(err))
你在 Puppeteer 中学到的等待策略在这里有直接的等价物。ajax_wait 选项告知 API 在返回之前等待异步内容,page_wait 在加载后等待固定毫秒数,使延迟渲染的元素出现。对于在按钮后面显示内容的页面,css_click_selector 接受 URL 编码的 CSS 选择器并在渲染后点击它,与 Puppeteer 的 page.click 后跟等待的想法相同。
Crawlbase 提供两种 token 类型。普通 token 获取静态 HTML;JavaScript(JS)token 先在真实浏览器中渲染页面。对于任何客户端渲染的页面(如上面的页面),你需要 JS token。普通 token 会返回与普通请求相同的空框架。
在单次调用中,在可信住宅 IP 后面渲染 JavaScript 密集型页面。Crawling API 接受 JS token,在服务器端的真实浏览器中运行页面,为你轮换 IP,并返回完成后的 HTML,让你无需自己运行无头浏览器集群、代理池和 CAPTCHA 技术栈。先在免费层级试试。
你应该选择哪种方式?
两者都有其适用场景,决策主要取决于数量以及目标的防御强度。
当你需要对页面进行精细控制时,使用自己的无头浏览器:复杂的多步骤交互、登录你自己的账户进行测试、为少量页面生成屏幕截图,或者在封锁不是问题的友好网站上进行抓取。控制无与伦比,小规模成本很低。
当你大规模抓取、目标积极封锁机器人,或者你只是不想拥有浏览器和代理基础设施时,选择托管 Crawling API。如果你只需要原始 IP 轮换而不需要渲染,Smart AI Proxy 可以满足这一需求;如果你想要支持网站的解析后 JSON 而不是原始 HTML,Crawling API 也负责提取。重点是将你的时间花在数据上,而不是保持集群存活。
核心要点
- 无头浏览器运行完整页面。 它执行 JavaScript 并构建真实的 DOM,因此能看到普通 HTTP 请求永远看不到的内容。
-
等待选择器,而不是计时器。
waitForSelector和waitForFunction将等待与你想要的数据绑定,比固定的 sleep 更健壮。 -
提取发生在页面上下文中。
page.evaluate(或 Playwright 的$$eval)在页面中运行 DOM 代码并返回干净的结构化对象。 - 规模是真实成本。 浏览器集群消耗内存,反爬虫防御在上面强制要求隐身、代理和 CAPTCHA 处理。
- JS token 折叠了两个问题。 Crawling API 在受信任的 IP 后面在服务器端渲染,并在单次调用中返回完成后的 HTML。
- 根据任务选择工具。 小规模控制选 DIY;大量抓取和难以应对的目标选托管 API。
常见问题
网络抓取中的无头浏览器是什么?
无头浏览器是一种没有可见窗口的真实浏览器引擎,例如 Chrome 或 Firefox。在网络抓取中,它加载页面,运行其 JavaScript,并构建人类会看到的相同 DOM,使你能够提取只有在脚本运行后才出现的内容。你用代码而不是点击来驱动它,因此非常适合普通 HTTP 请求无法读取的 JavaScript 密集型网站。
无头抓取应该使用 Puppeteer 还是 Playwright?
两者都很出色,非常相似。Puppeteer 专注于 Chrome 和 Firefox,入门简单。Playwright 通过一个 API 驱动 Chromium、Firefox 和 WebKit,具有更强的内置自动等待,可以省去大量手动等待代码。如果你需要跨浏览器覆盖或喜欢其定位器模型,选择 Playwright;如果需要精简的仅 Chrome 设置,选择 Puppeteer。本指南中的概念适用于任一框架。
在无头浏览器中等待内容的最佳方式是什么?
等待特定元素,而不是固定计时器。使用 waitForSelector 阻塞直到你想要的元素出现在 DOM 中,或使用 waitForFunction 进行更丰富的条件判断(如列表达到特定长度)。固定的 sleep 很脆弱:太短会提前解析,太长会拖慢每次运行。将等待与目标数据绑定平均更快,也更可靠。
为什么无头浏览器会被封锁?
默认的无头浏览器会泄露自动化信号:navigator.webdriver 标志、异常或缺失的指纹,以及检测系统立即标记的数据中心 IP 地址。严肃的网站正在监视这些信号。缓解这一问题意味着添加隐身配置、轮换住宅代理使请求来自真实用户的 IP,以及处理 CAPTCHA,每一项都是持续的工作。为你在受信任的 IP 后面渲染的托管 API 为你处理了这一切。
我可以用无头浏览器截取屏幕截图吗?
可以。Puppeteer 和 Playwright 都通过单次调用捕获视口或完整可滚动页面,这对视觉质量保证、存档和调试空抓取很有用。如果屏幕截图是你在大量场景下的主要需求,仅为拍照而运行浏览器集群是过度的;Screenshots API 在服务器端渲染并直接返回图像,无需管理任何浏览器。
什么时候应该使用 Crawling API 而不是运行自己的浏览器?
在量大或在友好网站上进行小规模精细控制时,使用自己的无头浏览器。当你大规模抓取、遇到积极的反爬虫防御,或不想拥有浏览器和代理基础设施时,切换到 Crawling API。带 JS token 的 Crawling API 在轮换住宅 IP 后在服务器端渲染页面,并在单次调用中返回完成后的 HTML,让你省去集群、代理池和 CAPTCHA 技术栈。
大规模爬取任何站点,无需与基础设施对抗。
Crawlbase 负责处理代理、指纹和 CAPTCHA,让你的团队专注于交付数据流水线,而非维护爬取管道。1,000 次请求免费,无需信用卡。
