---
title: "看起来一样的名字为什么查不到：Unicode 规范化与搜索键"
date: 2026-11-12
slug: unicode-normalization-search-keys
canonical: https://kernelranch.com/blog/2026/11/unicode-normalization-search-keys
markdown_url: https://kernelranch.com/blog/2026/11/unicode-normalization-search-keys.md
tags: ["unicode", "python", "search"]
---用户输入的 café 看起来和数据库里的 café 一样，等值查询却找不到。一个字符串的末尾可能是单个 é，另一个是 e 加组合重音。先检查码点，再根据产品需求生成搜索键。保留原始显示文本，能让以后调整匹配规则时重新建立索引。

## 1. 同样的显示文本，码点不同

保存为 unicode_lab.py，执行 python unicode_lab.py。

```python
import unicodedata as ud

def search_key(text):
    folded = ud.normalize("NFKC", text).casefold()
    return ud.normalize("NFKC", folded)

composed = "café"
decomposed = "café"
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("ＡＢＣ") == 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", 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 |
| 搜索时忽略大小写 | 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 下执行，检查正文中的断言与输出。
- 实验范围是文中构造的输入和本地运行条件，部署时需按实际数据规模与运行环境核验。
