公司动态
文本清洗:Unicode 修复、换行规范化与 C4 清洗
系列第三篇。数据采集进来的文本是「脏」的乱码、成串空行、样板套话。这一阶段用一个「修饰器链」把文本逐个洗一遍。我们会看到三个修饰器各自的实现以及它们背后那个优雅的抽象——DocumentModifier。一、先看抽象什么叫「修饰器」清洗阶段的所有动作都被统一成一种「文本 → 文本」的纯函数。这个项目用抽象基类把它固化了下来classDocumentModifier(ABC):abstractmethoddefmodify_document(self,text:str)-str:Transform a documents text.任何一个清洗动作只要实现了modify_document就能被装进同一条流水线。修 Unicode 是修饰器压换行是修饰器删样板句也是修饰器——它们没有本质区别都只是「把一段文本变成更好的另一段文本」。这个抽象带来的直接好处是编排逻辑CleaningPipeline可以完全不知道具体有哪些清洗动作只管「按顺序调用」classCleaningPipeline:modifiers:list[DocumentModifier]text_field:strtextdrop_empty:boolTruedefadd_modifier(self,modifier):self.modifiers.append(modifier)returnself# 支持链式调用defprocess(self,df:pd.DataFrame)-pd.DataFrame:resultdf.copy()formodifierinself.modifiers:result[self.text_field]result[self.text_field].apply(modifier.modify_document)ifself.drop_empty:resultresult[result[self.text_field].str.strip().astype(bool)]returnresult.reset_index(dropTrue)注意看这段编排代码里藏着两个关键设计add_modifier返回self让你能写出CleaningPipeline().add_modifier(a).add_modifier(b)这样的链式调用读起来像在「组装」一条生产线。drop_emptyTrue清洗完成后如果某篇文档被洗成了空串就把它整个丢掉。这个动作很关键——比如一篇文档全是样板句C4Cleaner逐行删光后变成空字符串与其留着一个空壳污染语料不如在清洗阶段就地清除。默认配置下这条链上有三个修饰器按顺序执行cleaning:modifiers:-name:unicode_reformatter# 1. 修乱码-name:newline_normalizer# 2. 压换行-name:c4_cleaner# 3. 删样板顺序本身是有讲究的先修编码、再统一换行、最后才做基于行/词的语义清洗——因为后两步都假设文本已经是「正常的 UTF-8 文本、正常的行结构」。二、UnicodeReformatter让乱码恢复原形网页抓取的文本里最常见的一类脏数据就是mojibake乱码。它的成因是「编码误解码」一段原本用 UTF-8 编码的文本被错误地当成 Latin-1 解码于是右单引号就变成了’这种谁也认不出的字符串。人工写规则去修这些乱码工作量是无穷的——因为乱码的组合千奇百怪。幸运的是这个问题已经被ftfyfixes text for you这个库近乎完美地解决了。它内置了一套「探测 修复」的启发式规则专门识别并纠正这类编码错误。代码里的实现干净到几乎就是ftfy的一个薄封装dataclassclassUnicodeReformatter(DocumentModifier):fix_encoding:boolTrueremove_control_chars:boolTrueunescape_html:boolTruemax_decode_length:int1_000_000defmodify_document(self,text:str)-str:importftfyreturnftfy.fix_text(text,fix_encodingself.fix_encoding,remove_control_charsself.remove_control_chars,unescape_htmlself.unescape_html,max_decode_lengthself.max_decode_length,)三个开关各管一件事fix_encoding修复 mojibake’→remove_control_chars移除那些会破坏文本解析的控制字符比如终端转义序列unescape_html把 HTML 实体还原成字符amp;→。一个容易被忽略的细节是max_decode_length。ftfy 在「修复编码」时会对文本做多次探测性解码这个探测过程对超长文本可能有性能开销所以用这个上限默认 100 万字符防止极端情况下的耗时失控。对绝大多数文档来说这个上限足够大几乎不会触发截断。顺带一提ftfy 并不是「万能药」。它擅长的是编码错乱这一类特定问题而不是「OCR 识别错误」或「机器翻译腔」。把工具用在它擅长的场景是数据清洗里的一门判断力。三、NewlineNormalizer一行正则的克制美学第二个修饰器只做一件事把过长的连续换行压缩成两行。dataclassclassNewlineNormalizer(DocumentModifier):max_consecutive:int2defmodify_document(self,text:str)-str:replacement\n*self.max_consecutive patternr\n{str(self.max_consecutive1)r,}returnre.sub(pattern,replacement,text)默认max_consecutive2于是正则模式变成\n{3,}把「3 个及以上的连续换行」替换成「恰好 2 个换行」。为什么是「压到 2」而不是「压到 1」因为空行是有语义的——它标志着一个段落paragraph的结束。网页抓取时一段内容往往被塞进十几个空行里视觉上稀稀拉拉如果一股脑压成 1 个换行段落之间的分隔就全没了后续按\n\n切分段的逻辑比如过滤阶段的RepeatedParagraphFilter也会跟着失效。所以正确的做法是保留一个空行作为段落分隔只消灭「多余的」空行。这个修饰器的代码只有几行但背后体现的是一种克制的工程判断清洗的目的是「去噪」而不是「抹平结构」。保留有意义的结构信息只删除真正的冗余——这一原则会贯穿后面几乎每一个清洗/过滤动作。四、C4Cleaner照抄一份「教科书」的清洗规则第三个修饰器最有分量。它的名字里的「C4」指的是 Google 当年训练 T5 时使用的C4Colossal Clean Crawled Corpus数据集——那个数据集用了一套著名的启发式规则把 Common Crawl 洗成了高质量的预训练语料。这套规则后来成了行业事实标准而这个项目的C4Cleaner就是它的一份「精简实现」。它的核心逻辑是逐行清洗dataclassclassC4Cleaner(DocumentModifier):remove_boilerplate:boolTruemin_words_per_line:int3require_end_punctuation:boolFalsedefmodify_document(self,text:str)-str:linestext.split(\n)cleaned[]forlineinlines:strippedline.strip()ifnotstripped:# 保留空行段落分隔cleaned.append()continueifself.remove_boilerplateandself._is_boilerplate(stripped):continue# 1. 删样板句wordsstripped.split()iflen(words)self.min_words_per_line:continue# 2. 删过短的行ifself.require_end_punctuationandnotself._ends_with_punctuation(stripped):continue# 3. 可选删无句尾标点的行cleaned.append(line)return\n.join(cleaned).strip()三条规则逐一拆解规则一删除样板句boilerplate核心是一张硬编码的「样板短语词表」_BOILERPLATE_PHRASES[terms of use,privacy policy,cookie policy,uses cookies,use of cookies,accept cookies,copyright ©,all rights reserved,lorem ipsum,sign up for,subscribe to our,follow us on,share this,click here,read more,powered by,]判定方式简单粗暴——只要某一行小写化后包含词表里的任一短语整行删除def_is_boilerplate(self,line:str)-bool:lowerline.lower()returnany(phraseinlowerforphrasein_BOILERPLATE_PHRASES)这些短语是网页的「套话指纹」cookie 通知、隐私政策、订阅按钮、社交分享按钮、版权声明……它们出现在几乎所有网页上却几乎不携带任何对语言学习有价值的信息。删掉它们等于替模型「去掉了互联网的呼吸噪声」。值得注意的是这种「词表黑名单」的局限它只能命中已知的样板短语换一种语言、换一种说法就失效了。所以它只是第一道粗筛——真正「语义级」的样板识别要交给后面默认关闭的ML 分类器去做。工程上「先便宜后昂贵、先规则后模型」的分层策略在这里再次体现。规则二删除过短的行wordsstripped.split()iflen(words)self.min_words_per_line:# 默认 3 个词就删continue导航菜单里的「Home」「About」「Contact」、页脚里的「Copyright 2024」、碎片化的列表项——这些「一行一两个词」的碎片通常不是连贯的自然语言。默认阈值是「少于 3 个词的行都删掉」。规则三默认关闭要求句尾标点require_end_punctuation:boolFalse# 默认关def_ends_with_punctuation(self,line:str)-bool:returnline.rstrip()[-1]in.!?\ifline.rstrip()elseFalse这条规则在默认配置里是关闭的False。为什么默认关因为「不以句号结尾」并不等于「不是正文」——标题、代码片段、列表项都常常不以句号结尾。这条规则在原版 C4 的某个变体里被用过但它过于激进容易误伤。代码把它留成一个可选项默认不启用把选择权交给使用者——这又是一个「克制」的体现。五、三个修饰器为何是「这个顺序」回头看默认配置里的顺序它并非随意排列而是有一条清晰的因果链UnicodeReformatter修编码 → NewlineNormalizer压换行 → C4Cleaner删样板先修编码因为乱码’本身可能包含会被后两步误判的字符。不先修好后续的「分词计数」「样板词匹配」都会在错误的基础上进行。再压换行让文本的行结构恢复正常C4Cleaner的「逐行处理」才能在正确的行粒度上工作。最后删样板这是语义最重、最「伤筋动骨」的一步放在最后确保前面的「基础修复」已经完成。这条链本身也体现了流水线设计的通用智慧每一步都只做一件小事但顺序编排让它们的合力远大于各自之和。六、小结清洗阶段的三板斧对应着脏数据的三个来源修饰器解决的脏手段依赖UnicodeReformatter编码乱码ftfy 探测修复ftfyNewlineNormalizer冗余空行一行正则无C4Cleaner样板套话 碎片行词表黑名单 词数阈值无贯穿始终的三条工程哲学也值得我们刻进肌肉记忆抽象先行用DocumentModifier把「清洗」统一成「文本 → 文本」让编排与算法解耦。克制去噪保留段落分隔、默认关掉激进规则——清洗是「去噪」不是「抹平结构」。顺序即设计先基础修复、再结构规范、最后语义清洗每一步都建立在上一步的成果之上。但「干净」不等于「优质」。一篇通顺的、但通篇都是链接导航的文本洗得再干净也不该进训练集。所以下一步我们要给每篇文档「打分」把低质量的挑出来扔掉。下一篇《质量过滤七个启发式过滤器如何剔除「文字垃圾」》。