爬虫抓取间隔是最容易被忽略的识别维度。本文讲清人的访问节奏和机器的差别、日志里怎么算出间隔与变异系数、Nginx 上怎么实时判定,以及哪些固定间隔的正常业务不能被误封。
Q:爬虫抓取间隔为什么能当识别依据?
- 识别爬虫大体有四个维度:网络(IP/ASN)、传输(TLS 指纹/JA3)、应用(UA/JS 指纹)、时间(抓取节奏)。前三层都能伪造,节奏最难装。
- 机器要稳定跑量,间隔就必须可控;人的点击是随机的。所以间隔序列的分布形态本身就是特征,和它伪装成什么浏览器无关。
- 核心指标是变异系数 CV=标准差÷均值。真人点击的 CV 通常大于 1,定时脚本常在 0.1 以下,差一个数量级。
Q:人的访问节奏和爬虫到底差在哪?
- 真人:进页面后 3 到 60 秒才有下一次点击,间隔呈长尾分布,还有滚动、停顿、切后台造成的大间隔,夜里基本不活动。
- 定时采集:固定 5 秒或 10 秒一次,间隔几乎不变,跨小时不变,凌晨三点照样跑。
- 并发型爬虫:多线程同时打,间隔趋近 0,呈现一阵一阵的突发,突发之间是整段空白。
- 搜索引擎蜘蛛:有间隔也有退避,站点更新少时抓取频次会下降,和内容更新节奏正相关。
- 一句话概括:人的间隔散,脚本的间隔齐。
Q:日志里怎么算间隔和变异系数?
- 不用上工具,一段 awk 就够。按 IP 取
[time_local]转成秒,算相邻两次的差值,再算均值和标准差。
awk '{
split($4, t, ":"); ts = t[2]*3600 + t[3]*60 + t[4];
if (ts < last[$1]) { last[$1] = ts; next } # 跨天回绕,跳过
if ($1 in last) {
d = ts - last[$1]; n[$1]++; s[$1] += d; q[$1] += d*d;
}
last[$1] = ts
} END {
for (ip in n) {
avg = s[ip] / n[ip];
var = q[ip]/n[ip] - avg*avg; if (var < 0) var = 0;
sd = sqrt(var);
if (n[ip] >= 20)
printf "%s n=%d avg=%.1f sd=%.1f cv=%.2f\n", ip, n[ip], avg, sd, (avg > 0 ? sd/avg : 0);
}
}' access.log | sort -t= -k5 -n | head -30
- 输出按 cv 升序,cv 小于 0.15 且 n 大于等于 20 的 IP 优先人工看;cv 大于 1 的九成是真人。
- 字段位置按你的
log_format调,$4只对默认 combined 格式成立。
Q:日志只精确到秒,精度够吗?
- 不够。秒级精度下同一秒内的多个请求会被压成间隔 0,高频爬虫全部落在 0 秒上,反而看不出节奏。
- 建议在
log_format里补$msec(毫秒时间戳)和$connection_requests(单连接已处理请求数),这两个字段后面判断限流和复用都要用。
log_format main '$remote_addr - [$time_local] "$request" $status '
'$body_bytes_sent "$http_referer" "$http_user_agent" '
'$request_time $msec $connection_requests $server_protocol';
# 只为动态请求单独记一份,静态资源会把间隔统计污染成毫秒级
location ~ \.php$ {
access_log /var/log/nginx/dynamic.log main;
}
- 用了
$msec之后做差,精度足够分辨 100 毫秒级的机器节奏。
Q:Nginx 上怎么实时判定抓取节奏?
- 原生 Nginx 算不了方差,得靠 OpenResty。用共享字典给每个 IP 存三个数:请求次数 n、间隔和 s、间隔平方和 q,当场算方差。
- 注意共享字典只能存字符串和数字,不能存 table,所以要拆成三个 key,这是新手最容易踩的坑。
http {
lua_shared_dict rhythm 20m; # 约可存 20 万个 IP 的三元组
server {
location / {
access_by_lua_block {
local dict = ngx.shared.rhythm
local ip = ngx.var.binary_remote_addr
local now = ngx.now()
local prev = dict:get(ip .. ":t")
if prev then
local d = now - prev
if d > 0 and d < 600 then # 超过 10 分钟的间隔不计入
dict:incr(ip .. ":n", 1, 0)
dict:incr(ip .. ":s", d, 0)
dict:incr(ip .. ":q", d * d, 0)
end
end
dict:set(ip .. ":t", now, 1800)
local n = dict:get(ip .. ":n") or 0
if n >= 20 then
local s = dict:get(ip .. ":s")
local q = dict:get(ip .. ":q")
local avg = s / n
local var = q / n - avg * avg
if var < 0 then var = 0 end
local cv = avg > 0 and math.sqrt(var) / avg or 0
if cv < 0.15 then
ngx.req.set_header("X-Crawl-Rhythm", "suspect") -- 只打标,不拦
end
end
}
proxy_set_header X-Crawl-Rhythm $http_x_crawl_rhythm;
}
}
}
- 打标之后交给外层 Nginx 的 map 或 WAF 规则处置,不要在 Lua 里直接 exit,否则误杀时你连日志都查不到。
- 原生 Nginx 没有 Lua 的,用离线思路:每小时跑一次上面的 awk,把 cv 低于阈值的 IP 写进
deny列表或 fail2ban 的自定义 filter,效果一样,只是延迟一小时。
Q:按节奏封禁最容易误伤谁?
- 拨测监控:固定 60 秒请求一次首页,CV 几乎是 0,按节奏判断必然被判成机器。
- 健康检查与回源探测:CDN 回源探测、负载均衡探针、容器 liveness 探针。
- 站内轮询:未读消息 30 秒拉一次、订单状态轮询、客服在线心跳、后台标签页的定时请求。
- 回调与重试:支付回调、webhook 重试(固定退避间隔)。
- 结论:节奏只能作加分项,不能单独定罪。至少要和 UA、无 JS 指纹、路径集中度三个条件组合使用。
Q:分布式爬虫每个 IP 只来一次,节奏还有用吗?
- 单 IP 没节奏,就换到资源维度:看同一个 URL 被不同 IP 访问的间隔序列,队列型采集会呈现出稳定的吞吐。
- 更狠的特征是顺序一致性:URL 的访问顺序和
sitemap.xml里的排列高度重合,正常用户几乎不可能按 sitemap 顺序浏览。 - 还有生命周期特征:一个 IP 只访问 1 个 URL、不加载任何静态资源、Referer 为空,和低频 CC 的画像完全重合。
# 每个 URL 被多少个不同 IP 访问过
awk '{print $7, $1}' access.log | sort -u \
| awk '{c[$1]++} END {for (u in c) print c[u], u}' | sort -rn | head -20
- 单个 URL 来自几百个 IP、每个只取一次,就是分布式采集。这类流量靠节奏识别只能做辅助,主力还得靠资源维度的访问序列。
Q:判定之后怎么分级处置?
- 一级,429 加
Retry-After:礼貌爬虫会退避,脚本多半不理,但这一步几乎零误伤。 - 二级,下发 JS 挑战:拿不到指纹的直接拒,能过的大概率是真浏览器。
- 三级,按 IP 段(/24)聚合限速或封禁,避免只封单个 IP 被秒换。
map $http_x_crawl_rhythm $rhythm_key {
default "";
"suspect" $binary_remote_addr;
}
limit_req_zone $rhythm_key zone=rhythm:10m rate=1r/s;
server {
location / {
limit_req zone=rhythm burst=5 nodelay;
limit_req_status 429;
add_header Retry-After 60 always;
}
}
- 关键点:limit_req 的 key 为空字符串时该规则不生效,所以只有被打标的 IP 受限,其他流量完全不受影响,这比写一堆 if 安全得多。
- 白名单也靠这个特性实现:在 map 里先把监控 IP、回调路径的 key 置空。
Q:上线后怎么验收没误伤?
- 先影子运行 7 天,只打标不拦截,统计被打标 IP 里有多少属于已知白名单业务。
- 盯三个数:搜索引擎抓取成功率(Search Console 抓取统计)、拨测告警条数、真实用户登录与下单成功率。
- 红线:被打标流量里正常用户占比超过 1%,立刻退回只打标模式,别硬扛。
- 每天把被打标 IP 与当日下单、登录 IP 求交集,交集非空就逐个查,这是最快发现误伤的方法。
Q:还有什么补充建议?
- 节奏识别真正的收益是抬高成本:脚本必须随机化间隔才能过关,随机化会直接拖慢采集速度,这本身就是防护效果。
- 别指望一劳永逸。间隔特征会被人学走,所以要把节奏、指纹、路径集中度做成加权评分,阈值按季度重算。
- 先把日志字段补齐(协议、毫秒、单连接请求数),再谈识别,字段不全的判断都是猜。
- 白名单必须写到期日。临时加的监控 IP 最容易变成永久后门,半年后没人记得它为什么在白名单里。