hreflang 审计工具:外贸多语言站谷歌SEO的隐性杀手

hreflang 审计工具:外贸多语言站谷歌SEO的隐性杀手

做外贸独立站,做英语站、德语站、法语站是常态。多语言站有个专门负责「告诉谷歌哪个版本给哪个地区」的标签,叫 hreflang。它写对了,德国买家搜到的是德语版,美国买家搜到的是英语版;写错了,谷歌可能把错版本推给用户,或者干脆忽略掉,几个版本自己互相打架。

hreflang 的坑很多:标签缺失、缺 x-default、语言代码写错、双向引用没对上、指向的 URL 打不开。人工一个个页面查,费时又容易漏。这篇用 Python 写一个审计工具,把这些检查全自动化,跑一遍就出报告。

hreflang 的官方规则可以看 Google 搜索中心的说明:本地化版本(hreflang) 和 管理多地区与多语言网站。

工具能检查什么

  1. 提取页面上所有 hreflang 标签
  2. 校验语言代码格式(en / de / fr / zh-CN 这类)
  3. 检查是否缺少 x-default
  4. 并发检查每个版本 URL 是否可访问(HTTP 200)
  5. 并发检查双向引用是否闭环
  6. 输出问题清单

依赖:requests、beautifulsoup4。

完整代码

python 复制代码
# -*- coding: utf-8 -*-
"""
hreflang 标签审计工具
用途:检查外贸多语言独立站的 hreflang 标签是否规范、可访问、双向引用闭环
依赖:pip install requests beautifulsoup4
"""

import re
import requests
from bs4 import BeautifulSoup
from urllib.parse import urljoin
from concurrent.futures import ThreadPoolExecutor, as_completed

HEADERS = {
    "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 "
                  "(KHTML, like Gecko) Chrome/120.0 Safari/537.36",
}

# 合法语言代码:en / de / fr / es / pt / zh-CN / zh-TW 这类
LANG_RE = re.compile(r"^[a-z]{2,3}(-[A-Za-z]{2,4})?$")


def fetch_html(url, timeout=10):
    """请求页面,返回 (status_code, html)"""
    try:
        resp = requests.get(url, headers=HEADERS, timeout=timeout, allow_redirects=True)
        return resp.status_code, resp.text
    except requests.RequestException as e:
        return None, str(e)


def extract_hreflang(url, html):
    """提取页面上的 hreflang 映射:{lang: 绝对URL}"""
    soup = BeautifulSoup(html, "html.parser")
    mapping = {}
    for link in soup.find_all("link", rel=lambda v: v and "alternate" in v):
        hreflang = link.get("hreflang")
        href = link.get("href")
        if hreflang and href:
            mapping[hreflang.lower()] = urljoin(url, href)
    return mapping


def check_lang_code(mapping):
    problems = []
    for lang in mapping:
        if lang != "x-default" and not LANG_RE.match(lang):
            problems.append(f"语言代码格式异常: {lang!r},应为 en / zh-CN 这类格式")
    return problems


def check_accessibility(mapping):
    problems = []

    def worker(item):
        lang, u = item
        code, _ = fetch_html(u)
        return lang, u, code

    with ThreadPoolExecutor(max_workers=8) as ex:
        futures = {ex.submit(worker, item): item for item in mapping.items()}
        for fut in as_completed(futures):
            lang, u, code = fut.result()
            if code != 200:
                problems.append(f"[{lang}] 无法访问 (HTTP {code}): {u}")
    return problems


def check_reciprocal(entry_url, entry_lang, mapping):
    problems = []

    def worker(item):
        lang, u = item
        code, html = fetch_html(u)
        if code != 200:
            return None
        back = extract_hreflang(u, html)
        if back.get(entry_lang) != entry_url:
            return f"[{lang}] 页面没有正确回指 {entry_lang} 版本: {u}"
        return None

    targets = [(l, u) for l, u in mapping.items() if l != "x-default"]
    with ThreadPoolExecutor(max_workers=8) as ex:
        futures = [ex.submit(worker, t) for t in targets]
        for fut in as_completed(futures):
            res = fut.result()
            if res:
                problems.append(res)
    return problems


def audit(url):
    print(f"目标页面: {url}\n" + "=" * 60)
    code, html = fetch_html(url)
    if code != 200:
        print(f"页面无法访问 (HTTP {code})")
        return

    mapping = extract_hreflang(url, html)
    if not mapping:
        print("[问题] 页面没有任何 hreflang 标签,请先补上")
        return

    print(f"发现 {len(mapping)} 个 hreflang 版本:")
    for lang, u in sorted(mapping.items()):
        print(f"  {lang:12s} -> {u}")

    # 本页语言 = 自引用那条的 hreflang
    entry_lang = next((l for l, u in mapping.items() if l != "x-default" and u == url), None)

    print("\n[1] 语言代码检查")
    lang_problems = check_lang_code(mapping)
    if not lang_problems:
        print("  全部合法")
    for p in lang_problems:
        print(" ", p)
    if "x-default" not in mapping:
        print("  [建议] 缺少 x-default,加一条兜底给未匹配语言的访客")

    print("\n[2] 可访问性检查(并发)")
    acc_problems = check_accessibility(mapping)
    if not acc_problems:
        print("  全部可访问 (HTTP 200)")
    for p in acc_problems:
        print(" ", p)

    print("\n[3] 双向引用检查(并发)")
    if not entry_lang:
        print("  [警告] 没找到自引用,无法判断本页语言,请人工确认")
    else:
        print(f"  本页语言判定为: {entry_lang}")
        rec_problems = check_reciprocal(url, entry_lang, mapping)
        if not rec_problems:
            print("  双向引用闭环正常")
        for p in rec_problems:
            print(" ", p)

    print("\n" + "=" * 60)
    print("审计结束。有问题的地方,对照 Google 官方文档修正即可。")


if __name__ == "__main__":
    audit("https://example.com/en/products/")

怎么用

bash 复制代码
pip install requests beautifulsoup4

把最后一行 audit(...) 里的 URL 换成你自己的页面,运行即可。

输出会分三块:语言代码检查、可访问性检查、双向引用检查。每一块有问题都会单独列出,对着改就行。

几个容易忽略的点

hreflang 是双向的。 A 页面声明了 B 的德语版,B 页面也必须回指 A 的英语版,缺一边谷歌就可能不认。这就是工具里「双向引用检查」在做的。

x-default 建议保留。 它用来兜底:当访问者的语言和地区跟任何版本都匹配不上时,跳到哪个页面。多数站点指向英文版或一个语言选择页。

语言代码要规范。 en 表示英语,zh-CN 表示简体中文,zh-TW 表示繁体中文。别写 en-US 去表示英语全站,那会被理解成「美国英语」,跟地区语义混在一起。

写在最后

工具代码可以直接拿去用,跑一遍就知道自己的多语言站 hreflang 有没有问题。觉得有用欢迎点赞、收藏、转发,有疑问评论区留言或私信交流。

相关推荐
QYRdata2 小时前
机器人3D视觉感知赛道爆发 2026-2032年复合增长率达81.7%
人工智能·3d·机器人·服务发现·恒州博智
WJ.Polar2 小时前
final、单例类、枚举类、抽象类、接口
java·开发语言
内容营销专家刘鑫炜3 小时前
主流 AI 大模型眼中的“刘鑫炜”之千问篇
人工智能
打工仔折腾 AI3 小时前
从零开始大模型微调:基于 PyTorch 与 ChatGLM 的 LoRA 实践指南
人工智能·pytorch·后端·python·深度学习·性能优化·ai agent 实战
小羊没烦恼!3 小时前
C#综合揭秘——Entity Framework 并发处理详解
java·开发语言·前端·javascript·数据库·mysql·c#
艾iYYY3 小时前
【无标题】
linux·开发语言·网络·操作系统
用户0332126663673 小时前
Python 自动化办公:如何从零快速生成结构清晰的 Excel 报表
python·excel
百度Geek说3 小时前
我们放弃了"全自动生成代码" —— 网盘 Coding Agent 落地与迭代实录
人工智能
回眸&啤酒鸭3 小时前
【回眸】Inbox Zero 高效邮件管理实战效果展示
人工智能