反爬规则上线后怎么维护?命中统计与定期清理

反爬规则写完就没人碰,UA 换代、IP 段迁移、新路径漏配,拦截率会悄悄归零。本文用 10 个问答讲清反爬规则怎么统计命中、怎么清理僵尸规则、怎么回归验证。

Q:反爬规则为什么会越用越失效?

  • 规则不是一次性资产,是有保质期的耗材。失效通常来自五个方向:
  • UA 换代:浏览器每四到六周升一次大版本。把 UA 写成「Chrome/120」再写个精确等于,半年后规则全废。
  • 蜘蛛 IP 段迁移:引擎会新增和回收网段。白名单不更新,要么误伤真蜘蛛,要么把已经转手的 IP 继续放行。
  • 业务新增路径:上了新目录、新接口,规则只写在老的 location 里,新路径一条都不生效。
  • 规则互相覆盖:Nginx 是从上往下匹配的,前面的规则先命中并 return 了,后面写得再漂亮也不会执行。
  • 业务变更:换 CDN、加子域、上 HTTPS 之后 set_real_ip_from 没跟着补,日志里全是节点 IP,按 IP 做的规则全部失灵。

Q:怎么统计出每条规则到底拦了多少?

  • 核心做法只有一步:给规则打标签,把标签落进访问日志,然后按周聚合。没有标签,你永远只知道「总共拦了多少」,不知道「哪一条在干活」。
  • map 是零成本的,命中判断在变量求值时完成,不额外发起子请求。
map $http_user_agent $bot_rule {
    default                     "";
    "~*SemrushBot"              "ua_semrush";
    "~*AhrefsBot"               "ua_ahrefs";
    "~*MJ12bot"                 "ua_mj12";
    "~*(python-requests|curl)"  "ua_tool";
    ""                          "ua_empty";
}

map $bot_rule $bot_blocked {
    ""      0;
    default 1;
}

log_format bot '$remote_addr - [$time_local] "$request" $status '
               'rule=$bot_rule blocked=$bot_blocked rt=$request_time';
  • 日志落好之后,一条 awk 就能出周报:
awk '{for(i=1;i<=NF;i++){if($i ~ /^rule=/){split($i,a,"="); if(a[2] != "") c[a[2]]++}}}
     END{for(k in c) printf "%8d  %s\n", c[k], k}' /var/log/nginx/bot.log | sort -rn
  • 把结果贴进周报,你立刻能看出:哪条规则一天拦几万次、哪条一周零命中。零命中的那批就是需要复核的对象。

Q:命中 0 的规则一定是废规则吗?

  • 不一定,先分清三种情况再动手删:
  • 真没流量:低频爬虫几个月来一次。这类规则留着不亏,但要确认它真的会被匹配。
  • 被前面的规则先拦了:两条规则覆盖同一批 UA,后一条永远轮不到。这种情况删掉后一条不算损失,但顺序要重新排。
  • 规则本身写错:正则漏了转义、大小写没处理、全角空格混进去、map 里用了 ~ 却写成 ~ *。这种规则上线即死,日志里零命中,最容易被当成「没问题」。
  • 判别方法:把它临时挪到规则链最前面,并且只记日志不拦截,跑满 24 小时看有没有命中。

Q:哪些规则应该直接删掉?

  • 满足下面任意一条,就可以进清理清单:
  • 连续四周零命中,且近 90 天 UA 榜单里查不到对应客户端。
  • 指名的 UA 已经不存在,或者该爬虫早已停止运营。
  • 已经被 CDN 或 WAF 侧的同名规则覆盖,源站再拦一遍属于重复劳动,还会让排查时分不清是谁拦的。
  • 两条规则判定条件完全等价,只是写法不同。
  • 清理顺序:先注释保留七天,确认拦截量曲线没有变化,再彻底删掉。上来就 rm 是给自己挖坑。

Q:蜘蛛 IP 段多久更新一次,能自动化吗?

  • 能。Google 和 Bing 都有官方 JSON,可以直接拉;百度没有公开 JSON,要用反向解析 PTR 校验。
# ① Google 官方 IP 段
curl -s https://developers.google.com/search/apis/ipranges/googlebot.json \
  | python3 -c 'import json,sys;d=json.load(sys.stdin);print("\n".join(p["ipv4Prefix"] for p in d["prefixes"] if "ipv4Prefix" in p))' \
  > /tmp/googlebot.txt

# ② 百度蜘蛛:PTR 必须落在 crawl.baidu.com
for ip in 116.179.32.1 220.181.108.1; do
  echo "$ip -> $(dig +short -x $ip)"
done

# ③ 生成 geo 白名单并热加载
{
  echo "geo \$realip_remote_addr \$is_engine {"
  echo "    default 0;"
  sed 's/.*/    & 1;/' /tmp/googlebot.txt
  echo "}"
} > /etc/nginx/conf.d/engine_geo.conf
nginx -t && nginx -s reload
  • 节奏建议:官方 JSON 每天拉一次做 diff,有变化才 reload;PTR 校验每月抽查一批;百度侧每次收到「抓取异常」告警时立刻复核。
  • 重点:白名单要按 IP 段而不是按 UA。UA 是客户端自己写的,IP 段你说了不算。

Q:规则改完怎么确认没误伤真蜘蛛?

  • 三步回归,缺一步都可能把收录搞掉:
  • 抽样请求:拿官方 UA 配真实引擎 IP,对首页、栏目页、详情页各打一次,必须全部 200。用 curl --resolve 强制打到源站,绕开 CDN 才验证得到源站规则。
  • 灰度只记不拦:新规则先把 $bot_blocked 固定为 0,观察 24 到 48 小时的命中量。命中量异常大,说明规则写宽了。
  • 上线后看两个指标:引擎抓取频次(站长平台)和源站 403 里来自引擎 IP 的条数。后者大于零就是误伤。

Q:规则越堆越多,会不会拖慢 Nginx?

  • map 里几百条 UA 不会有明显开销,它本质是哈希查找加正则顺序匹配。真正的性能杀手是另外三样:
  • 每个请求跑一遍 Lua 正则;在 if 里写 return 之外的复杂逻辑;用正则 location 铺满全站。
  • 验证方法很简单:同一台机器,规则全关和全开各压一轮,看 P99 和 CPU 的差值。
  • 正则别写 (.*)* 这种回溯炸弹,一个恶意 UA 就能让 worker 卡住。
# 规则前后各压一轮,对比 P99 与 CPU
wrk -t4 -c100 -d30s --latency https://www.example.com/ | grep -E "Latency|Requests/sec"

Q:CDN、WAF、源站三层都有规则,怎么避免打架?

  • 只维护一份来源:一个 txt 或 yaml,脚本生成三处的规则文件,各自带上版本号。
  • 优先级固定:白名单 → 静态资源放行 → 限流 → UA 与 IP 拦截。任何一层都不许打乱这个顺序。
  • 每层响应头带一个来源标记,比如 X-Block-By: edge 或 X-Block-By: origin。用户投诉时一眼就知道该去哪个控制台查。
  • 绝对不要三层各维护一份黑名单。三个月后没人说得清哪份是准的。

Q:维护频率和清单怎么定?

  • 每周:看命中 Top 10 和零命中清单,处理异常波动。
  • 每月:核对官方 IP 段,巡检新上线路径有没有漏配规则。
  • 每季度:全量复核,删僵尸规则,做一次压测确认开销可接受。
  • 每次变更触发:换 CDN、加子域、上 HTTPS,重新检查 set_real_ip_from 和白名单。

Q:还有什么补充建议?

  • 每条规则旁边写清楚为什么加、什么时候加、谁加的。半年后接手的人靠这三句注释才能判断能不能删。
  • 拦截率曲线要设告警。从 12% 突然掉到 2%,通常不是爬虫变乖了,而是规则失效或者被绕过了。
  • 规则文件必须版本化,reload 前备份一份。nginx -t 通过再 reload,别用 -s reload 赌运气。
  • 能靠限速解决的问题不要用黑名单。黑名单是精确打击,覆盖面越小越可靠;拿它当大扫帚用,误伤一定失控。
  • 最后一句:反爬的效果不取决于你写了多少条规则,取决于还有多少条在生效。