KernelRanch
返回文章列表

看起来一样的名字为什么查不到:Unicode 规范化与搜索键

2026-11-12 #unicode#python#search

用户输入的 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é 得到相同结果
搜索时兼容全角和部分兼容字符NFKCABC 可以匹配 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.htmlUnicode 数据库版本与 NFC、NFKC
Python 字符串文档https://docs.python.org/3.11/library/stdtypes.html#str.casefoldcasefold 的大小写折叠

核验说明

  • 主示例在本地 Python 3.11.9 下执行,检查正文中的断言与输出。
  • 实验范围是文中构造的输入和本地运行条件,部署时需按实际数据规模与运行环境核验。