当你写好一个爬虫脚本,兴冲冲地运行,结果收到的不是数据,而是 403 Forbidden、验证码页面、或者空空如也的响应体------这意味着你撞上了反爬系统。大多数反爬机制的核心逻辑并不复杂:服务器通过分析请求特征,判断请求来自真实浏览器还是程序。理解这些判断依据,才能有针对性地让程序的行为接近真实用户。
本文系统讲解六项反反爬策略,每项策略都包含原理分析和可直接运行的 Node.js 代码实现。
01 反爬系统在工作时关注什么
反爬系统本质上是一个请求特征分类器。它会从 HTTP 请求中提取多个维度的特征,综合判断请求是否来自自动化程序:
#mermaid-svg-7uwes7lBT8UQd7qJ{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-7uwes7lBT8UQd7qJ .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-7uwes7lBT8UQd7qJ .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-7uwes7lBT8UQd7qJ .error-icon{fill:#552222;}#mermaid-svg-7uwes7lBT8UQd7qJ .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-7uwes7lBT8UQd7qJ .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-7uwes7lBT8UQd7qJ .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-7uwes7lBT8UQd7qJ .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-7uwes7lBT8UQd7qJ .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-7uwes7lBT8UQd7qJ .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-7uwes7lBT8UQd7qJ .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-7uwes7lBT8UQd7qJ .marker{fill:#333333;stroke:#333333;}#mermaid-svg-7uwes7lBT8UQd7qJ .marker.cross{stroke:#333333;}#mermaid-svg-7uwes7lBT8UQd7qJ svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-7uwes7lBT8UQd7qJ p{margin:0;}#mermaid-svg-7uwes7lBT8UQd7qJ .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-7uwes7lBT8UQd7qJ .cluster-label text{fill:#333;}#mermaid-svg-7uwes7lBT8UQd7qJ .cluster-label span{color:#333;}#mermaid-svg-7uwes7lBT8UQd7qJ .cluster-label span p{background-color:transparent;}#mermaid-svg-7uwes7lBT8UQd7qJ .label text,#mermaid-svg-7uwes7lBT8UQd7qJ span{fill:#333;color:#333;}#mermaid-svg-7uwes7lBT8UQd7qJ .node rect,#mermaid-svg-7uwes7lBT8UQd7qJ .node circle,#mermaid-svg-7uwes7lBT8UQd7qJ .node ellipse,#mermaid-svg-7uwes7lBT8UQd7qJ .node polygon,#mermaid-svg-7uwes7lBT8UQd7qJ .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-7uwes7lBT8UQd7qJ .rough-node .label text,#mermaid-svg-7uwes7lBT8UQd7qJ .node .label text,#mermaid-svg-7uwes7lBT8UQd7qJ .image-shape .label,#mermaid-svg-7uwes7lBT8UQd7qJ .icon-shape .label{text-anchor:middle;}#mermaid-svg-7uwes7lBT8UQd7qJ .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-7uwes7lBT8UQd7qJ .rough-node .label,#mermaid-svg-7uwes7lBT8UQd7qJ .node .label,#mermaid-svg-7uwes7lBT8UQd7qJ .image-shape .label,#mermaid-svg-7uwes7lBT8UQd7qJ .icon-shape .label{text-align:center;}#mermaid-svg-7uwes7lBT8UQd7qJ .node.clickable{cursor:pointer;}#mermaid-svg-7uwes7lBT8UQd7qJ .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-7uwes7lBT8UQd7qJ .arrowheadPath{fill:#333333;}#mermaid-svg-7uwes7lBT8UQd7qJ .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-7uwes7lBT8UQd7qJ .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-7uwes7lBT8UQd7qJ .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-7uwes7lBT8UQd7qJ .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-7uwes7lBT8UQd7qJ .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-7uwes7lBT8UQd7qJ .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-7uwes7lBT8UQd7qJ .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-7uwes7lBT8UQd7qJ .cluster text{fill:#333;}#mermaid-svg-7uwes7lBT8UQd7qJ .cluster span{color:#333;}#mermaid-svg-7uwes7lBT8UQd7qJ div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-7uwes7lBT8UQd7qJ .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-7uwes7lBT8UQd7qJ rect.text{fill:none;stroke-width:0;}#mermaid-svg-7uwes7lBT8UQd7qJ .icon-shape,#mermaid-svg-7uwes7lBT8UQd7qJ .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-7uwes7lBT8UQd7qJ .icon-shape p,#mermaid-svg-7uwes7lBT8UQd7qJ .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-7uwes7lBT8UQd7qJ .icon-shape .label rect,#mermaid-svg-7uwes7lBT8UQd7qJ .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-7uwes7lBT8UQd7qJ .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-7uwes7lBT8UQd7qJ .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-7uwes7lBT8UQd7qJ :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 否
否
否
是
是
是
是
否
HTTP 请求到达服务器
反爬系统分析
UA 是否像浏览器?
是否有有效 Cookie?
请求头是否完整?
请求频率是否异常?
拦截
放行
对应这五个检测维度,反反爬策略也分为五个层面,再加上一个数据层面的容错处理,共六项策略。
┌────────────────────────────────────────────────────┐
│ 反反爬策略体系 │
├──────────────────┬─────────────────────────────────┤
│ 身份伪装 │ 策略一:UA 随机轮换 │
│ │ 策略三:完整请求头模拟 │
│ 会话建立 │ 策略二:预先获取 Cookie │
│ 行为模拟 │ 策略四:请求间随机延迟 │
│ 容错处理 │ 策略五:自动重试与指数退避 │
│ 数据兼容 │ 策略六:多格式解压兜底 │
├──────────────────┴─────────────────────────────────┤
│ 核心原则:不假设服务器配合,对各种情况做好准备 │
└────────────────────────────────────────────────────┘
02 策略一:User-Agent 随机轮换
原理
User-Agent(UA)是 HTTP 请求头中的一个字段,用于标识发起请求的客户端类型、操作系统和浏览器版本。一个典型的浏览器 UA 如下:
Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/123.0.0.0 Safari/537.36
各片段含义:
| 片段 | 含义 |
|---|---|
Mozilla/5.0 |
历史兼容标记,几乎所有现代浏览器都写这个 |
(Macintosh; Intel Mac OS X 10_15_7) |
操作系统信息 |
AppleWebKit/537.36 |
浏览器引擎及版本 |
Chrome/123.0.0.0 |
浏览器名称及版本 |
服务器会检查 UA 字段。如果 UA 缺失,或是已知的爬虫标识(如 Python-urllib/3.10、node-fetch/1.0、curl/7.88),服务器可能直接拒绝请求。
实现
维护一个包含多个真实浏览器 UA 的池子,每次请求随机选一个:
javascript
// UA 池:覆盖五大操作系统和多种浏览器
const USER_AGENTS = [
'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/123.0.0.0 Safari/537.36',
'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/123.0.0.0 Safari/537.36',
'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Edg/123.0.0.0 Chrome/123.0.0.0 Safari/537.36',
'Mozilla/5.0 (X11; Linux x86_64; rv:123.0) Gecko/20100101 Firefox/123.0',
'Mozilla/5.0 (iPhone; CPU iPhone OS 17_2 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/17.2 Mobile/15E148 Safari/604.1',
'Mozilla/5.0 (Linux; Android 14; Pixel 8 Pro) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/123.0.0.0 Mobile Safari/537.36',
// 建议至少准备 15-20 个,覆盖 Mac/Windows/Linux/iOS/Android
];
function getRandomUserAgent() {
return USER_AGENTS[Math.floor(Math.random() * USER_AGENTS.length)];
}
使用时,在每次请求的 headers 中设置:
javascript
const headers = {
'User-Agent': getRandomUserAgent(), // 每次请求随机选一个
};
关键点
为什么需要随机而不是固定一个------如果每次请求都用同一个 UA,服务器可以通过"同一 UA + 高频请求"的模式识别出爬虫。随机轮换让每次请求看起来来自不同设备的不同用户,显著降低被识别的概率。
UA 池的覆盖范围建议:
| 操作系统 | 建议数量 | 浏览器 |
|---|---|---|
| macOS | 3-5 | Chrome、Safari、Firefox |
| Windows | 3-4 | Chrome、Edge、Firefox |
| Linux | 2-3 | Chrome、Firefox |
| iOS / iPadOS | 2-3 | Safari Mobile |
| Android | 2-3 | Chrome Mobile |
03 策略二:预先获取 Cookie 建立会话
原理
Cookie 是服务器通过响应头 Set-Cookie 下发给客户端的一小段数据。客户端在后续请求中通过 Cookie 请求头带回服务器,服务器据此识别请求来源。
其中最关键的是会话标识(Session ID),它相当于服务器发放的临时身份凭证。没有 Cookie 的请求被视为"陌生访语",有 Cookie 的请求被视为"已登记用户"。
实现
分两步走:先访问一个反爬较松的页面获取 Cookie,再带着 Cookie 请求目标页面。
第一步:从响应头提取 Cookie
javascript
const https = require('https');
// 从 HTTP 响应头中提取 Set-Cookie 字段的值
function extractCookies(headers) {
const cookies = [];
const setCookieHeader = headers['set-cookie'];
if (setCookieHeader) {
setCookieHeader.forEach(cookie => {
// Set-Cookie 格式:"key=value; path=/; expires=..."
// 只取 "key=value" 部分
const cookieValue = cookie.split(';')[0];
if (cookieValue) {
cookies.push(cookieValue);
}
});
}
// 拼成 "key1=value1; key2=value2" 的格式
return cookies.join('; ');
}
第二步:先访问入口页面获取 Cookie,再访问目标页面
javascript
async function getRequestWithCookie(targetUrl) {
// 1. 先访问入口页面(反爬较松的页面),获取 Cookie
const entryResp = await httpsRequest('https://example.com/entry');
const cookieStr = extractCookies(entryResp.headers);
// 2. 带 Cookie 请求目标页面
const headers = {
'User-Agent': getRandomUserAgent(),
'Cookie': cookieStr, // 带上刚获取的 Cookie
};
const targetResp = await httpsRequest(targetUrl, headers);
return targetResp.body.toString('utf-8');
}
关键点
入口页面的选择 ------如果目标站点有多个子域名,选择反爬较松的子域名作为入口。例如访问 weixin.sogou.com(反爬严)之前,先访问 v.sogou.com(反爬松)获取 Cookie,因为两者同属 .sogou.com,Cookie 可以共享。
#mermaid-svg-D9v4qkt6oJzNkltA{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-D9v4qkt6oJzNkltA .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-D9v4qkt6oJzNkltA .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-D9v4qkt6oJzNkltA .error-icon{fill:#552222;}#mermaid-svg-D9v4qkt6oJzNkltA .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-D9v4qkt6oJzNkltA .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-D9v4qkt6oJzNkltA .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-D9v4qkt6oJzNkltA .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-D9v4qkt6oJzNkltA .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-D9v4qkt6oJzNkltA .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-D9v4qkt6oJzNkltA .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-D9v4qkt6oJzNkltA .marker{fill:#333333;stroke:#333333;}#mermaid-svg-D9v4qkt6oJzNkltA .marker.cross{stroke:#333333;}#mermaid-svg-D9v4qkt6oJzNkltA svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-D9v4qkt6oJzNkltA p{margin:0;}#mermaid-svg-D9v4qkt6oJzNkltA .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-D9v4qkt6oJzNkltA .cluster-label text{fill:#333;}#mermaid-svg-D9v4qkt6oJzNkltA .cluster-label span{color:#333;}#mermaid-svg-D9v4qkt6oJzNkltA .cluster-label span p{background-color:transparent;}#mermaid-svg-D9v4qkt6oJzNkltA .label text,#mermaid-svg-D9v4qkt6oJzNkltA span{fill:#333;color:#333;}#mermaid-svg-D9v4qkt6oJzNkltA .node rect,#mermaid-svg-D9v4qkt6oJzNkltA .node circle,#mermaid-svg-D9v4qkt6oJzNkltA .node ellipse,#mermaid-svg-D9v4qkt6oJzNkltA .node polygon,#mermaid-svg-D9v4qkt6oJzNkltA .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-D9v4qkt6oJzNkltA .rough-node .label text,#mermaid-svg-D9v4qkt6oJzNkltA .node .label text,#mermaid-svg-D9v4qkt6oJzNkltA .image-shape .label,#mermaid-svg-D9v4qkt6oJzNkltA .icon-shape .label{text-anchor:middle;}#mermaid-svg-D9v4qkt6oJzNkltA .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-D9v4qkt6oJzNkltA .rough-node .label,#mermaid-svg-D9v4qkt6oJzNkltA .node .label,#mermaid-svg-D9v4qkt6oJzNkltA .image-shape .label,#mermaid-svg-D9v4qkt6oJzNkltA .icon-shape .label{text-align:center;}#mermaid-svg-D9v4qkt6oJzNkltA .node.clickable{cursor:pointer;}#mermaid-svg-D9v4qkt6oJzNkltA .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-D9v4qkt6oJzNkltA .arrowheadPath{fill:#333333;}#mermaid-svg-D9v4qkt6oJzNkltA .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-D9v4qkt6oJzNkltA .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-D9v4qkt6oJzNkltA .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-D9v4qkt6oJzNkltA .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-D9v4qkt6oJzNkltA .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-D9v4qkt6oJzNkltA .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-D9v4qkt6oJzNkltA .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-D9v4qkt6oJzNkltA .cluster text{fill:#333;}#mermaid-svg-D9v4qkt6oJzNkltA .cluster span{color:#333;}#mermaid-svg-D9v4qkt6oJzNkltA div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-D9v4qkt6oJzNkltA .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-D9v4qkt6oJzNkltA rect.text{fill:none;stroke-width:0;}#mermaid-svg-D9v4qkt6oJzNkltA .icon-shape,#mermaid-svg-D9v4qkt6oJzNkltA .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-D9v4qkt6oJzNkltA .icon-shape p,#mermaid-svg-D9v4qkt6oJzNkltA .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-D9v4qkt6oJzNkltA .icon-shape .label rect,#mermaid-svg-D9v4qkt6oJzNkltA .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-D9v4qkt6oJzNkltA .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-D9v4qkt6oJzNkltA .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-D9v4qkt6oJzNkltA :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 1. 访问入口页面(反爬松)
服务器下发 Cookie
2. 脚本保存 Cookie
3. 带 Cookie 请求目标页面(反爬严)
服务器识别为已登记用户,正常返回
这个过程模拟了真实用户的行为:先进入网站首页,再跳转到具体功能页面,而不是直接访问接口。
04 策略三:模拟完整浏览器请求头
原理
除了 UA 和 Cookie,真实浏览器发送的请求还包含多个头字段。反爬系统会综合检查这些字段的存在性和一致性。一个 UA 声明是 Chrome 浏览器,但请求头中缺少 Accept 或 Referer,就会暴露爬虫身份。
实现
javascript
const HEADERS = {
'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8',
'Accept-Encoding': 'identity',
'Accept-Language': 'zh-CN,zh;q=0.9,en;q=0.8',
'Host': 'weixin.sogou.com',
'Referer': 'https://weixin.sogou.com/',
};
各字段的作用和缺失风险:
| 请求头 | 作用 | 缺失时的风险 |
|---|---|---|
Accept |
声明客户端可接受的内容类型 | 服务器可能认为请求不合法 |
Accept-Language |
声明客户端偏好语言 | 与 UA 中的地区不匹配会触发怀疑 |
Referer |
声明请求来源页面 | 直接访问深层接口显得异常 |
Host |
指定目标主机 | 请求可能被拒绝 |
关键点
请求头的一致性 ------各字段之间不能矛盾。例如 UA 声明是中文版 Chrome,但 Accept-Language 设为 en-US,这种不一致会被高级反爬系统捕获。建议直接从浏览器开发者工具中复制真实请求头作为模板。
05 策略四:请求间随机延迟
原理
真实用户浏览网页时,两次请求之间必然有间隔------阅读内容、移动鼠标、点击链接都需要时间。爬虫的请求间隔通常是固定且极短的。反爬系统会统计单位时间内的请求次数,高频请求是爬虫最明显的特征之一。
实现
javascript
// 随机延迟函数:返回 min 到 min+range 毫秒之间的随机值
function randomDelay(min = 500, range = 1000) {
const delay = min + Math.random() * range;
return new Promise(resolve => setTimeout(resolve, delay));
}
// 在每次请求之间调用
async function fetchMultiplePages(urls) {
const results = [];
for (let i = 0; i < urls.length; i++) {
const html = await httpGet(urls[i]);
results.push(html);
// 最后一次请求不需要延迟
if (i < urls.length - 1) {
await randomDelay(500, 1000); // 等待 500~1500ms
}
}
return results;
}
关键点
延迟必须是随机的,不能是固定值 ------固定间隔(如恰好 1000ms)同样会被识别为机器行为,因为真实用户的操作间隔不可能如此精确。500 + Math.random() * 1000 生成 500ms 到 1500ms 之间的随机值,比固定 1000ms 更接近人类行为。
延迟时长的选择需要平衡两个因素:
| 因素 | 延迟过短 | 延迟过长 |
|---|---|---|
| 被封风险 | 高 | 低 |
| 采集效率 | 高 | 低 |
建议基础延迟设为 500ms 以上,随机范围至少 1000ms。如果目标站点反爬较严,可以提高到 1000ms 基础延迟 + 2000ms 随机范围。
06 策略五:自动重试与指数退避
原理
网络请求可能因多种原因失败:服务器临时拒绝、网络波动、反爬限流。直接放弃会导致数据缺失,而立即重试可能加剧服务器的拒绝。指数退避的核心思想是:失败后等待越来越长的时间再重试。
实现
javascript
function sleep(ms) {
return new Promise(resolve => setTimeout(resolve, ms));
}
/**
* 带重试的请求函数
* @param {Function} requestFn - 实际请求函数,返回 Promise
* @param {number} retries - 最大重试次数
*/
async function requestWithRetry(requestFn, retries = 2) {
for (let attempt = 0; attempt <= retries; attempt++) {
try {
return await requestFn();
} catch (e) {
if (attempt >= retries) {
throw new Error(`请求失败(已重试 ${retries} 次): ${e.message}`);
}
// 指数退避:每次重试等待时间递增
const waitTime = 300 + attempt * 300;
console.error(`第 ${attempt + 1} 次失败,${waitTime}ms 后重试...`);
await sleep(waitTime);
}
}
}
重试间隔的递增规律:
| 重试次数 | 等待时间 | 适用场景 |
|---|---|---|
| 第1次 | 300ms | 偶发网络波动 |
| 第2次 | 600ms | 可能是服务器限流 |
| 第3次 | 900ms | 服务器可能完全拒绝 |
关键点
不是所有失败都应该重试------如果服务器返回 403 Forbidden 或验证码页面,重试只会加剧封禁。建议只对网络超时、502/503 等临时性错误进行重试,对 403、404 等确定性错误直接放弃。
javascript
async function smartRequest(url) {
return await requestWithRetry(async () => {
const resp = await httpsRequest(url);
// 403 和验证码页面不重试,直接抛出
if (resp.statusCode === 403) {
throw new Error('被拒绝访问(403),不建议重试');
}
if (resp.body.toString().includes('antispider')) {
throw new Error('触发反爬验证码,不建议重试');
}
return resp;
}, 2);
}
07 策略六:多格式解压兜底
原理
HTTP 服务器为了减少传输数据量,通常会对响应体进行压缩。客户端通过 Accept-Encoding 请求头声明自己支持的压缩算法,服务器通过 Content-Encoding 响应头告知实际使用的压缩算法。
三种常见压缩算法:
| 算法 | 诞生时间 | 压缩率 | 特点 |
|---|---|---|---|
| Deflate | 1996年 | 中等 | 老牌算法,兼容性最好 |
| Gzip | 1992年 | 中等 | 最常见,几乎所有服务器都支持 |
| Brotli | 2015年 | 最高 | Google 开发,现代浏览器标配 |
当你在请求头中声明 Accept-Encoding: identity(不压缩),有些服务器会配合返回明文,但也有很多服务器配置了强制压缩,无论客户端怎么声明都会返回压缩数据。如果不做解压处理,拿到的就是一堆乱码。
实现
Node.js 内置的 zlib 模块提供了三种解压方法:
javascript
const zlib = require('zlib');
/**
* 根据响应头的 Content-Encoding 自动解压
* @param {Buffer} buffer - 响应体原始数据
* @param {string} contentEncoding - 响应头中的 Content-Encoding 值
* @returns {Buffer} 解压后的数据
*/
function decompressBody(buffer, contentEncoding) {
if (!contentEncoding) return buffer; // 没有压缩,直接返回
const encoding = String(contentEncoding).toLowerCase();
try {
if (encoding.includes('gzip')) return zlib.gunzipSync(buffer); // gzip 解压
if (encoding.includes('deflate')) return zlib.inflateSync(buffer); // deflate 解压
if (encoding.includes('br')) return zlib.brotliDecompressSync(buffer); // brotli 解压
} catch {
// 解压失败时返回原始数据,避免影响主流程
}
return buffer; // 兜底:返回原始 Buffer
}
在请求回调中使用:
javascript
const req = https.request(reqOptions, (res) => {
const chunks = [];
res.on('data', (chunk) => chunks.push(chunk)); // 收集数据块
res.on('end', () => {
const raw = Buffer.concat(chunks); // 拼成完整 Buffer
const body = decompressBody(raw, res.headers['content-encoding']); // 解压
const html = body.toString('utf-8'); // 转成字符串
// 后续处理 HTML...
});
});
关键点
防御性编程------这个函数体现了三个层次的防御:
- 请求时 声明
Accept-Encoding: identity,优先让服务器返回明文 - 响应时 检查
Content-Encoding,如果有压缩就解压 - 解压失败时不崩溃,返回原始数据让后续流程自行判断
不假设服务器一定会按你的要求行事,对各种可能的响应都做好准备------这是网络编程的基本原则。
08 策略组合:完整请求流程
将六项策略组合起来,一个完整的反反爬请求流程如下:
javascript
const https = require('https');
const zlib = require('zlib');
// 完整的带反反爬策略的 HTTP GET 请求
async function antiCrawlerGet(url, cookieStr = '') {
const headers = {
...HEADERS, // 策略三:完整请求头
'User-Agent': getRandomUserAgent(), // 策略一:随机 UA
};
if (cookieStr) {
headers['Cookie'] = cookieStr; // 策略二:携带 Cookie
}
// 策略五:自动重试
return await requestWithRetry(async () => {
return await new Promise((resolve, reject) => {
const urlObj = new URL(url);
const req = https.request({
hostname: urlObj.hostname,
path: urlObj.pathname + urlObj.search,
method: 'GET',
headers,
}, (res) => {
const chunks = [];
res.on('data', (chunk) => chunks.push(chunk));
res.on('end', () => {
const raw = Buffer.concat(chunks);
// 策略六:自动解压
const body = decompressBody(raw, res.headers['content-encoding']);
resolve({
statusCode: res.statusCode,
headers: res.headers,
body: body.toString('utf-8'),
});
});
});
req.on('error', reject);
req.setTimeout(15000, () => {
req.destroy();
reject(new Error('Request timeout'));
});
req.end();
});
}, 2);
}
// 批量采集示例
async function crawlPages(urls) {
// 策略二:先获取 Cookie
const cookieStr = await getCookieFromEntryPage();
const results = [];
for (let i = 0; i < urls.length; i++) {
const resp = await antiCrawlerGet(urls[i], cookieStr);
results.push(resp.body);
// 策略四:随机延迟
if (i < urls.length - 1) {
await randomDelay(500, 1000);
}
}
return results;
}
09 局限性与进阶方向
上述六项策略属于基础伪装层面,能应对大多数基于请求特征的反爬系统。但反爬技术在持续演进,以下场景无法覆盖:
| 反爬手段 | 是否应对 | 说明 |
|---|---|---|
| UA 检测 | 已应对 | UA 池随机轮换 |
| Cookie 检测 | 已应对 | 预先获取再使用 |
| 请求频率限制 | 已应对 | 随机延迟 |
| 网络波动 | 已应对 | 自动重试 + 指数退避 |
| 内容压缩 | 已应对 | 多格式解压兜底 |
| 验证码 | 未应对 | 需要 OCR 或打码平台 |
| IP 封禁 | 未应对 | 需要代理 IP 池 |
| JS 动态渲染 | 未应对 | 需要 Headless Browser(如 Puppeteer) |
| 行为分析 | 未应对 | 需要模拟鼠标轨迹、滚动等操作 |
进阶方向
- 代理 IP 池:维护一批可轮换的代理 IP,避免单一 IP 被封禁。可以结合免费代理源或付费代理服务,定期检测可用性并淘汰失效节点。
- Headless Browser:当目标页面依赖 JS 渲染时,使用 Puppeteer 或 Playwright 模拟完整浏览器环境,能够执行 JS、渲染 DOM、甚至模拟用户交互。
- 验证码识别:对接 OCR 服务(如 Tesseract)或打码平台,自动识别简单的图形验证码。对于滑动验证码,需要分析验证逻辑并模拟滑动轨迹。
- 请求指纹对抗 :高级反爬系统会通过 TLS 指纹(JA3)、HTTP/2 指纹等识别请求来源。可以使用
curl-impersonate或got-scraping等工具伪造浏览器级指纹。
10 总结
六项策略的核心逻辑可以归纳为一句话:让程序的请求行为尽可能接近真实用户。
| 策略 | 解决的问题 | 核心实现 |
|---|---|---|
| UA 随机轮换 | 身份伪装 | 20个UA池随机选取 |
| 预先获取 Cookie | 会话建立 | 先访问入口页拿 Cookie |
| 完整请求头 | 请求特征一致性 | Accept/Referer/Language 齐全 |
| 随机延迟 | 请求频率模拟 | 500~1500ms 随机间隔 |
| 指数退避 | 容错与重试 | 失败后递增等待重试 |
| 多格式解压 | 数据兼容 | gzip/deflate/br 自动解压 |
其中防御性编程的思想------兜底处理、优雅降级、不假设外部系统配合------同样适用于 API 调用、自动化测试、微服务间通信等各类工程场景。