从访问日志里揪出假蜘蛛与无头浏览器

日志里那些不像人的访问

做站长久了,看访问日志会养成一种直觉:正常的浏览器请求是有节奏的,先要 HTML,再要 CSS 和 JS,图片分散在后几秒,Referer 有时有有时没有但大体连贯。而爬虫请求往往一眼就能看出来——它们在同一毫秒内请求五个页面,从来不要图片,User-Agent 要么空白要么写着某个已经三年没更新的浏览器版本。

这篇文章讲怎么用 Nginx 日志加几个脚本,系统地把这些「不像人」的流量识别出来,包括识别假 UA、识别不加载资源的爬虫、识别故意伪装成搜索引擎的采集器,以及在识别之后如何分级处置——为什么直接 403 往往是错的。

一、先把日志字段配好

默认的 combined 格式能用的信息太少。至少要加请求时间、上游耗时、UA 和真实的连接层信息:

log_format detailed '$remote_addr - $remote_user [$time_local] '
                     '"$request" $status $body_bytes_sent '
                     '"$http_referer" "$http_user_agent" '
                     'rt=$request_time urt=$upstream_response_time '
                     'host=$host method=$request_method';

几个字段的用途:$request_time 是 Nginx 处理总耗时,$upstream_response_time 是后端 PHP 耗时,两者之差就是 Nginx 与网络的开销。爬虫通常 urt 很小(很多请求的是静态页或被缓存),而真实用户点进文章时后端要查库,会出现明显的差值分布。

还有一个字段特别有用但很少人加:$http_accept_language。真实浏览器一定带 Accept-Language,而且与地区有关;大量采集脚本这个头是空的。这一条就能筛掉一批。

更彻底的做法是把 UA 里的关键特征单独抓出来。用 map 提取「是否带 Mozilla 前缀」「声明的是 Chrome 还是 Safari」「有没有 HeadlessChrome」:

map $http_user_agent $ua_class {
    default                     "unknown";
    ""                          "empty";
    ~*HeadlessChrome            "headless";
    ~*PhantomJS                 "headless";
    ~*python-requests           "script";
    ~*Go-http-client            "script";
    ~*curl|wget                 "script";
    ~*Googlebot                 "googlebot";
    ~*bingbot                   "bingbot";
    ~*Baiduspider               "baiduspider";
    ~*Mozilla/5\.0.*Chrome      "chrome";
    ~*Mozilla/5\.0.*Safari      "safari";
    ~*Mozilla                   "other_browser";
}

把这个变量加进 log_format,以后统计直接用 awk 分组,不用每次写正则。

二、第一层筛:不加载资源的请求

最可靠的爬虫特征是「只要 HTML,不要静态资源」。真实浏览器一天下来,HTML 与静态资源的比例大概是 1:8 到 1:15;纯采集器是 1:0。用一条 awk 就能算出来:

awk '{print $1}' /var/log/nginx/access.log \
 | sort | uniq -c | sort -rn | head -30

先看请求量前 30 的 IP。对其中排名靠前、又不在你预期里的,逐个看它们的请求构成:

awk '$1=="1.2.3.4"{print $7}' /var/log/nginx/access.log \
 | sed 's/?.*//' \
 | awk -F. '{print $NF}' | sort | uniq -c | sort -rn | head

输出如果是 1200 html 而没有任何 css/js/jpg,基本可以确认是采集器。这一个判据的准确率远高于任何 UA 匹配,因为 UA 可以随便伪造,而「要不要下载 CSS」是行为层面的选择,很难伪装得毫无破绽——真正去做无头浏览器的成本高得多。

反过来,要注意一种误判:RSS 阅读器和搜索引擎的预渲染也会只请求 HTML。所以这个判据要配合「是否访问了 robots.txt」「请求频率」「是否只抓列表页不抓正文」一起看。

三、第二层筛:频率与路径模式

正常人类不可能一秒请求二十次。按 IP 统计每分钟请求数,是最直接的手段。日志里没有分钟级时间戳时,可以先用 $time_local 截到分钟:

awk '{print $1, substr($4,2,17)}' /var/log/nginx/access.log \
 | sort | uniq -c | sort -rn | awk '$1>60' | head -20

这里 substr($4,2,17) 取的是 27/Oct/2026:14:3 这一段,粒度到分钟。超过 60 次/分钟的就值得看一眼。

路径模式比频率更有辨识度。几种典型形态:

顺序遍历:/1.html、/2.html、/3.html 依次递增。这是最笨的采集器,也最常见,直接用递增数字访问全站。判据是同一 IP 的请求 URL 里有大量纯数字文件名,且 body_bytes_sent 都接近(因为它不看内容)。

暴力探测:/.env、/wp-admin/、/phpmyadmin/、/.git/config。这类请求 99% 是 404,直接在 Nginx 层 return 444 就行,不用等应用层判断。

参数乱扫:同一路径带不同的查询参数,比如 /search?q= 后面跟各种关键词。注意这类请求会打进你的动态处理,成本最高,也最容易被拿去消耗资源。

awk '{print $1}' /var/log/nginx/access.log | sort | uniq -c \
 | sort -rn | awk '$1>1000{print $2}' > /tmp/heavy_ips.txt
while read ip; do
    n404=$(grep -c "^$ip .* 404 " /var/log/nginx/access.log)
    echo "$ip total404=$n404"
done < /tmp/heavy_ips.txt

404 占比超过 80% 的高频 IP,通常是扫描器,可以直接封。

四、第三层筛:假 Googlebot

搜索引擎爬虫的 UA 是最常被冒用的,因为它通常被站长放行。区分真假只有一个办法:反向 DNS 验证。Google 官方的做法是:

第一,对访问来源 IP 做 PTR 反查,结果必须是以 googlebot.com 或 google.com 结尾的域名。

第二,对这个域名再做一次正向解析,A 记录必须包含原来的 IP。

dig +short -x 66.249.66.1
dig +short A crawl-66-249-66-1.googlebot.com

两次结果能对上,才是真的。只做第一步是不够的——攻击者可以给自己的 IP 段配上 PTR 记录(如果他能控制反向解析区)。

写成脚本,把日志里所有声称是 Googlebot 的 IP 都跑一遍:

#!/bin/bash
grep -i 'Googlebot' /var/log/nginx/access.log \
 | awk '{print $1}' | sort -u | while read ip; do
    ptr=$(dig +short -x "$ip" | head -1)
    case "$ptr" in
        *googlebot.com.|*google.com.)
            fwd=$(dig +short A "$ptr" | grep -c "^${ip}$")
            [ "$fwd" -ge 1 ] && echo "OK   $ip $ptr" || echo "FAKE $ip $ptr 正向不含该IP"
            ;;
        *) echo "FAKE $ip ptr=${ptr:-none}" ;;
    esac
done

Bing 用 *.search.msn.com,百度用 *.baidu.com 和 *.baidu.jp,Yandex 用 *.yandex.com 或 *.yandex.ru。规则一样:PTR 后缀匹配加正向回查。

这里有个成本问题:dig 每次都走网络,几百个 IP 就要跑几十秒。实际做法是把验证结果缓存到 /var/lib/botverify/,同一个 IP 一天只查一次。

五、无头浏览器是最难识别的一类

前面那些方法对付普通脚本足够,但遇到用 Puppeteer 或 Playwright 写的采集器就失效了:它会真的加载 CSS 和图片,会执行 JS,频率也可以压到很低。这时候要靠浏览器指纹层面的差异。

特征一:UA 里的 HeadlessChrome。默认配置下 Puppeteer 的 UA 会带这个标识。~*HeadlessChrome 匹配即可。但采集者通常会手动改掉,所以不能只靠这个。

特征二:navigator.webdriver。无头模式下这个值是 true,而真实浏览器是 false。可以在页面上用一小段 JS 上报:

if (navigator.webdriver) {
    navigator.sendBeacon('/track.php', 'flag=webdriver');
}

但要注意,这会给所有真实用户也执行一次 JS,所以要放在 requestIdleCallback 里,别影响首屏。而且这个值在启动时加 --disable-blink-features=AutomationControlled 就能改掉,属于「能筛掉 80% 的廉价实现」而不是绝对判据。

特征三:缺少插件与字体。真实浏览器里 navigator.plugins.length 通常大于 0(Chrome 内置 PDF 查看器等),无头环境常见为 0。navigator.languages 为空也是常见特征。

特征四:WebGL 渲染器字符串。无头环境常用软件渲染 SwiftShader,真实显卡会是具体的 GPU 型号。取法:

var gl = document.createElement('canvas').getContext('webgl');
var dbg = gl && gl.getExtension('WEBGL_debug_renderer_info');
var renderer = dbg ? gl.getParameter(dbg.UNMASKED_RENDERER_WEBGL) : '';

命中 SwiftShader 或 llvmpipe 的,大概率是无头或虚拟机。

特征五:时序异常。真实用户滚动页面、移动鼠标,会有 mousemove、scroll、visibilitychange 事件。完全没有任何交互却把整页读完的会话,值得打个分。可以用一组轻量信号累加一个可疑分:无 webdriver 得 0 分、有得 30 分;无交互事件得 20 分;WebGL 是 SwiftShader 得 20 分;Accept-Language 为空得 15 分。累计超过 50 分就记录到单独的日志,人工过一遍再决定。

这样做的好处是不误伤——真实用户即使因为隐私插件隐藏了部分信息,也很少同时命中三项。

六、识别之后怎么处理才不伤自己

很多人识别出爬虫就直接 deny,结果把自己坑了:误封了 CDN 回源 IP、误封了监控探针、误封了搜索引擎。分四档处理比较稳:

第一档:静态资源类探测直接 444。路径匹配 \.env|\.git|wp-login|phpmyadmin|\.php$ 且 UA 是 scripts 类,直接 return 444(不断开连接、不返回任何内容,比 403 更省资源)。这一类没有误伤风险。

第二档:高频采集限速而不是封禁。用 Nginx 的 limit_req,给「不带静态资源请求的 IP」单独一个 zone,速率压到 1r/s、burst 5。这样真爬虫只能慢慢爬,不影响 SEO 收录(真搜索引擎走 botverify 白名单放行),也不至于把正常用户误判成敌人。

limit_req_zone $binary_remote_addr zone=human:10m rate=2r/s;
limit_req_status 429;

第三档:可疑会话给「慢页面」。对累计可疑分超过阈值的会话,不返回 403,而是返回一个内容稍简化的页面(少几个推荐位、去掉高成本查询)。这样采集者拿到的是不完整的站点,而真实用户几乎感受不到差别。这个思路比封 IP 更温和,也不容易被判定为「拒绝服务搜索引擎」。

第四档:真爬虫一律放行。前面验证过的 Googlebot、Bingbot、Baiduspider 加白名单,不受任何限速影响。这一点非常重要——很多站「做了反爬之后收录掉了」,原因就是把正经蜘蛛一起限了。

七、把结论固化成日报

识别脚本最有价值的产出不是「封了谁」,而是每天早上一封报表:

昨日总请求 128,443  独立IP 9,842
静态资源占比 76%    HTML 占比 12%
疑似采集IP 14 个,占总请求 8.3%(比前日 +2.1%)
假 Googlebot 3 个,已加入限速
404 占比 6.8%,其中扫描器路径占 91%

这个报表坚持看两周,你会对站点的真实流量结构有非常具体的认识:哪些是搜索来的、哪些是直接访问、哪些是 RSS 阅读器、哪些是采集。再往后,判断一个 SEO 波动是「被降权」还是「采集变多」,就有了数据依据,而不是凭感觉。

最后提醒一句:反爬的边界要清楚。屏蔽恶意采集是正当的,但如果你的规则把正常的搜索引擎、无障碍工具、隐私浏览器一起挡了,损失的是自己的流量。所有规则上线前,先用一天的日志做离线回放,统计「会被拦下的请求里,有多少是正常用户的路径」,这个比例超过 1% 就该重新调阈值。

Last modification:October 27th, 2026 at 10:25 pm

Leave a Comment