本文档完整记录:发现可疑访问 → 理解数据库结构 → 编写 SQL 分析 → 识别各类爬虫 → 部署三层 Nginx 防护 → 验证效果 的全过程。 适用对象:站长本人(后续按此流程复查日志、排查新爬虫)。 最后更新:2026-09-09
1. 事件背景:可疑来源的发现
在后台访问统计中发现一类来源记录:
http://x1.example.com/article/post-1788793769430;jsessionid=xxx
http://x2.example.com/article/Ref_Rea_industry;jsessionid=xxx
两个可疑点:
x1、x2这两个子域名从未在 DNS 中配置过——是伪造的;- 路径带
;jsessionid=会话标识参数——这是服务端带会话跟踪的 Web 应用特征,爬虫作者可能在批量扫描同类型站点。
经排查确认:这是内容采集爬虫,使用住宅代理 IP 池(每次请求换 IP)、伪造 Chrome UA、伪造站内跳转 Referer,逐篇抓取文章内容,目的可能是洗稿搬运或做 AI 训练语料。
本事件的完整处理链路:识别 → 分析(本文档)→ 限流 → 防盗链 → 伪造 Referer 拦截。
2. 数据库结构:访问日志表
2.1 数据从哪来
每次访问文章详情页(/article/{slug}),后端会写入一条访问记录,字段包括:访问的 IP(优先取 Nginx 设置的 X-Real-IP 头,防止伪造)、User-Agent、Referer、浏览器、操作系统、访问时间。还可通过本地 IP 归属库解析出 IP 所在地区(离线解析,无外部依赖、无隐私外泄)。
2.2 表结构
为便于说明,本文以"访问日志表"代称实际表名,字段为示意(各框架/项目命名略有差异),仅作分析思路参考。
| 字段 | 类型 | 说明 |
|---|---|---|
id |
自增主键 | 记录 ID |
article_id |
长整型 | 访问的文章 ID(空 = 非文章页) |
ip |
变长字符串 | 访客 IP(来自 X-Real-IP) |
user_agent |
变长字符串 | 浏览器 UA 字符串,爬虫可用空值或伪造 |
referer |
变长字符串 | 来源页面地址,爬虫可伪造 |
device_type |
变长字符串 | 设备类型(mobile/desktop/tablet) |
browser |
变长字符串 | 解析出的浏览器名 |
os |
变长字符串 | 解析出的操作系统 |
created_at |
时间戳 | 访问时间(非空,自动写入) |
2.3 一个关键限制(务必理解)
访问日志表只记录穿透到应用层的请求——被 Nginx 限流(429)、防盗链(403)拦掉的请求根本到不了应用,不会进这张表。
含义:这张表看到的是"漏过来的爬虫",看不到"被挡掉的爬虫"。评估防护效果要看 Nginx 日志的 429/403 数量,而不是看数据库里爬虫少了没有。
3. 识别原理:爬虫在日志上暴露的五个特征
真人浏览和程序抓取,在日志上的痕迹有本质区别。所有分析都围绕以下五个维度:
3.1 Referer(来源页)——"直接访问"是什么
浏览器从 A 页点链接跳到 B 页时,会自动附带 A 页地址作为 Referer 头。**Referer 为空(显示"直接访问")**说明访问不是通过网页链接进来的,常见于:
- 手动输入网址、书签、历史记录(老读者,正常);
- 微信/QQ/邮件/App 内点链接(很多 App 内置浏览器会剥离 Referer);
- 隐私模式、追踪拦截插件(uBlock 等)主动裁剪 Referer;
- 爬虫裸请求直连 URL(不伪造 Referer 的爬虫,在统计里全部显示为直接访问)。
所以"直接访问"多 ≠ 都是真人,要结合其他特征判断。
3.2 User-Agent —— 程序的身份露馅点
- 无 UA:正规浏览器一定会带 UA。请求没有 UA → 几乎可以断定是程序(
curl、脚本、自研爬虫)。 - 脚本 UA:UA 里带
curl、python、Go-http、Java/、HttpClient、okhttp、Scrapy、wget等字样 → 未伪装的脚本。 - 伪 UA:伪造成 Chrome/手机 UA 的爬虫,UA 看起来正常,需要结合其他特征(见下)。
3.3 代理池特征 —— 多 IP 共用同一个 UA
住宅代理池爬虫每次请求换一个 IP(绕过 IP 限流),但脚本是同一个程序,UA 通常固定不变。于是出现一个显著特征:同一个 UA 字符串被多个不同 IP 使用。真人设备不可能这样。
本次发现的 36.99.136.x 系列(10 个 IP 全用同一款假 Chrome UA)、113.215.188/189.x 系列(全用同一款 ASUS_X00TD; Flow 假 Android UA),都是典型的代理池。
3.4 频率与突发 —— 真人是"看",程序是"扫"
- 真人:一篇文章读几分钟,请求间隔长;
- 程序:短时间内连续请求多个 URL。单分钟请求数 ≥ 10 基本可判定程序行为(正常人 1 分钟内打开 10 篇以上文章极罕见)。
3.5 伪造站内 Referer —— 最隐蔽的一种
代理池爬虫不仅伪造 UA,还把 Referer 伪造成 http://x1.example.com/... 这种本站子域名,意图是伪装成"站内跳转",绕过防盗链。
它为什么能绕过?因为防盗链的 valid_referers *.example.com 是字符串通配——只要 Referer 以 example.com 结尾就放行,不校验子域名是否真实存在。所以 x1、x2 这种不存在的子域名也能通过。这正是后续要单独拦截它的原因。
4. 分析 SQL:一张表汇总全部结果
4.1 文件位置
项目本地 sql/ 目录下有分析脚本,在数据库客户端工具(如 Navicat)中打开直接运行主查询即可。默认统计最近 30 天,改两处 now() - interval '30 days' 可调整。
4.2 完整代码
表名与字段用下述占位符(
visit_logs/created_at等为示意名),请按你实际环境替换。下文的visit_logs一律等同第 2 节的"访问日志表"。
WITH stats AS (
SELECT
ip,
COUNT(*) AS reqs,
COUNT(DISTINCT article_id) AS articles,
COUNT(*) FILTER (WHERE user_agent IS NULL OR btrim(user_agent) = '')
AS no_ua,
COUNT(*) FILTER (WHERE referer IS NULL OR btrim(referer) = '')
AS no_ref,
COUNT(DISTINCT user_agent) AS ua_variants,
MIN(created_at) AS first_seen,
MAX(created_at) AS last_seen,
MODE() WITHIN GROUP (ORDER BY user_agent) AS top_ua,
MODE() WITHIN GROUP (ORDER BY referer) AS top_ref
FROM visit_logs
WHERE created_at >= now() - interval '30 days'
GROUP BY ip
),
burst AS ( -- 每个 IP 的单分钟请求峰值
SELECT ip, MAX(cnt) AS peak_per_min
FROM (
SELECT ip, date_trunc('minute', created_at) AS m, COUNT(*) AS cnt
FROM visit_logs
WHERE created_at >= now() - interval '30 days'
GROUP BY ip, date_trunc('minute', created_at)
) t
GROUP BY ip
),
ua_shared AS ( -- 同一 UA 被 >= 3 个不同 IP 使用(代理池轮换特征)
SELECT user_agent, COUNT(DISTINCT ip) AS shared_ips
FROM visit_logs
WHERE created_at >= now() - interval '30 days'
AND user_agent IS NOT NULL AND btrim(user_agent) <> ''
GROUP BY user_agent
HAVING COUNT(DISTINCT ip) >= 3
)
SELECT
s.ip,
s.reqs AS 请求数,
s.articles AS 覆盖文章数,
s.no_ua AS 无UA数,
ROUND(100.0 * s.no_ua / NULLIF(s.reqs, 0), 1) AS 无UA占比,
s.no_ref AS 无Referer数,
ROUND(100.0 * s.no_ref / NULLIF(s.reqs, 0), 1) AS 无Ref占比,
b.peak_per_min AS 单分钟峰值,
s.ua_variants AS UA种类,
s.first_seen::date AS 首访日期,
s.last_seen::date AS 末访日期,
LEFT(s.top_ua, 60) AS 常用UA,
LEFT(s.top_ref, 60) AS 常用Referer,
CASE -- 判定标签(按优先级取首个命中)
WHEN s.no_ua >= 5 AND (100.0 * s.no_ua / NULLIF(s.reqs, 0)) >= 50
THEN '裸爬虫·无UA'
WHEN s.top_ua ~* 'curl|wget|python|Go-http|Java/|HttpClient|okhttp|Scrapy|Apache'
THEN '脚本UA'
WHEN u.shared_ips IS NOT NULL
THEN '代理池·UA共用'
WHEN s.reqs >= 30 AND b.peak_per_min >= 10
THEN '高频突发'
ELSE '观察'
END AS 判定,
(CASE WHEN s.no_ua >= 5 AND (100.0 * s.no_ua / NULLIF(s.reqs, 0)) >= 50 THEN 5 ELSE 0 END)
+ (CASE WHEN s.no_ref >= 5 AND (100.0 * s.no_ref / NULLIF(s.reqs, 0)) >= 60 THEN 2 ELSE 0 END)
+ (CASE WHEN b.peak_per_min >= 10 THEN 2 ELSE 0 END)
+ (CASE WHEN u.shared_ips IS NOT NULL THEN 3 ELSE 0 END)
+ (CASE WHEN s.top_ua ~* 'curl|wget|python|Go-http|Java/|HttpClient|okhttp|Scrapy|Apache' THEN 3 ELSE 0 END)
+ (CASE WHEN s.reqs >= 50 THEN 1 ELSE 0 END) AS 可疑分
FROM stats s
LEFT JOIN burst b ON b.ip = s.ip
LEFT JOIN ua_shared u ON u.user_agent = s.top_ua
WHERE s.reqs >= 10
ORDER BY 可疑分 DESC, 请求数 DESC
LIMIT 50;
-- ---------------------------------------------------------------------
-- 抽查明细(可选):汇总结果里挑一个可疑 IP,替换后查看它的完整访问记录
-- ---------------------------------------------------------------------
SELECT created_at, article_id, ip, user_agent, referer, browser, os
FROM visit_logs
WHERE ip = '替换为可疑IP'
ORDER BY created_at DESC
LIMIT 100;
4.3 查询结构说明(三个 CTE + 主查询)
| 部分 | 作用 |
|---|---|
stats |
按 IP 聚合全部维度:请求数、覆盖文章数、无 UA/无 Referer 计数与占比、UA 种类数、首末访日期、出现次数最多的 UA 和 Referer(MODE() WITHIN GROUP 是统计数据库取众数的语法) |
burst |
把时间按分钟切块(date_trunc('minute', ...)),算每个 IP 单分钟请求峰值,识别"速扫"行为 |
ua_shared |
找出被 ≥ 3 个不同 IP 共用的 UA(代理池轮换特征,HAVING COUNT(DISTINCT ip) >= 3) |
| 主查询 | 三表关联(LEFT JOIN,因为 burst/ua_shared 可能缺行),算判定标签 + 可疑分,按分数排序取前 50 |
4.4 判定标签逻辑(CASE 按优先级取首个命中)
| 优先级 | 判定 | 条件 | 含义 |
|---|---|---|---|
| 1 | 裸爬虫·无UA |
无 UA ≥ 5 且占比 ≥ 50% | 连伪装都懒得做,最高危 |
| 2 | 脚本UA |
常用 UA 含 curl/python/Java 等 | 未伪装的程序 |
| 3 | 代理池·UA共用 |
该 IP 的 UA 被 ≥ 3 个 IP 共用 | 换 IP 不换 UA,代理池铁证 |
| 4 | 高频突发 |
请求 ≥ 30 且单分钟峰值 ≥ 10 | 速扫行为 |
| 5 | 观察 |
其余 | 请求多但无明显异常,需人工复核 |
4.5 可疑分权重
- 裸爬虫(无 UA):+5(最强信号)
- 脚本 UA:+3
- 代理池 UA 共用:+3
- 单分钟峰值 ≥ 10:+2
- 无 Referer ≥ 5 且占比 ≥ 60%:+2
- 总请求 ≥ 50:+1
- 满分 16,分数越高越可疑,按分数降序排,优先处理高分 IP。
5. 分析方法论:从查询结果到结论
拿到汇总表后,按以下顺序分析,不要直接按分数封禁:
第 1 步:先看高分段(判定明确的那批)
分数 ≥ 5 且判定为「裸爬虫」「脚本UA」「代理池」的,直接进入第 4 步抽查。
第 2 步:识别"不是爬虫"的高频 IP(最重要,防误封)
汇总表不知道 IP 是谁的,人工复核时必须排除三类:
- 搜索引擎官方爬虫:Googlebot 的 IP 段(66.249.66.0/24 等)、Bingbot、百度蜘蛛等。它们的 UA 会注明身份(如
Googlebot/2.1),必须放行——封了影响收录。 - 本机回环:
0:0:0:0:0:0:0:1或127.0.0.1是你在服务器/本地的访问(后台操作、测试),不是爬虫。 - 合规 AI / 监控爬虫:声明了身份的(如
DeepSeekBot/1.0、AwarioBot/1.0),按 robots.txt 处理即可,不必封禁。
第 3 步:看"观察"段,结合业务判断
请求多但特征不明(如 Referer 是本站首页、UA 正常)的,可能是真人读者(尤其你发给过朋友、自己多设备访问的)。存疑的不动。
第 4 步:抽查明细确认
把可疑 IP 替换进第 4.2 节底部的抽查查询,看它的完整访问记录:
- 连续、无间隔、固定频率 → 程序;
- 有阅读节奏、停留时间长、Referer 多样 → 真人;
- 只反复抓同一篇文章(覆盖文章数远小于请求数)→ 定向抓取。
第 5 步:归并画像,针对性防护
把确认的爬虫按"共性"分组(同一个 IP 段、同一个 UA、同一类伪造 Referer),对共性下手,而不是逐个封 IP(住宅代理 IP 会一直换,封单个没用)。
6. 真实结果解读:本次发现的爬虫画像
本次汇总分析(2026-08-23 ~ 09-09)发现的典型爬虫:
画像 A:伪造站内 Referer 的住宅代理池(最顽固)
- IP:36.99.136.x 系列(约 10 个,浙江电信住宅段)、111.7.100.x 等
- UA:统一的假 Chrome UA(
Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36) - Referer:伪造
x1.example.com、x2.example.com子域名 +;jsessionid=会话标识 - 行为:低频(几分钟一个请求,绕开限流)、跨多天、覆盖多篇文章
- 结论:内容采集,防爬机制更新后(09-08)仍在活跃
画像 B:假 Android UA 的单篇重复抓取
- IP:113.215.188.x / 113.215.189.x 系列(8 个)
- UA:统一的
Mozilla/5.0 (Linux; Android 9; ASUS_X00TD; Flow)假 UA - 行为:请求 10~22 次但只覆盖 2 篇文章——反复抓同一篇,典型定向采集
画像 C:全站速扫
- IP:158.69.55.x(OVH 机房)——42 个请求、覆盖 21 篇、单分钟峰值 42,一天内扫完全站
- IP:117.132.188.x——19 请求 1 分钟完成
- 行为:一次性批量抓取,抓完就走
画像 D:单篇深度抓取
- IP:34.24.83.x——26 次请求只打 1 篇文章,UA 是残缺的假 Chrome(
Linux; Android 10; K)
必须放行的"误判项"(重要)
| IP | 实际身份 | 说明 |
|---|---|---|
| 66.249.66.x | Googlebot | UA 伪装成 Nexus 5X 是 Google 官方做法,属搜索引擎收录,禁封 |
| 0:0:0:0:0:0:0:1 | 本机 | IPv6 回环,自己后台/测试的访问 |
| 61.111.250.x | DeepSeekBot | 合规 AI 爬虫,已声明身份 |
| 37.27.129.x | AwarioBot | 社交媒体监控爬虫,已声明身份 |
疑似真实人(保留观察)
- 61.170.244.x:109 次、8 种 UA 变体、Referer 为本站首页——多设备正常浏览特征
- 119.252.196.x / 217.181.74.x:Referer 从首页来、一天翻 13 篇,像真人阅读
7. 防护落地:三层 Nginx 配置(含全部命令)
部署位置均为服务器(Linux)。系统目录(
/etc)通常无写权限、不能通过可视化工具(如 FileZilla)直接上传,一律 SSH + sudo。可视化工具只用于应用部署目录。以下用example.com代称;路径/参数为示意。
7.1 第一层:动态请求限流(拦截高频爬虫)
文件:/etc/nginx/conf.d/rate-limit.conf(已部署)
map $request_uri $pz_limit_key {
~^/admin/ ""; # 排除后台管理路径
~*\.(css|js|png|jpg|jpeg|gif|svg|webp|woff2?|txt)$ ""; # 排除静态资源
default $binary_remote_addr;
}
limit_req_zone $pz_limit_key zone=pz_dyn:10m rate=5r/s;
limit_req_status 429;
原理:用 map 把后台路径和静态资源排除("" 表示不参与限流),只对动态请求按 IP 限速,每 IP 每秒 5 次、突发 20,超限返回 429。
在 your-site.conf 的 location / 中应用:
location / {
limit_req zone=pz_dyn burst=20 nodelay;
# ……其余原有配置……
}
创建命令(SSH 执行):
sudo tee /etc/nginx/conf.d/rate-limit.conf > /dev/null <<'EOF'
map $request_uri $pz_limit_key {
~^/admin/ ""; # 排除后台管理路径
~*\.(css|js|png|jpg|jpeg|gif|svg|webp|woff2?|txt)$ ""; # 排除静态资源
default $binary_remote_addr;
}
limit_req_zone $pz_limit_key zone=pz_dyn:10m rate=5r/s;
limit_req_status 429;
EOF
sudo nginx -t && sudo systemctl reload nginx
7.2 第二层:图片防盗链(拦截图片被外站盗用)
位置:your-site.conf 的 location /uploads/(已部署)
location /uploads/ {
valid_referers none blocked server_names *.example.com
~^(https?://)?(www\.)?(baidu|google|bing|sogou)\.com/;
if ($invalid_referer) {
return 403;
}
# ……其余原有配置(压缩图回退等)……
}
原理:valid_referers 定义"合法来源"白名单——直接访问(none)、隐私工具(blocked)、本站域名、主流搜索引擎。Referer 不在白名单 → $invalid_referer=1 → 403。
已知漏洞(第 3.5 节讲过):*.example.com 是字符串通配,伪造子域名 Referer 也能通过——这正是第三层要补的洞。
7.3 第三层:伪造子域名 Referer 拦截(补第二层的洞,本次新增)
文件:/etc/nginx/conf.d/fake-referer.conf(新)
创建命令(SSH 执行,一条命令搞定,无需 FileZilla):
sudo tee /etc/nginx/conf.d/fake-referer.conf > /dev/null <<'EOF'
# 伪造子域名 Referer 拦截
# 规则:Referer 指向 example.com 的【非白名单】子域名 → $fake_ref=1 → 403。
# 真实用户 Referer 为空 / 本站主域 / 外站 → 全部放行,零误伤。
# 注意:今后新增真实子域名(如 blog.example.com)时,必须先加入下方白名单再启用。
map $http_referer $fake_ref {
default 0;
# —— 白名单:实际拥有的域名(新增子域名在此追加)——
"~(?i)^https?://example\.com(/|:|$)" 0;
"~(?i)^https?://www\.example\.com(/|:|$)" 0;
# —— 其余 example.com 子域名一律视为伪造 ——
"~(?i)^https?://[a-z0-9.-]+\.example\.com(/|:|$)" 1;
}
EOF
sudo chown root:root /etc/nginx/conf.d/fake-referer.conf
在 your-site.conf 的 443 server 块中应用(location 之前插入):
server {
listen 443 ssl http2;
server_name example.com www.example.com;
# ……其余原有配置……
# 伪造子域名 Referer 一律 403(防代理池爬虫伪装站内跳转)
if ($fake_ref = 1) {
return 403;
}
# ……原有 location 配置……
}
修改命令(SSH 执行):
sudo nano /etc/nginx/conf.d/your-site.conf
# 找到 443 的 server 块,在第一个 location 之前粘贴上面的 if 块,保存退出
sudo nginx -t && sudo systemctl reload nginx
原理:map 按书写顺序匹配(先白名单后黑名单)。Referer 为空/主域/外站 → default 0 放行;Referer 是 *.example.com 但子域名不在白名单 → 1 → 403。正则用 (?i) 忽略大小写,(/|:|$) 兼容路径、端口、结尾三种情况。相比防盗链的通配,这里改成显式白名单,伪造子域名无从漏过。
8. 效果验证方法
8.1 配置正确性验证(三条 curl)
# ① 伪造子域名 Referer → 必须返回 403
curl -s -o /dev/null -w '%{http_code}\n' -H 'Referer: http://spoof.example.com/article/x' https://example.com/
# ② 空 Referer(直接访问)→ 必须返回 200
curl -s -o /dev/null -w '%{http_code}\n' https://example.com/
# ③ 真实主域 Referer → 必须返回 200
curl -s -o /dev/null -w '%{http_code}\n' -H 'Referer: https://example.com/' https://example.com/
预期:403 / 200 / 200。任何一项不符,先 sudo nginx -t 查语法。
8.2 防护效果统计(看 Nginx 日志,不是看数据库)
# 最近被限流(429)和被防盗链/伪造Referer拒绝(403)的总数
grep -c '429' /var/log/nginx/access.log
grep -c '403' /var/log/nginx/access.log
# 429/403 按 IP 排行(看是谁在被拦)
awk '$9==429{print $1}' /var/log/nginx/access.log | sort | uniq -c | sort -rn | head
awk '$9==403{print $1}' /var/log/nginx/access.log | sort | uniq -c | sort -rn | head
判定标准:403/429 数量持续上升而数据库无对应记录 → 防护在生效(请求被挡在应用之外)。如果某爬虫还在进库,说明它绕过了 Nginx 层(低频、伪 UA、无 Referer 组合),回到第 4 节重新分析它的特征再针对性补规则。
9. 注意事项与误区
- 别封搜索引擎:Googlebot/Bingbot/百度蜘蛛封了会掉收录。它们 UA 会声明身份,IP 段可查官方文档。
- 别把本机当爬虫:
0:0:0:0:0:0:0:1(回环)是你自己。 - 别只封单个 IP:住宅代理池 IP 一直轮换,封单个没用。要对"共性"下手——UA、伪造 Referer、IP 段。
- IP 段封禁有误伤:36.99.136.x 是电信住宅段,可能混有真实用户,用限流/拦截而非直接封整段。
- 低频爬虫绕限流:限流只挡高频(如每秒几次以上)。低频代理池爬虫靠第 7.3 节的 Referer 白名单拦截,这是互补关系。
- 数据库看不到被挡的请求:评估效果必须看 Nginx 日志(8.2 节),这是最容易误解的一点。
- 新增真实子域名记得加白名单:
fake-referer.conf白名单只有主域和 www,将来开新子域(如 blog.example.com)必须先在白名单追加,否则新站点的站内跳转会 403。 - robots.txt 只约束守规矩的:搜索引擎和声明身份的 AI 爬虫会遵守,恶意爬虫根本不会读它,不能作为唯一防线。
10. 附录 A:从 Nginx 日志获取访问记录与分析筛选
A.1 为什么还需要看 Nginx 日志
两个数据源各有分工,必须配合使用:
| 数据源 | 记录范围 | 用途 |
|---|---|---|
| 访问日志表(数据库) | 只记录穿透到应用的请求 | 分析"漏过来的"爬虫行为画像 |
Nginx access.log |
所有请求(含被 429/403 挡掉的) | 评估防护效果、发现被拦的对象 |
数据库看不到被挡的请求,Nginx 日志全都有——判断防护有没有生效,唯一可靠的数据源是 Nginx 日志。
A.2 日志格式与字段位置
Nginx 默认 combined 格式一条记录长这样:
36.99.136.x - - [09/Sep/2026:09:12:33 +0800] "GET /article/post-1788793769430 HTTP/2.0" 200 12345 "http://spoof.example.com/..." "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36"
按空格分词后的字段位置(awk 用的列号):
| 列号 | 内容 | 说明 |
|---|---|---|
$1 |
IP | 访问者 IP |
$4 |
日期时间 | 形如 [09/Sep/2026:09:12:33 |
$6~$8 |
请求行 | 方法 + 路径 + 协议 |
$9 |
状态码 | 200/403/429/500 等 |
$11 |
Referer | 带引号 |
$12 |
User-Agent | 带引号 |
如果服务器的
log_format自定义过(加了字段),列号会偏移。动手前先sudo head -1 /var/log/nginx/access.log数一遍,以实际为准。
A.3 常用分析命令(按场景,全部 SSH 执行)
# 0) 先看一条样例,确认字段位置
sudo head -1 /var/log/nginx/access.log
# 1) 实时跟踪(观察爬虫当前在抓什么)
sudo tail -f /var/log/nginx/access.log
# 2) 全部请求按 IP 排行(前 20)
sudo awk '{print $1}' /var/log/nginx/access.log | sort | uniq -c | sort -rn | head -20
# 3) 只看文章页请求按 IP 排行(排除静态资源,聚焦内容抓取)
sudo grep 'GET /article/' /var/log/nginx/access.log | awk '{print $1}' | sort | uniq -c | sort -rn | head -20
# 4) 被限流的 429 按 IP 排行(第一层防护在拦谁)
sudo awk '$9==429{print $1}' /var/log/nginx/access.log | sort | uniq -c | sort -rn | head -20
# 5) 被拒绝的 403 按 IP 排行(防盗链 + 伪造Referer拦截的都在这里)
sudo awk '$9==403{print $1}' /var/log/nginx/access.log | sort | uniq -c | sort -rn | head -20
# 6) 未伪装脚本 UA 的请求(curl/python/Java 等)
sudo grep -iE 'curl|wget|python|java/|httpclient|okhttp|scrapy|go-http' \
/var/log/nginx/access.log | awk '{print $1}' | sort | uniq -c | sort -rn | head -20
# 7) 伪造子域名 Referer 的请求($11 是 Referer 列,spoof.example.com 改为你实际拦截的伪造子域名)
sudo awk '$11 ~ /spoof\.example\.com/' \
/var/log/nginx/access.log | awk '{print $1}' | sort | uniq -c | sort -rn | head -20
# 8) 按日期筛选($4 形如 [09/Sep/2026,统计今天被拦的)
sudo grep '\[09/Sep/2026' /var/log/nginx/access.log | awk '$9==429{print $1}' | sort | uniq -c | sort -rn | head
# 9) 抽查单个 IP 的完整请求序列(看抓取节奏:连续?间隔固定?)
sudo grep '^36\.99\.136\.x ' /var/log/nginx/access.log | tail -30
# 10) 含轮转日志一起统计(zcat -f 兼容文本和 .gz 文件)
sudo zcat -f /var/log/nginx/access.log* | awk '$9==429{print $1}' | sort | uniq -c | sort -rn | head
# 11) 最近 7 天每天被拦(429+403)的数量趋势
for f in $(sudo ls /var/log/nginx/access.log*); do
echo "== $f : $(sudo zcat -f $f | awk '$9==429||$9==403' | wc -l)"
done
A.4 与数据库分析的配合流程
- Nginx 日志:跑 A.3 第 4、5 条,拿到被 429/403 拦截的 IP 排行——回答"防护在拦谁、拦了多少";
- 数据库:跑第 4 节汇总 SQL,拿到穿透到应用层的可疑 IP——回答"还有谁绕过了防护";
- 合并去重:两边都有名单后取并集,就是当前完整爬虫清单;
- 针对性补规则:只出现在数据库里的(说明绕过了 Nginx)→ 分析它的共性特征(UA/Referer/行为),回到第 7 节加对应规则;
- 复查周期:建议每周跑一次 A.3 第 4、5 条 + 第 4 节 SQL,确认被拦数上升、进库的爬虫不再新增。
A.5 Nginx 日志分析的注意点
- 日志轮转:
/var/log/nginx/access.log是当前日志,旧日志轮转为access.log.1、.gz压缩包。涉及"近几天"的统计务必用第 10 条的zcat -f覆盖所有文件,否则漏数据。 - 字段位置验证:自定义
log_format会导致列号偏移,先用样例日志核对(A.2 提醒过)。 - 只统计文章页时:用
grep 'GET /article/'过滤静态资源(css/js/图片)和后台路径,避免 UA/Referer 特征被页面资源请求稀释。 - 别在超大日志上反复 grep:连续运行几天的日志可能有几十万行,逐条 grep 较慢。可以按日期先截取当天再分析,或定期把 Nginx 日志归档(如按周切割)。
- 403 是混合来源:防盗链(图片)、伪造 Referer 拦截(页面)、后台访问受限都可能返回 403。要区分是哪种拦截,看请求路径(
/uploads/开头的多是防盗链)和 Referer 列。
评论 (0)
还没有评论,来抢沙发。