🚀

用 Python 做一个网站死链检测工具,批量发现并修复失效链接

爬虫式扫描全站链接,标记 404 和超时页面,导出修复清单。SEO 优化的第一步就是清理死链。

👁️ - 次阅读

网站上有几十个链接,哪个还能用、哪个已经挂了,凭感觉猜不到。

手动一个个点开?太慢了。

今天做一个死链检测工具。输入一个网址,自动爬取全站链接,标记所有 404 和超时的页面,最后输出一份修复清单。

项目背景

搜索引擎会把死链计入你的网站质量评分。死链越多,排名越容易掉。

很多站长知道这个问题,但懒得查。手动检查又慢又不全。

用脚本扫一遍,几分钟就能拿到完整报告。比花几百块买第三方服务划算多了。

这个工具适合:

  • 刚迁移网站的站长,旧链接可能全挂了
  • 长期运营的站点,定期清理失效链接
  • 内容聚合站,外链经常失效

技术选型

组件选择理由
HTTP 请求httpx支持异步并发,速度快
HTML 解析lxml比 BeautifulSoup 更快
并发控制asyncio不阻塞,同时检查多个链接
报告输出CSV + HTML方便查看和分享

实现步骤

第一步:安装依赖

pip install httpx lxml colorama

第二步:核心检测脚本

创建 dead_link_detector.py

import asyncio
import csv
import os
import sys
from datetime import datetime
from urllib.parse import urljoin, urlparse

import httpx
from lxml import html


class DeadLinkDetector:
    def __init__(self, base_url, max_concurrent=20, timeout=10):
        self.base_url = base_url.rstrip("/")
        self.max_concurrent = max_concurrent
        self.timeout = timeout
        self.visited = set()
        self.results = []
        self.seen_links = set()
        self.semaphore = asyncio.Semaphore(max_concurrent)

    async def check_url(self, url):
        """检查单个链接的状态"""
        async with self.semaphore:
            try:
                async with httpx.AsyncClient(timeout=self.timeout) as client:
                    resp = await client.head(url)
                    status = resp.status_code
                    return {"url": url, "status": status, "error": None}
            except httpx.HTTPStatusError as e:
                return {"url": url, "status": e.response.status_code, "error": str(e)}
            except Exception as e:
                return {"url": url, "status": 0, "error": str(e)}

    async def parse_page_links(self, url):
        """解析页面中的所有内部链接"""
        internal_links = []
        try:
            async with httpx.AsyncClient(timeout=self.timeout) as client:
                resp = await client.get(url)
                tree = html.fromstring(resp.text)
                for anchor in tree.xpath("//a[@href]"):
                    href = anchor.get("href", "").strip()
                    if not href or href.startswith("#") or href.startswith("javascript"):
                        continue
                    # 只检测同域名的内部链接
                    full_url = urljoin(url, href)
                    parsed = urlparse(full_url)
                    base_parsed = urlparse(self.base_url)
                    if parsed.netloc == base_parsed.netloc and parsed.path:
                        clean_url = full_url.split("#")[0]
                        if clean_url not in self.seen_links:
                            self.seen_links.add(clean_url)
                            internal_links.append(clean_url)
        except Exception:
            pass
        return internal_links

    async def crawl_and_check(self, start_url):
        """爬取并检查链接"""
        queue = asyncio.Queue()
        await queue.put(start_url)
        self.visited.add(start_url)
        self.results.append({"url": start_url, "status": "pending", "error": None})

        while not queue.empty():
            url = await queue.get()
            print(f"\r扫描中: {url}", end="", flush=True)

            # 检查当前链接
            result = await self.check_url(url)
            self.results[-1] = result

            # 如果是正常页面,继续爬取其中的链接
            if result["status"] == 200:
                links = await self.parse_page_links(url)
                for link in links:
                    if link not in self.visited:
                        self.visited.add(link)
                        self.results.append({"url": link, "status": "pending", "error": None})
                        await queue.put(link)

            queue.task_done()

    def generate_report(self):
        """生成检测报告"""
        timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
        dead_links = [r for r in self.results if r["status"] >= 400 or r["status"] == 0]
        alive_links = [r for r in self.results if 200 <= r["status"] < 400]

        # 保存 CSV
        csv_path = f"dead_links_{timestamp}.csv"
        with open(csv_path, "w", newline="", encoding="utf-8") as f:
            writer = csv.writer(f)
            writer.writerow(["URL", "状态码", "错误信息"])
            for r in self.results:
                writer.writerow([r["url"], r["status"], r.get("error", "")])

        # 保存 HTML 报告
        html_path = f"report_{timestamp}.html"
        with open(html_path, "w", encoding="utf-8") as f:
            f.write(f"""<!DOCTYPE html>
<html><head><meta charset="utf-8"><title>死链检测报告</title>
<style>
body {{ font-family: sans-serif; margin: 20px; }}
.dead {{ color: red; }}
.alive {{ color: green; }}
table {{ border-collapse: collapse; width: 100%; }}
th, td {{ border: 1px solid #ddd; padding: 8px; text-align: left; }}
th {{ background: #f4f4f4; }}
</style></head><body>
<h1>死链检测报告</h1>
<p>检测时间: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}</p>
<p>总计: {len(self.results)} 个链接 | 正常: {len(alive_links)} | 异常: {len(dead_links)}</p>
<h2>异常链接 ({len(dead_links)})</h2>
<table>
<tr><th>URL</th><th>状态码</th><th>错误信息</th></tr>
""")
            for r in dead_links:
                f.write(f'<tr class="dead"><td>{r["url"]}</td><td>{r["status"]}</td><td>{r.get("error", "")}</td></tr>\n')
            f.write("""</table>
<h2>正常链接</h2>
<table>
<tr><th>URL</th><th>状态码</th></tr>
""")
            for r in alive_links:
                f.write(f'<tr class="alive"><td>{r["url"]}</td><td>{r["status"]}</td></tr>\n')
            f.write("</table></body></html>")

        print(f"\n\n✅ CSV 报告: {csv_path}")
        print(f"✅ HTML 报告: {html_path}")
        print(f"📊 总计: {len(self.results)} 个链接 | 正常: {len(alive_links)} | 异常: {len(dead_links)}")

        return dead_links


async def main():
    if len(sys.argv) < 2:
        url = input("请输入要检测的网站地址 (如 https://example.com): ").strip()
    else:
        url = sys.argv[1]

    print(f"开始检测: {url}")
    detector = DeadLinkDetector(url)
    await detector.crawl_and_check(url)
    detector.generate_report()


if __name__ == "__main__":
    asyncio.run(main())

第三步:运行

# 检测自己的网站
python dead_link_detector.py https://navbox.com.cn

# 指定并发数和超时时间
python dead_link_detector.py https://yoursite.com --concurrent 30 --timeout 15

第四步:配合 Crontab 定期检测

把下面这行加到 crontab 里,每周日凌晨三点自动检测:

0 3 * * 0 cd ~/dead-link-detector && python dead_link_detector.py https://yoursite.com >> /var/log/deadlink.log 2>&1

收到异常链接后,手动检查是否需要修复或设置 301 跳转。

运行效果

假设检测一个有 50 个页面的网站,输出类似这样:

开始检测: https://example.com
扫描中: https://example.com/about
扫描中: https://example.com/contact
扫描中: https://example.com/old-page

✅ CSV 报告: dead_links_20260703_103000.csv
✅ HTML 报告: report_20260703_103000.html
📊 总计: 52 个链接 | 正常: 48 | 异常: 4

HTML 报告可以直接在浏览器打开,红色标注的就是需要修复的链接。CSV 可以用 Excel 打开,方便批量处理。

优化方向

基础版已经能跑,想更强的话可以加这些:

  • 自动修复建议:对 404 链接自动匹配最可能的替代页面,给出 301 跳转建议
  • 邮件告警:发现新死链时自动发邮件通知
  • 历史对比:每次检测结果存档,对比死链数量的变化趋势
  • sitemap 优先:先读取 sitemap.xml,针对性检测收录页面
  • 图片检测:不仅检查链接,还扫描页面内的图片是否全部可加载
  • 子域名扩展:递归检测主域名下的所有子域名

网站维护不是做一次就完事的事。

定期扫一遍,把死链清干净,搜索引擎对你的评价才会一直稳着。

跑一次看看你的站有多少链接已经挂了。

这篇文章对你有帮助吗?