← 中文资源

简体中文版本

将 Unicode 标题转为 URL Slug

通过 NFKD 转写、ASCII 过滤和分隔符折叠,把多语言标题转换成稳定 URL slug。

阅读英文原文

功能说明

这段代码把文章标题转换成适合放入 URL 的 ASCII slug:先做 Unicode 兼容分解,把带重音的字符拆成基字母和组合符号;再丢弃无法用 ASCII 表示的符号;最后把连续的非字母数字字符折叠成单个连字符。

它适合博客、文档站和内容平台的静态路由生成。输出只包含小写字母、数字和连字符,便于阅读、复制和在不同系统间保持稳定。

关键代码

unicodedata.normalize("NFKD", text) 把 é 拆成 e 加组合重音,随后 encode("ascii", "ignore") 丢弃组合符号,得到纯 ASCII。lower() 统一大小写,正则 [^a-z0-9]+ 把空格、标点和破折号统一替换为 -,再用 strip("-") 去掉首尾分隔符。

超过 max_length 时,先截断再按最后一个连字符切分,尽量不把单词切成两半;结果为空时回退到 untitled。

使用注意

中文、日文、阿拉伯文等非拉丁文字在 ASCII 过滤后会变成空串,因此统一返回 untitled,无法保留语义。ß、æ、ø 等字符不一定会被 NFKD 分解成 ASCII 组合,可能被直接丢弃,必要时需要额外的音译表。

不同标题可能生成相同 slug,入库前必须做唯一性检查并追加短后缀。保留字(如 api、admin)需要额外黑名单。长度限制按 ASCII 字符数计算,不是字节数;函数也不保证可逆。

验证方式

示例断言 Hello, World! 转为 hello-world、Café au lait 转为 cafe-au-lait、带破折号的标题转为 python-3-13-new-features,中文标题回退为 untitled。长标题在 max_length=20 时输出不超过 20 个字符,并在单词边界截断。

代码示例

from __future__ import annotations

import re
import unicodedata


def slugify(text: str, *, max_length: int = 60) -> str:
    normalized = unicodedata.normalize("NFKD", text)
    ascii_text = normalized.encode("ascii", "ignore").decode("ascii")
    ascii_text = ascii_text.lower()
    ascii_text = re.sub(r"[^a-z0-9]+", "-", ascii_text).strip("-")
    if len(ascii_text) > max_length:
        ascii_text = ascii_text[:max_length].rsplit("-", 1)[0].strip("-")
    return ascii_text or "untitled"


def main() -> None:
    cases = {
        "Hello, World!": "hello-world",
        "  Café au lait  ": "cafe-au-lait",
        "Über alles": "uber-alles",
        "Python 3.13 — New Features": "python-3-13-new-features",
        "你好世界": "untitled",
    }
    for source, expected in cases.items():
        result = slugify(source)
        print(f"slug -> {result}")
        assert result == expected
    assert len(slugify("word " * 30, max_length=20)) <= 20


if __name__ == "__main__":
    main()