功能说明
这段代码把文章标题转换成适合放入 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()