让爬虫请求像真实用户:六项反反爬策略详解

当你写好一个爬虫脚本,兴冲冲地运行,结果收到的不是数据,而是 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.10node-fetch/1.0curl/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

原理

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 浏览器,但请求头中缺少 AcceptReferer,就会暴露爬虫身份。

实现

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...
  });
});

关键点

防御性编程------这个函数体现了三个层次的防御:

  1. 请求时 声明 Accept-Encoding: identity,优先让服务器返回明文
  2. 响应时 检查 Content-Encoding,如果有压缩就解压
  3. 解压失败时不崩溃,返回原始数据让后续流程自行判断

不假设服务器一定会按你的要求行事,对各种可能的响应都做好准备------这是网络编程的基本原则。

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-impersonategot-scraping 等工具伪造浏览器级指纹。

10 总结

六项策略的核心逻辑可以归纳为一句话:让程序的请求行为尽可能接近真实用户

策略 解决的问题 核心实现
UA 随机轮换 身份伪装 20个UA池随机选取
预先获取 Cookie 会话建立 先访问入口页拿 Cookie
完整请求头 请求特征一致性 Accept/Referer/Language 齐全
随机延迟 请求频率模拟 500~1500ms 随机间隔
指数退避 容错与重试 失败后递增等待重试
多格式解压 数据兼容 gzip/deflate/br 自动解压

其中防御性编程的思想------兜底处理、优雅降级、不假设外部系统配合------同样适用于 API 调用、自动化测试、微服务间通信等各类工程场景。

相关推荐
跨境数据猎手2 小时前
多平台电商比价系统从零搭建合规
开发语言·爬虫·架构
wangruofeng1 天前
68K Star,一个爬虫框架凭什么统一了 HTTP、浏览器和 AI Agent
爬虫·github·ai编程
Bryce学亮1 天前
1688 半自动化拟人浏览信息处理工具爬虫
爬虫·python
简简单单就是我_hehe2 天前
前端埋点日志解决方案:完整 User-Agent 解析工具实现,爬虫识别、设备品牌、操作系统、浏览器版本全解析(附完整源码)
前端·爬虫
上海云盾-小余2 天前
遭 CC 攻击店铺直接瘫痪?WAF + 流量防护双重锁死恶意爬虫
爬虫
IP搭子来一个2 天前
爬虫代理IP怎么选?短效代理 vs 隧道代理选型指南(2026最新)
爬虫·tcp/ip
Web极客码2 天前
跨国网络抖动下的 AI 爬虫流调优:我如何用 Claude 打造“智能退避”资讯清洗管道
网络·人工智能·爬虫
武子康2 天前
调查研究-217 Fortress:当浏览器 Agent 开始被网站识别,开源 Chromium 反拦截引擎来了
人工智能·爬虫·agent
标致的自行车3 天前
Selenium 爬虫固定开头:
爬虫·selenium·测试工具