皮皮舟岁月

A JOURNAL OF TRAVEL · CODE · MARKETS
第三十六期 · 第 36 号 二〇二六年八月 · 立秋之后

网站防爬虫技术详解:从原理到实践

2026-09-08 267 阅读

本文是一篇通用技术文章,介绍网站如何识别、应对与防御网络爬虫。 文中所有示例中的域名 example.com、IP 203.0.113.x 均为占位符,请替换为你的实际环境。

爬虫与反爬虫,是一场持续多年的攻防拉锯。这篇文章按照「是什么 → 为什么 → 怎么攻 → 怎么防」的顺序,把整条链路讲清楚:先理解爬虫本身,再明白它为何有害,接着拆解爬虫的实现技术,最后给出分层防御的注意点与通用代码实现。


一、什么是网络爬虫

网络爬虫(Web Crawler / Spider / Bot),是一段自动化的程序,按照预定的规则,批量地向网站发送 HTTP 请求、解析返回的 HTML 页面、提取其中的数据,并通过页面里的链接不断跳转到下一个页面,像蜘蛛结网一样遍历整个站点。

一个最简单的爬虫,本质上只做四件事:

  1. 请求:向目标 URL 发送 HTTP 请求(如 GET /article/1)。
  2. 解析:把返回的 HTML 字符串解析成可操作的结构(DOM 树)。
  3. 提取:按规则从 DOM 中取出标题、正文、图片、价格等数据。
  4. 遍历:从当前页面中发现新的链接,入队继续抓取,直到没有新 URL。
flowchart LR
    A[种子 URL 队列] --> B[发送 HTTP 请求]
    B --> C[解析 HTML / DOM]
    C --> D[提取目标数据]
    C --> E[提取页面中的新链接]
    D --> F[清洗 / 结构化 / 入库]
    E --> A
    F --> G[存储:数据库 / 文件]

爬虫的两副面孔

爬虫本身是中性的技术工具,关键在于使用者的目的与行为是否合规:

类型 代表 特征 是否友好
搜索引擎爬虫 Googlebot、Bingbot、Baiduspider 声明 UA、遵守 robots.txt、低频礼貌抓取 友好,应该放行
通用采集爬虫 数据采集工具、自写脚本 高频抓取、随意请求 不友好,消耗资源
定向竞争爬虫 比价、情报、监控类 只抓目标页面,频率可能很高 不友好
恶意爬虫 撞库、漏洞扫描、内容搬运 伪装、暴力、无节制 必须防御

搜索引擎爬虫是网站流量的重要来源,应当配合;而后面三类,才是防爬虫要对付的对象。这也是防爬虫的第一原则——不是把所有爬虫都挡掉,而是把不守规矩的挡掉


二、爬虫的危害:抓走的数据去干什么了

爬取的数据用来干什么

明确"爬虫抓了数据去干嘛",才能理解为什么值得防:

  • 内容搬运与洗稿:整站抓取文章、图片,搬去自己的站,或改头换面后发布。原创内容被复制,搜索引擎会判定重复内容,导致原创站点排名下降。
  • 价格监控与竞争情报:抓电商商品的价格、库存、活动,用于自动比价、跟价,或分析对手的运营策略。
  • 数据倒卖:抓取用户公开信息(联系方式、评论、社交资料),打包后售卖,可能构成个人信息侵权。
  • AI 训练语料:抓取大量文章、问答、代码,作为大模型训练的语料来源,近年尤甚。
  • 统计污染与广告作弊:伪造大量访问,刷高 PV/UV 或触发广告点击,扭曲数据分析结论,骗取广告分成。
  • 攻击前的侦察:爬虫遍历站点结构,顺带探测后台路径、接口参数、敏感文件,是后续攻击(注入、爆破、撞库)的前哨。

为什么要防爬虫

归纳起来,危害集中在四个层面:

  1. 服务器资源被耗尽:高频请求吃满带宽、CPU 与数据库连接。一个小站点可能被几只爬虫打到服务不可用,真实用户反而打不开。
  2. 内容资产流失:辛苦产出的原创内容被无成本搬运,SEO 权重、广告价值都被转移。
  3. 用户数据安全:公开信息被大规模聚合倒卖,隐私风险与法律风险并存。
  4. 业务数据失真:访问统计、转化数据被污染,基于数据的决策全部失效。

一句话总结:爬虫用极低的成本,占用了你的资源、拿走了你的资产、污染了你的数据。 防爬虫,本质上是保护自己的资源、内容与决策依据。


三、爬虫用什么技术实现

理解爬虫的技术栈,是理解反爬虫手段的前提——每个反爬手段,都是在针对爬虫技术链路中的某一环。

基础技术栈

环节 常用工具 说明
HTTP 请求 Python requestscurl、Node axios、Go net/http 发出请求、携带 headers、处理 cookie
HTML 解析 BeautifulSoup、lxml、正则表达式 把 HTML 转成可查询的 DOM
数据提取 XPath、CSS 选择器、JSONPath 精准定位目标字段
并发加速 多线程、协程(asyncio)、Scrapy 框架 单线程太慢,并发可提升数倍到数十倍
分布式 Scrapy + Redis 调度、Celery 多台机器协同,适合海量站点
浏览器模拟 Selenium、Playwright、Puppeteer 对付纯 JS 渲染的页面,真实运行浏览器

反检测技术

真实世界的爬虫,远不止"发请求解析 HTML"这么简单。为了绕过网站防护,成熟爬虫会叠加以下手段:

  • 代理 IP 池:购买大量代理 IP(甚至住宅代理),每次请求换 IP,绕过"按 IP 限流封禁"。
  • 伪造身份特征:随机切换 User-Agent、伪造 Referer、模拟 Cookie 会话,让请求看起来像真实浏览器。
  • 浏览器指纹模拟:通过 Playwright 等工具伪造 Canvas 指纹、WebGL 信息、时区、语言环境,通过 JS 人机检测。
  • 验证码识别:对接打码平台或训练 OCR 模型,自动识别图形/滑块验证码。
  • JS 渲染执行:用无头浏览器完整执行页面 JavaScript,抓取动态渲染后的内容。
  • 频率与行为伪装:随机延迟、模拟滚动与鼠标轨迹、错峰抓取,让访问模式"像人"。

一个通用爬虫的实现示例

下面是一个最小的通用爬虫(Python),目标是抓取一个博客站的文章标题列表。理解它的结构,就能理解它每一步都在对抗什么。

import requests
from bs4 import BeautifulSoup

# 1. 请求:携带伪装过的 User-Agent 和 Referer
headers = {
    "User-Agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) "
                  "AppleWebKit/537.36 (KHTML, like Gecko) Chrome/125.0 Safari/537.36",
    "Referer": "https://example.com/",
}

resp = requests.get("https://example.com/archive", headers=headers, timeout=10)
resp.raise_for_status()

# 2. 解析 HTML
soup = BeautifulSoup(resp.text, "html.parser")

# 3. 提取数据:取所有文章卡片里的标题与链接
for card in soup.select(".post-card"):
    title = card.select_one("h2").get_text(strip=True)
    link = card.select_one("a")["href"]
    print(title, "->", link)

要加速,就开多线程并发;要伪装更深,就上代理池 + 无头浏览器。爬虫的一切技术演进,都是为了回答同一个问题:怎么让服务器相信"我是人"。 反爬虫的答案也因此很简单:想尽办法证明"你不是人"。


四、防爬虫:注意点、原理与通用实现

4.1 先记住五个注意点

1. 防爬不等于挡死所有爬虫。 搜索引擎爬虫是流量来源,必须在白名单里放行。判断"该不该防",看的是行为(频率、路径、UA 是否合规),而不是"是不是程序"。

2. 单点防护必被绕过。 UA 过滤会被伪造、IP 限流会被代理池绕过、验证码有打码平台。任何单一手段都只是"增加成本",只有多层叠加,才能把爬虫的边际成本抬到不值得。

3. 防护要分层、按成本递增。 便宜的规则(UA、频率)挡掉 90% 的初级爬虫;贵的方案(JS 挑战、验证码)留给少数顽固分子。不要一开始就对所有用户上重型方案,那会误伤真实用户。

4. 误伤真实用户比被爬更糟。 限流太狠,正常用户被 429;验证码太频繁,读者流失。防爬方案上线前必须评估对真实流量的影响,并留出宽松的豁免通道(如放行搜索引擎、放行无 Referer 的直接访问)。

5. 防护要与数据、合规平衡。 记录 IP 用于封禁时要遵守隐私法规;使用验证码要考虑无障碍(无障碍用户无法通过图形验证码)。

4.2 手段全景:原理与实现

下面按「成本从低到高、强度从弱到强」排列,每一层都给出原理和通用实现。

手段一:robots.txt 与 meta robots —— 声明层面

原理robots.txt 是网站与"守规矩的爬虫"之间的君子协定,声明哪些路径允许抓取。它只能约束合规爬虫(搜索引擎等),恶意爬虫根本不会读它。所以它是"最低成本的第一道声明",不是防护。

实现(放到站点根目录 https://example.com/robots.txt):

User-agent: *
Disallow: /admin/
Disallow: /api/
Disallow: /search

User-agent: Googlebot
Allow: /

页面上也可以单独声明"本页不要被索引":

<meta name="robots" content="noindex, nofollow">

手段二:User-Agent 识别与过滤 —— 身份层面

原理:友好爬虫会声明自己的 UA(如 Googlebot/2.1),恶意爬虫常常用伪造的浏览器 UA 或干脆不声明。通过维护"已知友好爬虫白名单 + 黑名单特征"来过滤。成本极低,但 UA 可以伪造,只能挡新手。

实现(Nginx):

# 放行搜索引擎爬虫(节选常见搜索引擎)
map $http_user_agent $search_bot {
    default 0;
    ~*Googlebot     1;
    ~*bingbot       1;
    ~*Baiduspider   1;
    ~*YisouSpider   1;
}

# 拦截已知恶意 UA 特征(黑名单,按需扩展)
if ($http_user_agent ~* (curl|wget|python-requests|scrapy|Go-http-client)) {
    return 403;
}

手段三:频率限制(Rate Limit)—— 行为层面,性价比最高

原理:人的访问频率天然有限(一秒内打开 10 个页面已经算快),而爬虫可以一秒发上百个请求。按 IP 统计单位时间内的请求数,超过阈值就限流。实现上常用漏桶 / 令牌桶算法:漏桶把请求均匀排空,令牌桶允许一定程度的突发。

实现(Nginx limit_req,令牌桶思想):

# 1. 定义限流区域:以客户端 IP 为键,10m 共享内存约能记录 16 万 IP
limit_req_zone $binary_remote_addr zone=req_per_ip:10m rate=5r/s;

# 2. 应用:每秒超过 5 个请求时,允许突发 20 个,其余返回 429
location / {
    limit_req zone=req_per_ip burst=20 nodelay;
    limit_req_status 429;
}

注意:静态资源(css/js/图片)不要计入限流,否则浏览器加载一个页面几十个静态请求会误伤真实用户。用 map 按路径区分动态与静态请求:

# 只有动态请求参与限流,静态资源与后台路径豁免
map $request_uri $rate_key {
    ~^/admin/            "";
    ~*\.(css|js|png|jpg|jpeg|gif|svg|webp|woff2?|txt)$ "";
    default $binary_remote_addr;
}
limit_req_zone $rate_key zone=req_dyn:10m rate=5r/s;

手段四:IP 封禁与黑白名单 —— 身份层面

原理:把高频异常 IP 加入黑名单直接拒绝。可以手动维护,也可以让工具自动检测(fail2ban 监控日志,连续失败/高频访问自动封禁)。局限是代理池让封禁变得低效,封一个换一个,因此 IP 封禁通常与频率限制配合使用。

实现(fail2ban 思路 + Nginx 黑名单文件):

# 黑名单文件 /etc/nginx/conf.d/blacklist.conf,fail2ban 或脚本追加
deny 203.0.113.7;
deny 203.0.113.0/24;
# fail2ban 配置文件 /etc/fail2ban/jail.local(通用示例)
[nginx-badbots]
enabled = true
filter = nginx-badbots
logpath = /var/log/nginx/access.log
maxretry = 5
findtime = 60
bantime = 3600

手段五:Referer 校验与防盗链 —— 来源层面

原理:正常用户在页面内浏览,请求图片等资源时会携带本站页面的 Referer;而外部直接引用(盗链)或爬虫直连资源,Referer 为空或来自外站。通过白名单校验 Referer 拦截非站内来源。注意要放行"直接访问(none)"——否则从地址栏输入地址打开图片的用户会被误伤。

实现(Nginx 防盗链):

location /uploads/ {
    valid_referers none blocked server_names *.example.com
                   baidu.com *.baidu.com google.com *.google.com
                   bing.com *.bing.com;
    if ($invalid_referer) {
        return 403;
    }
}

手段六:人机验证(验证码 / 滑块 / 点击)—— 交互层面

原理:让客户端完成只有"人"才能完成的交互任务(识别图形、拖动滑块、点击指定目标、完成拼图),由服务端校验结果。验证码本身可被打码平台破解,因此强度要匹配内容价值——普通文章不需要,敏感接口才值得上。

实现(通用思路,服务端校验伪代码):

# 以极简的"点击坐标校验"为例:服务端下发题目,客户端回传答案
# 实际生产建议使用成熟方案:reCAPTCHA、极验、腾讯云验证码等
@app.post("/api/captcha/verify")
def verify_captcha(payload):
    answer = payload["answer"]          # 客户端回传的答案
    token = payload["token"]            # 服务端下发的题目标识
    expected = cache.get(f"captcha:{token}")
    return {"ok": answer == expected}

手段七:行为分析与 JS 挑战 —— 深层人机识别

原理:观察"人"的特征——鼠标轨迹是否平滑、页面停留是否有滚动、请求间隔是否呈自然分布、浏览器能否执行复杂 JS。著名的 Cloudflare、Akamai 等防护,核心就是给浏览器下发一段 JS 计算挑战,只有真实浏览器才能算出正确结果,程序化请求直接拿不到响应。

实现(通用思路:先下发 JS 挑战,通过后签发短期 Cookie):

// 服务端下发到浏览器执行的挑战脚本(示意),
// 计算一个需要浏览器环境才能得出的值,回传后服务端签发会话
const answer = (() => {
  const canvas = document.createElement("canvas");
  const ctx = canvas.getContext("2d");
  /* 基于浏览器环境生成指纹签名 */
  return signature;
})();
fetch("/challenge/verify", { method: "POST", body: JSON.stringify({ answer }) });

自建 JS 挑战工程量大且易被绕过,中小站点建议直接用 Cloudflare 等托管方案。

手段八:接口签名与动态令牌 —— 数据链路层面

原理:爬虫通常直连后端接口拿 JSON,而不是渲染页面。给接口加上"签名 + 时效令牌":前端把参数、时间戳、密钥按规则算出签名,服务端校验,过期作废。签名机制让爬虫无法直接构造合法请求,必须逆向前端代码,成本大增。

实现(前后端签名校验,通用思路):

# 服务端校验示例(Python / Flask 风格)
import hashlib, hmac, time

SECRET = "your-app-secret"

@app.get("/api/articles")
def api_articles():
    ts = request.args.get("ts")
    sign = request.args.get("sign")
    # 1. 校验时间戳时效(例如 5 分钟内)
    if abs(int(time.time()) - int(ts)) > 300:
        return {"error": "expired"}, 403
    # 2. 用同样的规则重算签名并比对
    raw = f"/api/articles?ts={ts}&key={SECRET}"
    expect = hmac.new(SECRET.encode(), raw.encode(), hashlib.sha256).hexdigest()
    if not hmac.compare_digest(expect, sign):
        return {"error": "bad sign"}, 403
    return {"list": articles}

手段九:数据与内容层面的迷惑 —— 混淆层面

原理:让"即使抓到了,数据也难用"——正文图片化(爬走的是图片而非文本)、关键字段字体混淆、接口返回加密内容、图片懒加载(真实地址在 JS 里才出现)。这类手段不阻止抓取,但大幅提高数据清洗成本。

实现(图片懒加载,真实地址延迟注入):

<!-- 爬虫直接抓 HTML 只能拿到占位图,真实图片地址由 JS 填入 -->
<img data-src="/img/real-20260908.png" src="/img/placeholder.png" alt="文章配图">
<script>
  document.querySelectorAll("img[data-src]").forEach(img => {
    img.src = img.dataset.src;
  });
</script>

手段十:蜜罐陷阱(Honeypot)—— 主动钓鱼

原理:在页面里放一个"正常用户永远看不见、不会点"的隐藏链接(如不可见的 <a href="/honeypot-trap">)。爬虫抓取页面时会顺着所有链接去抓,一访问蜜罐地址就暴露身份,随即封禁其 IP。真实用户看不到、不会点,所以零误伤,这是它最优雅的地方。

实现(HTML 隐藏蜜罐 + 服务端检测):

<!-- 页面中:用 CSS 隐藏,正常用户不可见 -->
<a href="/trap/verify-bot" style="display:none" aria-hidden="true">管理员入口</a>
# 服务端:访问了蜜罐地址的,基本可判定为爬虫
@app.get("/trap/verify-bot")
def trap():
    ip = request.remote_addr
    banned.add(ip)          # 加入封禁名单
    return "ok", 404        # 返回 404,不暴露痕迹

4.3 分层防御总览

没有任何单一手段能一劳永逸,正确姿势是按成本分层叠加,让每一层过滤掉一部分,最终把顽固爬虫的抓取成本抬到不值得:

flowchart TD
    A[所有请求] --> B{robots.txt 声明}
    B -->|守规矩的爬虫| C[放行并抓取]
    B -->|不守规矩| D{UA 识别}
    D -->|黑名单特征| E[403 拒绝]
    D -->|白名单搜索爬虫| F[放行]
    D -->|其他| G{频率限制}
    G -->|超阈值| H[429 限流]
    G -->|正常| I{Referer 校验}
    I -->|非法来源| J[403 防盗链]
    I -->|正常| K{行为分析 / JS 挑战}
    K -->|疑似机器| L[验证码 / 封禁]
    K -->|通过| M[正常访问]
    M --> N{蜜罐监控}
    N -->|命中| O[IP 拉黑]
    N -->|未命中| P[正常服务]

最后回到最开始的提醒:防爬虫的目标不是"一个爬虫都进不来",而是"让爬虫的抓取成本高于收益"。对个人站点而言,robots.txt + UA 过滤 + 动态请求限流 + 防盗链这四件套,已经能挡住绝大多数初级爬虫;只有内容价值足够高、或遭遇针对性采集时,才需要引入验证码与 JS 挑战这类重型手段。层数多不代表安全,每一层都要评估对真实用户的代价——让读者顺畅访问,永远比让爬虫难受更重要。


参考阅读

  • RFC 9110(HTTP 语义与缓存,含 Referer 语义)
  • Nginx 官方文档:ngx_http_limit_req_modulengx_http_referer_module
  • robots.txt 协议规范:https://www.robotstxt.org/
分享此文

评论 (0)

还没有评论,来抢沙发。

发表评论