看起来一样的名字为什么查不到:Unicode 规范化与搜索键
用户输入的 café 看起来和数据库里的 café 一样,等值查询却找不到。一个字符串的末尾可能是单个 é,另一个是 e 加组合重音。先检查码点,再根据产品需求生成搜索键。保留原始显示文本,能让以后调整匹配规则时重新建立索引。
1. 同样的显示文本,码点不同
保存为 unicode_lab.py,执行 python unicode_lab.py。
import unicodedata as ud
def search_key(text):
folded = ud.normalize("NFKC", text).casefold()
return ud.normalize("NFKC", folded)
composed = "café"
decomposed = "café"
assert composed != decomposed
assert ud.normalize("NFC", composed) == ud.normalize("NFC", decomposed)
print("raw lengths:", len(composed), len(decomposed))
print("NFC equal:", ud.normalize("NFC", composed) == ud.normalize("NFC", decomposed))
assert search_key("ABC") == search_key("abc")
assert search_key("Straße") == search_key("STRASSE")
assert search_key("a") != search_key("а")
print("width match: True; casefold match: True")
groups = {}
for original in ["ABC", "abc", composed, decomposed]:
groups.setdefault(search_key(original), []).append(original)
assert sorted(map(len, groups.values())) == [2, 2]
print("collision group sizes:", sorted(map(len, groups.values())))
print("Unicode database:", ud.unidata_version)
2. 按产品目的选择规范化方式
| 目的 | 示例策略 | 需要验证的行为 |
|---|---|---|
| 合并规范等价的组合字符 | NFC | 两种 café 得到相同结果 |
| 搜索时兼容全角和部分兼容字符 | NFKC | ABC 可以匹配 ABC |
| 搜索时忽略大小写 | casefold 后再次规范化 | Straße 可以匹配 STRASSE |
| 显示用户输入 | 保留原文 | 用户能看到自己填写的内容 |
本例实测原文长度分别为 4 和 5,NFC 后相等。搜索键将全角 ABC 和小写 abc 合并,也将上述两个德语拼写合并。拉丁 a 与西里尔字母 а 仍然不同。规范化只处理指定的等价关系,视觉混淆检测需要额外设计。
3. 上线前先找碰撞
新增 search_key 列时,先按同一算法回填,再分组统计重复键。若字段要求唯一,碰撞组需要通过明确的业务规则处理后才能创建唯一索引。已有用户的名字不能仅凭搜索键相等就自动合并账号。
记录规范化算法版本和运行时 Unicode 数据库版本。升级时重新计算一份候选键,比较碰撞变化,再切换查询。外部协议的签名原文、密码和大小写敏感标识应遵守各自协议,搜索键只在约定的检索边界使用。
小结
先用码点和规范化实验定位差异,再为搜索生成独立键。保留原文、检查碰撞、记录算法版本,能让后续升级有可回溯的依据。
数据来源与检索时间
检索时间 2026-11-12。
| 来源 | URL | 对应内容 |
|---|---|---|
| Python 3.11 官方文档 | https://docs.python.org/3.11/library/unicodedata.html | Unicode 数据库版本与 NFC、NFKC |
| Python 字符串文档 | https://docs.python.org/3.11/library/stdtypes.html#str.casefold | casefold 的大小写折叠 |
核验说明
- 主示例在本地 Python 3.11.9 下执行,检查正文中的断言与输出。
- 实验范围是文中构造的输入和本地运行条件,部署时需按实际数据规模与运行环境核验。