现场是这样的。清洗后的中文语料送进 tokenizer 训练,分词结果里出现大量空 token,样本平均长度比预期短一截;抽样看十六进制才发现,网页抓下来的正文里夹着零宽空格和字节序标记,正则里完全没覆盖。

中间走过弯路,把「归一化放在分词之后,空 token 已经生成」当成主因,改完现象没变。

把日志和配置对完,能解释这个现象的基本就这几处。抓取时保留了零宽空格和字节序标记;清洗规则只处理了可见的空白字符;不同来源编码混用,同一个字符有多种表示。

先统计语料里零宽字符的种类与数量

python -c "from collections import Counter;s=open('corpus.txt',encoding='utf-8').read();print(Counter(c for c in s if c in '\u200b\ufeff\u200c\u200d'))"

接着分词之前统一清掉并做 NFKC 归一

python -c "import unicodedata,re;s=open('corpus.txt',encoding='utf-8').read();s=unicodedata.normalize('NFKC',re.sub(r'[\u200b-\u200f\ufeff]','',s));open('clean.txt','w',encoding='utf-8').write(s)"

然后重跑分词对比空 token 比例

python tokenize_check.py --in clean.txt --report empty_ratio

剩下的就是把清洗规则登记进流水线固定下来

python pipeline.py --stage clean --rules zero-width,nfkc --dry-run

别只看报错消失,验证要多做一步。空 token 比例从千分之四降到接近零,样本平均长度回到预期区间,同一批文本两次分词结果完全一致。

零宽字符在渲染上完全不可见,只能按码位处理,清洗要放在分词和编码之前,越靠后越难补救。拖得越久,排查成本越高,因为现场状态会被后续操作覆盖。

作者 9823189