KEEL · 龙骨 · A CURRICULUM FOR THE AI ERA

02 · 日志驱动的诊断工程 — keel 龙骨

本章目标:把 nginx access log 从"一堆文本"变成"可查询的事实表",建立爬虫行为基线与异常检测。 验收标准:能对任意一天、任意爬虫、任意路径做 SQL 式查询,并能检测抓取异常。

本章目标:把 nginx access log 从"一堆文本"变成"可查询的事实表",建立爬虫行为基线与异常检测。
验收标准:能对任意一天、任意爬虫、任意路径做 SQL 式查询,并能检测抓取异常。

为什么要把日志工程化:既有《SEO 收录与优化》第 05 章已经给出了一次性的统计脚本。但当你需要回答"上周三 Googlebot 对 /courses/ 的抓取量比前一周少了多少、是不是那天的 5xx 导致"这类问题时,grep 已经不够。你需要一张事实表。


一、从文本到事实表:解析设计

一条 nginx combined 日志行拆成结构化字段:

log_format seo '$remote_addr\t$time_iso8601\t$status\t$request_time\t'
               '$body_bytes_sent\t$request_method\t$request_uri\t$http_user_agent';
access_log /var/log/nginx/learning.access.log seo;
字段 类型 用途
remote_addr string 双向 DNS 验证真假爬虫
time_iso8601 timestamp 时间序列聚合
status int 浪费/故障识别
request_time float 速率上限计算
request_uri string 路径与参数分析
http_user_agent string 爬虫归类

解析成 TSV,再导入 SQLite(无需额外服务,单文件即可):

# 1) 把 UA 归类成一个 bot 字段,输出 TSV
LOG=/var/log/nginx/learning.access.log
zcat -f $LOG | perl -ne '
  my @f = split /\t/;
  next unless @f >= 8;
  my ($ip,$ts,$st,$rt,$bytes,$m,$uri,$ua) = @f[0..7];
  my $bot = "human";
  $bot = "googlebot"   if $ua =~ /Googlebot/i;
  $bot = "bingbot"     if $ua =~ /bingbot/i;
  $bot = "baiduspider" if $ua =~ /Baiduspider/i;
  $bot = "gptbot"      if $ua =~ /GPTBot/i;
  $bot = "claudebot"   if $ua =~ /ClaudeBot/i;
  $uri =~ s/\t/ /g; $ua =~ s/\t/ /g;
  print join("\t", $ts,$bot,$st,$rt,$uri), "\n";
' > /tmp/crawl.tsv

# 2) 建表并导入
sqlite3 /tmp/seo.db <<'SQL'
DROP TABLE IF EXISTS crawl;
CREATE TABLE crawl(ts TEXT, bot TEXT, status INT, rt REAL, uri TEXT);
.mode tabs
.import /tmp/crawl.tsv crawl
CREATE INDEX idx_bot_ts ON crawl(bot, ts);
SQL

二、事实表能回答的问题

-- Q1:近 7 天各爬虫每日抓取量
SELECT bot, substr(ts,1,10) d, count(*) n
FROM crawl WHERE bot <> 'human'
GROUP BY bot, d ORDER BY d DESC, n DESC;

-- Q2:Googlebot 抓取的状态码分布(浪费比例)
SELECT status, count(*) n FROM crawl
WHERE bot='googlebot' GROUP BY status ORDER BY n DESC;

-- Q3:抓取量最高的 20 条路径(是否集中在低价值页)
SELECT uri, count(*) n FROM crawl
WHERE bot='googlebot' GROUP BY uri ORDER BY n DESC LIMIT 20;

-- Q4:参数页占 Googlebot 抓取的比例
SELECT
  sum(CASE WHEN uri LIKE '%?%' THEN 1 ELSE 0 END) * 1.0 / count(*) AS param_ratio
FROM crawl WHERE bot='googlebot';

-- Q5:抓取耗时分布(是否撞速率上限)
SELECT round(rt,1) s, count(*) n FROM crawl
WHERE bot='googlebot' GROUP BY s ORDER BY n DESC LIMIT 10;

这一步的意义:从"凭印象优化"升级为"用查询找问题"。Q3 与 Q4 往往一眼就能看出预算是不是被参数页吃掉了。


三、爬虫行为基线

基线 = 正常情况下爬虫的抓取量、抓取时间分布、命中路径分布。有了基线,异常才能被发现。

-- 建立基线:近 4 周每周各爬虫的抓取总量
SELECT bot, strftime('%W', ts) week, count(*) n
FROM crawl GROUP BY bot, week ORDER BY bot, week;
基线维度 正常特征 异常信号
日抓取量 围绕均值波动 ±30% 骤降 80% 或归零
抓取时段 全天分散 集中在某小时(可能是限流)
状态码 95%+ 是 200 5xx 上升
命中路径 主要是有价值页 突然大量参数页
响应时间 稳定 明显变慢
# 简易异常检测:某爬虫今日抓取量 vs 近 30 天均值
sqlite3 /tmp/seo.db <<'SQL'
WITH daily AS (
  SELECT bot, substr(ts,1,10) d, count(*) n FROM crawl
  WHERE bot='googlebot' GROUP BY bot, d
), stats AS (
  SELECT bot, avg(n) mean, count(*) days FROM daily GROUP BY bot
)
SELECT d.bot, d.d, d.n, round(s.mean,1) AS mean
FROM daily d JOIN stats s USING(bot)
ORDER BY d.d DESC LIMIT 10;
SQL

判据:当日抓取量 < 均值 × 0.3 时,标记为异常,触发排查。


四、抓取量与收录量的因果分离

这是本章最难、也最有价值的一节。抓取量下降,不一定是"被降权",可能是你自己挂了。

假设 日志证据 其他证据
服务故障 抓取时大量 5xx 同期站点监控告警
限流误伤 爬虫收到 429/503 网关限流规则变更
需求下降 抓取量缓降但状态码正常 内容长期未更新
内链变化 某目录抓取骤减 发版改过导航
算法/人工处理 抓取正常但收录降 GSC 手动操作通知

分离方法:把"抓取量"与"服务健康度"两条时间序列画在一起。

-- 同一天里:抓取总量 与 5xx 数量
SELECT substr(ts,1,10) d,
       count(*) total,
       sum(CASE WHEN status>=500 THEN 1 ELSE 0 END) err5xx,
       sum(CASE WHEN status=429 THEN 1 ELSE 0 END) too_many
FROM crawl WHERE bot='googlebot'
GROUP BY d ORDER BY d DESC LIMIT 14;

判读规则:


五、把诊断做成常态化告警

#!/usr/bin/env bash
# /usr/local/bin/seo-alert.sh —— 每天跑一次
DB=/tmp/seo.db
today=$(date +%F)
yesterday=$(date -d yesterday +%F)

total=$(sqlite3 $DB "SELECT count(*) FROM crawl WHERE bot='googlebot' AND substr(ts,1,10)='$yesterday';")
err5xx=$(sqlite3 $DB "SELECT count(*) FROM crawl WHERE bot='googlebot' AND substr(ts,1,10)='$yesterday' AND status>=500;")
mean=$(sqlite3 $DB "SELECT avg(n) FROM (SELECT count(*) n FROM crawl WHERE bot='googlebot' GROUP BY substr(ts,1,10));")

echo "昨日 Googlebot 抓取=$total 5xx=$err5xx 历史均值=$mean"
awk -v t="$total" -v m="$mean" 'BEGIN{ if (t < m*0.3) exit 1 }' \
  || echo "⚠️ 抓取量异常偏低,触发排查"
[ "$err5xx" -gt 20 ] && echo "⚠️ 5xx 偏高,先查服务"

六、故障速查

现象 日志侧证据 处理
抓取归零 某爬虫计数为 0 查 robots 5xx、提交状态
抓取骤降 当日 < 均值 30% 按第四节做因果分离
抓取集中参数页 Q3/Q4 显示参数占比高 屏蔽参数页(第 01 章)
5xx 后抓取减少 5xx 与抓取量负相关 修服务,恢复后观察回升
日志里出现假爬虫 反向解析失败 用双向 DNS 校验后忽略

七、本章验收

# 本章核心:一次查询看清抓取健康度
sqlite3 /tmp/seo.db "SELECT substr(ts,1,10) d, count(*) total,
  sum(status>=500) err5xx FROM crawl WHERE bot='googlebot'
  GROUP BY d ORDER BY d DESC LIMIT 7;"

日志能告诉你"发生了什么",但解释不了"为什么渲染出问题"。下一章进入 JavaScript SEO 的深水区。

进入 keel 阅读