第 14 章:用 Common Crawl 做外挂语料——从海量网页到可检索知识库
1. 开篇段落
在第 13 章中,我们学会了如何“搬运” Common Crawl 的数据。现在,你的硬盘里可能已经躺着数百 GB 的 WET 文件。但请注意:原始的 Common Crawl 数据是“有毒”的。
如果你直接将这些数据喂给 Embedding 模型并存入向量库,你将面临“垃圾进,垃圾出”(GIGO)的灾难:
- 检索中毒:用户问“Python 教程”,结果检索出 SEO 内容农场的垃圾页面。
- 上下文浪费:Claude Code (CC) 的上下文窗口是昂贵的。如果检索到的片段充满了“点击这里”、“Cookie 政策”或乱码,你实际上是在浪费 token 并降低模型智商。
- 甚至安全风险:包含恶意 Prompt 注入的网页可能会诱导 CC 执行危险操作。
本章将详细讲解如何构建一个工业级的数据清洗漏斗(Data Processing Funnel)。我们将深入探讨从域名过滤、文本去噪、质量评分到大规模去重的全流程。这一章的内容是将“公开数据”转化为“私有资产”的核心壁垒。
2. 数据处理漏斗:架构总览
处理 Common Crawl 数据的核心哲学是 “分层过滤,激进丢弃”。我们不需要索引整个互联网,只需要索引对目标领域最有价值的 1%。
以下是一个工业级的数据处理流水线架构:
[ 原始数据流 (WET Stream) ] <-- 吞吐量: ~100 MB/s (单节点)
|
v
+-----------------------------+
| Layer 1: 粗筛 (Metadata) | <-- 成本极低
| - 域名黑/白名单 (TLD/Domain)|
| - URL 模式过滤 (Regex) | <-- 过滤掉 60% 无用数据
| - 语言检测 (Language ID) |
+-----------------------------+
|
v
+-----------------------------+
| Layer 2: 细洗 (Structure) | <-- 基于规则
| - 样板代码移除 (Boilerplate)|
| - 短文本/低密度文本丢弃 | <-- 过滤掉 20% 噪声
| - 个人隐私信息 (PII) 擦除 |
+-----------------------------+
|
v
+-----------------------------+
| Layer 3: 质检 (Semantics) | <-- 计算密集
| - 困惑度 (Perplexity) 阈值 |
| - 敏感/有害内容分类 | <-- 确保语料"高智商"且"安全"
+-----------------------------+
|
v
+-----------------------------+
| Layer 4: 去重 (Global) | <-- 内存密集
| - MinHash + LSH (模糊去重) |
| - 权威性重排 | <-- 消除 40%+ 的重复内容
+-----------------------------+
|
v
[ 黄金 RAG 语料库 (JsonL) ]
3. Layer 1: 守门员——域名与 URL 策略
这是性价比最高的一层。在解压 WET 内容之前,甚至在下载 WARC 之前,就应该通过 URL 决定去留。
3.1 域名策略 (The Trust Spectrum)
在 RAG 场景下,我们通常采用 “白名单优先,黑名单兜底” 的混合策略。
- 高信任白名单 (High-Trust Whitelist):
- 策略:对于特定领域的 RAG(如编程辅助),只全量索引特定域名。
- 示例:
*.readthedocs.io, github.com, stackoverflow.com, docs.*.com。
- Rule of Thumb:如果你的目标是技术文档,
.edu 和 .gov 并不总是好选择(包含大量无关行政文档),不如针对性的技术社区名单。
- 垃圾黑名单 (The UT1 Blacklist):
- 资源:使用开源的 URL 过滤列表(如 UT1 list),屏蔽
adult, gambling, malware 分类。
- SEO 农场:这是 RAG 的大敌。像
geeksforgeeks 的低质镜像站、自动生成的聚合站,必须手动加入黑名单。
3.2 URL 模式过滤 (Regex Is Your Friend)
很多页面虽然是 HTML,但对 RAG 毫无价值。使用正则表达式快速剔除。
| 过滤类型 |
典型特征 (Regex Pattern) |
理由 |
| 功能页 |
/login, /cart, /signup, /search, /forgot-password |
动态交互页,无持久知识 |
| 索引页 |
/tag/, /category/, /author/, /page/\d+ |
仅仅是链接列表,正文密度低 |
| 非内容后缀 |
\.xml$, \.json$, \.rss$, \.php$ (带参数) |
往往是配置或接口,而非文档 |
| 特定参数 |
\?share=, \?lang=, \?print= |
重复页面的变体 |
3.3 语言检测 (Language Identification)
Common Crawl 的元数据中提供了语言标签,但并不完全准确。必须进行二次校验。
- 工具推荐:
- fastText (Facebook): 速度极快,精度高。
- CLD3 (Google): 针对短文本优化。
- 策略:
- 计算文本的语言概率。
- 如果
Probability(Target_Lang) < 0.8,丢弃。
- 注意:技术文档中常夹杂大量英文代码。如果你的目标是中文 RAG,容忍高比例的英文(代码),或者使用专门针对 Code-Switching 优化的模型。
4. Layer 2: 结构化清洗——去除“样板噪音”
WET 格式虽然去除了 HTML 标签,但保留了网页的视觉结构(换行符)。这导致页眉、页脚、侧边栏、广告词依然存在。这些被称为 Boilerplate(样板文本)。
4.1 基于行的启发式规则 (Line-Based Heuristics)
由于 WET 是纯文本,我们无法使用 DOM 树去噪,必须依赖文本特征。遍历每一行,应用以下规则:
- 页脚/版权特征:
- 正则:
(?i)(copyright|©|all rights reserved|隐私政策|联系我们)
- 动作:删除该行及后续所有行(通常页脚在最后)。
- 导航/菜单特征:
- 特征:短文本,包含常见导航词,或者大量管道符
|、>。
- 示例:
首页 > 产品 > 软件 或 Login | Sign Up | Help
- 规则:如果
len(words) < 5 且包含导航符号,删除。
- 社交媒体噪音:
- 特征:
Share on Facebook, Tweet this, 点赞, 评论。
- 动作:删除。
4.2 文本密度与块过滤 (Block Filtering)
将文本按空行分割成“段落块(Paragraph Blocks)”。
- Rule of Thumb:
- 平均行长:如果一个块包含 10 行,但平均每行只有 3 个单词(典型的侧边栏链接列表),丢弃该块。
- 标点符号分布:自然语言的句子通常以
. 。 ! ? 结尾。如果一个块中 80% 的行都不以标点结尾,它很可能不是正文。
- Javascript 残留:如果块中包含
{ } function var 等关键词密度过高,且不在代码块标记中,视为 JS 脚本残留,删除。
5. Layer 3: 质量评估——让数据“高智商”
仅仅是“干净的文本”还不够,我们需要“有意义的文本”。
5.1 困惑度过滤 (Perplexity Filtering)
这是 OpenAI 和 DeepMind 处理预训练数据的标准法。
- 原理:使用一个轻量级的语言模型(如 KenLM,训练于高质量 Wikipedia 语料)来评估一段文本的“自然程度”。
- 逻辑:
- 低 PPL:常见的句子,语法通顺。 -> 保留
- 极高 PPL:乱码、OCR 错误、随机关键词堆砌(SEO Spam)。 -> 丢弃
- 异常低 PPL:极度重复的文本(如 “text text text text”)。 -> 丢弃
5.2 停用词比率 (Stopword Ratio)
- 原理:自然语言(无论是中文还是英文)都包含一定比例的停用词(如 “的”, “是”, “the”, “and”)。
- 规则:
- 如果一段文本中停用词占比 < 20%,它很可能是一个列表、数据表或代码,而不是叙述性文档。
- 对于 RAG 检索,叙述性文档(包含推理过程)通常比纯数据更有价值。
6. Layer 4: 大规模去重 (Deduplication)
互联网上 50% 的内容是重复的。如果不去重,你的 RAG 系统会变成“复读机”。
6.1 为什么 MD5/SHA256 不够用?
传统的哈希算法是“雪崩效应”的:改动一个标点符号,哈希值全变。
网页常见情况:
- 网页 A:
<p>Hello World</p>
- 网页 B:
<p>Hello World</p><footer>2024</footer>
- 结论:MD5 认为它们完全不同,但在 RAG 看来它们就是重复的。
6.2 MinHash + LSH (局部敏感哈希)
这是工业界处理海量文本去重的标准方案。
- Shingling:将文本切分为 N-gram 集合(例如 5-grams)。
- MinHash:通过多个哈希函数生成文档的“签名”(Signature)。两个文档的签名相似度近似于它们的 Jaccard 相似度。
- LSH (Locality Sensitive Hashing):将签名分段索引。只有在同一个桶(Bucket)里的文档才需要进行具体的相似度对比。
6.3 权威性重排策略
当发现 10 个重复文档时,保留哪一个?
- URL 长度:倾向于保留 URL 更短的(通常是原文,长 URL 往往归档或带参数的)。
- HTTPS 优先:HTTPS 站点通常比 HTTP 站点维护得更好。
- 时间戳:通常保留最新抓取的版本(内容可能修正过),或者保留最早的版本(如果是为了原创性归因)。对于 RAG,推荐保留最新版。
7. Layer 5: 元数据封装与输出
清洗后的数据需要封装为标准 JSONL 格式,供 Ingest 脚本(第 5 章)使用。
关键字段规范:
{
"id": "CC-MAIN-2024-10-...",
"text": "清洗后的纯净正文...",
"metadata": {
"source": "common_crawl",
"url": "https://example.com/docs/api",
"domain": "example.com",
"title": "API Documentation - Example",
"date_crawled": "2024-03-15T10:00:00Z",
"date_published": "2023-12-01", // (可选) 如果能从正文提取到
"language": "en",
"quality_score": 0.87, // 根据 PPL 或规则打分
"original_warc_id": "<urn:uuid:...>" // 用于溯源
}
}
Rule of Thumb:务必保留 date_crawled。在 RAG 检索时,可以将其作为“时效性”特征,告诉 LLM:“这是 2021 年的信息,可能已过时。”
8. 本章小结
- 漏斗思维:处理 CC 数据是“做减法”的艺术。你要做的不是“获取数据”,而是“剔除垃圾”。
- 正则 > AI:在第一层过滤中,写好正则表达式比用 AI 模型快 1000 倍且便宜得多。
- 去重是关键:Near-Dedup (MinHash) 是区分“玩具 RAG”和“生产级 RAG”的分水岭。
- 元数据即上下文:没有 URL 和时间戳的文本,对 Claude Code 来说只是无根之木,无法进行有效的引用和事实核查。
9. 练习题
基础题
Q1: 域名黑白名单策略
假设你要构建一个“云原生技术 (Kubernetes/Docker)”的 RAG 知识库。
- 请给出 3 个你会优先加入白名单的域名(或子域名模式)。
- 请给出 2 个你绝对要加入黑名单的网站类型或特征。
点击查看提示与参考答案
**提示**:
思考云原生技术的权威源头在哪里(官方文档、基金会)。反之,思考当你搜索报错信息时,哪些网站总是给你复制粘贴的无用废话。
**参考答案**:
1. **白名单**:
* `kubernetes.io` (及 `*.kubernetes.io`)
* `docs.docker.com`
* `cncf.io` (云原生计算基金会博客/报告)
* (备选) `medium.com` 的特定组织账号(如 Netflix TechBlog),而不是全站。
2. **黑名单**:
* **代码聚合站**:`coderanch.com`, `programcreek.com` (往往是无上下文的代码片段堆砌)。
* **SEO 问答镜像**:任何将 StackOverflow 内容机器翻译或直接抓取并插入大量广告的站点。
Q2: 正则表达式实战
你需要编写一个正则表达式,从 CC 的 URL 列表中剔除不适合做知识库的页面。
要求剔除:
- 图片文件 (.jpg, .png)
- 压缩包 (.zip, .tar.gz)
- 动态查询页 (search?q=…)
- 录页 (login, signin)
点击查看提示与参考答案
**提示**:
关注 URL 的后缀(extension)和路径中的关键词。
**参考答案**:
```regex
# Python re 风格
pattern = r".*\.(jpg|jpeg|png|gif|bmp|zip|tar|gz|rar|exe|iso)$|.*(search\?|/search/|/login|/signin|/signup|/cart).*"
```
**解释**:
- `.*\.(...)$` 匹配以特定后缀结尾的 URL。
- `.*(...).*` 匹配路径中包含特定关键词的 URL。
Q3: 样板代码(Boilerplate)识别
以下是从 WET 提取的三段文本块,请判断哪些应该被清洗掉,并说明理由。
A. “2023-10-12 | By Admin | Comments (0)”
B. “Pod 的生命周期包含 Pending, Running, Succeeded, Failed, Unknown 五种状态。”
C. “主页 产品 解决方案 价格 关于我们”
点击查看提示与参考答案
**提示**:
分析文本的信息密度、语法结构和是否具有导航属性。
**参考答案**:
* **A -> 清洗**:这是典型的文章元数据行(Meta Line),虽然有日期,但本身不是知识。
* **B -> 保留**:这是高质量的定义性陈述,包含完整的主谓宾结构。
* **C -> 清洗**:这是导航栏(Menu),缺乏语义连接词,全是名词堆砌。
挑战题
Q4: 对抗“关键词填充” (SEO Spam)
Common Crawl 中经常混入 SEO 垃圾页,这些页面为了骗取流量,会在正文中隐藏大量热门关键词(如 “Python tutorial free download bitcoin casino”)。
仅靠正则很难发现它们。请设计一个基于统计特征的算法来识别并剔除这类页面。
点击查看提示与参考答案
**提示**:
正常的语言具有一定的词频分布(Zipf 定律)。SEO 垃圾通常会有异常的词对共现(Word Co-occurrence)或极高的困惑度。
**参考答案**:
1. **困惑度 (Perplexity, PPL)**:使用 KenLM 模型计算文本 PPL。这类关键词堆砌的句子在语法上是不通顺的,PPL 会显著高于正常文本。设定阈值(如 PPL > 1000)直接丢弃。
2. **独特词比率 (Vocabulary Richness)**:计算 `Unique Words / Total Words`。如果比率极低(一直在重复几个词),或者比率异常高(全是随机词,没有功能词连接),都是可疑的。
3. **顶级高频词检查**:统计该文档出现频率最高的 5 个词。如果其中包含 `casino`, `loan`, `sex`, `buy` 等垃圾词,且该文档并未被分类为相关主题,则判定为 Spam。
Q5: 时效性与冲突处理
你的 RAG 库中通过 CC 索引了关于 “React Router” 的文档。
- 文档 A (Crawl Date: 2020) 说:使用
Switch 组件。
- 文档 B (Crawl Date: 2024) 说:使用
Routes 组件(Switch 已废弃)。
当用户问“怎么配置路由”时,如何确保 CC 不会产生幻觉或混淆?请提出存储层和检索层的解决方案。
点击查看提示与参考答案
**提示**:
不仅要存数据还要存元数据。在检索时,要么过滤旧的,要么让 LLM 知道时间。
**参考答案**:
* **存储层**:
* 必须在 Metadata 中存储 `crawl_date` 或从文中提取的 `publish_date`。
* 在去重阶段,如果发现 URL 相同但内容不同,**Upsert(覆盖)** 旧数据,只保留最新版。
* **检索层 (Retrieve & Pack)**:
* **时间衰减重排 (Time-Decay Reranking)**:在计算相关性分数时,加入时间因子,新文档加分。
* **Prompt 注入上下文**:在组装 Prompt 时,显式带上时间:“[Reference 1 (Date: 2020-01)]: ... [Reference 2 (Date: 2024-03)]: ...”。并系统提示 CC:“如果存在冲突,优先采纳日期较新的信息”。
Q6: 构建代码感知的去重策略
你正在做一个面向代码库的 RAG。你发现 Common Crawl 抓取了大量 GitHub 的镜像站或 Fork 仓库,导致同一个 utils.py 出现了 1000 次。
使用标准的 MinHash 对代码去重效果佳,因为代码中只要改一个变量名或加一行注释,MinHash 签名就可能变化。请设计一种针对代码的更鲁棒的去重指纹算法。
点击查看提示与参考答案
**提示**:
代码的本质是抽象语法树(AST)。去重应忽略变量名、注释和空白,只关注结构。
**参考答案**:
1. **AST 提取**:使用 `tree-sitter` 或 `ast` 模块将代码解析为抽象语法树 (AST)。
2. **归一化 (Normalization)**:
* 移除所有注释。
* 将所有变量名替换为占位符(如 `VAR_1`, `VAR_2`)。
* 将所有字符串字面量替换为 `STR_LITERAL`。
3. **结构指纹**:对归一化后的代码序列计算哈希(如 SHA-256)。
4. **效果**:这样,即使开发者把 `def calculate_sum(a, b)` 改名为 `def add(x, y)`,只要逻辑结构没变,指纹就是一样的,从而实现高效去重。
10. 常见陷阱与错误 (Gotchas)
- 编码灾难 (Encoding Hell)
- 现象:提取出的中文变成
é 或 �。
- 原因:Common Crawl 的 WET 默认假设 UTF-8,但很多老旧中文网站是 GBK 或 Big5,且 HTTP Header 声明错误。
- 解决:不要盲目信任 WET。如果乱码率高,建议使用
ftfy (Fix Text For You) 库自动修复,或者回退到 WARC 格式,使用 cchardet 检测原始二进制流的编码。
- PDF 解析的陷阱
- 现象:WET 文件中包含 PDF 转换来的文本,但全是断行,单词被切断(如
Pyt hon)。
- 原因:CC 的自动 PDF 转文本工具对排版复杂的文档处理不佳。
- Rule of Thumb:对于外挂 RAG,尽量过滤掉 WET 中的 PDF 内容(通过 content-type 判断)。PDF 通常需要专门的 Pipeline(如使用 OCR 或专门的 PDF 解析器)才能得到高质量文本,直接用 CC 的转换结果效果很差。
- 内存溢出 (OOM) 就在一瞬间
- 错误:
all_hashes = set()。试图把 10 亿个指纹放在 Python 的 Set 集合里。
- 解决:
- 使用 Redis 或 KeyDB 存储去重指纹。
- 使用 Bloom Filter (布隆过滤器) 做第一层快速去重(极省内存,但有误判率)。
- 或者使用 磁盘上的键值存储 (如 RocksDB)。
- Robots.txt 的“事后诸葛亮”
- 风险:CC 抓取时网站允许抓取,但现在网站更新了
robots.txt 禁止 AI 使用。
- 建议:虽然 CC 数据在法律上通常被认为是合规的(Fair Use),但在构建企业级 RAG 时,如果你想规避风险,可以定期(如每月)检查源域名的
robots.txt,如果发现对方禁止了 CCBot 或 GPTBot,主动从你的索引中删除该域名的数据。