适用场景
站点内容被大量自动化抓取器批量搬运,服务器带宽和 CPU 被吃满,而对方只承担极低的成本。这套方案适合有独立服务器或可控 Nginx 的站长、运维,通过「识别 → 分级限速 → 高成本响应 → 计费墙」四层动作,让合规的普通读者不受影响,让大规模抓取方付出时间与带宽代价。方案不依赖任何商业爬虫管理服务,全部在 Nginx 内部完成。
环境与前置条件
- 操作系统:主流 Linux 发行版均可(Debian/Ubuntu、Rocky/Alma、CentOS Stream 等),需要 root 或 sudo 权限。
- Web 服务器:Nginx,以官方文档当前版本为准。需要确认内置模块
http_limit_req_module、http_limit_conn_module、http_map_module、http_geo_module、http_realip_module均已编译进去,主流发行版软件包默认都带。用nginx -V 2>&1 | tr ' ' '\n' | grep -E 'limit_req|limit_conn|http_map|http_geo|realip'检查。 - 辅助工具:
curl用于验证,jq用于解析日志(可选但强烈建议)。 - 资源建议:识别与限速本身几乎不占资源;真正消耗在「高成本响应」上,因为慢速传输会长时间占用连接。按每 1000 个并发慢速连接预留约 2MB 内存估算,并把
worker_connections调到能承受的量级。磁盘上为投喂文件预留几十 MB 即可,不要生成超大文件。 - 前置确认:如果站点前面有 CDN 或负载均衡,必须先配好
real_ip模块,否则所有请求的$remote_addr都是同一个代理 IP,限速会误伤全体用户。
分步骤部署
第 1 步:把访问日志改成可解析的 JSON
先让日志能算账,后面所有分析和封禁都靠它。新建 /etc/nginx/conf.d/ai-wallet-log.conf(确认它被 http {} 块 include 到):
```nginx
log_format wallet escape=json
'{"ts":"$time_iso8601","ip":"$remote_addr","m":"$request_method",'
'"u":"$request_uri","s":$status,"bytes":$body_bytes_sent,'
'"ua_class":$ua_class,"head_class":$head_class,"rt":$request_time,'
'"ua":"$http_user_agent"}';
```
escape=json 必须写,否则 User-Agent 里出现引号会让整行 JSON 失效。
第 2 步:按 User-Agent 识别「自报家门」的智能体
新建 /etc/nginx/conf.d/ai-wallet-map.conf。注意 map 只能写在 http 块级别,不能写进 server 或 location:
```nginx
map $http_user_agent $ua_class {
default 0;
"" 2; # 完全没有 UA,优先级最高
"~*GPTBot" 1;
"~*OAI-SearchBot" 1;
"~*ChatGPT-User" 1;
"~*ClaudeBot" 1;
"~*anthropic-ai" 1;
"~*CCBot" 1;
"~*Bytespider" 1;
"~*AI 词典:Perplexity">PerplexityBot" 1;
"~*meta-externalagent" 1;
"~*Amazonbot" 1;
"~*Applebot-Extended" 1;
"~*cohere-ai" 1;
"~*Diffbot" 1;
"~*ImagesiftBot" 1;
"~*YouBot" 1;
}
```
0 表示未识别,1 表示声明自己是 AI 抓取器,2 表示空 UA。这里的片段只是常见示例,各家 UA 会调整,请以各家官方文档当前的 UA 说明为准,并把这张表当作需要定期更新的配置项。注意不要误伤 Googlebot、Bingbot 这类搜索爬虫,除非你确认要这么做。
第 3 步:用请求头指纹识别「不说实话」的抓取器
伪装 UA 很容易,但完整模拟浏览器要难得多。真实浏览器几乎总会带 Accept、Accept-Language,现代浏览器还会带 Sec-Fetch-* 系列头;而大量脚本的 Accept 是 */* 或干脆为空。
```nginx
map $http_accept $accept_class {
default 0;
"" 1;
"~*^\*/\*\s*$" 1;
}
map "$accept_class|$http_accept_language" $head_class {
default 0;
"1|" 1;
}
```
含义是:Accept 为空或只有 */*,且没有 Accept-Language,就记为可疑。这个组合对正常浏览器几乎不会触发,对命令行脚本触发率较高。
第 4 步:配置限速与并发上限
限速分两类:声明式爬虫直接进计费墙,可疑流量进慢速通道。关键在于只给可疑流量计数,正常用户完全不受影响。
```nginx
key 为空字符串时,Nginx 不计入该 zone,正常用户因此不受限
map $head_class $suspect_key {
0 "";
1 $binary_remote_addr;
}
limit_req_zone $suspect_key zone=ai_suspect:10m rate=20r/m;
limit_conn_zone $suspect_key zone=ai_conn:10m;
limit_req_zone $binary_remote_addr zone=all_req:20m rate=60r/s;
limit_conn_zone $binary_remote_addr zone=all_conn:20m;
```
rate=20r/m 大约每 3 秒一个请求,比正常人类阅读节奏还慢。all_req 是兜底防 CC,正常用户碰不到。
第 5 步:生成投喂文件(高成本响应)
让可疑抓取器每次请求都拿到一个体积不小、内容无关的文本,并限速发送。它要么浪费带宽,要么花更长的解析时间。
```bash
sudo mkdir -p /var/www/tarpit
python3 - <<'PY'
import random
words = ["market","window","harbor","signal","granite","orbit","timber",
"ledger","compass","meadow","quartz","lantern","atlas","bridge"]
with open("/var/www/tarpit/feed.txt", "w") as f:
for i in range(20000):
f.write(f"{i}: " + " ".join(random.choices(words, k=18)) + "\n")
PY
ls -lh /var/www/tarpit/feed.txt
```
文件控制在 1–2MB 即可。配合下面的 8KB/s 限速,单个请求会被拖住两分钟左右,足够劝退大多数低成本爬虫。
第 6 步:在站点 server 块中组合处置策略
```nginx
server {
listen 443 ssl;
http2 on;
server_name example.com;
root /var/www/site;
default_type text/plain;
声明式 AI 抓取器:直接返回计费墙
if ($ua_class = 1) {
return 402 "402 Payment Required\nAutomated retrieval is billed. See /pricing for terms.\n";
}
头部指纹异常:转到慢速投喂
if ($head_class = 1) {
rewrite ^ /tarpit/feed.txt last;
}
location / {
limit_req zone=all_req burst=200 nodelay;
limit_conn all_conn 60;
limit_req_status 429;
try_files $uri $uri/ =404;
}
location = /tarpit/feed.txt {
internal;
limit_rate 8k;
limit_rate_after 16k;
}
location = /robots.txt {
try_files /robots.txt =404;
}
}
```
limit_rate_after 16k 表示先快速发 16KB(让对方的客户端认为连接正常),之后才降到 8KB/s。
第 7 步:在 robots.txt 里把「计费墙」说清楚
```txt
User-agent: GPTBot
Disallow: /
User-agent: CCBot
Disallow: /
User-agent: *
Allow: /
Crawl-delay: 1
```
robots.txt 只是君子协定,但不写它会让「计费墙」缺少说明。同时准备一个 /pricing 页面,写明允许的抓取方式和联系方式,避免误伤合规方。
第 8 步:按日志生成封禁名单
新建 /usr/local/bin/ai-blocklist.sh:
```bash
#!/usr/bin/env bash
set -euo pipefail
LOG=/var/log/nginx/wallet.log
OUT=/etc/nginx/conf.d/ai-blocklist.conf
THRESHOLD=200
cp "$OUT" "${OUT}.bak" 2>/dev/null || true
{
echo "# generated $(date -Iseconds)"
jq -r 'select(.s == 429 or .s == 402) | .ip' "$LOG" \
| sort | uniq -c | awk -v t="$THRESHOLD" '$1 >= t {print $2 " 1;"}'
} > "$OUT"
nginx -t && nginx -s reload
```
配套在 http 块加:
```nginx
geo $blocked_ip {
default 0;
include /etc/nginx/conf.d/ai-blocklist.conf;
}
```
再在 server 块顶部加 if ($blocked_ip) { return 429; }。挂到 cron 里每 30 分钟跑一次。阈值 200 按站点体量调整,先观察一周再收紧。
验证部署是否成功
```bash
sudo nginx -t
sudo nginx -s reload
```
预期输出 syntax is ok 与 test is successful。然后逐条验证:
```bash
1. 声明式 AI 爬虫 → 应返回 402
curl -s -o /dev/null -w '%{http_code}\n' -A 'GPTBot/1.0' https://example.com/
2. 伪装成 curl 但没有 Accept-Language → 应命中慢速通道,耗时应明显变长
curl -s -o /dev/null -w '%{http_code} %{time_total}\n' \
-A 'Mozilla/5.0 (compatible)' -H 'Accept: */*' https://example.com/
3. 模拟真实浏览器 → 应立即返回 200,耗时在百毫秒级
curl -s -o /dev/null -w '%{http_code} %{time_total}\n' \
-A 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36' \
-H 'Accept: text/html,application/xhtml+xml' \
-H 'Accept-Language: zh-CN,zh;q=0.9' https://example.com/
4. 连续请求验证限速 → 后续请求应出现 429
for i in $(seq 1 40); do
curl -s -o /dev/null -w '%{http_code} ' -H 'Accept: */*' https://example.com/
done; echo
5. 确认日志字段齐全
tail -n 1 /var/log/nginx/wallet.log | jq .
```
预期结果:第 1 条为 402;第 2 条 time_total 显著大于第 3 条;第 4 条输出中能看到若干 429;第 5 条能正确解析出 ua_class、head_class 字段。
常见报错与解决
报错:nginx: [emerg] "map" directive is not allowed here
→ 原因是 map、limit_req_zone、limit_conn_zone 被写进了 server 或 location 块。
→ 解决:把这些指令移到 http 块级别(放进 conf.d/*.conf,或 nginx.conf 的 http {} 内),然后 nginx -t 确认。
报错:nginx: [emerg] unknown limit_req zone "ai_suspect"
→ 原因是 limit_req 引用的 zone 名与 limit_req_zone 定义的名字不一致,或 zone 定义所在文件没有被 include。
→ 解决:grep -rn "limit_req_zone" /etc/nginx/ 核对名称;确认 nginx.conf 中 include /etc/nginx/conf.d/*.conf; 位于 http {} 内。
现象:正常用户被大量 429,投诉页面打不开
→ 原因一是站点前面有 CDN,$remote_addr 全是回源 IP,所有人共用一个限速桶;原因二是 head_class 规则过宽,把某些 API 客户端或老旧浏览器判成可疑。
→ 解决:先配 real_ip;再临时把 limit_req 的 rate 放宽,用 tail -f 观察 wallet.log 里的 head_class 分布,确认误伤来源后再收紧规则。
现象:站点出现大量超时,worker_connections are not enough
→ 原因是慢速投喂把连接长时间占住,连接池被耗尽。
→ 解决:把 limit_conn 的上限调低、把 feed.txt 缩小、把 limit_rate 调大,或在 http 块提高 worker_connections 并适当增加 worker_processes。
现象:limit_req 触发后返回 503,监控告警「服务不可用」
→ 原因是 Nginx 默认用 503 表示限流。
→ 解决:在 location 中加 limit_req_status 429;,并同步更新监控规则,把 429 单独统计而不是算进 5xx。
后续维护
- 规则更新:AI 抓取器的 UA 与 IP 段会变,建议每季度对照各家官方文档更新一次
$ua_class映射表,并保留变更记录,方便回滚。 - 名单备份:
ai-blocklist.conf由脚本覆盖生成,脚本里已做.bak备份;把备份目录纳入日常备份范围,避免误封后无从追溯。 - 日志轮转:给
wallet.log配置 logrotate,按天切割、保留 14–30 天并压缩。分析脚本只读当天和前一天的文件,避免日志变大后jq拖慢机器。 - 升级流程:升级 Nginx 前先在测试环境跑一遍完整配置,
nginx -t通过后再灰度。limit_*与map都是稳定模块,但版本间对if块内指令的处理有差异,以官方文档当前版本说明为准。 - 监控指标:重点盯四个数——402 与 429 的占比、平均
request_time、出网带宽、并发连接数。若平均响应时间随 429 一起上涨,说明慢速通道占比过高,需要收紧limit_conn。 - 白名单机制:为监控探针、合作方、搜索引擎保留独立放行规则(例如按 Header 中的密钥或固定 IP 段),并在改动策略前先确认白名单仍然生效。
- 成本核算:每隔一段时间统计投喂通道消耗的带宽,并与抓取量对比。如果对方改用分布式 IP 绕过限速,说明单点限速已经不够,此时应把策略前移到 CDN 或边缘层,配置思路不变。
