2. 文本预处理基础:清洗、归一化、Unicode标准化

好,咱们正式开始第二讲。在把文本丢给 Tokenizer 之前,有一道绕不开的工序,就是预处理。

说白了,Tokenizer 很笨。你给它一坨脏数据,它就给你吐一坨乱七八糟的 。我这些年踩过最大的坑,基本都是预处理没做干净导致的。

这一节,我带着你把预处理讲透。核心就三件事:清洗、归一化、Unicode 标准化。

2.1 清洗:把垃圾扫出门

清洗的目的很单纯——去掉那些对后续分析没有意义、甚至有害的信息。

常见清洗项:

  • HTML 标签:从网页扒下来的文本,经常带一堆 <div>、<p>。Tokenizer 不需要这些。
  • 特殊符号:一些奇怪的 Unicode 符号,比如emoji里的某些控制字符,很容易让分词器卡住。
  • 多余空白:多个空格、制表符、换行符,最好统一成一个空格或直接去掉。
  • 不可见字符:像零宽空格(\u200B)这种东西,肉眼看不见,但程序不认,很容易造成 对齐错误。

我记得有一次,线上模型预测某条数据的标签总是错。排查了好久,才发现源文本里藏了一个零宽空格。清洗代码加上 \u200B 过滤之后,问题就解决了。

小建议:写清洗脚本时,别只用正则。推荐用 ftfy 或 textcleaner 这类现成的库,它们能覆盖很多你意想不到的边界情况。
# 一个欠完备的清洗示例(Python)
import re

def basic_clean(text):
    text = re.sub(r'<[^>]+>', '', text)       # 去 HTML 标签
    text = re.sub(r'\s+', ' ', text).strip()   # 合并空白
    text = re.sub(r'[\x00-\x08\x0b\x0c\x0e-\x1f]', '', text)  # 去控制字符
    return text

2.2 归一化:把形式统一起来

归一化和清洗不一样。清洗是“去掉错的”,归一化是“把对的统一”。

几个典型场景:

  1. 大小写统一:中文不用管,但英文最好全转成小写。否则 Apple 和 apple 会变成两个 ,白白增加词表大小。
  2. 数字占位符:像 666 和 123,在大多数任务里,具体数值不重要。可以用一个统一的 如 <NUM> 代替。
  3. 缩写展开:像 “it's” 可以展开为 “it is”。
  4. 简繁转换:如果数据集里既有简体又有繁体,最好统一转成一种。我个人习惯用 opencc 来做。

我遇到过最典型的归一化问题,是用户输入的 “3.14” 和 “3,14”。如果不对标点做归一化,分词器会把逗号和小数点当成不同的东西。你想想看,训练语料里“3.14”出现1000次,“3,14”出现1次,那模型肯定不认识后面那个。

# 一个简单的归一化流程
def normalize(text):
    text = text.lower()                     # 小写
    text = re.sub(r'\d+', '<NUM>', text)  # 数字归一化
    text = re.sub(r'[\.,,。]', ' ', text)  # 标点统一成空格
    return text

2.3 Unicode 标准化:别让编码坑了你

这部分容易被忽视,但它真的很重要。

你看,“é”这个字符,在 Unicode 里可以有多种表示方式:

  • NFD:分解成 “e” + 组合符号 ◌́
  • NFC:合成单个字符 “é”

如果没有统一标准化,“café” 和 “café”(第二种写法)在字节级别就是完全不同的字符串。Tokenizer 会认为它们是两个词,这显然不对。

常见标准化形式:

形式 说明 使用场景
NFC 优先合成 大多数通用场景,比如社交文本
NFD 优先分解 音标、注音符号等需要分析基字符的场景
NFKC 兼容分解后再合成 金融、数字处理;能把“②”转为“2”
NFKD 兼容分解后保持分解 搜索、索引,尽可能保留可搜索性

我曾经在做一个多语言 OCR 项目时,发现同一篇文档里“ü”的编码不一致。有的地方是 NFC 合成字,有的是 NFD 分解字。结果分词器把 “Müller” 拆成了 “Muller” 和 “ü”。后来加上 unicodedata.normalize('NFC', text) 才搞定。

注意:中文看起来不受影响,但汉字中的一些罕见字和生僻部首在 Unicode 里也有类似的分解/合成情况。比如 “𨨏” 这种 CJK 扩展区的字,不同机器上编码可能不同。建议对中文也做一次 NFC 标准化。
import unicodedata

def unicode_normalize(text, form='NFC'):
    return unicodedata.normalize(form, text)

2.4 预处理整体流程

这三个步骤串在一起,才算完整的预处理流水线。我画了一张图,帮你理清它们之间的关系。

文本预处理整体流程 文本预处理整体流程 原始文本 1. 清洗 2. 归一化 3. Unicode 标准化 干净语料 → 分词 每步的输出 清洗 → 剔除噪声 归一化 → 统一形式 标准化 → 消除编码歧义

你看,流程是线性的。先清洗,再归一化,最后做 Unicode 标准化。做完这三步,文本基本就干净了,可以喂给 Tokenizer 了。

我在实际项目中,一般会把这三步封装成一个函数。线上推理和线下训练用同一套预处理逻辑。为什么这么做?因为训练语料和线上输入如果预处理不一致,再好的模型也会扑街。

2.5 避坑指南:我踩过的几个坑

  • 不要相信“原始文本”:很多公开数据集号称已经清洗过了。我建议你拿到手后,自己再跑一遍清洗流程。不信你试试,随便找一个 text 文件,隐藏字符可能多到让你怀疑人生。
  • 归一化别过度:比如“3.14”和“3,14”,有时候真的不能统一。能不能统一,取决于下游任务。如果是数值解析任务,逗号和小数点都得保留。我之前在金融领域的一个 NER 项目里,把小数点误归一化成了空格,结果金额识别全错了。
  • Unicode 标准化必须做在分词前:如果你先分词再标准化,编码不一致导致的分词错误已经发生了,标准化救不了你。所以顺序一定要对。

好了,文本预处理这块我就讲到这里。字数不多,但全是我实战中的血泪经验。你把这些基础打牢了,后面讲 Tokenizer 的时候,咱们才能真正深入下去。