2026年用Python批量审计XML Sitemap:揪出sitemap.xml中的“配置错误/死链/过期页“(附完整代码)

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增长」有同步更新,搜名字就能找到。

相关推荐
一条泥憨鱼1 小时前
苍穹外卖【day11| 用户统计,订单统计,销量排名统计功能实现】
java·后端·苍穹外卖
huaweichenai1 小时前
spring boot操作PDF
java·spring boot·pdf
钱栈up1 小时前
自动化多平台发布:脚本报FAIL时用三个信号判断真实状态
python
余槐i1 小时前
拆解 Agent 核心原理|从零动手实现简易 AI 智能体(三)
人工智能·python·fastapi·ai agent
她的男孩1 小时前
缓存明明命中了却报 ClassCastException:拆完多级缓存控制面,我挖出 5 个静默失效的坑
java·后端·架构
程序员AlbertTu1 小时前
# Mantissa 使用教程 — Python 版与 C++ 版
c++·python·数值运算
T_Apollo1 小时前
2.5 Java 8.0 版本新增特性和类
java
SimonKing1 小时前
SpringBoot 集成 SSE 实现服务端推送或可代替Websocket
java·后端·程序员
童园管理札记1 小时前
从政策驱动到课堂落地:2026年“人工智能+教育”全景解读与技术实践指南
人工智能·python·深度学习·职场和发展·学习方法