本文是一篇通用技术文章,介绍网站如何识别、应对与防御网络爬虫。 文中所有示例中的域名
example.com、IP203.0.113.x均为占位符,请替换为你的实际环境。
爬虫与反爬虫,是一场持续多年的攻防拉锯。这篇文章按照「是什么 → 为什么 → 怎么攻 → 怎么防」的顺序,把整条链路讲清楚:先理解爬虫本身,再明白它为何有害,接着拆解爬虫的实现技术,最后给出分层防御的注意点与通用代码实现。
一、什么是网络爬虫
网络爬虫(Web Crawler / Spider / Bot),是一段自动化的程序,按照预定的规则,批量地向网站发送 HTTP 请求、解析返回的 HTML 页面、提取其中的数据,并通过页面里的链接不断跳转到下一个页面,像蜘蛛结网一样遍历整个站点。
一个最简单的爬虫,本质上只做四件事:
- 请求:向目标 URL 发送 HTTP 请求(如
GET /article/1)。 - 解析:把返回的 HTML 字符串解析成可操作的结构(DOM 树)。
- 提取:按规则从 DOM 中取出标题、正文、图片、价格等数据。
- 遍历:从当前页面中发现新的链接,入队继续抓取,直到没有新 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 或触发广告点击,扭曲数据分析结论,骗取广告分成。
- 攻击前的侦察:爬虫遍历站点结构,顺带探测后台路径、接口参数、敏感文件,是后续攻击(注入、爆破、撞库)的前哨。
为什么要防爬虫
归纳起来,危害集中在四个层面:
- 服务器资源被耗尽:高频请求吃满带宽、CPU 与数据库连接。一个小站点可能被几只爬虫打到服务不可用,真实用户反而打不开。
- 内容资产流失:辛苦产出的原创内容被无成本搬运,SEO 权重、广告价值都被转移。
- 用户数据安全:公开信息被大规模聚合倒卖,隐私风险与法律风险并存。
- 业务数据失真:访问统计、转化数据被污染,基于数据的决策全部失效。
一句话总结:爬虫用极低的成本,占用了你的资源、拿走了你的资产、污染了你的数据。 防爬虫,本质上是保护自己的资源、内容与决策依据。
三、爬虫用什么技术实现
理解爬虫的技术栈,是理解反爬虫手段的前提——每个反爬手段,都是在针对爬虫技术链路中的某一环。
基础技术栈
| 环节 | 常用工具 | 说明 |
|---|---|---|
| HTTP 请求 | Python requests、curl、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_module、ngx_http_referer_module - robots.txt 协议规范:
https://www.robotstxt.org/
评论 (0)
还没有评论,来抢沙发。