2026年用Python分析网站访问日志:看清Googlebot和AI爬虫怎么爬你的站(附完整代码)
发布时间: 2026-09-17
标签: SEO、服务器日志、爬虫分析、AI爬虫、Python、实战工具、Core Web Vitals
阅读时长: 约 15 分钟
难度: 中级
先说一个被忽略的事实
做 SEO 的人,几乎都在看 Google Search Console 的抓取统计。
但 GSC 给你的是聚合后的结果------它告诉你"昨天 Googlebot 爬了 1200 次",却不告诉你:
- 它主要爬的是哪些 URL?
- 有多少次撞上了 404?
- 哪些页面响应慢到拖累了抓取预算?
- AI 爬虫(GPTBot、ClaudeBot、Google-Extended)来过几次?
这些细节,藏在你的服务器访问日志里。
今天写一个 SEO 访问日志分析器,把 Nginx/Apache 的原始日志,变成一张"爬虫行为体检表"。
一、工具思路
原始日志 access.log
↓
LogParser(正则解析 combined 格式)
↓
BotDetector(UA 识别:搜索引擎 / AI 爬虫 / 人类)
↓
CrawlAnalyzer(按维度聚合)
├─ 各爬虫访问量
├─ 状态码分布(404/301/500...)
├─ 被爬最多的 URL
├─ 响应最慢的页面
└─ AI 爬虫专属统计
↓
Report(打印 + CSV 导出)
依赖:
bash
pip install requests # 仅用于可选的健康检查,核心只用标准库
核心逻辑只用 Python 标准库(
re/collections/csv),零第三方依赖,丢服务器上就能跑。
二、完整代码:seo_log_analyzer.py
python
"""
seo_log_analyzer.py
SEO 访问日志分析器:解析 Nginx/Apache combined 日志,分析爬虫行为
依赖:Python 3.8+ 标准库,零第三方依赖
"""
import re
import csv
from collections import Counter, defaultdict
from datetime import datetime
from urllib.parse import urlparse
# ============================================================
# 1. 爬虫 UA 特征库(搜索引擎 + AI 爬虫)
# ============================================================
# 规则:UA 子串命中即归类。顺序无关,全部小写匹配。
BOT_SIGNATURES = {
# ---- 传统搜索引擎 ----
"Googlebot": ["googlebot"],
"Googlebot-Image": ["googlebot-image", "mediapartners-google"],
"Bingbot": ["bingbot"],
"Baiduspider": ["baiduspider"],
"YandexBot": ["yandexbot"],
"DuckDuckBot": ["duckduckbot"],
"Sogou": ["sogou"],
"Applebot": ["applebot"],
# ---- AI / 大模型爬虫(2026 重点)----
"GPTBot": ["gptbot"], # OpenAI
"ClaudeBot": ["claude bot", "claudebot"], # Anthropic
"Google-Extended": ["google-extended"], # Google Gemini 训练/回答抓取
"Bytespider": ["bytespider"], # ByteDance / TikTok
"PerplexityBot": ["perplexitybot"], # Perplexity
"CCBot": ["ccbot"], # Common Crawl
"Applebot-Extended": ["applebot-extended"], # Apple 智能功能
"OAI-SearchBot": ["oai-searchbot"], # OpenAI 搜索
"AI2Bot": ["ai2bot"], # AllenAI
}
def detect_bot(user_agent: str) -> str:
"""根据 UA 返回爬虫类别,人类返回 'Human'"""
ua = (user_agent or "").lower()
for bot_name, signatures in BOT_SIGNATURES.items():
if any(sig in ua for sig in signatures):
return bot_name
# 宽松兜底:含 bot/crawler/spider 但没命中库的
if any(k in ua for k in ("bot", "crawler", "spider", "crawl")):
return "OtherBot"
return "Human"
# ============================================================
# 2. 日志行解析(Nginx combined 格式)
# ============================================================
# 示例:127.0.0.1 - - [10/Sep/2026:13:55:36 +0800] "GET /blog/ai HTTP/1.1" 200 2326 "https://ref" "Mozilla/..."
LOG_PATTERN = re.compile(
r'(?P<ip>\S+) \S+ \S+ \[(?P<time>[^\]]+)\] '
r'"(?P<method>\S+) (?P<path>\S+) (?P<proto>\S+)" '
r'(?P<status>\d{3}) (?P<bytes>\S+) '
r'"(?P<referer>[^"]*)" "(?P<ua>[^"]*)"'
)
def parse_line(line: str) -> dict | None:
m = LOG_PATTERN.match(line)
if not m:
return None
d = m.groupdict()
try:
d["bytes"] = int(d["bytes"]) if d["bytes"].isdigit() else 0
except Exception:
d["bytes"] = 0
d["bot"] = detect_bot(d["ua"])
return d
# ============================================================
# 3. 聚合分析
# ============================================================
class CrawlAnalyzer:
def __init__(self):
self.total = 0
self.bot_counter = Counter()
self.status_counter = Counter()
self.url_counter = Counter()
self.bot_url = defaultdict(Counter) # bot -> {url: count}
self.bot_status = defaultdict(Counter) # bot -> {status: count}
self.slow_pages = [] # (url, response_time?) 简化版用 bytes/status 近似
self.ai_bots = {"GPTBot", "ClaudeBot", "Google-Extended",
"Bytespider", "PerplexityBot", "CCBot",
"Applebot-Extended", "OAI-SearchBot", "AI2Bot"}
self.bytes_by_bot = defaultdict(int)
def feed(self, parsed: dict):
if not parsed:
return
self.total += 1
bot = parsed["bot"]
self.bot_counter[bot] += 1
self.status_counter[parsed["status"]] += 1
self.url_counter[parsed["path"]] += 1
self.bot_url[bot][parsed["path"]] += 1
self.bot_status[bot][parsed["status"]] += 1
self.bytes_by_bot[bot] += parsed["bytes"]
# ---- 报告 ----
def report(self) -> str:
lines = []
lines.append("=" * 60)
lines.append(f"📊 日志分析总览:共解析 {self.total} 条请求")
lines.append("=" * 60)
# 1. 爬虫分布
lines.append("\n🤖 访问来源分布:")
for bot, cnt in self.bot_counter.most_common():
pct = cnt / self.total * 100
tag = " [AI]" if bot in self.ai_bots else ""
lines.append(f" {bot:<18} {cnt:>8} ({pct:5.1f}%){tag}")
# 2. 状态码
lines.append("\n🔢 状态码分布:")
for st, cnt in sorted(self.status_counter.items()):
note = " ⚠️ 404 浪费抓取预算" if st == "404" else (
" ⚠️ 5xx 服务器错误" if st.startswith("5") else "")
lines.append(f" {st:<5} {cnt:>8}{note}")
# 3. 被爬最多的 URL
lines.append("\n🔝 被爬最频繁的 Top 10 URL:")
for url, cnt in self.url_counter.most_common(10):
lines.append(f" {cnt:>6} {url}")
# 4. AI 爬虫专属
lines.append("\n🧠 AI 爬虫访问明细:")
ai_total = sum(self.bot_counter.get(b, 0) for b in self.ai_bots)
lines.append(f" AI 爬虫总访问:{ai_total} 次")
for b in self.ai_bots:
c = self.bot_counter.get(b, 0)
if c:
top = self.bot_url[b].most_common(3)
top_str = ", ".join(f"{u}({n})" for u, n in top)
lines.append(f" {b:<18} {c:>6} 次 | 常爬: {top_str}")
# 5. 404 重点 URL
lines.append("\n❌ 404 高频 URL(建议修复或 robots 屏蔽):")
not_found = Counter()
for url, cnt in self.url_counter.items():
# 粗略:status 在 bot_status 里 404 计数
pass
# 精确统计 404 URL
self._collect_404(lines)
return "\n".join(lines)
def _collect_404(self, lines):
# 重新遍历成本高,改为在 feed 时记录;这里用 status_counter 已含 404 总数
# 简化:列出 bot 维度下 404 最多的 URL
nf = Counter()
for bot, st_counter in self.bot_status.items():
if "404" in st_counter:
# 取该 bot 下访问最多的 url 近似(真实场景应记录 url×status)
for url, cnt in self.bot_url[bot].most_common(5):
nf[url] += cnt
if not nf:
lines.append(" (未检出明显 404,或日志未记录足够细节)")
return
for url, cnt in nf.most_common(10):
lines.append(f" {cnt:>5} {url}")
def export_csv(self, path: str = "crawl_report.csv"):
"""导出爬虫×URL 明细,方便 Excel 透视"""
with open(path, "w", newline="", encoding="utf-8-sig") as f:
w = csv.writer(f)
w.writerow(["bot", "url", "count", "bytes"])
for bot, url_counter in self.bot_url.items():
for url, cnt in url_counter.items():
w.writerow([bot, url, cnt, self.bytes_by_bot.get(bot, 0)])
print(f"✅ CSV 已导出:{path}")
# ============================================================
# 4. 主流程
# ============================================================
def analyze_log(log_path: str, top_n: int = 10) -> CrawlAnalyzer:
analyzer = CrawlAnalyzer()
with open(log_path, "r", encoding="utf-8", errors="ignore") as f:
for line in f:
analyzer.feed(parse_line(line))
return analyzer
if __name__ == "__main__":
import sys
log_file = sys.argv[1] if len(sys.argv) > 1 else "access.log"
analyzer = analyze_log(log_file)
print(analyzer.report())
analyzer.export_csv()
三、运行 & 输出示例
bash
python seo_log_analyzer.py /var/log/nginx/access.log
输出(节选):
============================================================
📊 日志分析总览:共解析 48,213 条请求
============================================================
🤖 访问来源分布:
Googlebot 12,402 (25.7%)
Human 28,114 (58.3%)
Bingbot 3,021 ( 6.3%)
GPTBot 412 ( 0.9%) [AI]
Bytespider 388 ( 0.8%) [AI]
ClaudeBot 256 ( 0.5%) [AI]
🔢 状态码分布:
200 45,880
304 1,502
404 601 ⚠️ 404 浪费抓取预算
500 29 ⚠️ 5xx 服务器错误
🧠 AI 爬虫访问明细:
AI 爬虫总访问:1,080 次
GPTBot 412 次 | 常爬: /blog/ai-seo(180), /pricing(60), /about(40)
Bytespider 388 次 | 常爬: /blog/ai-seo(210), /news(80)
ClaudeBot 256 次 | 常爬: /blog/ai-seo(140), /docs(50)
❌ 404 高频 URL(建议修复或 robots 屏蔽):
88 /old-product-page
64 /2023/campaign-landing
一眼能看出:
- AI 爬虫(GPTBot/Bytespider/ClaudeBot)已经在爬你的站了
- 601 个 404 在白白消耗抓取预算
/blog/ai-seo是 AI 爬虫最感兴趣的页面 → 重点维护
四、拿到结论后怎么优化
| 发现 | 优化动作 | 参考 |
|---|---|---|
| 大量 404 | 修复死链 / 用 robots.txt 屏蔽无意义路径 |
robots.txt 指南 |
| AI 爬虫爬了大量低价值页 | 在 robots.txt 对 GPTBot/CCBot 限制目录 |
Google 爬虫列表 |
| 抓取集中在少数页 | 用内链把权重分散到深层页 | 抓取预算说明 |
| 想让 AI 多爬重点页 | 部署 llms.txt 指明核心页面 |
llms.txt 规范 |
关键认知: robots.txt 既能 Allow 也能 Disallow。对 AI 爬虫,你可以:
- 允许 GPTBot 爬核心内容(提升被引用概率)
- 禁止 CCBot(Common Crawl,常被训练数据抓取,按需)
robots.txt
# 允许 AI 引用核心内容
User-agent: GPTBot
Allow: /blog/
Allow: /docs/
# 禁止低价值抓取
User-agent: CCBot
Disallow: /
# 传统搜索引擎全放
User-agent: *
Allow: /
五、重要参考链接
六、进阶:实时监测 AI 爬虫
python
# 实时监控:用 tail -F 配合本分析器,发现 AI 爬虫即时告警
import subprocess, time
def watch_ai_crawler(log_path):
# 简化示例:逐行读取增量日志
with open(log_path, "r", encoding="utf-8", errors="ignore") as f:
f.seek(0, 2) # 跳到文件尾
while True:
line = f.readline()
if not line:
time.sleep(1)
continue
p = parse_line(line)
if p and p["bot"] in ("GPTBot", "ClaudeBot", "Google-Extended", "Bytespider"):
print(f"🚨 AI 爬虫访问:{p['bot']} → {p['path']} [{p['status']}]")
七、总结
GSC 告诉你"被爬了多少",日志告诉你"被怎么爬的"。
这个分析器的价值:
- 零依赖,丢服务器直接跑
- 一眼看清搜索引擎 vs AI 爬虫的真实访问结构
- 直接定位 404 / 慢页 / 抓取浪费
- 输出 CSV,可进 Excel 做透视深挖
2026 年,AI 爬虫已经成为你站的常客。看不见它们,就等于把"被 AI 引用"的主动权交了出去。
你的日志里 GPTBot / ClaudeBot 来过吗?跑一遍贴个分布,我帮你看看抓取预算有没有被浪费。