当 axios + cheerio 搞不定的时候,是时候请出大杀器了。
🎬 开场:一个让我抓狂的页面
上周,老板让我爬一个招聘网站的数据。
我信心满满地写好了爬虫,运行,结果:
javascript
const response = await axios.get("https://xxx.com/jobs")
console.log(response.data)
// 输出:
// <html>
// <body>
// <div id="app"></div>
// <script src="app.js"></script>
// </body>
// </html>
啥也没有,就一个空的 <div id="app">。
原因很简单:这是一个 SPA(单页应用),所有内容都是通过 JavaScript 动态渲染的。
axios 只能拿到原始 HTML,拿不到 JS 执行后的内容。
这时候,就需要请出我们的大杀器:Puppeteer。
🎭 Puppeteer 是什么?
Puppeteer 是 Google 出品的 Node.js 库,它提供了一个高级 API 来控制 Chrome/Chromium 浏览器。
简单说,它能:
- 🌐 打开一个真实的浏览器
- 📄 加载页面并等待 JS 执行完成
- 🖱️ 模拟点击、输入、滚动等操作
- 📸 截图、生成 PDF
- 🍪 管理 Cookie 和登录状态
本质上,Puppeteer 就是一个可以用代码控制的浏览器。
🛠️ 安装与配置
安装 Puppeteer
bash
# 完整版(会下载 Chromium,约 300MB)
npm install puppeteer
# 精简版(需要自己指定浏览器路径)
npm install puppeteer-core
基础用法
javascript
const puppeteer = require("puppeteer")
async function basicExample() {
// 1. 启动浏览器
const browser = await puppeteer.launch({
headless: "new", // 无头模式(不显示浏览器窗口)
// headless: false, // 有头模式(显示浏览器,方便调试)
})
// 2. 创建新页面
const page = await browser.newPage()
// 3. 设置视口大小
await page.setViewport({ width: 1920, height: 1080 })
// 4. 访问页面
await page.goto("https://example.com", {
waitUntil: "networkidle2", // 等待网络空闲
timeout: 30000,
})
// 5. 获取页面内容
const content = await page.content()
console.log(content)
// 6. 截图
await page.screenshot({ path: "screenshot.png" })
// 7. 关闭浏览器
await browser.close()
}
basicExample()
🚀 实战:爬取动态渲染的页面
场景:爬取某技术社区的文章列表
假设我们要爬取一个 Vue/React 写的技术社区,页面内容是动态加载的。
javascript
// src/dynamic-crawler.js
const puppeteer = require("puppeteer")
const chalk = require("chalk")
class DynamicCrawler {
constructor() {
this.browser = null
this.page = null
}
/**
* 初始化浏览器
*/
async init() {
console.log(chalk.blue("🚀 启动浏览器..."))
this.browser = await puppeteer.launch({
headless: "new",
args: [
"--no-sandbox",
"--disable-setuid-sandbox",
"--disable-dev-shm-usage",
"--disable-accelerated-2d-canvas",
"--disable-gpu",
"--window-size=1920,1080",
],
})
this.page = await this.browser.newPage()
// 设置 User-Agent
await this.page.setUserAgent(
"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36"
)
// 设置视口
await this.page.setViewport({ width: 1920, height: 1080 })
// 设置超时
this.page.setDefaultTimeout(30000)
console.log(chalk.green("✅ 浏览器启动成功"))
}
/**
* 爬取文章列表
*/
async crawlArticles(url) {
console.log(chalk.blue(`\n📄 正在爬取: ${url}`))
// 访问页面
await this.page.goto(url, {
waitUntil: "networkidle2",
})
// 等待文章列表加载完成
await this.page.waitForSelector(".article-item", {
timeout: 10000,
})
// 在浏览器环境中执行 JS,提取数据
const articles = await this.page.evaluate(() => {
const items = document.querySelectorAll(".article-item")
return Array.from(items).map((item, index) => ({
rank: index + 1,
title: item.querySelector(".title")?.textContent?.trim(),
url: item.querySelector("a")?.href,
author: item.querySelector(".author")?.textContent?.trim(),
views: item.querySelector(".views")?.textContent?.trim(),
likes: item.querySelector(".likes")?.textContent?.trim(),
date: item.querySelector(".date")?.textContent?.trim(),
}))
})
console.log(chalk.green(`✅ 成功爬取 ${articles.length} 篇文章`))
return articles
}
/**
* 关闭浏览器
*/
async close() {
if (this.browser) {
await this.browser.close()
console.log(chalk.gray("🔒 浏览器已关闭"))
}
}
}
// 使用示例
async function main() {
const crawler = new DynamicCrawler()
try {
await crawler.init()
const articles = await crawler.crawlArticles("https://example.com/articles")
console.log(articles)
} finally {
await crawler.close()
}
}
main().catch(console.error)
📜 处理无限滚动加载
很多网站使用"无限滚动"来加载更多内容。我们需要模拟滚动操作:
javascript
/**
* 自动滚动页面,加载更多内容
* @param {Page} page - Puppeteer 页面对象
* @param {number} maxScrolls - 最大滚动次数
* @param {number} scrollDelay - 每次滚动后等待时间(毫秒)
*/
async function autoScroll(page, maxScrolls = 10, scrollDelay = 1000) {
console.log(chalk.blue("📜 开始自动滚动..."))
let scrollCount = 0
let previousHeight = 0
while (scrollCount < maxScrolls) {
// 获取当前页面高度
const currentHeight = await page.evaluate(() => document.body.scrollHeight)
// 如果高度没变化,说明已经到底了
if (currentHeight === previousHeight) {
console.log(chalk.yellow("⚠️ 已到达页面底部"))
break
}
previousHeight = currentHeight
// 滚动到底部
await page.evaluate(() => {
window.scrollTo(0, document.body.scrollHeight)
})
// 等待新内容加载
await page.waitForTimeout(scrollDelay)
scrollCount++
console.log(chalk.gray(` 滚动 ${scrollCount}/${maxScrolls}`))
}
console.log(chalk.green(`✅ 滚动完成,共滚动 ${scrollCount} 次`))
}
// 使用示例
async function crawlWithScroll(url) {
const browser = await puppeteer.launch({ headless: "new" })
const page = await browser.newPage()
await page.goto(url, { waitUntil: "networkidle2" })
// 自动滚动加载更多
await autoScroll(page, 5, 2000)
// 现在可以获取所有加载的内容了
const items = await page.evaluate(() => {
return Array.from(document.querySelectorAll(".item")).map((el) => ({
title: el.textContent,
}))
})
await browser.close()
return items
}
🔐 模拟登录
有些数据需要登录后才能访问。Puppeteer 可以模拟登录操作:
javascript
/**
* 模拟登录
*/
async function login(page, username, password) {
console.log(chalk.blue("🔐 开始登录..."))
// 访问登录页
await page.goto("https://example.com/login", {
waitUntil: "networkidle2",
})
// 等待登录表单加载
await page.waitForSelector("#username")
await page.waitForSelector("#password")
// 清空输入框(以防有默认值)
await page.click("#username", { clickCount: 3 })
await page.keyboard.press("Backspace")
// 输入用户名(模拟人类打字速度)
await page.type("#username", username, { delay: 100 })
// 输入密码
await page.type("#password", password, { delay: 100 })
// 点击登录按钮
await Promise.all([
page.waitForNavigation({ waitUntil: "networkidle2" }),
page.click("#login-button"),
])
// 检查是否登录成功
const isLoggedIn = await page.evaluate(() => {
return !!document.querySelector(".user-avatar")
})
if (isLoggedIn) {
console.log(chalk.green("✅ 登录成功"))
} else {
throw new Error("登录失败")
}
}
/**
* 保存 Cookie(下次可以直接使用,不用重新登录)
*/
async function saveCookies(page, filepath) {
const cookies = await page.cookies()
await fs.writeJson(filepath, cookies)
console.log(chalk.green(`💾 Cookie 已保存到 ${filepath}`))
}
/**
* 加载 Cookie
*/
async function loadCookies(page, filepath) {
if (await fs.pathExists(filepath)) {
const cookies = await fs.readJson(filepath)
await page.setCookie(...cookies)
console.log(chalk.green("🍪 Cookie 已加载"))
return true
}
return false
}
// 使用示例
async function crawlWithLogin() {
const browser = await puppeteer.launch({ headless: "new" })
const page = await browser.newPage()
const cookiePath = "./data/cookies.json"
// 尝试加载已保存的 Cookie
const hasCookies = await loadCookies(page, cookiePath)
if (!hasCookies) {
// 没有 Cookie,需要登录
await login(page, "your-username", "your-password")
await saveCookies(page, cookiePath)
}
// 现在可以访问需要登录的页面了
await page.goto("https://example.com/dashboard")
// ... 爬取数据
await browser.close()
}
🛡️ 处理常见反爬措施
1. 检测 Headless 浏览器
很多网站会检测你是不是用的无头浏览器。我们可以用插件来隐藏特征:
bash
npm install puppeteer-extra puppeteer-extra-plugin-stealth
javascript
const puppeteer = require("puppeteer-extra")
const StealthPlugin = require("puppeteer-extra-plugin-stealth")
// 使用 Stealth 插件
puppeteer.use(StealthPlugin())
async function stealthCrawl() {
const browser = await puppeteer.launch({ headless: "new" })
const page = await browser.newPage()
// 现在浏览器的特征更像真实用户了
await page.goto("https://bot.sannysoft.com/")
await page.screenshot({ path: "stealth-test.png" })
await browser.close()
}
2. 处理验证码
简单的验证码可以用 OCR 识别,复杂的建议用打码平台:
javascript
// 简单示例:截图验证码,手动输入
async function handleCaptcha(page) {
// 等待验证码图片加载
await page.waitForSelector("#captcha-image")
// 截图验证码
const captchaElement = await page.$("#captcha-image")
await captchaElement.screenshot({ path: "captcha.png" })
console.log(chalk.yellow("⚠️ 请查看 captcha.png 并输入验证码:"))
// 从命令行读取输入
const readline = require("readline")
const rl = readline.createInterface({
input: process.stdin,
output: process.stdout,
})
const captchaCode = await new Promise((resolve) => {
rl.question("验证码: ", (answer) => {
rl.close()
resolve(answer)
})
})
// 输入验证码
await page.type("#captcha-input", captchaCode)
}
3. 使用代理 IP
javascript
const browser = await puppeteer.launch({
headless: "new",
args: ["--proxy-server=http://proxy-server:port"],
})
// 如果代理需要认证
await page.authenticate({
username: "proxy-user",
password: "proxy-pass",
})
4. 随机化请求特征
javascript
// 随机 User-Agent
const userAgents = [
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36...",
"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36...",
"Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36...",
]
await page.setUserAgent(
userAgents[Math.floor(Math.random() * userAgents.length)]
)
// 随机视口大小
const viewports = [
{ width: 1920, height: 1080 },
{ width: 1366, height: 768 },
{ width: 1536, height: 864 },
]
await page.setViewport(viewports[Math.floor(Math.random() * viewports.length)])
// 随机延迟
function randomDelay(min, max) {
const ms = Math.floor(Math.random() * (max - min + 1)) + min
return new Promise((resolve) => setTimeout(resolve, ms))
}
await randomDelay(1000, 3000)
📸 实用技巧
拦截请求(加速加载)
javascript
// 拦截不需要的资源,加快页面加载
await page.setRequestInterception(true)
page.on("request", (request) => {
const resourceType = request.resourceType()
// 阻止加载图片、字体、样式表
if (["image", "font", "stylesheet"].includes(resourceType)) {
request.abort()
} else {
request.continue()
}
})
监听网络请求
javascript
// 监听 XHR/Fetch 请求,直接获取 API 数据
page.on("response", async (response) => {
const url = response.url()
if (url.includes("/api/articles")) {
const data = await response.json()
console.log("捕获到 API 数据:", data)
}
})
执行自定义 JavaScript
javascript
// 在页面中注入并执行 JS
const result = await page.evaluate(() => {
// 这里的代码在浏览器环境中执行
return {
title: document.title,
url: window.location.href,
cookies: document.cookie,
}
})
// 传递参数到浏览器环境
const selector = ".article"
const texts = await page.evaluate((sel) => {
return Array.from(document.querySelectorAll(sel)).map((el) => el.textContent)
}, selector)
🎯 完整示例:爬取掘金热门文章
javascript
// src/juejin-crawler.js
const puppeteer = require("puppeteer-extra")
const StealthPlugin = require("puppeteer-extra-plugin-stealth")
const { Storage } = require("./storage")
const chalk = require("chalk")
puppeteer.use(StealthPlugin())
const storage = new Storage("./data/juejin")
async function crawlJuejin() {
console.log(chalk.blue("\n🚀 开始爬取掘金热门文章\n"))
const browser = await puppeteer.launch({
headless: "new",
args: ["--no-sandbox", "--disable-setuid-sandbox"],
})
try {
const page = await browser.newPage()
// 设置视口和 UA
await page.setViewport({ width: 1920, height: 1080 })
await page.setUserAgent(
"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36"
)
// 拦截不必要的资源
await page.setRequestInterception(true)
page.on("request", (req) => {
if (["image", "font"].includes(req.resourceType())) {
req.abort()
} else {
req.continue()
}
})
// 访问掘金首页
await page.goto("https://juejin.cn/", {
waitUntil: "networkidle2",
timeout: 30000,
})
// 等待文章列表加载
await page.waitForSelector(".entry-list .item")
// 滚动加载更多
for (let i = 0; i < 3; i++) {
await page.evaluate(() => window.scrollTo(0, document.body.scrollHeight))
await page.waitForTimeout(2000)
console.log(chalk.gray(` 滚动 ${i + 1}/3`))
}
// 提取文章数据
const articles = await page.evaluate(() => {
const items = document.querySelectorAll(".entry-list .item")
return Array.from(items)
.map((item, index) => {
const titleEl = item.querySelector(".title")
const metaEl = item.querySelector(".meta-container")
const statEl = item.querySelector(".action-list")
return {
rank: index + 1,
title: titleEl?.textContent?.trim(),
url: titleEl?.closest("a")?.href,
author: metaEl
?.querySelector(".user-message .name")
?.textContent?.trim(),
date: metaEl?.querySelector(".date")?.textContent?.trim(),
views: statEl?.querySelector(".view")?.textContent?.trim(),
likes: statEl?.querySelector(".like")?.textContent?.trim(),
comments: statEl?.querySelector(".comment")?.textContent?.trim(),
}
})
.filter((item) => item.title) // 过滤掉空数据
})
console.log(chalk.green(`\n✅ 成功爬取 ${articles.length} 篇文章\n`))
// 打印前 5 篇
articles.slice(0, 5).forEach((article) => {
console.log(`${article.rank}. ${article.title}`)
console.log(
chalk.gray(
` 👤 ${article.author} | 👍 ${article.likes} | 💬 ${article.comments}`
)
)
console.log("")
})
// 保存数据
const filename = `hot-${new Date().toISOString().split("T")[0]}.json`
await storage.saveJson(filename, articles)
return articles
} finally {
await browser.close()
}
}
crawlJuejin().catch(console.error)
🔮 下集预告
这篇文章讲了 Puppeteer 的使用方法和动态页面爬取技巧。
下一篇,我们将探讨:
《Node.js 爬虫实战指南(三):分布式爬虫架构,让你的爬虫飞起来》
内容包括:
- 为什么需要分布式爬虫?
- 任务队列设计(Redis + Bull)
- 多进程/多机器协作
- 数据去重与增量爬取
- 监控与告警
💬 互动时间:你用 Puppeteer 爬过什么有趣的网站?遇到过什么坑?评论区分享一下!
觉得有用的话,点赞 + 在看 + 转发,让更多人学会动态页面爬取~