皮皮舟岁月

A JOURNAL OF TRAVEL · CODE · MARKETS
第三十六期 · 第 36 号 二〇二六年八月 · 立秋之后

防爬实战·爬虫识别与防爬配置(访问日志分析全记录)

2026-09-09 177 阅读

本文档完整记录:发现可疑访问 → 理解数据库结构 → 编写 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

两个可疑点:

  • x1x2 这两个子域名从未在 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 里带 curlpythonGo-httpJava/HttpClientokhttpScrapywget 等字样 → 未伪装的脚本。
  • 伪 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 结尾就放行,不校验子域名是否真实存在。所以 x1x2 这种不存在的子域名也能通过。这正是后续要单独拦截它的原因。


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 和 RefererMODE() 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 是谁的,人工复核时必须排除三类:

  1. 搜索引擎官方爬虫:Googlebot 的 IP 段(66.249.66.0/24 等)、Bingbot、百度蜘蛛等。它们的 UA 会注明身份(如 Googlebot/2.1),必须放行——封了影响收录。
  2. 本机回环0:0:0:0:0:0:0:1127.0.0.1 是你在服务器/本地的访问(后台操作、测试),不是爬虫。
  3. 合规 AI / 监控爬虫:声明了身份的(如 DeepSeekBot/1.0AwarioBot/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.comx2.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.conflocation / 中应用

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.conflocation /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. 注意事项与误区

  1. 别封搜索引擎:Googlebot/Bingbot/百度蜘蛛封了会掉收录。它们 UA 会声明身份,IP 段可查官方文档。
  2. 别把本机当爬虫0:0:0:0:0:0:0:1(回环)是你自己。
  3. 别只封单个 IP:住宅代理池 IP 一直轮换,封单个没用。要对"共性"下手——UA、伪造 Referer、IP 段。
  4. IP 段封禁有误伤:36.99.136.x 是电信住宅段,可能混有真实用户,用限流/拦截而非直接封整段。
  5. 低频爬虫绕限流:限流只挡高频(如每秒几次以上)。低频代理池爬虫靠第 7.3 节的 Referer 白名单拦截,这是互补关系。
  6. 数据库看不到被挡的请求:评估效果必须看 Nginx 日志(8.2 节),这是最容易误解的一点。
  7. 新增真实子域名记得加白名单fake-referer.conf 白名单只有主域和 www,将来开新子域(如 blog.example.com)必须先在白名单追加,否则新站点的站内跳转会 403。
  8. 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 与数据库分析的配合流程

  1. Nginx 日志:跑 A.3 第 4、5 条,拿到被 429/403 拦截的 IP 排行——回答"防护在拦谁、拦了多少";
  2. 数据库:跑第 4 节汇总 SQL,拿到穿透到应用层的可疑 IP——回答"还有谁绕过了防护";
  3. 合并去重:两边都有名单后取并集,就是当前完整爬虫清单;
  4. 针对性补规则:只出现在数据库里的(说明绕过了 Nginx)→ 分析它的共性特征(UA/Referer/行为),回到第 7 节加对应规则;
  5. 复查周期:建议每周跑一次 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)

还没有评论,来抢沙发。

发表评论