Node.js 爬虫实战指南(四):反反爬策略大全,和网站斗智斗勇

道高一尺,魔高一丈。爬虫与反爬的战争,永无止境。


🎬 开场:一场猫鼠游戏

我曾经爬过一个电商网站。

第一天,一切顺利,数据哗哗地进来。

第二天,开始出现 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("检测结果已保存为截图,请查看")
}

⚠️ 法律与道德提醒

在使用这些技术之前,请务必注意:

  1. 遵守 robots.txt - 这是基本礼仪
  2. 控制请求频率 - 不要给目标网站造成压力
  3. 不爬取敏感数据 - 个人隐私、付费内容等
  4. 不用于非法目的 - 不倒卖、不诈骗
  5. 尊重网站的服务条款 - 有些网站明确禁止爬取

记住:技术是中立的,但使用技术的人要有底线。


🔮 系列总结

到这里,《Node.js 爬虫实战指南》系列就完结了。

让我们回顾一下:

  1. 第一篇:基础入门 - 爬虫原理、法律边界、简单实战
  2. 第二篇:Puppeteer - 动态页面爬取、模拟登录、无限滚动
  3. 第三篇:分布式架构 - 任务队列、Worker、去重、监控
  4. 第四篇:反反爬策略 - 代理池、指纹伪装、验证码处理

希望这个系列能帮你成为一个合格的爬虫工程师。

记住:爬虫是一门技术,更是一门艺术。


💬 互动时间:你遇到过最难搞的反爬措施是什么?是怎么解决的?评论区分享一下!

觉得这个系列有用的话,点赞 + 在看 + 转发,让更多人学会爬虫技术~


相关推荐
程序员爱钓鱼2 小时前
Node.js 编程实战:博客系统 —— 数据库设计
前端·后端·node.js
程序员agions3 小时前
Node.js 爬虫实战指南(二):动态页面爬取,Puppeteer 大显身手
爬虫·node.js
嫂子的姐夫3 小时前
017-续集-贝壳登录(剩余三个参数)
爬虫·python·逆向
李昊哲小课4 小时前
Selenium 自动化测试教程
爬虫·selenium·测试工具
Direction_Wind5 小时前
抖音视频下载,直播间监控,直播间发言采集,最新加密算法
python·node.js
深蓝电商API5 小时前
Scrapy爬取Ajax动态加载页面三种实用方法
爬虫·python·scrapy·ajax
cnxy18818 小时前
Python爬虫进阶:反爬虫策略与Selenium自动化完整指南
爬虫·python·selenium
奶糖的次元空间18 小时前
带你用 Javascript 生成器玩转「会暂停」的函数
node.js
深蓝电商API19 小时前
Scrapy管道Pipeline深度解析:多方式数据持久化
爬虫·python·scrapy