2026年用Python批量审计XML Sitemap:揪出sitemap.xml中的"配置错误/死链/过期页"(附完整代码)
发布时间: 2026-09-21
标签: SEO、XML Sitemap、站点地图、技术SEO、Python、实战工具、审计
阅读时长: 约 15 分钟
难度: 中级
先说一个"提交了但白提交"的真相
很多人以为 sitemap.xml 写好、提交给 Google,就完事了。
但实际上------
sitemap 里有大量配置错误 ,Google 虽然收下,但根本不会按你期望的方式使用它。
常见的沉默失败:
- sitemap 里有 404 页面(你告诉 Google"来爬这个"→Google 跑来撞死)
- sitemap 里有
noindex页面(你让 Google 索引,但页面自己说"不要索引我") - 某个页面在 sitemap 里但 GSC 从未见过它(优先级或更新频率配置错误)
- sitemap 里有几十个 tag 归档页(无独特内容,稀释重要页的抓取权重)
- sitemap 索引文件(sitemap-index)里子 sitemap URL 全 404
- sitemap 大小超标(超过 5 万 URL / 50MB 时部分引擎会截断)
这些问题 GSC 不会主动告诉你------你得自己查。
今天写一个 XML Sitemap 健康度审计工具,把 sitemap 里的每个 URL 全部跑一遍,自动把错误列出来。
一、工具思路
sitemap.xml(或 sitemap-index.xml)
↓
解析:提取全部 <loc> URL
↓
并发检测每个 URL 的 HTTP 状态
↓
交叉检查(可选):
├─ robots.txt:sitemap 里的 URL 是否被 Disallow
├─ noindex:页面是否在 HTML 里说了"不要索引"
└─ GSC Coverage:哪些 sitemap 里的 URL 从未被索引
↓
报告:错误分类 + 优先级排序 + CSV 导出
依赖:
bash
pip install requests beautifulsoup4
二、完整代码:sitemap_auditor.py
python
"""
sitemap_auditor.py
XML Sitemap 健康度审计:解析 sitemap → 并发检测每个 URL → 交叉检查 robots/noindex
依赖:pip install requests beautifulsoup4
"""
import csv
import time
import xml.etree.ElementTree as ET
from collections import Counter, defaultdict
from concurrent.futures import ThreadPoolExecutor, as_completed
import requests
from bs4 import BeautifulSoup
DEFAULT_HEADERS = {"User-Agent": "Mozilla/5.0 (compatible; SitemapBot/1.0)"}
THREADS = 20
TIMEOUT = 10
# -------- 解析 --------
def parse_sitemap(url):
"""解析 sitemap,返回 list[url]。支持 sitemap-index"""
r = requests.get(url, headers=DEFAULT_HEADERS, timeout=15)
root = ET.fromstring(r.content)
ns = {"sm": "http://www.sitemaps.org/schemas/sitemap/0.9",
"xhtml": "http://www.w3.org/1999/xhtml"}
urls = []
# sitemap-index:找 <sitemap><loc>
sitemaps = root.findall(".//sm:sitemap/sm:loc", ns)
if sitemaps:
for sm_loc in sitemaps:
urls.extend(parse_sitemap(sm_loc.text))
return urls
# 普通 sitemap:找 <url><loc>
for loc in root.findall(".//sm:url/sm:loc", ns):
if loc.text:
urls.append(loc.text)
# xhtml 多语言 sitemap(hreflang in sitemap)
for loc in root.findall(".//xhtml:link", ns):
href = loc.get("href")
if href:
urls.append(href)
return urls
def check_url(url, session, timeout=TIMEOUT):
"""检测单个 URL 的状态和 noindex/nofollow"""
result = {
"url": url,
"status": None,
"error": None,
"noindex": False,
"canonical": None,
"canonical_self": True,
}
try:
r = session.get(url, headers=DEFAULT_HEADERS, timeout=timeout,
allow_redirects=True)
result["status"] = r.status_code
if r.status_code == 200 and "text/html" in r.headers.get("Content-Type", ""):
soup = BeautifulSoup(r.text, "html.parser")
# noindex 检查
meta = soup.find("meta", attrs={"name": "robots"})
if meta:
result["noindex"] = "noindex" in meta.get("content", "").lower()
# canonical
canon = soup.find("link", rel="canonical")
if canon and canon.get("href"):
result["canonical"] = canon["href"].strip()
result["canonical_self"] = (canon["href"].strip().rstrip("/") ==
url.rstrip("/"))
except requests.exceptions.Timeout:
result["error"] = "超时"
except requests.exceptions.ConnectionError:
result["error"] = "连接失败"
except Exception as e:
result["error"] = str(e)[:60]
return result
def get_robots_rules(site_url, session):
"""拉取 robots.txt,返回 set of disallowed paths"""
from urllib.parse import urlparse
base = f"{urlparse(site_url).scheme}://{urlparse(site_url).netloc}/robots.txt"
try:
r = session.get(base, headers=DEFAULT_HEADERS, timeout=10)
if r.status_code == 200:
disallowed = set()
for line in r.text.splitlines():
if line.lower().startswith("disallow:"):
path = line.split(":", 1)[1].strip()
if path:
disallowed.add(path)
return disallowed
except Exception:
pass
return set()
def audit_sitemap(sitemap_url, max_workers=THREADS):
print(f"📥 解析 sitemap:{sitemap_url}")
urls = parse_sitemap(sitemap_url)
print(f" 共发现 {len(urls)} 个 URL")
# 检查 robots.txt
robots = get_robots_rules(sitemap_url, requests.Session())
# 并发检测
session = requests.Session()
results = []
with ThreadPoolExecutor(max_workers=max_workers) as ex:
futures = {ex.submit(check_url, u, session): u for u in urls}
for i, fut in enumerate(as_completed(futures), 1):
r = fut.result()
results.append(r)
if i % 50 == 0:
print(f" 已检测 {i}/{len(urls)} ...")
# 分析
issues = defaultdict(list)
status_dist = Counter(r["status"] for r in results if r["status"])
errors = [r for r in results if r["error"] or (r["status"] and r["status"] >= 400)]
noindex_pages = [r for r in results if r["noindex"]]
wrong_canonical = [r for r in results
if r["canonical"] and not r["canonical_self"]]
disallow_in_sitemap = [r for r in results
if any(r["url"].endswith(p) for p in robots)]
for r in errors:
issues["死链 / 错误"].append(r)
for r in noindex_pages:
issues["noindex 页"].append(r)
for r in wrong_canonical:
issues["canonical 错误"].append(r)
for r in disallow_in_sitemap:
issues["robots Disallow"].append(r)
return {
"total": len(urls),
"status_dist": dict(status_dist),
"issues": dict(issues),
}
def report(res):
print("=" * 62)
print(f"🗺️ Sitemap 健康度审计报告")
print(f" 总 URL 数:{res['total']}")
print("=" * 62)
print(f"\n📊 HTTP 状态分布:")
for st, cnt in sorted(res["status_dist"].items(), key=lambda x: -x[1]):
tag = " ✅" if st == 200 else (" ❌" if st >= 400 else " ⚠️")
print(f" {st}{tag} {cnt} 个 URL")
for category, items in res["issues"].items():
print(f"\n🔴 {category}({len(items)} 个):")
for r in items[:15]:
note = ""
if r["error"]:
note = f" [{r['error']}]"
elif r["status"]:
note = f" [{r['status']}]"
if category == "noindex 页":
note = " (页面自己说不要索引)"
elif category == "canonical 错误":
note = f" → canonical={r['canonical']}"
print(f" {r['url']}{note}")
if len(items) > 15:
print(f" ...还有 {len(items)-15} 个,详见 CSV 导出")
def export_all(results, path="sitemap_audit.csv"):
with open(path, "w", newline="", encoding="utf-8-sig") as f:
w = csv.writer(f)
w.writerow(["url", "status", "error", "noindex", "canonical", "canonical_self"])
for r in results:
w.writerow([
r["url"], r["status"], r["error"] or "",
"Y" if r["noindex"] else "",
r["canonical"] or "", "Y" if r["canonical_self"] else "N"
])
print(f"✅ 完整报告已导出:{path}")
if __name__ == "__main__":
import sys
sm = sys.argv[1] if len(sys.argv) > 1 else "https://example.com/sitemap.xml"
all_results = []
# 复用 session 减少连接开销
from collections import defaultdict
issues = defaultdict(list)
# 先解析
urls = parse_sitemap(sm)
print(f"📥 共发现 {len(urls)} 个 URL,开始检测...")
session = requests.Session()
robots = get_robots_rules(sm, session)
all_res = []
with ThreadPoolExecutor(max_workers=20) as ex:
futures = [ex.submit(check_url, u, session) for u in urls]
for fut in as_completed(futures):
r = fut.result()
all_res.append(r)
# 分类
err = [r for r in all_res if r["error"] or (r["status"] and r["status"] >= 400)]
ni = [r for r in all_res if r["noindex"]]
wc = [r for r in all_res if r["canonical"] and not r["canonical_self"]]
dis = [r for r in all_res if any(r["url"].endswith(p) for p in robots)]
issue_map = {
"死链/错误": err, "noindex页": ni,
"canonical错误": wc, "robotsDisallow": dis
}
st_dist = Counter(r["status"] for r in all_res if r["status"])
res = {"total": len(urls), "status_dist": dict(st_dist), "issues": issue_map}
report(res)
export_all(all_res)
三、运行 & 输出示例
bash
python sitemap_auditor.py https://your-site.com/sitemap.xml
输出(节选):
📥 解析 sitemap:https://your-site.com/sitemap.xml
共发现 4,821 个 URL
📊 HTTP 状态分布:
200 ✅ 4,793 个 URL
404 ❌ 18 个 URL
301 10 个 URL
🔴 死链 / 错误(18 个):
https://your-site.com/old-product-page [404]
https://your-site.com/2023-campaign [404]
...还有 16 个,详见 CSV 导出
🔴 noindex 页(3 个):
https://your-site.com/tag/seo (页面自己说不要索引)
https://your-site.com/author/admin (页面自己说不要索引)
🔴 robots Disallow(2 个):
https://your-site.com/tag/* → robots Disallow
✅ 完整报告已导出:sitemap_audit.csv
立刻能做的事:
- 18 个 404 → 从 sitemap 删除,或 301 重定向到相关页面
- 3 个 noindex 还在 sitemap 里 → 删掉,告诉 Google"别索引这个"(已在 HTML 里说了,但 sitemap 还让 Google 来白跑)
- tag 归档页 robots Disallow 还在 sitemap → 删掉,两者自相矛盾
四、常见 sitemap 配置错误与修法
| 错误类型 | 后果 | 修法 | 参考 |
|---|---|---|---|
| sitemap 含 404 页 | 白占 5 万 URL 限额,白耗抓取预算 | 删除,或 301 重定向 | Sitemap 规范 |
| sitemap 含 noindex 页 | 自相矛盾,Google 困惑 | 删除,或去掉页面 noindex | noindex 规则 |
| tag/author 归档页入 sitemap | 无独特内容,分散重要页权重 | 加 noindex,或移出 sitemap | 低价值页面处理 |
| sitemap-index 里有子 sitemap 404 | 整块 sitemap 失效 | 修复子 sitemap URL | 多 sitemap |
| URL 数超 5 万 | 超出部分被忽略 | 拆成多个 sitemap,用 sitemap-index 聚合 | 大小限制 |
| canonical 不指向自身 | 权重分散,不知道哪篇是主文 | canonical 改为自引用 | canonical 用法 |
五、关键参考链接
| 主题 | 链接 |
|---|---|
| XML Sitemap 官方指南 | https://developers.google.com/search/docs/crawling-indexing/sitemaps/overview |
| Sitemap 大小限制 | https://developers.google.com/search/docs/crawling-indexing/sitemaps/overview |
| robots meta tag(noindex) | https://developers.google.com/search/docs/crawling-indexing/robots-meta-tag |
| 合并重复 URL(canonical) | https://developers.google.com/search/docs/crawling-indexing/consolidate-duplicate-urls |
| 多 sitemap 与 sitemap-index | https://developers.google.com/search/docs/crawling-indexing/sitemaps/overview |
| Google Search Console Sitemap 报告 | https://search.google.com/search-console/sitemaps |
六、进阶
6.1 配合 GSC Coverage 报告做"二次验证"
sitemap 审计能发现"死链/noindex",但无法知道"哪些 URL 提交了 sitemap 却从没有被 Google 索引"。这个要去 GSC Coverage 报告里手动查------两个工具结合,才算完整的 sitemap 质量评估。
6.2 定期巡检 + 变化告警
把脚本挂 cron 每周跑,对比上次结果:新增 404 / 新增 noindex 页 / URL 数突增------这些都是"网站有改动但 sitemap 没跟上"的信号。
python
import json
def notify_diff(before_csv, after_csv):
before = set(open(before_csv).read().splitlines()[1:])
after = set(open(after_csv).read().splitlines()[1:])
added = after - before
removed = before - after
if added or removed:
print(f"⚠️ sitemap 变化:新增 {len(added)} 条,删除 {len(removed)} 条")
6.3 大站分批检测
超过 5000 URL 的站点,线程数提到 30~50,或分批跑(按字母分段)避免超时。
七、总结
sitemap 提交了不等于 sitemap 健康------那些藏在里面的 404、noindex、Disallow,是沉默的排名杀手。
这个工具的价值:
- 自动解析 sitemap-index + 多语言 sitemap
- 并发检测每个 URL 状态 + noindex + canonical,数千 URL 分钟级跑完
- 把错误分类:高优先级(死链/noindex)→ 低优先级(canonical 偏移)
- 输出 CSV,可进监控、可对比变化、可交接给开发
每季度跑一次sitemap审计,相当于给网站做一次"健康体检"------省下的抓取预算,比你想的多得多。
你的 sitemap 有多少 URL?跑一遍看看有没有 404 和 noindex,评论区贴数字我帮你判断优先级。
📮 想要本文完整代码包 + 更多 SEO/GEO 实战工具合集?评论区留言或私信我即可,看到都会回。同名公众号「全域SEO增长」有同步更新,搜名字就能找到。