编码、文本与规范化
本节目标
理解 bytes、字符、编码和 Unicode 的区别;用严格、可审计的规则处理 BOM、换行与规范等价字符;同时保留“可引用原文”和“适合检索的派生文本”。
字节不是字符
磁盘和网络保存的是 bytes。编码(encoding)规定 bytes 如何映射到字符;Unicode 给字符分配码点;字体再决定字符如何显示。UTF-8 是一种编码方式,不是语言,也不保证内容是中文或英文。
raw = "中文 Agent".encode("utf-8")
text = raw.decode("utf-8", errors="strict")strict 在遇到非法序列时抛出 UnicodeDecodeError。解析入口不应默认使用 errors="ignore":它会静默删字,可能把金额、否定词或代码字符删掉。replace 会插入 U+FFFD,只适合明确的降级流程,并要统计替换位置、发出质量警告、保留原始 bytes。
编码检测不是魔法
BOM 能明确提示某些 UTF 编码;HTTP charset 或文档声明是上游证据;统计检测器只能给候选和置信信号。推荐决策顺序:
- 读取协议或格式规范规定的编码;
- 使用可信元数据或 BOM;
- 尝试允许清单中的严格解码;
- 若仍不确定,进入人工/专用检测器复核,而不是遍历所有编码直到“不报错”。
本库项目允许普通文本使用 UTF-8、带 BOM 的 UTF-8 和带 BOM 的 UTF-16;JSON 按 RFC 8259 的开放交换要求只接受 UTF-8。这个选择是工程策略,不代表所有历史文件都只有这些编码。
BOM、换行和原始哈希
Windows 文本常见 CRLF(\r\n),Unix 常见 LF(\n)。内部可以统一成 LF,但要区分两个哈希:
raw_sha256:对原始 bytes 计算,用于版本与重放;text_sha256:对声明规则下的规范化 UTF-8 文本计算,用于元素比较。
先规范化再算“原始哈希”会把不同输入错误地合并。反过来,只保存原始哈希又无法解释为什么两个视觉相同元素的检索行为不同。
UTF-8 BOM 是 EF BB BF。Python 的 utf-8-sig 会在文件开头消费 BOM;若输出又随意加回,跨工具比较会出现差异。规则要版本化并有测试。
Unicode 规范化的直觉
é 可以是单个预组字符,也可以由 e 加组合重音组成。它们视觉相同,bytes 与 len() 却可能不同:
import unicodedata
left = "é"
right = "e\u0301"
print(left == right) # False
print(unicodedata.normalize("NFC", left) == unicodedata.normalize("NFC", right)) # TrueUnicode UAX #15 定义四种规范化形式:
| 形式 | 直觉 | 使用提醒 |
|---|---|---|
| NFC | 规范分解后尽量组合 | 常用于通用文本互操作;本项目采用 |
| NFD | 保持规范分解形式 | 某些文件系统或文本处理场景会出现 |
| NFKC | 兼容分解后组合 | 会折叠圈号、宽度、上标等区别 |
| NFKD | 兼容分解形式 | 同样可能损失有语义的视觉区别 |
UAX #15 明确提醒不能把 NFKC/NFKD 盲目用于任意文本,因为数学符号、上标、分数和全角形式等区别可能有意义。NFC 也不是“清除所有异常字符”;它只处理标准定义的规范等价关系。
四层文本模型
为了同时满足审计、展示与检索,可以保存:
- 原始 bytes:不可变源版本,受权限保护;
- 解析器原始文本:带页/行/坐标,用于复核解析行为;
- 规范化文本:换行与 Unicode 规则固定,仍保持语义;
- 检索派生文本:可做大小写折叠、表格行描述或空白策略,但不能冒充引用原文。
每层记录规则版本和父级哈希。Agent 给用户引用时应回到可定位的解析原文或源页面,而不是只展示为召回优化过的派生字符串。
控制字符、零宽字符与空白
“不可见”不等于“无意义”。不换行空格影响断行,零宽连接符会影响某些文字书写,制表符和换行对代码/表格有结构意义。安全做法是:
- 先检测并统计码点类别,不急于删除;
- 为明确风险建立小型允许/拒绝规则;
- 对代码、自然语言、标识符分别处理;
- 保存变更日志和原文坐标;
- 用真实语种、公式和代码样本回归。
终端显示乱码也不能直接证明文件损坏;还要检查终端字体、输出编码和管道编码。
JSON、CSV 与 HTML 的差异
- JSON:使用真正的 JSON 解析器;生产入口应考虑重复键和
NaN/Infinity等非标准扩展。本项目严格拒绝这些值。 - CSV:RFC 4180 描述了常见格式,但现实中分隔符、编码和方言仍需显式约定;含逗号或换行的字段必须由
csv模块处理。 - HTML:实体、嵌套和可见结构要交给 HTML 解析器。用正则删除标签会把脚本、样式、单元格和段落边界混在一起。
常见错误与排查
- “能 decode 就是正确编码”:许多错误编码组合也能生成合法字符;需要协议、元数据和样本验证。
- “NFKC 总能提高搜索”:它可能把有语义的格式差异折叠;只能用在明确字段或派生检索层。
- “删掉所有空白更干净”:词、代码缩进、段落和单元格会粘连。
- “规范化后无需原文”:没有原文就无法精确引用、排查或更换规则。
- “JSON 库一定拒绝 NaN”:不同实现和默认值不同;契约要显式测试。
练习
- 比较
"é"与"e\u0301"的码点、UTF-8 bytes、原始哈希和 NFC 文本哈希。 - 构造一个 UTF-8 非法序列,分别运行
strict、replace、ignore,记录每种模式丢失了什么。 - 用
csv解析含逗号、双引号和嵌入换行的字段;解释为什么split(',')失败。 - 为“证据引用文本”和“搜索文本”各写一份允许的规范化规则,标出不可逆步骤。
自测
- 我能从 bytes、码点、字体三个层次解释乱码。
- 我知道 BOM、声明编码和统计检测的证据强弱。
- 我能解释 NFC 与 NFKC 的不同风险。
- 我会同时保存原始版本和规则化派生版本。
- 我不会把搜索文本直接当作原文引用。
参考资料与下一步
- Unicode Standard Annex #15: Normalization Forms
- RFC 8259: JSON
- RFC 4180: CSV
- Python
codecs - Python
unicodedata
来源获取日期:2026-07-22。下一步:结构、标题、列表与表格。