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;
判读规则:
total降 +err5xx升 → 你的服务问题,不是 SEO 问题。total降 +err5xx正常 + 内容长期未变 → 需求下降,去更新内容。total正常 + 收录降 → 索引环节问题,回到结构化数据与内容质量。
五、把诊断做成常态化告警
#!/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 校验后忽略 |
七、本章验收
- access log 已改为结构化格式并落地为 SQLite 事实表
- 能对任意爬虫/日期/路径执行查询(Q1–Q5)
- 已建立至少 4 周的抓取量基线
- 有异常检测脚本,抓取量骤降可告警
- 能用"抓取 vs 5xx"两条序列分离服务故障与需求下降
# 本章核心:一次查询看清抓取健康度
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 的深水区。