章节导航 第 5 章 / 共 30 章
文本抽取与清洗
从 HTML/PDF/Office 中抽取正文,去除样板、导航、广告与 boilerplate。
“数据清洗不性感,但它决定了你的模型是优秀还是平庸。”
这一章可能是本书中最”脏活累活”的一章,但也是最能拉开差距的一章。因为绝大多数团队在这一步犯的错,比他们意识到的要多得多。
5.1 从 HTML 到纯文本:第一步就充满陷阱
为什么文本提取是第一步也是最容易出错的一步
当你从 Common Crawl 获取到一批网页数据时,你拿到的是原始的 HTML。你需要做的第一件事是把 HTML 转换成干净的纯文本——去掉 HTML 标签、CSS、JavaScript、导航栏、侧边栏、广告、页脚、Cookie 弹窗。
这听起来像是一个简单的工程任务。事实上,它是整个数据管线中信息损失最大的一步——你在这一步做的决策,直接决定了后续所有步骤能看到的内容。
2026 年 2 月的一篇论文《Beyond a Single Extractor》(arXiv: 2602.19548)揭示了一个令人吃惊的发现:文本提取是 LLM 预训练数据策展中一个被严重低估的环节。 尽管互联网内容的多样性极高,现有的标准做法是对所有 Common Crawl 网页使用同一个提取器。但论文发现,不同提取器在不同类型的网页上表现差异巨大——没有任何单一提取器在所有类型的网页上都是最优的。
主流提取工具对比
目前常用的 HTML 文本提取工具有以下几个:
Trafilatura
- 最流行的选择,也是 FineWeb 使用的提取器
- 基于启发式规则 + 机器学习的混合方法
- 优点:对结构良好的文章页面(新闻、博客、维基百科)效果很好
- 缺点:对非标准页面(论坛、电商、社交媒体)容易遗漏内容或引入噪声
- 处理速度:中等
- 版本提示:2.0(2024-12)是主版本升级,默认行为与配置接口均有变化——trafilatura 2.0.0 于 2024-12-03 发布,三档版本必须分开看,不能混用 :榜值 2.0.0(WebMainBench 细粒度榜的被测版本,即本节所有 trafilatura 分数的实际出处)→ 上次锚点 2.2.0(2026-07-31)→ 当前 2.3.1(2026-10-06)。本节的 0.6402 / 0.1305 等分数对应的是最左边那一档,比当前发行版落后一个大版本。若你的代码沿用 1.x 时期写法(
trafilatura.settings.use_config、config.set('DEFAULT', ...)、extract(..., favor_recall=True, deduplicate=True)等,参见第 4 章 §4.3.3 的完整示例),升级到 2.x 前请回官方 migration 文档逐项核对——本处只提示风险,不给出具体改写,因为这些接口的 2.x 兼容性本轮未实测。
Resiliparse
- 由 Chatnoir/Web Archive 团队开发
- 基于 C++ 实现,速度极快
- 优点:处理速度是 trafilatura 的 5-10 倍,适合大规模处理
- 缺点:提取质量低于 trafilatura,对复杂布局的适应性差一些(在表格维度上差距尤其极端,见 §5.1 表 B:resiliparse 的表格保留为 0)
- FineWeb 在早期测试中发现,用 resiliparse 替换 trafilatura 会导致下游模型在某些 benchmark 上下降 2-3 个点
jusText
- 较老的工具,基于段落级别的分类
- 优点:简单可靠,对”正文 vs 噪声”的区分比较清晰
- 缺点:对复杂布局支持差,不能处理嵌套结构
- 适合作为辅助验证工具
readability(Mozilla)
- 从 Firefox 阅读模式衍生
- 专门设计用于提取”文章主体”
- 优点:对新闻和博客文章效果极佳
- 缺点:对非文章类页面(如论坛帖子列表、产品页面)可能完全提取不到内容
一个案例:同一个网页的不同输出
让我用一个实际例子来说明差异。考虑一个典型的技术博客页面,包含:
- 导航栏(首页/关于/联系)
- 文章标题和正文
- 代码块(用
<pre><code>标记) - 侧边栏(推荐阅读、分类标签)
- 评论区(20 条评论)
- 页脚(版权信息)
不同提取器的输出差异:
| 内容区域 | Trafilatura | Resiliparse | jusText | readability |
|---|---|---|---|---|
| 文章正文 | ✅ 完整 | ✅ 完整 | ✅ 完整 | ✅ 完整 |
| 代码块 | ⚠️ 大部分保留 | ⚠️ 可能丢失格式 | ❌ 常被过滤 | ✅ 保留 |
| 导航栏 | ❌ 过滤 | ⚠️ 部分残留 | ❌ 过滤 | ❌ 过滤 |
| 侧边栏 | ❌ 过滤 | ⚠️ 部分残留 | ❌ 过滤 | ❌ 过滤 |
| 评论区 | ⚠️ 取决于设置 | ✅ 保留 | ⚠️ 部分保留 | ❌ 过滤 |
| 页脚 | ❌ 过滤 | ⚠️ 部分残留 | ❌ 过滤 | ❌ 过滤 |
量化评测:WebMainBench 基准
上述对比是定性的。2025 年,OpenDataLab 发布了 WebMainBench——一个包含 7,809 个人工标注网页、覆盖 5,434 个独特域名、150 个 TLD 和 46 种语言的标准化评测基准。它在 HTML 标签级别进行标注,为工具对比提供了量化依据:
WebMainBench 实际提供两套互不相同的评测,本节此前只引了其中一套——这是本章”MinerU-HTML 到底是多少分”长期悬置的根因。
表 A|ROUGE-N F1,全量 7,809 页(N=5,jieba 分词,抽取结果经 html2text 归一化为 Markdown 后计算):
| 工具 | 版本/模式 | All | Simple | Mid | Hard | 是否需要 GPU |
|---|---|---|---|---|---|---|
| DeepSeek-V3.2* | Html+MD | 0.9098 | 0.9415 | 0.9104 | 0.8771 | 需要 |
| GPT-5* | Html+MD | 0.9024 | 0.9382 | 0.9042 | 0.8638 | 需要 |
| Gemini-2.5-Pro* | Html+MD | 0.8979 | 0.9345 | 0.8978 | 0.8610 | 需要 |
| Dripper_fallback | Html+MD | 0.8925 | 0.9325 | 0.8958 | 0.8477 | 需要 |
| Dripper (0.6B) | Html+MD | 0.8779 | 0.9205 | 0.8804 | 0.8313 | 需要 |
| magic-html | Html+MD | 0.7138 | 0.7857 | 0.7121 | 0.6434 | 不需要 |
| Readability | Html+MD | 0.6543 | 0.7415 | 0.6550 | 0.5652 | 不需要 |
| Trafilatura | Html+MD | 0.6402 | 0.7309 | 0.6417 | 0.5466 | 不需要(CPU 友好) |
| Resiliparse | TEXT | 0.6290 | 0.7140 | 0.6323 | 0.5388 | 不需要 |
* 前沿模型,作为 Dripper 流水线内的 drop-in 替代参与评测,不是独立的提取器产品。
🔴 关于 MinerU-HTML 在这一族里的分数,本轮有一个必须写明的发现:表 A 的官方榜里根本没有 MinerU-HTML 这一行。也就是说,本章原表的 0.840 与第 4 章 §4.3.1 引用的 0.9001,在这一族中都定位不到官方出处——前者出处至今未明,后者出自 MinerU-HTML 自己的 README(不是 WebMainBench 数据集卡)。按本书此前确立的口径纪律,两个值继续保留不删(它们可能在别处有出处),但不得再被当作”WebMainBench ROUGE-N F1 榜值”引用。MinerU-HTML 目前唯一有官方数据集卡出处的分数在表 B。
表 B|细粒度 edit-distance,545 页子集(人工校准 ground-truth markdown;分数均在 [0,1],越高越好):
| Extractor | 被测版本 | overall | text_edit | code_edit | formula_edit | table_edit | table_TEDS |
|---|---|---|---|---|---|---|---|
| mineru-html | 4.1.1 | 0.8256 | 0.8621 | 0.9093 | 0.9399 | 0.6780 | 0.7388 |
| magic-html | 0.1.5 | 0.5141 | 0.7791 | 0.4117 | 0.7204 | 0.2611 | 0.3984 |
| trafilatura (md) | 2.0.0 | 0.3858 | 0.6887 | 0.1305 | 0.6242 | 0.1653 | 0.3203 |
| resiliparse | 0.14.5 | 0.2954 | 0.7381 | 0.0641 | 0.6747 | 0.0000 | 0.0000 |
| trafilatura (txt) | 2.0.0 | 0.2657 | 0.7126 | 0.0000 | 0.6162 | 0.0000 | 0.0000 |
⚠️ 两族不可互相比较。表 A 是 ROUGE-N F1、全量 7,809 页;表 B 是编辑距离类指标(含 TEDS 树编辑距离)、545 页子集。同一个工具在两族里的数值没有可比性,把表 A 的 0.8779 与表 B 的 0.8256 放在一起排高低是错的。第 4 章 §4.3.1 已有的”同源工具、异口径分数不可直接横向比较”提示保留,并指向此处。
基准版本与快照声明(2026-09-18 补):
- 表中 Dripper 0.8779、magic-html 0.7138、trafilatura 0.6402 均可在官方 WebMainBench README 的「ROUGE-N F1 on Full Dataset」榜逐项核对;MinerU-HTML 的官方值见表格内注释。
- 数据集规模:v1.0 = 7,887 页,v1.1 = 7,809 页(v1.1 剔除了部分低质量页面)。引用页面数时必须带版本号。
- ⚠️ 这是一张会腐烂的表:Dripper 与 MinerU-HTML 都在快速迭代,榜单本身也在更新(Dripper 论文 v1 原报 0.8182 / fallback 0.8399,当前官方榜已更新为 0.8779 / 0.8925)。引用时请以榜单最新版本为准,不要直接沿用本表的排名顺序做长期架构决策。
- 表中「是否需要 GPU」一列是本节选型的第一道分水岭:四个工具里只有 trafilatura 与 magic-html 能在纯 CPU 环境跑满,另两个必须配 GPU。
两个值得注意的发现:
- SLM-powered 工具(Dripper、MinerU-HTML)大幅领先传统工具——Dripper 的 F1 比 trafilatura 高出 37%,这不是微小的差距,而是范式级的提升。
- 多语言性能下降:SIGIR 2025 的一项复现研究发现,现有提取工具在非英语语言上的表现显著下降。如果你的目标语言不是英语,需要额外的验证和适配。
- 模型驱动提取器对结构化内容的保留远超启发式方法 :仅改变提取器、其他管线步骤不变,模型驱动提取器对关键结构化内容的保留远优于启发式方法:
| 内容类型 | Trafilatura(md, 2.0.0) | MinerU-HTML(4.1.1) | 提升 |
|---|---|---|---|
| 代码块保留相似度 | 0.1305 | 0.9093 | +597% |
| 公式保留相似度 | 0.6242 | 0.9399 | +51% |
⚠️ 这一组数字证明了什么,需要重读。原表把强侧标为”Dripper/Pulpie”,而榜上对照样本实际是 MinerU-HTML 4.1.1。这一格的意义因此变了:它证明的不是 Pulpie 那条 encoder 路线在代码/公式保真上更强,而是 MinerU-HTML 在这两类内容上最强。做工具选型时把这条记到 Pulpie 头上,会选错对象。来源方面:该组数值原转引自 Pulpie 博客并标注为 AICC 实验数据,AICC(Ma et al., 2025)是否独立做过这组对照本轮未核——若读者手上有 AICC 原文,可补双来源标注;在此之前请按”与 WebMainBench 细粒度榜一致,来源待核”引用。下游 +1.08 个百分点一个数来源独立,不受此影响,仍单独保留。
实验下游验证:模型在 13 个 benchmark 上平均 +1.08 个百分点。这直接量化了前面提到的”代码块过滤是灾难性的”定性警告——代码块保真度从 0.1305 提升到 0.9093,意味着模型驱动提取器能让模型看到几乎完整的技术内容,而启发式提取器几乎丢光了所有代码。对于技术文档和代码类网页尤其关键。
4. 输出格式的选择,其影响可以大于工具的选择。上面第 3 条常被读成”换个更好的工具”,但表 B 里藏着一条更便宜的结论:同一个 trafilatura 2.0.0,txt 模式的 code_edit 与 table_edit 都是 0.0000,而 md 模式分别是 0.1305 与 0.1653。也就是说,用 output_format='txt' 抽取技术类网页,等于把代码块与表格整体丢弃——不是丢一半,是归零。
对处理技术文档、API 文档、教程类语料的团队,这是全章性价比最高的一处改动:零成本、单行改动、不需要换工具、不需要 GPU,把输出切到 Markdown 模式即可,优先级高于本节讨论的任何一次工具替换。同理,resiliparse 0.14.5 的 table_edit 与 table_TEDS 均为 0.0000——有表格需求的场景下 resiliparse 不可用,这与本章上文”resiliparse 提取质量略低于 trafilatura”的定性描述差距很大,“略低”应改为”在表格维度上归零”。
⚠️ 本章自身的一处矛盾(2026-10-08 记):第 4 章 §4.3.3 与本章的 trafilatura 示例代码都使用
output_format='txt',而按上表,该模式的代码保留为 0。本节建议读者切到 Markdown 模式,但示例代码本轮未改——因为改动需实测验证(且 2.x 的接口兼容性仍是挂起的待验证项)。读者在自己的技术语料上动手时,请把这一行作为第一个要改的地方。
注意几个关键差异:
-
代码块的处理:对于技术内容,代码块是核心信息。jusText 基于”短行密集”的判断,经常把代码块当作”非正文”过滤掉。这对于训练代码能力是灾难性的。
-
评论区的处理:评论区是否保留是一个两难选择。保留评论区可以增加多样性(对话风格的内容),但也引入了大量低质量噪声。readability 直接过滤评论区,trafilatura 可以通过配置选择。
-
导航和侧边栏的残留:resiliparse 比较激进地保留内容,可能导致”首页 / 关于 / 联系”这样的导航文本污染到训练数据中。虽然单条影响微小,但在万亿 token 规模下,这些噪声的累积效应不可忽视。
选择建议
对于大规模预训练数据处理:
- 传统方案首选 trafilatura——纯 CPU 环境下质量和速度的最佳平衡
- 新兴高性能替代:Dripper——如果有 GPU 资源,Dripper 在质量上大幅领先(WebMainBench F1: 0.878 vs 0.640),单 A100 吞吐量 3.08 页/秒,且成本仅为 LLM 直接提取的 4.42%
- 如果处理速度是瓶颈,可以考虑 resiliparse 作为初筛,trafilatura 或 Dripper 作为精筛
- 对于技术类网页,注意检查代码块的保留情况
Dripper 三个关键数字的来源与口径(2026-09-18 补) :
- 0.878 —— 已核。官方 WebMainBench README 榜值 0.8779(Dripper 0.6B,Html+MD 模式;启用 fallback 的 Dripper_fallback 为 0.8925)。口径为全量 7,809 页、ROUGE-N(N=5,jieba 分词)、抽取结果经
html2text归一化为 Markdown 后计算。- 3.08 页/秒 —— 已核。论文摘要原文:「reaching a throughput of 3.08 pages per second on a single A100 GPU」。口径为单张 A100、Dripper-0.6B 自回归序列标记。
- 4.42% —— ⚠️ 待核:在 Dripper 论文正文中未检索到该数字。论文自身报告的开销对比是另一组口径:相对 naive 生成式基线,输入 token 降至 12.83%(中位数 9.72%)、输出 token 降至 16.64%(中位数 27.70%)、推理开销(FLOPs 估计)降至 5.18%(中位数 1.64%)。请注意:4.42% 若按”成本”而非”算力”计,则对 GPU 时租与模型定价极其敏感,模型降价会直接改变结论——引用时务必带上计价基准与价格时点。
⚠️ MinerU 主版本升级提示:
mineru已于 2026-09-16 发布 4.0.0(截至 2026-09-18 PyPI 最新为 4.0.2),属主版本升级。本节的选型结论(MinerU-HTML 与 Dripper 并列为 SLM-powered 一档)建立在 MinerU 2.x 时代的性能数字上,HTML 抽取组件是否随主版本同步升级、分数是否变化,需以官方最新 release notes 与 WebMainBench 最新榜单为准,不要直接沿用本节表格做长期选型。同一提示亦见第 4 章 §4.3.1。
对于特殊类型的网页:
- 论坛/社区:标准提取器效果都不好,可能需要定制解析器
- 电商页面:产品描述通常嵌在复杂的 DOM 结构中,建议跳过或用专门的爬虫
- SPA(单页应用):静态 HTML 中可能几乎没有内容,需要 JavaScript 渲染
前沿方向:
方向 1:多提取器智能调度。 《Beyond a Single Extractor》(2026)提出了一个新思路:根据网页类型选择不同的提取器。先用一个轻量分类器判断网页类型(文章、论坛、产品页等),然后调度最适合该类型的提取器。这种方法在下游模型评估中取得了显著提升。
方向 2:SLM-powered 提取——范式转变。 2026 年,上海 AI 实验室等团队提出 Dripper,开辟了一个全新范式:用小语言模型(SLM)将 HTML 文本提取重新定义为受约束的序列标记任务。Dripper 使用 Qwen3-0.6B(仅 6 亿参数)通过”简化 HTML + 映射 HTML”双分支策略,将输入 token 减少 84.4%。效果上,Dripper 在 WebMainBench 上 F1 达到 0.8779,远超 trafilatura (0.6402),接近 GPT-5 (0.9024),而推理成本仅为 LLM 直接提取的 4.42%。更关键的是,用 Dripper 清洗的 630 亿 token 语料训练 1B 模型,在 13 个 benchmark 上平均得分 50.82%,优于 trafilatura (49.74%)、FineWeb (49.61%) 和 RefinedWeb (49.13%)。这意味着文本提取工具的选择,可以在不改变任何其他管线步骤的情况下,直接提升下游模型约 1 个百分点。
方向 3:多提取器 Union 组合——多样性胜过单点最优。 同一研究(EACL 2026 Findings)更激进的策略是:对多个提取器结果取并集。在 26.7% 的域名中,60%+ 的存活页面仅来自一个提取器——不同提取器捕获的内容大量不重叠。实验表明,trafilatura + jusText + resiliparse 三提取器 Union 后,DCLM-Baseline 的 token 增量高达 71%,同时维持基准性能。关键设计原则:更严格的阈值 + 多提取器 Union > 更宽松的阈值 + 单提取器——Union 不是简单引入重复,Union 后仍需严格过滤。对结构化内容的影响尤其显著:WikiTQ 差 10pp、HumanEval 差 3pp。Dripper 作为高质量单提取器,可与规则提取器组合实现 Union 策略,兼顾质量与多样性。
方向 4:Pulpie——帕累托最优的轻量级替代方案。 2026 年 7 月,Feyn Inc 发布了 Pulpie 系列模型,采用了不同于 Dripper 的技术路线。Dripper 基于自回归解码器架构做序列标记,而 Pulpie 使用 encoder 架构——对 HTML 的每个 block 做单次 forward pass 判别 content/boilerplate,无需逐 token 生成:
| 维度 | Dripper 0.6B | Pulpie Orange Small 210M |
|---|---|---|
| WebMainBench ROUGE-5 F1 | 0.864 | 0.862 |
| 参数量 | 600M | 210M(1/3) |
| 吞吐量(NVIDIA L4) | 0.68 页/秒 | 13.7 页/秒(~20×) |
| 清洗 10 亿页成本 | ~$159,000 | ~$7,900(~1/20) |
⚠️ Dripper 那一格的口径分歧(2026-10-08 补):上表 Dripper 的 0.864 出自 Pulpie 官方博客,而 WebMainBench 官方全量榜的 Dripper (0.6B) 为 0.8779、Dripper_fallback 为 0.8925。两者相差约 1.4 个百分点,差异原因未明(可能是不同子集、不同评测时点或不同 fallback 设置)。本表”几乎追平”的结论是否成立,取决于 0.864 与 0.862 是否同口径。引用时应同时给出两值并各自标注来源,不要只写博客一侧。(Pulpie 的成本与吞吐数据 13.7 页/秒、$7,900 / 10 亿页来源独立,不受此影响。)
Dripper 与 Pulpie 代表了 SLM 提取的两条技术路线:自回归序列标注(精度略微占优)vs encoder 逐块分类(成本/速度大幅占优)。对于预算有限的中小团队,Pulpie 是极具吸引力的替代方案——pip install pulpie 即可使用,模型已在 HuggingFace 开源(feyninc/pulpie-orange-small-v1)。
踩坑记录
坑 1:JavaScript 渲染页面
越来越多的网站使用 JavaScript 框架(React、Vue、Angular),页面内容通过 JavaScript 动态加载。Common Crawl 的 WARC 文件存储的是原始 HTTP 响应,不包含 JavaScript 渲染后的内容。对于这些页面,所有提取工具拿到的都是空壳。
影响范围:这在 Common Crawl 2020 年之后的快照中越来越严重,估计有 15-25% 的现代网页存在这个问题。
应对策略:
- 对于 Common Crawl,目前无好的解决方案——这些页面基本只能放弃
- 如果自建爬虫,可以使用 Playwright 或 Puppeteer 进行 JavaScript 渲染
- 识别这些空壳页面并过滤掉,避免它们作为”正常”短文本混入训练数据
坑 2:编码问题
网页的字符编码声明不总是正确的。一个 HTML 页面可能声明 charset=utf-8,但实际内容是 GBK 编码。这会导致中文内容变成乱码。
应对策略:
- 不要盲目信任 HTTP header 和 HTML meta 中的编码声明
- 使用
chardet或cchardet做编码检测 - 对于中文数据,建立一个简单的中文字符比例检查:如果声称是中文但中文字符比例 < 50%,很可能是编码问题
坑 3:无限滚动和分页
很多内容站使用”无限滚动”或深度分页来展示内容。Common Crawl 通常只爬取了第一页,导致长列表内容被截断。
影响:对于论坛类网站,可能只拿到了前几条帖子,丢失了大量高质量讨论。
多模态特化:OCR 与版面解析——文档进入 VLM 前的抽取
本章讨论的提取器有一个隐含前提:目标是把页面压成一段连贯纯文本。trafilatura / Resiliparse 干得越干净,版面结构、图表与公式就丢得越彻底。进入 VLM 时代,这个取舍必须反过来——文档类数据要喂给模型的不只是”字”,还有”字在页面上的位置与层级”。
| 输入类型 | 传统做法 | VLM 时代的做法 | 产物 |
|---|---|---|---|
| PDF 有文本层 | PyMuPDF / pdfplumber 按文本流导出 | 版面解析:布局检测 + 阅读顺序恢复 | 结构化 markdown / HTML |
| 扫描件 | 视为噪声直接丢弃 | OCR + 版面解析 | 文本 + 坐标 + 置信度 |
| 表格 / 公式 | 压成一行纯文本 | 保留结构:HTML table / LaTeX | 可渲染、可计算的结构 |
标准范式是先检测后排序:用 LayoutLMv3 或 DiT 一类文档预训练模型做区域检测,DocLayNet 提供 11 类布局标注作为常用训练与评测基准;再做阅读顺序恢复——多栏、跨页、浮动图注是最常见的错序来源;最后按结构输出 markdown 或 HTML。跳过顺序恢复、直接按坐标从上到下拼接,是文档 VLM 语料里最常见的一类静默噪声。
三个工程判断:
- 表格与公式必须保结构。十行表格压成一行文本后,模型学到的是”表格长这样”而非表格语义;公式同理,LaTeX 保结构、纯文本只留残渣。这直接决定下游文档与图表问答能力。
- OCR 置信度是质量信号,不是摆设。扫描件逐行记录置信度,低置信区域在质量过滤阶段降权或剔除,而不是混进语料——这与 5.3 的启发式过滤同构,只是信号源从规则换成了模型输出。
- DPI 是文档 VLM 的铁门槛。渲染分辨率过低会直接废掉 OCR 类能力:文字在输入图里就已不可辨,再强的视觉编码器也无力回天。DPI 不该在”省存储”环节被随手调低,它与 token 预算同属一等参数。
最后是误差的级联性。本章说文本提取是”最容易出错的第一步”,在 VLM 下这句话更狠:抽取阶段丢的不只是文本,还有图文对齐关系,而对齐错误会顺着版面解析、切块编码一路传到训练样本,且几乎无法在下游用文本侧信号检测出来。
与第 26 章的接口:本节只讲”文档如何变成结构化文本”,图像侧的切块编码与 token 预算见第 26 章多模态数据工程。
5.2 语言识别与过滤
为什么语言识别是必要的
预训练数据通常需要按语言组织。即使是多语言模型,也需要知道每段数据是什么语言,以便控制语言配比。而 Common Crawl 中不同语言的数据混杂在一起,网页的 HTML lang 属性又不可靠(大量网页的 lang 属性缺失或错误)。
主流语言识别工具
fastText lid(lid.176.bin / lid.176.ftz)
- Meta 发布的语言识别模型,支持 176 种语言
- 目前的事实标准(de facto standard)
- 优点:速度极快(可以在 CPU 上每秒处理数万条文本)、准确率高(对主流语言 >95%)
- 缺点:
- 对短文本(< 20 个词)准确率显著下降
- 对混合语言文本只返回一个语言标签
- 某些相似语言(如印尼语和马来语、塞尔维亚语和克罗地亚语)容易混淆
- 模型是在”理想”的单语文本上训练的,对真实世界的嘈杂文本泛化不够
GlotLID
- 支持更多语言(2000+),特别是低资源语言
- FineWeb-2 使用它来做多语言数据的语言识别
- 比 fastText lid 在低资源语言上更准确
跨章对照评估集:CommonLID(2026-09-18 补) 上面三种工具”谁更准”这个问题,不能只凭厂商自评或干净评测集下结论。CommonLID 提供了一个可直接用于对照的第三方评估集:
- 规模与构成:覆盖 109 种语言变体(其中 78 种至少各有 100 行数据),样本直接从 Common Crawl 的 WET 文件(CC-MAIN-2024-22 与 CC-MAIN-2025-05 快照)与 MADLAD-400 中抽取,超过 35 万行人工标注(CC 官方博客精确到 373,230 行),由全球数十位母语者贡献。ACL 2026 主会论文(arXiv:2601.18026)。
- 核心结论:语言识别远不是一个已解决的问题。在 CommonLID 上测试 8 个主流 LID 系统,最好的模型 F1 也只有 60,远低于它们在 FLORES+、UDHR 这类干净集合上的分数——现有评测普遍高估了真实网络文本上的 LID 准确率。
- 怎么用:把 CommonLID 作为 fastText lid 与 GlotLID 的对照评估集,在你自己的语种分布上重跑一次,再决定用哪个工具、阈值设多少(阈值选择见下节)。
- ⚠️ 该数据集明确声明仅用于评测,不要拿来训练 LID 模型或其他模型。
CLD3(Compact Language Detector 3, Google)
- 基于神经网络的语言识别
- 优点:对短文本的准确率比 fastText 好
- 缺点:速度较慢,支持的语言数量有限
langdetect / lingua
- 其他可选方案,各有优缺点
- lingua 在短文本上表现最好,但速度最慢
阈值选择:一个被低估的决策
语言识别工具通常返回一个置信度分数。你需要设定一个阈值来决定:置信度高于多少才认定文本属于该语言?
这个看似微小的决策,影响可能出乎意料地大。
以中文数据为例:
| 置信度阈值 | 保留的数据量 | 数据质量 | 典型问题 |
|---|---|---|---|
| 0.5 | 100%(基准) | 中 | 混入较多中日、中英混合的低质量内容 |
| 0.65 | ~92% | 中-高 | 大部分明显的混合内容被过滤 |
| 0.7 | ~88% | 高 | 少量中英混合的技术文章被误过滤 |
| 0.8 | ~78% | 很高 | 较多含英文术语的技术内容被误过滤 |
| 0.9 | ~60% | 极高但偏窄 | 大量正常内容被过滤,多样性严重下降 |
关键发现:
- 阈值从 0.5 提高到 0.65,数据量只减少 8%,但质量提升显著——这是高性价比的过滤
- 阈值从 0.7 提高到 0.8,数据量减少 10%,但开始误杀有价值的中英混合技术文章
- 阈值超过 0.8 后,损失远大于收益
推荐做法:
- 对于中英文数据,置信度阈值 0.65-0.70 是一个好的起点
- 对于日文、韩文等与中文字符重叠的语言,需要额外的判断逻辑(不能只依赖置信度分数)
- 建议在设定阈值后,人工审查阈值边界附近(±0.05)的样本,感受一下什么被保留了、什么被过滤了
混合语言文本的处理
真实世界中大量文本是多语言混合的:
- 学术论文中的术语:中文论文中频繁出现英文专有名词(“Transformer”、“attention mechanism”、“fine-tuning”)
- 代码注释:中文开发者写的英文变量名 + 中文注释
- 社交媒体:中英混合的口语表达(“这个 feature 很 nice”)
- 翻译内容:机器翻译网站的中日/中韩混合内容
处理策略:
-
对于学术/技术领域的中英混合:这些通常是高质量内容,应该保留。可以通过以下方式避免误过滤:
- 检查英文部分是否主要是专业术语
- 如果中文比例 > 60% 且英文部分主要是名词和术语,归类为中文
-
对于代码注释的混合:
- 如果是代码文件,用代码处理管线而非自然语言管线
- 如果是包含代码块的技术博客,尊重文档整体的语言标记
-
对于劣质机器翻译的混合:这些通常是低质量内容,应该过滤掉。特征:
- 中文和另一种语言交替出现,且不是自然的代码切换
- 语法不自然,类似直译
- HTML 的
lang属性与实际内容不匹配
5.3 启发式过滤规则
语言识别之后,需要用启发式规则进一步过滤低质量内容。这是数据清洗的”主战场”,也是细节最密集的地方。
行级过滤
行级过滤检查文档中的每一行,移除明显的噪声行。
常见规则:
def should_remove_line(line: str) -> bool:
"""判断一行文本是否应该被移除"""
# 1. 空行或极短行(< 5 个字符)
if len(line.strip()) < 5:
return True
# 2. 导航文本模式
nav_patterns = [
r'^(首页|关于|联系我们|登录|注册|返回顶部)',
r'^(Home|About|Contact|Login|Sign up)',
r'^\s*[\|/]\s*', # 分隔符开头
]
for pattern in nav_patterns:
if re.match(pattern, line.strip()):
return True
# 3. 高比例特殊字符
special_char_ratio = len(re.findall(r'[^\w\s]', line)) / max(len(line), 1)
if special_char_ratio > 0.5:
return True
# 4. Cookie/隐私提示
cookie_patterns = ['cookie', '隐私政策', 'privacy policy', 'consent']
if any(p in line.lower() for p in cookie_patterns):
return True
return False
注意:行级过滤要保守。宁可留一些噪声,也不要误删有价值的内容。特别是:
- 代码行可能看起来像”高特殊字符比例”——不要过滤
- 数学公式可能包含大量特殊符号——不要过滤
- 短行可能是诗歌或列表项——阈值不要设太高
文档级过滤
文档级过滤检查整篇文档的统计特征。
核心指标和推荐阈值:
| 指标 | 过低阈值 | 推荐范围 | 过高阈值 | 说明 |
|---|---|---|---|---|
| 文档长度(字符) | < 100 | 200-50000 | > 200000 | 太短通常是碎片,太长可能是数据库 dump |
| 平均行长度 | < 10 | 20-200 | > 500 | 过短可能是列表,过长可能是无换行的脏数据 |
| 字母/数字占比 | < 0.5 | 0.6-0.95 | > 0.99 | 过低可能是乱码,过高可能是纯表格数据 |
| 重复行比例 | - | < 0.3 | > 0.5 | 高重复通常意味着模板化内容 |
| 句号/句尾标点比例 | < 0.01 | 0.02-0.15 | - | 过低可能不是连贯文本 |
中文特有的过滤规则:
def chinese_document_filter(text: str) -> bool:
"""中文文档的额外过滤规则,返回 True 表示保留"""
# 1. 中文字符比例检查
chinese_chars = len(re.findall(r'[\u4e00-\u9fff]', text))
total_chars = len(text)
chinese_ratio = chinese_chars / max(total_chars, 1)
if chinese_ratio < 0.3: # 中文字符太少,可能不是真正的中文内容
return False
# 2. 常见垃圾模式
spam_patterns = [
r'点击这里下载',
r'免费领取',
r'加微信',
r'扫码关注',
r'阅读全文请',
]
spam_count = sum(1 for p in spam_patterns if re.search(p, text))
if spam_count >= 3: # 命中多个垃圾模式
return False
# 3. 段落结构检查
paragraphs = [p for p in text.split('\n') if len(p.strip()) > 20]
if len(paragraphs) < 2: # 至少要有 2 个有实质内容的段落
return False
return True
C4 的过滤规则:哪些有用,哪些有害
C4(Colossal Clean Crawled Corpus)是 T5 模型的训练数据,其过滤规则曾被广泛参考。但 FineWeb 团队在 2024 年的研究中发现,C4 的一些过滤规则实际上是有害的。
C4 的过滤规则回顾:
- 只保留以句号、问号、感叹号或引号结尾的行
- 移除包含”坏词”(bad words)列表中词语的任何页面
- 移除少于 3 个句子的页面
- 移除任何包含
{的页面(本意是过滤代码,但也过滤了数学内容) - 使用 langdetect 只保留英文内容
- 去重
问题分析:
规则 1(句尾标点)的问题:
- 列表项(如”• 第一步:安装依赖”)不以句号结尾,但可能是有价值的内容
- 标题行不以句号结尾
- 代码注释不以句号结尾
- 影响:导致大量结构化内容(教程、文档、FAQ)被误过滤
规则 2(坏词过滤)的问题:
- C4 的坏词列表非常激进——包含了一些在医学、法律、性教育等正当领域中正常使用的词汇
- 整个页面因为包含一个”坏词”就被删除——即使这个词出现在完全正常的上下文中(如医学文献讨论某种疾病)
- FineWeb 估计这个规则误杀了约 10-15% 的优质内容
规则 4(过滤 {)的问题:
- 这个规则的本意是过滤代码,但同时过滤了:
- 包含 JSON 示例的技术文章
- 包含数学公式的页面(LaTeX 使用
{}) - 包含模板语法的技术文档
FineWeb 的改进:
- 不再使用 C4 的坏词列表做页面级过滤,改为更精细的行级/段落级处理
- 不再要求行必须以标点结尾
- 不再过滤包含
{的页面 - 用质量分类器替代简单的规则过滤
认知变化:从”严格过滤”到”适度过滤”
认知演变时间线:
2019: "过滤越严格越好" (C4 的设计理念)
2020: "严格过滤确实有效" (GPT-3 采用类似策略)
2022: "等等,过滤太严可能有问题" (开始有人注意到误杀)
2024: "过度过滤确实有害" (FineWeb 的系统性证据)
→ C4 的一些规则导致约 10-15% 的优质内容被误过滤
→ 过滤后数据集的多样性明显下降
2025: "适度过滤 + 质量分类器" (第二代方法论成熟)
→ 用宽松的启发式规则做初筛
→ 用模型(质量分类器)做精筛
→ 对被过滤掉的数据做抽样检查,评估误杀率
2026: "质量分类器的'质量幻觉'" (第三代方法论出现)
→ Apple 研究发现分类器过滤的"质量幻觉":过滤后模型在
下游任务上表现更好,但在高质量数据本身的语言建模能力
并未提升——分类器可能捕捉的是分布差异而非真正的语言质量
→ 第三代方案:"分类器集成 + 智能绕过 + LLM 改写"
→ 将数据保留率从 10% 恢复到 40%,同时维持质量(MMLU +14%)
这条时间线揭示了一个深层趋势:过滤策略正在从”二分法”(保留或丢弃)演变为”恢复策略”(尝试拯救被错误丢弃的数据)。
三代数据清洗方法论
上面的时间线其实对应了三代清洗方法论的演进:
| 代际 | 代表 | 核心方法 | 数据保留率 | MMLU 提升 |
|---|---|---|---|---|
| Gen1 | FineWeb / C4 | 启发式规则过滤 | ~35% | 基线 |
| Gen2 | DCLM | fastText 质量分类器 | ~10% | +9% |
| Gen3 | Nemotron-CC | 分类器集成 + 智能绕过 + LLM 改写 | ~40% | +14% |
Gen2 的问题:虽然 MMLU 提升了 9%,但代价是丢弃了 90% 的数据。Apple 的”数据质量幻觉”研究(arXiv: 2510.00866)解释了为什么——分类器并不是真正在度量”语言质量”,而是在捕捉与参考数据集的分布差异。
Gen3 的核心创新在于三个环节:
- 分类器集成:用多个分类器投票,而非依赖单一模型
- 智能绕过(smart bypass):对被过滤的内容给予”第二次机会”——如果内容在其他维度上有价值(如包含稀缺领域知识),可以绕过质量过滤
- LLM 改写:对中等质量的内容,用大模型进行改写提升,而不是简单丢弃
NVIDIA 的 Nemotron-CC(2024)是 Gen3 的代表性工作,开源社区也有了可复现的实现(如 fineweb-pipeline)。不过需要注意,Gen3 的 LLM 改写环节在大规模应用时成本较高,需要根据实际预算权衡。
5.4 PII(个人身份信息)处理
为什么 PII 处理不可跳过
训练数据中不可避免地包含个人身份信息(Personally Identifiable Information, PII):
- 电子邮件地址:网页上大量公开的联系邮箱
- 电话号码:商业网站、分类广告
- 物理地址:商户信息、个人页面
- 身份证号:中国特有的问题——某些政府公示、法律文书中出现
- 银行卡号:泄露数据、钓鱼页面
- 姓名+关联信息:简历、个人博客
如果不处理 PII,模型可能:
- 记忆并泄露真实个人信息——用户问”张三的电话号码是多少”,模型可能真的回答出来
- 产生法律风险——GDPR、中国个人信息保护法等法规对 PII 的处理有明确要求
- 生成看似真实的 PII——模型可能在生成文本时输出训练数据中记忆的真实电话号码或邮箱
PII 检测方法
基于正则表达式的检测:
import re
PII_PATTERNS = {
# 邮箱
'email': re.compile(r'[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}'),
# 中国手机号
'phone_cn': re.compile(r'(?<!\d)1[3-9]\d{9}(?!\d)'),
# 中国身份证号(18位)
'id_card_cn': re.compile(
r'(?<!\d)[1-9]\d{5}(?:19|20)\d{2}(?:0[1-9]|1[0-2])'
r'(?:0[1-9]|[12]\d|3[01])\d{3}[\dXx](?!\d)'
),
# 国际电话号码(简化版)
'phone_intl': re.compile(r'\+\d{1,3}[-.\s]?\d{4,14}'),
# 银行卡号(16-19位数字)
'bank_card': re.compile(r'(?<!\d)\d{4}[\s-]?\d{4}[\s-]?\d{4}[\s-]?\d{4,7}(?!\d)'),
# IP 地址
'ip_address': re.compile(
r'(?<!\d)(?:(?:25[0-5]|2[0-4]\d|[01]?\d\d?)\.){3}'
r'(?:25[0-5]|2[0-4]\d|[01]?\d\d?)(?!\d)'
),
}
def detect_pii(text: str) -> dict:
"""检测文本中的 PII,返回各类型的匹配列表"""
results = {}
for pii_type, pattern in PII_PATTERNS.items():
matches = pattern.findall(text)
if matches:
results[pii_type] = matches
return results
基于 NER 的检测:
- 使用命名实体识别模型检测人名、组织名、地址等
- 优点:能捕获正则表达式无法匹配的非结构化 PII(如”住在北京海淀区中关村南大街5号”)
- 缺点:速度较慢,需要 GPU,且会有较多假阳性
混合方案(推荐):
经典的做法是两步走:正则表达式做第一遍快速扫描(高速、高召回),NER 模型做第二遍精确确认(低速、高精度)。NVIDIA NeMo-Curator 提供了一个开箱即用的 PII 检测管线。
但实践中,两步方案面临一个顽固问题:歧义实体。一个 11 位数字是手机号还是订单号?一个地名是 PII 中的住址还是普通的地理描述?RECAP 框架(Centific, 2025)提出了更精细的三阶段管道:
阶段 1:基础混合检测(正则 + LLM/NER)
→ 广覆盖,但可能有多标签和误报
→ 例如:检测到 "13812345678" 同时匹配手机号和订单号模式
阶段 2:上下文消歧(LLM 分析语境选择最合适标签)
→ 解决"11 位数字到底是手机号还是订单号"的问题
→ 根据前后文语境("请拨打"vs"订单编号")选择正确标签
阶段 3:歧义解决与误报过滤(规则合并 + 上下文验证)
→ 消除短数字的误报,合并重叠的检测结果
→ 最终输出高置信度的 PII 标注
RECAP 支持 13 种语言、300+ PII 实体类型,在中文(zh_SG)上 F1 达到 0.76,相比零样本 LLM 提升 17%,相比微调 NER 提升 82%。
一个更深层的隐忧:隐式记忆泄露。 即使你做了完善的 PII 检测和替换,模型仍然可能通过隐式记忆泄露个人信息。ACL 2025 Findings 上的 R.R.(Recollect and Rank)攻击 展示了一种两步隐私窃取方法——攻击者可以从 LLM 中重建训练数据中的 PII 实体,即使这些实体在原始数据中已被处理过。这强化了一个结论:PII 处理是必要的但不充分的,后训练阶段的隐私对齐同样不可或缺。
处理策略:替换 vs 删除 vs 掩码
检测到 PII 后,有三种处理方式:
替换(Replacement)——推荐
原文:请联系张先生,手机 13812345678,邮箱 zhang@example.com
处理后:请联系[人名],手机 [电话号码],邮箱 [邮箱地址]
- 优点:保留了文本的语义结构,模型仍然能学到”联系方式的语境”
- 缺点:需要为每种 PII 设计合理的替换标记
删除(Deletion)
原文:请联系张先生,手机 13812345678,邮箱 zhang@example.com
处理后:请联系,手机 ,邮箱
- 优点:最简单
- 缺点:可能破坏句子结构,产生不自然的文本
掩码(Masking)
原文:请联系张先生,手机 13812345678,邮箱 zhang@example.com
处理后:请联系张先生,手机 138****5678,邮箱 z***@example.com
- 优点:保留了部分信息(类型和格式),比完全删除更自然
- 缺点:实现较复杂,且部分掩码可能仍然有隐私风险
对模型能力的影响:
研究表明,PII 替换对模型的通用能力几乎没有影响(< 0.5% benchmark 差异)。但如果过于激进地删除所有看似 PII 的内容,可能会:
- 损失模型对地址、电话格式的理解(影响信息提取任务)
- 损失模型对邮件写作格式的理解
建议:使用替换策略,保留 PII 的类型信息但移除具体值。
中文 PII 的特殊挑战
中国手机号的特殊性:
- 11 位纯数字,以 1 开头,第二位是 3-9
- 但 11 位数字在其他场景也很常见(如订单号、编号)
- 需要上下文判断:如果前面有”电话”、“手机”、“联系”等关键词,更可能是手机号
身份证号的验证:
- 18 位,最后一位是校验码
- 可以用校验码验证来减少误报
- 但注意:验证通过的身份证号也可能是虚构的(如小说中编造的)
def validate_cn_id_card(id_number: str) -> bool:
"""验证中国身份证号的校验码"""
if len(id_number) != 18:
return False
weights = [7, 9, 10, 5, 8, 4, 2, 1, 6, 3, 7, 9, 10, 5, 8, 4, 2]
check_codes = '10X98765432'
try:
total = sum(int(id_number[i]) * weights[i] for i in range(17))
expected = check_codes[total % 11]
return id_number[-1].upper() == expected
except (ValueError, IndexError):
return False
5.5 敏感内容过滤
问题定义
预训练数据中不可避免地包含”有害”内容:色情、暴力、仇恨言论、歧视性内容、虚假信息等。完全不处理这些内容,模型可能学到并生成类似的有害输出;但过度过滤又会导致数据多样性下降,甚至影响模型在合法领域的理解能力。
分类器方案
fastText 分类器:
- 训练一个 fastText 分类器来区分安全/不安全内容
- 优点:速度极快,可以处理万亿 token 级别的数据
- 缺点:精度有限,对上下文理解不够
专用 NSFW 检测器:
- 使用在大量标注数据上训练的专门模型
- 如 Jigsaw Toxicity Classifier、OpenAI Moderation API
- 精度更高,但速度较慢
LLM-based 检测:
- 用大模型(如 Llama Guard)来评估内容安全性
- 最高精度,但成本最高
- 通常只用于抽样检查或最终验证,不用于全量数据过滤
阈值困境
敏感内容过滤的核心困难是阈值的设定。设得太松,有害内容混入训练数据;设得太严,正常内容被误杀。
一些反直觉的误杀案例:
- 医学内容:描述人体解剖、性传播疾病、药物副作用的专业内容,经常被 NSFW 分类器标记为”不安全”
- 法律内容:描述犯罪事实、暴力行为的法律文书和新闻报道
- 文学作品:包含暴力或性描写的经典文学作品(如莎士比亚的部分作品)
- 安全教育:讨论网络安全、毒品危害、性教育的正当内容
- 历史记录:描述战争、种族歧视历史的教育材料
推荐策略:多级过滤
第一级(粗筛):fastText 分类器
- 高置信度(>0.95)直接过滤——明确的色情/暴力/极端内容
- 中等置信度(0.7-0.95)标记为"需复审"
- 低置信度(<0.7)通过
第二级(精筛):对"需复审"的内容
- 使用更精准的模型(如 Llama Guard)二次判断
- 或用规则补充判断(如检查是否来自医学/法律/教育类网站)
第三级(抽检):
- 定期从"通过"的数据中抽样人工检查
- 定期从"被过滤"的数据中抽样检查误杀率
- 根据抽检结果调整阈值
一个重要的设计哲学
宁可在后训练阶段用 RLHF/DPO 教模型”不输出有害内容”,也不要在预训练阶段把模型对”有害内容”的理解能力也一并删掉。
预训练阶段适度包含一些”边缘”内容(如医学文献中的敏感描述),有助于模型理解这些领域。模型的”安全性”更适合在后训练阶段通过对齐技术来实现,而非在数据层面”一刀切”。
5.6 清洗管线的组装与评估
管线的标准流程
将上述各步骤串联起来,一个完整的文本清洗管线通常是:
原始 HTML (WARC)
│
▼
[1] 文本提取 (trafilatura / resiliparse)
│
▼
[2] 编码修复与标准化 (UTF-8 统一)
│
▼
[3] 语言识别 (fastText lid, 阈值 0.65-0.70)
│
▼
[4] 行级过滤 (导航文本、广告、Cookie 提示)
│
▼
[5] 文档级过滤 (长度、字符比例、重复行)
│
▼
[6] PII 处理 (检测 + 替换)
│
▼
[7] 敏感内容过滤 (多级过滤策略)
│
▼
[8] 标准化 (统一换行符、去除多余空白、Unicode 规范化)
│
▼
干净的纯文本
各步骤的数据损耗
一个典型的 Common Crawl 快照处理后的数据保留率:
| 步骤 | 输入 | 输出 | 保留率 | 累计保留率 |
|---|---|---|---|---|
| 原始 CC 快照 | 100% | - | - | 100% |
| 文本提取 | 100% | 85% | 85% | 85% |
| 语言识别(中文) | 85% | 8% | ~9% | ~8% |
| 行级过滤 | 8% | 7.2% | 90% | ~7.2% |
| 文档级过滤 | 7.2% | 5.5% | 76% | ~5.5% |
| PII 处理 | 5.5% | 5.4% | 98% | ~5.4% |
| 敏感内容过滤 | 5.4% | 5.0% | 93% | ~5.0% |
也就是说,从一个 Common Crawl 快照的原始数据到最终可用的中文训练数据,保留率大约是 5%。对于英文来说,由于英文内容占比更大,保留率会更高(约 15-20%)。
质量评估
如何知道你的清洗管线是好是坏?
方法 1:人工抽样评估
- 从清洗后的数据中随机抽取 500-1000 条,人工逐条评估
- 评估维度:语言正确性、信息完整性、噪声残留、格式规范性
- 目标:好样本比例 > 90%
方法 2:与已知高质量数据对比
- 计算清洗后数据与维基百科等高质量数据在统计特征上的相似度
- 特征包括:词频分布、句子长度分布、语言困惑度分布
方法 3:下游训练评估(最终标准)
- 用清洗后的数据训练一个小模型(如 400M 参数,100B token)
- 在标准 benchmark 上评估
- 与其他数据集训练的同等模型对比
方法 4:消融测试
- 逐步移除清洗管线中的各个步骤,观察最终模型表现的变化
- 确认每个步骤都在贡献正面价值
- 如果某个步骤移除后模型反而更好,说明该步骤可能过度过滤了
方法 5:种子数据优化验证
- 传统质量分类器的一个根本问题是:种子数据的选择依赖人工经验,具有主观性。不同的人对”高质量”的定义不同,选出的正负样本也不同,训练出的分类器自然也不同
- Ultra-FineWeb(清华大学刘知远团队, 2025)提出了一种高效验证策略:以最小计算成本快速评估不同种子数据选择对 LLM 训练效果的影响,从而优化种子选择本身
- 核心思路:不再”凭经验选种子 → 训分类器 → 祈祷效果好”,而是”系统搜索种子空间 → 快速验证 → 选最优种子”
- 实验证明在英文 1 万亿 token + 中文 1200 亿 token 数据集上均有显著效果提升
⚠️ 质量分类器的”质量幻觉”警示
在使用上述方法时,请注意 Apple 研究团队的一个重要发现(arXiv: 2510.00866):基于分类器的质量过滤(CQF)存在”质量幻觉”——虽然过滤后数据训练的模型在下游任务上表现更好,但模型在高质量数据集本身的语言建模性能并未提升。这意味着分类器可能捕捉的是分布差异而非真正的语言质量。不要盲目信赖分类器的分数,要结合多种评估方法交叉验证。
5.7 显微镜案例:一条脏数据是怎么逃过所有过滤的
让我用一个详细的案例来展示数据清洗的复杂性。
这条数据:一个看似正常的中文技术文章页面,约 2000 字,讨论的是数据库索引优化。语言流畅,技术准确,各项统计指标都在正常范围内。
但实际上:这个页面是一个 SEO 垃圾站点,它做了以下操作:
- 从一篇高质量的技术博客上抄袭了主要内容
- 在文章末尾隐藏了大量关键词堆砌(用白色文字在白色背景上)
- 文章中间插入了一个伪装成技术链接的推广链接
- 页面的 meta description 和实际内容不匹配(SEO 手段)
为什么它逃过了各步过滤:
| 过滤步骤 | 结果 | 原因 |
|---|---|---|
| 文本提取 | ✅ 通过 | trafilatura 成功提取了正文,但也保留了隐藏的关键词堆砌(这些词在 HTML 中存在,只是用 CSS 隐藏了) |
| 语言识别 | ✅ 通过 | 主体内容是正常中文,置信度 0.92 |
| 行级过滤 | ✅ 通过 | 没有明显的导航文本或广告模式 |
| 文档级过滤 | ✅ 通过 | 长度、字符比例、重复率都在正常范围 |
| 敏感内容过滤 | ✅ 通过 | 内容是技术主题,没有敏感词 |
如何才能抓住它:
- CSS 感知的文本提取:在提取文本时考虑 CSS 样式,过滤掉
display:none、visibility:hidden、color:white; background:white等隐藏内容。trafilatura 的最新版本对此有一定支持。 - 重复检测:如果在去重步骤中与原始的高质量文章进行比对,可以发现这是一个近似副本。
- 质量分类器:一个好的质量分类器可能会给关键词堆砌部分打低分(如果它能看到完整的提取文本的话)。
启示:没有完美的过滤管线。每一条规则都有死角。关键是多层防御——通过不同机制的叠加来最大化覆盖率,并通过定期的人工抽样来发现盲区。
动手环节:实现一个迷你文本清洗管线
目标:动手搭建一个简化版的文本清洗管线,体验从原始文本到干净语料的全流程。
练习 1:行级和文档级过滤器
"""
迷你文本清洗管线
实现本章介绍的行级过滤、文档级过滤和 PII 检测
"""
import re
from dataclasses import dataclass, field
from typing import List
@dataclass
class CleaningResult:
original_text: str
cleaned_text: str
removed_lines: List[str] = field(default_factory=list)
pii_found: List[dict] = field(default_factory=list)
passed: bool = True
reason: str = ""
def line_filter(line: str) -> bool:
"""行级过滤,返回 True 表示保留"""
stripped = line.strip()
# 空行保留(作为段落分隔)
if not stripped:
return True
# 极短行
if len(stripped) < 3:
return False
# 导航文本
nav_patterns = [
r'^(首页|关于我们|联系我们|登录|注册|返回顶部|下一页|上一页)',
r'^(Home|About|Contact|Login|Sign up|Next|Previous)',
r'^\s*[|/]\s*$',
]
for pattern in nav_patterns:
if re.match(pattern, stripped, re.IGNORECASE):
return False
# Cookie/隐私提示
cookie_words = ['cookie', '隐私政策', 'privacy policy', '我们使用cookies']
if any(w in stripped.lower() for w in cookie_words):
return False
# 高比例特殊字符(但排除代码行)
if not stripped.startswith(('```', ' ', '\t')):
special = len(re.findall(r'[^\w\s\u4e00-\u9fff]', stripped))
if special / max(len(stripped), 1) > 0.6:
return False
return True
def document_filter(text: str) -> tuple:
"""文档级过滤,返回 (passed, reason)"""
if len(text) < 100:
return False, "文档过短 (<100 字符)"
if len(text) > 200000:
return False, "文档过长 (>200K 字符)"
lines = text.split('\n')
non_empty = [l for l in lines if l.strip()]
# 平均行长
if non_empty:
avg_line_len = sum(len(l) for l in non_empty) / len(non_empty)
if avg_line_len < 5:
return False, f"平均行长过短 ({avg_line_len:.1f})"
# 重复行比例
if len(non_empty) > 3:
unique_ratio = len(set(non_empty)) / len(non_empty)
if unique_ratio < 0.5:
return False, f"重复行比例过高 (unique={unique_ratio:.2f})"
# 中文字符比例(如果目标是中文数据)
chinese_chars = len(re.findall(r'[\u4e00-\u9fff]', text))
if chinese_chars / max(len(text), 1) < 0.2:
return False, f"中文字符比例过低 ({chinese_chars/max(len(text),1):.2f})"
return True, "通过"
def detect_pii(text: str) -> list:
"""简易 PII 检测"""
patterns = {
"email": r'[\w.+-]+@[\w-]+\.[\w.]+',
"cn_phone": r'(?<!\d)1[3-9]\d{9}(?!\d)',
"cn_id": (r'(?<!\d)[1-9]\d{5}(?:19|20)\d{2}'
r'(?:0[1-9]|1[0-2])(?:0[1-9]|[12]\d|3[01])\d{3}[\dXx](?!\d)'),
}
findings = []
for pii_type, pattern in patterns.items():
for match in re.finditer(pattern, text):
findings.append({
"type": pii_type,
"value": match.group()[:4] + "****", # 只展示前4字符
"position": match.start(),
})
return findings
def clean_document(text: str) -> CleaningResult:
"""完整的清洗流程"""
result = CleaningResult(original_text=text, cleaned_text="")
# Step 1: 文档级过滤
passed, reason = document_filter(text)
if not passed:
result.passed = False
result.reason = reason
return result
# Step 2: 行级过滤
lines = text.split('\n')
cleaned_lines = []
for line in lines:
if line_filter(line):
cleaned_lines.append(line)
else:
result.removed_lines.append(line.strip())
result.cleaned_text = '\n'.join(cleaned_lines)
# Step 3: PII 检测
result.pii_found = detect_pii(result.cleaned_text)
return result
# === 测试 ===
test_docs = [
# 正常文档
"""人工智能的发展已经进入了大模型时代。
预训练技术让模型具备了通用的语言理解能力。
数据质量对模型性能有决定性影响。
从 Common Crawl 到精选语料,每一步都需要精心处理。
请联系 zhangsan@example.com 获取更多信息。
电话:13812345678""",
# 含噪声的文档
"""首页 | 关于我们 | 联系我们
本站使用cookies来提升您的体验
深度学习是机器学习的一个重要分支。
它通过多层神经网络来学习数据的表示。
点击这里下载 >>> 点击这里下载 >>>
@#$%^&*()_+
登录 | 注册""",
# 太短的文档
"""你好世界""",
]
for i, doc in enumerate(test_docs):
print(f"\n{'='*50}")
print(f"文档 {i+1}:")
result = clean_document(doc)
print(f" 通过: {result.passed}")
if not result.passed:
print(f" 原因: {result.reason}")
else:
print(f" 移除行数: {len(result.removed_lines)}")
if result.removed_lines:
for line in result.removed_lines[:3]:
print(f" - '{line}'")
if result.pii_found:
print(f" 发现 PII: {len(result.pii_found)} 处")
for pii in result.pii_found:
print(f" - {pii['type']}: {pii['value']}")
练习 2:PII 替换策略实现
扩展上面的 PII 检测,实现本章推荐的替换策略(而非删除):
def replace_pii(text: str) -> str:
"""将 PII 替换为类型占位符"""
replacements = {
"email": "[邮箱地址]",
"cn_phone": "[手机号码]",
"cn_id": "[身份证号]",
}
patterns = {
"email": r'[\w.+-]+@[\w-]+\.[\w.]+',
"cn_phone": r'(?<!\d)1[3-9]\d{9}(?!\d)',
"cn_id": (r'(?<!\d)[1-9]\d{5}(?:19|20)\d{2}'
r'(?:0[1-9]|1[0-2])(?:0[1-9]|[12]\d|3[01])\d{3}[\dXx](?!\d)'),
}
result = text
for pii_type, pattern in patterns.items():
result = re.sub(pattern, replacements[pii_type], result)
return result
# 测试
test = "请联系张先生,手机 13812345678,邮箱 zhang@example.com"
print(f"原文: {test}")
print(f"替换: {replace_pii(test)}")
# 预期输出: 请联系张先生,手机 [手机号码],邮箱 [邮箱地址]
练习 3:清洗效果评估
对你的清洗管线做一个简单的评估:准备 20 条样本(10 条干净、10 条脏),检查管线的准确率和误杀率。
思考题:
- 你的行级过滤器有哪些可能的误杀场景?(提示:代码行、诗歌、数学公式)
- 如果要处理万亿 token 的数据,你的管线需要做哪些优化才能在合理时间内完成?
本章要点回顾
- 文本提取是被低估的关键步骤——不同提取工具在同一网页上的输出差异可达 2-3 个 benchmark 点。传统方案中 trafilatura 是最佳选择,但 SLM-powered 工具(Dripper)已在 WebMainBench 上大幅领先(F1: 0.878 vs 0.640),代表了文本提取的范式转变
- 语言识别的阈值选择需要精细调优——0.65-0.70 是中文的推荐范围;混合语言文本需要特殊处理,不能一刀切
- C4 的过滤规则教训——过度过滤会误杀 10-15% 的优质内容。当前最佳实践从”宽松启发式初筛 + 模型精筛”演进到第三代”分类器集成 + 智能绕过 + LLM 改写”
- 质量分类器存在”质量幻觉”——Apple 研究发现分类器可能捕捉的是分布差异而非真正的语言质量,不要盲目信赖分类器分数
- PII 处理用替换而非删除——三阶段管道(检测 → 消歧 → 验证)比传统两步方案更精确;同时注意隐式记忆泄露风险
- 敏感内容过滤用多级策略——粗筛 + 精筛 + 抽检,避免一刀切误杀医学/法律/教育类正当内容
- 从 Common Crawl 到可用的中文训练数据,保留率约 5%——大部分数据在语言过滤这一步就被排除了
- 没有完美的清洗管线,多层防御 + 定期抽样是关键
🛠️ 本章衍生工具
数据工坊·清洗管道搭建器
《AI 数据全书》第 5 章「文本提取 · 语言识别 · 启发式过滤」配套产品原型(系列第五个实用工具)——把清洗从「背规则」变成「搭管线」 ⭐ 24 个算子可自由增删、改序、调阈值:Unicode NFKC / 控制字符 / 空白 / 空行,语言识别(中文·英文双目标),行级过滤(极短行、导航 UI 文本、Cookie 提示、高特殊字符比例),文档级过滤(长度上下限、平均行长、字母数字汉字占比、重复行比例、句尾标点比例、中文字符占比、中文垃圾模式、实质段落、关键词堆砌),PII 掩码 ⭐ 逐算子瀑布与淘汰原因:每一步输入/输出/保留率/累计保留率/删了多少行/为什么被删,一条不少 ⭐ 顺序敏感性实验:同一套算子只改「行级 vs 文档级」的相对顺序,结果就会变——先把导航短行清掉,企业简介页的重复行比例从 0.47 降到 0,正文保住;反过来先算重复率,整篇会被当成模板页删掉(这就是 §5.6 把行级排在文档级之前的原因) ⭐ 误杀审查:内置 21 篇带真值标注的示例语料,逐条看谁被删、删它的规则是什么、是误杀还是活该,并可自己打标 ⭐ 三代方法论实跑对比:C4 原教旨(坏词连坐砍掉医学摘要、花括号规则砍掉 JSON 文档、句尾标点把列表教程删成空壳,4 篇好文档误杀 3 篇却仍放过 SEO 垃圾)/ FineWeb / DCLM / Gen3(与 DCLM 同阈值,只多一道智能绕过就把英文教程捞回来)——⚠ 标记有害的 C4 规则保留在库里,就是让你亲眼看到误杀 ⭐ 8 套预设:全书标准 / C4 原教旨 / FineWeb / FineWeb-Edu(0.72) / DCLM(0.78) / Gen3 保留优先 / FLUX 风格(2026 SOTA,双分类器 + 行级切除替代整篇丢弃) / 轻量快速 ⭐ 帕累托前沿:把每套预设画成「保留率 × 保留集质量分」平面上的点并标出前沿,同时用误杀率/漏放率打脸二维最优(轻量管线两维都好看,漏放率却 43%)· 可粘贴自己的语料,也支持从「数据质量评分」带文本跳转过来,导出清洗后文本 + 管线 JSON + 执行报告 MD,数据不出本机
数据管线博弈
用 arXiv:2603.13972 的真实 per-stage 读数装配管线 — 级联口径对照(加和 vs 连乘 vs 实测 8.84%)、真·帕累托前沿(两个 SOTA 基线都不在上面)、单阈值暴政与双 bin OR 机制沙盒