道高一尺,魔高一丈。爬虫与反爬的战争,永无止境。
🎬 开场:一场猫鼠游戏
我曾经爬过一个电商网站。
第一天,一切顺利,数据哗哗地进来。
第二天,开始出现 403 错误,大概 10% 的请求被拦截。
第三天,50% 的请求返回验证码页面。
第四天,我的 IP 被永久封禁了。
这就是爬虫工程师的日常:和网站的反爬系统斗智斗勇。
今天,我们来聊聊常见的反爬措施,以及如何(合法地)应对它们。
🛡️ 常见反爬措施
1. User-Agent 检测
最基础的反爬措施,检查请求头中的 User-Agent。
javascript
// ❌ 默认的 axios User-Agent(会暴露你在用爬虫)
// axios/1.7.9
// ❌ 默认的 Puppeteer User-Agent(包含 HeadlessChrome 字样)
// Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) HeadlessChrome/131.0.0.0 Safari/537.36
// ✅ 正常浏览器的 User-Agent
// Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.36
应对方案:
javascript
// 随机 User-Agent
const userAgents = [
// Chrome on Windows
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36",
// Chrome on Mac
"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36",
// Firefox on Windows
"Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:121.0) Gecko/20100101 Firefox/121.0",
// Safari on Mac
"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/17.2 Safari/605.1.15",
// Edge on Windows
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36 Edg/120.0.0.0",
]
function getRandomUserAgent() {
return userAgents[Math.floor(Math.random() * userAgents.length)]
}
// 使用
await page.setUserAgent(getRandomUserAgent())
2. IP 频率限制
同一 IP 请求过于频繁会被封禁。
应对方案:代理 IP 池
javascript
// src/utils/proxyPool.js
const axios = require("axios")
class ProxyPool {
constructor() {
this.proxies = []
this.currentIndex = 0
this.failedProxies = new Set()
}
/**
* 从代理服务商获取代理列表
*/
async fetchProxies() {
// 示例:从免费代理 API 获取
// 生产环境建议使用付费代理服务
const response = await axios.get("https://api.proxy-provider.com/list", {
params: {
apiKey: process.env.PROXY_API_KEY,
count: 100,
country: "CN",
},
})
this.proxies = response.data.proxies.map((p) => ({
host: p.ip,
port: p.port,
protocol: p.protocol || "http",
username: p.username,
password: p.password,
}))
console.log(`✅ 获取到 ${this.proxies.length} 个代理`)
}
/**
* 获取下一个可用代理
*/
getNext() {
// 过滤掉失败的代理
const availableProxies = this.proxies.filter(
(p) => !this.failedProxies.has(`${p.host}:${p.port}`)
)
if (availableProxies.length === 0) {
throw new Error("没有可用的代理")
}
// 轮询
this.currentIndex = (this.currentIndex + 1) % availableProxies.length
return availableProxies[this.currentIndex]
}
/**
* 标记代理为失败
*/
markFailed(proxy) {
this.failedProxies.add(`${proxy.host}:${proxy.port}`)
}
/**
* 获取代理 URL
*/
getProxyUrl(proxy) {
const { protocol, username, password, host, port } = proxy
if (username && password) {
return `${protocol}://${username}:${password}@${host}:${port}`
}
return `${protocol}://${host}:${port}`
}
}
module.exports = { ProxyPool }
在 Puppeteer 中使用代理:
javascript
const { ProxyPool } = require("./utils/proxyPool")
const proxyPool = new ProxyPool()
await proxyPool.fetchProxies()
async function crawlWithProxy(url) {
const proxy = proxyPool.getNext()
const browser = await puppeteer.launch({
headless: "new",
args: [`--proxy-server=${proxy.host}:${proxy.port}`],
})
const page = await browser.newPage()
// 如果代理需要认证
if (proxy.username && proxy.password) {
await page.authenticate({
username: proxy.username,
password: proxy.password,
})
}
try {
await page.goto(url, { timeout: 30000 })
return await page.content()
} catch (error) {
// 代理失败,标记并重试
proxyPool.markFailed(proxy)
throw error
} finally {
await browser.close()
}
}
3. Cookie/Session 检测
网站通过 Cookie 追踪用户行为,异常行为会被标记。
应对方案:Cookie 管理
javascript
// src/utils/cookieManager.js
const fs = require("fs-extra")
const path = require("path")
class CookieManager {
constructor(cookieDir = "./data/cookies") {
this.cookieDir = cookieDir
}
/**
* 保存 Cookie
*/
async save(domain, cookies) {
const filepath = path.join(this.cookieDir, `${domain}.json`)
await fs.ensureDir(this.cookieDir)
await fs.writeJson(filepath, cookies)
}
/**
* 加载 Cookie
*/
async load(domain) {
const filepath = path.join(this.cookieDir, `${domain}.json`)
if (await fs.pathExists(filepath)) {
return fs.readJson(filepath)
}
return null
}
/**
* 应用 Cookie 到页面
*/
async applyToPage(page, domain) {
const cookies = await this.load(domain)
if (cookies) {
await page.setCookie(...cookies)
return true
}
return false
}
/**
* 从页面保存 Cookie
*/
async saveFromPage(page, domain) {
const cookies = await page.cookies()
await this.save(domain, cookies)
}
}
module.exports = { CookieManager }
4. 浏览器指纹检测
网站通过 JavaScript 收集浏览器特征,识别爬虫。
常见的指纹特征:
- Canvas 指纹
- WebGL 指纹
- 字体列表
- 屏幕分辨率
- 时区
- 语言
- 插件列表
应对方案:使用 Stealth 插件
javascript
const puppeteer = require("puppeteer-extra")
const StealthPlugin = require("puppeteer-extra-plugin-stealth")
// 使用 Stealth 插件
puppeteer.use(StealthPlugin())
// Stealth 插件会自动处理:
// - navigator.webdriver 属性
// - Chrome 运行时特征
// - 语言和插件
// - WebGL 渲染器
// - 等等...
const browser = await puppeteer.launch({ headless: "new" })
手动伪装指纹:
javascript
async function setupFingerprint(page) {
// 覆盖 navigator.webdriver
await page.evaluateOnNewDocument(() => {
Object.defineProperty(navigator, "webdriver", {
get: () => undefined,
})
})
// 覆盖 navigator.plugins
await page.evaluateOnNewDocument(() => {
Object.defineProperty(navigator, "plugins", {
get: () => [
{ name: "Chrome PDF Plugin" },
{ name: "Chrome PDF Viewer" },
{ name: "Native Client" },
],
})
})
// 覆盖 navigator.languages
await page.evaluateOnNewDocument(() => {
Object.defineProperty(navigator, "languages", {
get: () => ["zh-CN", "zh", "en"],
})
})
// 覆盖 WebGL 渲染器
await page.evaluateOnNewDocument(() => {
const getParameter = WebGLRenderingContext.prototype.getParameter
WebGLRenderingContext.prototype.getParameter = function (parameter) {
if (parameter === 37445) {
return "Intel Inc."
}
if (parameter === 37446) {
return "Intel Iris OpenGL Engine"
}
return getParameter.call(this, parameter)
}
})
}
5. 验证码
最常见的反爬措施之一。
应对方案:
方案 1:使用打码平台
javascript
const axios = require("axios")
/**
* 使用打码平台识别验证码
*/
async function solveCaptcha(imageBase64, type = "common") {
// 示例:使用某打码平台 API
const response = await axios.post("https://api.captcha-solver.com/solve", {
apiKey: process.env.CAPTCHA_API_KEY,
image: imageBase64,
type: type, // common, slide, click, etc.
})
return response.data.result
}
// 在爬虫中使用
async function handleCaptcha(page) {
// 检测是否有验证码
const captchaElement = await page.$(".captcha-image")
if (!captchaElement) return true
// 截图验证码
const imageBuffer = await captchaElement.screenshot()
const imageBase64 = imageBuffer.toString("base64")
// 调用打码平台
const captchaCode = await solveCaptcha(imageBase64)
// 输入验证码
await page.type("#captcha-input", captchaCode)
await page.click("#submit-captcha")
// 等待验证结果
await page.waitForNavigation({ timeout: 5000 })
return true
}
方案 2:滑块验证码
javascript
/**
* 处理滑块验证码
*/
async function handleSliderCaptcha(page) {
// 获取滑块和背景图
const slider = await page.$(".slider-button")
const background = await page.$(".slider-background")
if (!slider || !background) return
// 获取滑块位置
const sliderBox = await slider.boundingBox()
const bgBox = await background.boundingBox()
// 计算需要滑动的距离(实际项目中需要图像识别)
// 这里简化处理,假设已知距离
const distance = 200
// 模拟人类滑动轨迹
const startX = sliderBox.x + sliderBox.width / 2
const startY = sliderBox.y + sliderBox.height / 2
await page.mouse.move(startX, startY)
await page.mouse.down()
// 生成人类化的滑动轨迹
const tracks = generateHumanTrack(distance)
for (const track of tracks) {
await page.mouse.move(startX + track.x, startY + track.y)
await page.waitForTimeout(track.delay)
}
await page.mouse.up()
}
/**
* 生成人类化的滑动轨迹
*/
function generateHumanTrack(distance) {
const tracks = []
let current = 0
// 先加速后减速
const mid = distance * 0.7
while (current < distance) {
let step
if (current < mid) {
// 加速阶段
step = Math.random() * 10 + 5
} else {
// 减速阶段
step = Math.random() * 3 + 1
}
current = Math.min(current + step, distance)
tracks.push({
x: current,
y: Math.random() * 2 - 1, // 轻微的 Y 轴抖动
delay: Math.random() * 20 + 10,
})
}
return tracks
}
6. JavaScript 混淆与加密
网站使用混淆的 JS 代码来隐藏数据或生成签名。
应对方案:
javascript
/**
* 拦截并分析网络请求
*/
async function interceptRequests(page) {
await page.setRequestInterception(true)
const apiRequests = []
page.on("request", (request) => {
// 记录 API 请求
if (request.url().includes("/api/")) {
apiRequests.push({
url: request.url(),
method: request.method(),
headers: request.headers(),
postData: request.postData(),
})
}
request.continue()
})
page.on("response", async (response) => {
if (response.url().includes("/api/")) {
try {
const data = await response.json()
console.log("API 响应:", response.url(), data)
} catch (e) {
// 非 JSON 响应
}
}
})
return apiRequests
}
/**
* 在浏览器中执行 JS 获取加密参数
*/
async function getEncryptedParams(page, data) {
// 假设网站有一个加密函数 window.encrypt
const encrypted = await page.evaluate((data) => {
// 调用网站自己的加密函数
if (typeof window.encrypt === "function") {
return window.encrypt(data)
}
return null
}, data)
return encrypted
}
🎭 高级伪装技巧
1. 模拟真实用户行为
javascript
/**
* 模拟人类浏览行为
*/
async function simulateHumanBehavior(page) {
// 随机滚动
await page.evaluate(() => {
const scrollHeight = document.body.scrollHeight
const randomScroll = Math.floor(Math.random() * scrollHeight * 0.5)
window.scrollTo(0, randomScroll)
})
// 随机停留
await page.waitForTimeout(Math.random() * 2000 + 1000)
// 随机移动鼠标
const viewport = page.viewport()
const randomX = Math.floor(Math.random() * viewport.width)
const randomY = Math.floor(Math.random() * viewport.height)
await page.mouse.move(randomX, randomY)
// 随机点击(非链接区域)
// await page.click('body')
}
/**
* 模拟打字
*/
async function humanType(page, selector, text) {
await page.click(selector)
for (const char of text) {
await page.keyboard.type(char)
// 随机打字间隔
await page.waitForTimeout(Math.random() * 150 + 50)
}
}
2. 请求头完整性
javascript
/**
* 设置完整的请求头
*/
async function setFullHeaders(page) {
await page.setExtraHTTPHeaders({
Accept:
"text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8",
"Accept-Encoding": "gzip, deflate, br",
"Accept-Language": "zh-CN,zh;q=0.9,en;q=0.8",
"Cache-Control": "max-age=0",
Connection: "keep-alive",
"Sec-Ch-Ua":
'"Not_A Brand";v="8", "Chromium";v="120", "Google Chrome";v="120"',
"Sec-Ch-Ua-Mobile": "?0",
"Sec-Ch-Ua-Platform": '"macOS"',
"Sec-Fetch-Dest": "document",
"Sec-Fetch-Mode": "navigate",
"Sec-Fetch-Site": "none",
"Sec-Fetch-User": "?1",
"Upgrade-Insecure-Requests": "1",
})
}
3. Referer 链伪造
javascript
/**
* 模拟正常的访问路径
*/
async function simulateNormalPath(page, targetUrl) {
// 先访问首页
await page.goto("https://example.com/", {
waitUntil: "networkidle2",
})
await page.waitForTimeout(2000)
// 模拟搜索
await page.type("#search-input", "关键词")
await page.click("#search-button")
await page.waitForNavigation()
await page.waitForTimeout(1500)
// 点击搜索结果进入目标页面
// 这样 Referer 就是搜索结果页
await page.goto(targetUrl, {
referer: page.url(),
})
}
📊 反爬检测自测
在开始爬取之前,可以用这些网站测试你的爬虫是否会被检测:
javascript
async function testAntiDetection() {
const browser = await puppeteer.launch({ headless: "new" })
const page = await browser.newPage()
// 测试 1: Bot 检测
await page.goto("https://bot.sannysoft.com/")
await page.screenshot({ path: "test-bot.png" })
// 测试 2: 浏览器指纹
await page.goto("https://browserleaks.com/canvas")
await page.screenshot({ path: "test-canvas.png" })
// 测试 3: WebRTC 泄露
await page.goto("https://browserleaks.com/webrtc")
await page.screenshot({ path: "test-webrtc.png" })
await browser.close()
console.log("检测结果已保存为截图,请查看")
}
⚠️ 法律与道德提醒
在使用这些技术之前,请务必注意:
- 遵守 robots.txt - 这是基本礼仪
- 控制请求频率 - 不要给目标网站造成压力
- 不爬取敏感数据 - 个人隐私、付费内容等
- 不用于非法目的 - 不倒卖、不诈骗
- 尊重网站的服务条款 - 有些网站明确禁止爬取
记住:技术是中立的,但使用技术的人要有底线。
🔮 系列总结
到这里,《Node.js 爬虫实战指南》系列就完结了。
让我们回顾一下:
- 第一篇:基础入门 - 爬虫原理、法律边界、简单实战
- 第二篇:Puppeteer - 动态页面爬取、模拟登录、无限滚动
- 第三篇:分布式架构 - 任务队列、Worker、去重、监控
- 第四篇:反反爬策略 - 代理池、指纹伪装、验证码处理
希望这个系列能帮你成为一个合格的爬虫工程师。
记住:爬虫是一门技术,更是一门艺术。
💬 互动时间:你遇到过最难搞的反爬措施是什么?是怎么解决的?评论区分享一下!
觉得这个系列有用的话,点赞 + 在看 + 转发,让更多人学会爬虫技术~