2026年用Python检查多语言站点hreflang:自动揪出"回链缺失/标错语言"等致命错误(附完整代码)
发布时间: 2026-09-20
标签: SEO、多语言、hreflang、国际化、外贸独立站、Python、实战工具
阅读时长: 约 15 分钟
难度: 中级
先说一个外贸独立站最隐蔽的坑
做外贸 / 多语言独立站的,几乎都配置了 hreflang。
但配错比不配更糟。
hreflang 是 Google 用来判断"哪个语言/地区版本给哪国用户看"的标签。它有一套双向校验规则 ,只要有一条不满足,Google 会直接忽略整组标签------你辛苦做的中英日版本,可能全被当成重复内容,互相抢排名。
最常见的致命错误:
- 回链缺失(return-tag error):A 页标了"去看 B 页的日语版",但 B 页没标回来 → 整组失效
- 缺自引用 :每个语言页必须包含指向自己的那条 hreflang
- 语言码写错 :
en-us写成en_us、zh写成cn、zH-CN大小写混 - 冲突 :同一个
hreflang值指向了两个不同的 URL - 缺
x-default:没给"其他所有地区"兜底
这些错误,手动检查几十个页面就头大,几百个页面根本查不过来。
今天写一个 hreflang 多语言检查工具,自动爬完全站,把每一类错误列出来。
一、工具思路
站点入口(首页或多语言页集合)
↓
BFS 爬取站内页面(同域名)
↓
每个页面解析 <link rel="alternate" hreflang="xx" href="...">
↓
构建「页面 → {hreflang: 目标URL}」映射
↓
校验规则:
├─ 自引用:页面是否包含指向自己的 hreflang
├─ 回链:A→B 则 B 必须→A
├─ 语言码合法性
├─ 同页内 hreflang 不冲突
└─ 是否缺 x-default(警告)
↓
报告:错误清单 + CSV 导出
依赖:
bash
pip install requests beautifulsoup4
二、完整代码:hreflang_checker.py
python
"""
hreflang_checker.py
多语言站点 hreflang 检查器:爬取全站,校验自引用/回链/语言码等
依赖:pip install requests beautifulsoup4
"""
import csv
import re
from collections import deque, defaultdict
from urllib.parse import urljoin, urlparse, urldefrag
import requests
from bs4 import BeautifulSoup
# 常见合法语言码前缀(ISO 639-1)+ 可选地区(ISO 3166-1 alpha-2)
LANG_CODE_RE = re.compile(r"^[a-z]{2}(-[A-Za-z]{2})?$")
# 地区码必须大写两位字母
REGION_RE = re.compile(r"^[A-Z]{2}$")
class HreflangChecker:
def __init__(self, start_url: str, max_pages: int = 300, delay: float = 0.3):
self.start_url = start_url.rstrip("/")
self.domain = urlparse(start_url).netloc
self.max_pages = max_pages
self.delay = delay
self.headers = {"User-Agent": "Mozilla/5.0 (compatible; HreflangBot/1.0)"}
self.annotations = {} # url -> {hreflang: target_url}
self.pages = set()
def _norm(self, url):
url, _ = urldefrag(url)
return url.rstrip("/") or url
def _is_internal(self, url):
return urlparse(url).netloc == self.domain
# ---------------- 爬取 + 解析 hreflang ----------------
def crawl(self):
queue = deque([self.start_url])
seen = set()
while queue and len(self.pages) < self.max_pages:
url = self._norm(queue.popleft())
if url in seen:
continue
seen.add(url)
try:
r = requests.get(url, headers=self.headers, timeout=10)
except Exception:
continue
if r.status_code >= 400 or "text/html" not in r.headers.get("Content-Type", ""):
continue
self.pages.add(url)
soup = BeautifulSoup(r.text, "html.parser")
tags = {}
for link in soup.find_all("link", rel="alternate"):
hreflang = (link.get("hreflang") or "").strip().lower()
href = (link.get("href") or "").strip()
if not hreflang or not href:
continue
target = self._norm(urljoin(url, href))
tags.setdefault(hreflang, set()).add(target)
self.annotations[url] = tags
# 收集站内链接继续爬
for a in soup.find_all("a", href=True):
t = self._norm(urljoin(url, a["href"]))
if self._is_internal(t) and t not in seen \
and len(self.pages) < self.max_pages:
queue.append(t)
return self
# ---------------- 校验 ----------------
def validate(self) -> dict:
errors = defaultdict(list) # url -> [error_msg]
warnings = defaultdict(list)
for url, tags in self.annotations.items():
# 1. 自引用:必须有一条 hreflang 指向自己
if url not in set().union(*tags.values()) if tags else True:
# 简化:检查是否有 target == 自身
if not any(url in targets for targets in tags.values()):
errors[url].append("❌ 缺自引用(页面未用 hreflang 指向自己)")
# 2. 语言码合法性
for hl in tags:
if hl == "x-default":
continue
if not LANG_CODE_RE.match(hl):
errors[url].append(f"❌ 非法语言码:{hl}(应为 xx 或 xx-XX)")
else:
parts = hl.split("-")
if len(parts) == 2 and not REGION_RE.match(parts[1]):
errors[url].append(f"⚠️ 地区码应大写两位:{hl}")
# 3. 同页内同一 hreflang 指向多个 URL(冲突)
for hl, targets in tags.items():
if len(targets) > 1:
errors[url].append(
f"❌ hreflang「{hl}」冲突,指向 {len(targets)} 个URL")
# 4. 回链(return-tag):A 标注 B,则 B 必须标注 A
for url, tags in self.annotations.items():
for hl, targets in tags.items():
for tgt in targets:
if tgt in self.annotations:
back = self.annotations[tgt]
# B 是否有一条 hreflang==hl 指向 url?
if not any(url in back_targets
for back_targets in back.get(hl, set()).__iter__() if False):
pass
# 简化回链检查
back_has = any(url in back.get(hl, set())
for _ in [0])
if not back_has:
errors[url].append(
f"❌ 回链缺失:{url} →[{hl}]→ {tgt},但 {tgt} 未回指")
# 5. 全站是否缺 x-default(仅警告)
has_xdefault = any("x-default" in tags
for tags in self.annotations.values())
if not has_xdefault:
warnings["__global__"].append(
"⚠️ 全站未检测到 x-default(建议为「其他所有地区」设兜底)")
return {"errors": errors, "warnings": warnings}
# ---------------- 报告 ----------------
def report(self, res: dict):
errors, warnings = res["errors"], res["warnings"]
total_err = sum(len(v) for v in errors.values())
print("=" * 62)
print(f"🌐 hreflang 检查:{self.domain}({len(self.pages)} 页)")
print(f" 发现错误 {total_err} 条")
print("=" * 62)
for url, msgs in errors.items():
print(f"\n🔴 {url}")
for m in msgs:
print(f" {m}")
for key, msgs in warnings.items():
for m in msgs:
print(f"\n🟡 {m}")
# 错误最多的页面排序
top = sorted(errors.items(), key=lambda x: -len(x[1]))[:10]
if top:
print("\n📌 错误最集中的页面 Top:")
for url, msgs in top:
print(f" {len(msgs)} 处 {url}")
def export(self, res: dict, path: str = "hreflang_errors.csv"):
with open(path, "w", newline="", encoding="utf-8-sig") as f:
w = csv.writer(f)
w.writerow(["page", "level", "issue"])
for url, msgs in res["errors"].items():
for m in msgs:
w.writerow([url, "ERROR", m])
for url, msgs in res["warnings"].items():
for m in msgs:
w.writerow([url, "WARN", m])
print(f"✅ 错误清单已导出:{path}")
if __name__ == "__main__":
import sys, time
start = sys.argv[1] if len(sys.argv) > 1 else "https://example.com/"
checker = HreflangChecker(start, max_pages=300).crawl()
res = checker.validate()
checker.report(res)
checker.export(res)
三、运行 & 输出示例
bash
python hreflang_checker.py https://your-store.com/
输出(节选):
==============================================================
🌐 hreflang 检查:your-store.com(142 页)
发现错误 37 条
==============================================================
🔴 https://your-store.com/ja/products/widget
❌ 缺自引用(页面未用 hreflang 指向自己)
❌ 回链缺失:/ja/.../widget →[en]→ /en/products/widget,但 /en/... 未回指
🔴 https://your-store.com/zh/product/x
❌ 非法语言码:zh_cn(应为 xx 或 xx-XX)
🟡 ⚠️ 全站未检测到 x-default(建议为「其他所有地区」设兜底)
一眼能看出: 日语页漏了自引用和回链 → 整组 hreflang 失效,Google 把日英版本当重复内容互搏。修好这几处,多语言排名通常立刻回正。
四、拿到结果怎么改
| 错误 | 修法 | 参考 |
|---|---|---|
| 缺自引用 | 每个语言页的 <link> 列表里,必须含一条 hreflang="本页语言" href="本页URL" |
hreflang 规范 |
| 回链缺失 | A→B 的同时,B 必须→A(双向一致) | 回链说明 |
| 语言码错误 | 用 xx 或 xx-XX 格式,XX 大写(en-US、zh-CN) |
管理多区域站点 |
| 冲突 | 同一 hreflang 只指向一个 URL |
本地化版本 |
| 缺 x-default | 加一条 hreflang="x-default" 指向默认版本 |
x-default 用法 |
核心铁律: hreflang 是双向契约。只要有一处不对,Google 宁可当作没看见------所以全站一致性比单页正确更重要。
五、关键参考链接
六、进阶
6.1 用 sitemap 的 hreflang 一起查
多语言站点常在 sitemap 里 也写 hreflang(<xhtml:link rel="alternate">)。建议把 sitemap 的标注也解析出来,和页面 <head> 里的相互印证------两边不一致也是常见坑。
6.2 只对"语言页"爬取,提速
python
# 若已知语言前缀,限定爬取范围
checker = HreflangChecker("https://store.com/", max_pages=300)
checker.crawl()
# 然后只校验 URL 含 /en/ /ja/ /zh/ 的页面
6.3 接入 CI,发版前自动拦截
把校验做成 CI 步骤,每次部署跑一遍,错误数 > 0 就阻断发版。多语言站规模一大,人工根本兜不住,必须自动化。
6.4 配合 GEO:多语言 = 多市场 AI 引用
2026 年 AI 搜索也是分语言的。你的英文版被 ChatGPT 引用,中文版被豆包/DeepSeek 引用------hreflang 标对了,AI 才能把"对的语言版本"喂给"对的用户"。多语言 SEO 和 GEO 是同一套底层纪律。
七、总结
hreflang 是外贸独立站最容易被配错、却又最影响多市场排名的一块。
这个工具的价值:
- 自动爬全站,无需逐个页面肉眼查
- 一次性揪出:缺自引用 / 回链缺失 / 语言码错 / 冲突 / 缺 x-default
- 输出 CSV,可进 CI、可交接开发、可定期复跑
- 纯 Python + bs4,零 API key
多语言站排名不稳,先别怪内容------十有八九是 hreflang 有断链。
你的站 hreflang 自检过吗?跑一遍贴错误数,我帮你看最该先修哪几处。
📮 想要本文完整代码包 + 更多 SEO/GEO 实战工具合集?评论区留言或私信我即可,看到都会回。同名公众号「全域SEO增长」有同步更新,搜名字就能找到。