第 3 章 第二部分:预训练数据工程

数据采集与解析

从Common Crawl的WARC字节到干净纯文本——主流数据源盘点、HTML正文抽取(trafilatura/resiliparse)、PDF解析(MinerU/Nougat/Marker)、采集架构与解析时序

数据采集 Common Crawl WARC trafilatura MinerU PDF解析 HTML抽取

预训练数据工程的入口是从原始字节到纯文本的转换。

这一步的产出质量直接决定了后续所有环节的”地基”是否稳固。


3.1 主流数据源盘点

预训练数据的主流来源可以归纳为”七大源”:

数据源内容类型规模(tokens级别)质量关键挑战
Common Crawl (CC)网页快照100T+参差不齐HTML清洗、去重、低质过滤
GitHub代码仓库1T+中-高许可证、低质代码过滤
arXiv学术论文100B+高PDF公式解析
Wikipedia百科4B+高多语种平衡
Books (Books3/Gutenberg/PBLLM)书籍200B+高-中版权、PDF解析
StackExchange问答100B+中-高答案质量、投票排序
OpenWebMath数学网页10B+高LaTeX公式还原

3.1.1 数据源贡献排序

以Llama3的15T tokens为例,典型配比:

Common Crawl清洗后:   60-70%  (10T+)
GitHub代码:           10-15%  (1.5T)
Books:                5-10%   (1T)
arXiv:                3-5%    (500B)
Wikipedia:            1-2%    (200B)
StackExchange:        1-2%    (200B)
其他(数学/多语/合成):  5-10%   (1T)

Common Crawl是绝对主力,但代码、论文、书籍贡献了”高密度知识”的绝大部分。

3.2 Common Crawl实战:WARC/WAT/WET

Common Crawl(CC)是最大的开源网页爬虫数据集,每月发布一次快照,单快照约 100TB 压缩后数据。CC提供三种格式:

3.2.1 三种格式对比

格式内容大小(单快照)用途
WARC完整响应(含HTTP头+HTML+元数据)~100TB主流选择,信息最完整
WAT提取的链接和元数据~30TB链接分析、PageRank
WET预提取的纯文本~20TB简化使用,但解析质量一般

工业级流水线首选 WARC——自己控制解析质量,不依赖CC的预提取。WET虽然方便,但CC的trafilatura版本可能落后,且无法做精细的正文/导航/页脚切分。

3.2.2 WARC文件结构

WARC是行式存储的容器格式,每条记录由header+payload组成:

WARC/1.0
WARC-Type: response
WARC-Record-ID: <urn:uuid:...>
WARC-Date: 2024-12-15T08:30:00Z
WARC-Target-URI: https://example.com/article
Content-Type: application/http; msgtype=response
Content-Length: 8456

HTTP/1.1 200 OK
Content-Type: text/html
...

<html><head>...</head><body>...</body></html>

WARC/1.0
WARC-Type: response
...

3.2.3 WARC下载与解析代码

# CC的WARC文件按月发布在AWS S3上
# 路径:s3://commoncrawl/crawl-data/CC-MAIN-< YYYY >-< WW >/segments/.../warc/CC-MAIN-< timestamp >-< index >.warc.gz
# 示例快照:CC-MAIN-2026-26 (2026-06采集,2.10B网页,354.59 TiB) <!-- edit: 2026-07-24 更新至 2026-06 快照,CC 已通过 HuggingFace Storage Bucket 分发(2026-04 快照起) -->
# CC-MAIN-2024-51 仍可用作示例,生产环境建议跟踪最新快照

import boto3
import gzip
import io
from warcio import ArchiveIterator

def download_warc_list(snapshot="CC-MAIN-2026-26"):
    """获取某个月CC快照的WARC文件路径列表"""
    s3 = boto3.client('s3')
    paginator = s3.get_paginator('list_objects_v2')
    paths = []
    for page in paginator.paginate(
        Bucket='commoncrawl',
        Prefix=f'cc-index/collections/{snapshot}/indexes/'
    ):
        for obj in page.get('Contents', []):
            paths.append(obj['Key'])
    return paths


def stream_warc_and_extract(warc_path, output_queue):
    """
    流式读取WARC.gz,逐条记录解析
    采用流式是为了避免单文件解压占用过多内存
    """
    s3 = boto3.client('s3')
    response = s3.get_object(Bucket='commoncrawl', Key=warc_path)
    
    # 流式解压 + WARC迭代器
    for record in ArchiveIterator(response['Body']):
        if record.rec_type != 'response':
            continue
        
        uri = record.rec_headers.get_header('WARC-Target-URI')
        content_type = record.http_headers.get_header('Content-Type', '')
        
        # 只处理HTML响应
        if 'text/html' not in content_type:
            continue
        
        html_bytes = record.content_stream().read()
        
        # 推入下游解析队列
        output_queue.put({
            'url': uri,
            'html': html_bytes,
            'warc_date': record.rec_headers.get_header('WARC-Date')
        })

3.2.3.1 工业方案:NeMo Curator

以上手动WARC解析提供了原理理解,但工业级场景推荐使用 NVIDIA NeMo Curator 的 CommonCrawlDownloadExtractStage(NeMo Curator 26.04,基于 Nemotron-CC 流水线开源):

三阶段流水线(26.04 架构更新):

  1. URL 生成:根据CC快照索引自动生成WARC文件URL列表
  2. 下载:支持HTTPS和S3双模式(use_s3=True 可直接从 S3 传输,避免 HTTPS 限速),可自动 fallback
  3. 迭代抽取:流式迭代WARC记录 → 内置抽取,内存友好;内置三种抽取器——jusText(通用)、resiliparse(高速)、trafilatura(高质量),可按需切换

26.04 新增能力:

  • 自动语种识别(LID),无需单独调用fastText
  • PDF 流水线(Nemotron-Parse Xenna):四阶段 PDF 解析,基于 pypdfium2
  • vLLM 嵌入去重:语义去重默认启用,模型从 all-MiniLM-L6-v2 切换为 google/embeddinggemma-300m
  • 与NeMo Curator后续质量过滤流水线无缝集成
  • 分布式处理支持(Dask/Spark)
# NeMo Curator Common Crawl 流水线示例
from nemo_curator import Sequential
from nemo_curator.download import download_common_crawl
from nemo_curator.datasets import DocumentDataset

# 单行启动CC下载+抽取+语种识别
pipeline = Sequential([
    download_common_crawl(
        snapshot="CC-MAIN-2026-26",
        output_dir="./cc_output",
        extractor="trafilatura",  # 或 "jusText" / "resiliparse"
        download_method="s3",      # 或 "https";use_s3=True 直接S3传输,避免限速
        language="en,zh",          # 自动LID过滤
        seed=42
    ),
])

dataset = pipeline.run()
print(f"下载完成:{len(dataset.df)} 篇文档")

对于从头搭建流水线的团队,直接使用NeMo Curator可以跳过大部分手动编码,将精力聚焦于数据策略而非工程实现。

3.2.4 工程参数:CC下载的并发控制

CC单快照有约 90,000 个 WARC 文件(每个约 1GB 压缩)。下载时需要注意:

# 推荐的S3并发下载配置
S3_CONFIG = {
    'max_pool_connections': 50,      # S3连接池
    'multipart_threshold': 8 * 1024**2,  # 8MB以上启用分片
    'multipart_chunksize': 8 * 1024**2,
}

# 千万别用requests直接下载CC——AWS S3限速会让你怀疑人生
# 必须用boto3 + s3 transfer manager

踩坑经验:CC的WARC.gz是单文件gzip,不能seek。如果想并行处理单文件,需要先解压再分块,或者用 warcio 的流式接口。

3.3 HTML正文抽取

HTML解析的目标:从一坨带导航/广告/页脚的HTML中提取真正的正文内容,去除模板化噪音。

3.3.1 主流工具对比

工具准确率速度多语种公式/LaTeX推荐场景
trafilatura高快(500 docs/s)支持弱通用网页正文
resiliparse高极快(2000 docs/s)支持弱大规模批处理
boilerpy3中快一般无简单网页
readability-lxml中快一般无新闻类文章
html2text低极快支持无粗提取
MinerU-HTML v1.1极高 (0.9001 ROUGE-N F1)快(VLLM GPU)支持强高质量正文抽取(SLM驱动)

工业级首选 trafilatura(HuggingFace FineWeb同款),双路fallback可用resiliparse。

3.3.1.1 多抽取器 Union:2026 年新共识

Apple、Stanford 和 UW 联合团队在 2026 年 2 月发表的研究(arXiv:2602.19548,“Beyond a Single Extractor”)颠覆了”选一个最好的抽取器就够了”的假设。核心发现:

  • 不同抽取器覆盖的网页子集高度互补——仅 39% 的网页能被多个抽取器同时保留
  • trafilatura、resiliparse、jusText 各有”擅长”的网页类型:resiliparse 在 WikiTableQuestions 上比 trafilatura 高 10.3pp;代码块抽取器选择导致 HumanEval 差 3pp
  • 取 Union(trafilatura ∪ resiliparse ∪ jusText) 在 DCLM-Baseline 流水线下 token yield 提升 71%(去重后仍提升 58%),且不牺牲 benchmark 性能

工业最佳实践更新:

场景推荐策略
GPU 可用trafilatura ∪ MinerU-HTML ∪ resiliparse(三抽取器 Union)
CPU 场景trafilatura ∪ resiliparse(双抽取器 Union)
资源受限trafilatura(首选)+ resiliparse(fallback,仅当 trafilatura 产出 < 200 字符时启用)

多抽取器 Union 的核心代价是引入更多重复内容(需在后续去重环节统一处理),但 token yield 提升远超去重损失。

3.3.2 trafilatura 实战

from trafilatura import extract
from trafilatura.settings import use_config

# 必须调整配置,否则会过度删除"看起来像导航"的正文段落
config = use_config()
config.set('DEFAULT', 'EXTRACTION_TIMEOUT', '0')  # 不超时
config.set('DEFAULT', 'MIN_EXTRACTED_SIZE', '200')  # 至少200字符
config.set('DEFAULT', 'MIN_OUTPUT_SIZE', '100')   # 至少100字符输出
config.set('DEFAULT', 'MAX_FILE_SIZE', '10000000')  # 10MB上限

def extract_html(html_text, url=''):
    """
    带fallback的HTML正文抽取
    """
    # 第一路:trafilatura(推荐)
    text = extract(
        html_text,
        url=url,
        config=config,
        output_format='txt',    # 纯文本输出
        include_comments=False,  # 去掉评论
        include_tables=True,    # 保留表格
        favor_recall=True,      # 偏向召回(宁可多留也不漏)
        deduplicate=True        # 模板去重
    )
    
    if text and len(text) > 100:
        return text, 'trafilatura'
    
    # 第二路:resiliparse fallback
    from resiliparse.extract_html2text import extract_preserve_structure
    text_fallback = extract_preserve_structure(html_text)
    if text_fallback and len(text_fallback) > 100:
        return text_fallback, 'resiliparse'
    
    return None, 'rejected'

3.3.3 解析质量检查

抽取后的文本必须经过质量检查,否则会留下大量”半解析”的脏数据:

def check_extraction_quality(text):
    """
    检查抽取质量,返回 True/False
    """
    if not text or len(text) < 200:
        return False
    
    # 1. HTML标签残留检查(解析失败的标志)
    html_tag_ratio = count_html_tags(text) / len(text)
    if html_tag_ratio > 0.05:  # 5%以上HTML残留
        return False
    
    # 2. JS代码残留检查
    if 'function(' in text or 'var ' in text[:500]:
        return False
    
    # 3. 模板化检查(导航菜单类)
    lines = text.split('\n')
    short_lines = sum(1 for l in lines if len(l) < 30)
    if short_lines / len(lines) > 0.7:  # 70%短行=导航
        return False
    
    # 4. 字符分布异常(乱码检查)
    ascii_ratio = sum(c.isascii() for c in text) / len(text)
    if ascii_ratio < 0.6 and ascii_ratio > 0.3:  # 既不是纯英文也不是纯中文,可能是乱码
        return False
    
    return True

3.4 PDF解析:论文与书籍的核心挑战

PDF是arXiv论文和Books的主要来源。PDF解析的难点在于:PDF是为打印设计的,不是为文本提取设计的。

3.4.1 主流工具对比

工具公式还原表格速度多栏适用
PyPDF2/pdfminer无差快差简单PDF
Grobid部分中慢好学术论文
Nougat (Meta)强(LaTeX)中慢(GPU)好arXiv论文
Marker强好中(GPU)好通用PDF
MinerU强(LaTeX)强中(GPU)好论文+书籍+复杂版式
docling中好快好工业文档

论文PDF首选 Nougat 或 MinerU;书籍+复杂版式首选 MinerU。

3.4.2 MinerU 实战

MinerU(OpenDataLab, 2024)是当前最强的开源PDF解析器。 2025-06 的 2.0 版本完成架构重构:输入范围从 PDF/图片扩展到 DOCX/PPTX/XLSX/网页,新增化学论文解析(分子结构识别、化学反应提取,与 A4Chemistry 合作),综合解析准确率较前代提升约 22%,核心模型约 0.9B 参数(decoupled 视觉-语言双编码器),GitHub 星标截至 2026-08 已破 81K。支持:

  • 公式 → LaTeX 还原
  • 表格 → HTML/Markdown 还原
  • 多栏阅读顺序识别
  • 图表与正文分离
# MinerU 通过命令行调用,最简用法(2.0 起 CLI 由 magic-pdf 更名为 mineru)
# mineru -p input.pdf -o output/

# 工业级批量处理脚本
import subprocess
from pathlib import Path

def batch_parse_pdfs(pdf_dir, output_dir, gpu_id=0):
    """
    批量解析PDF为Markdown
    MinerU 2.0 三引擎:pipeline(纯 CPU 可跑)/ vlm-engine(vLLM/LMDeploy)/ hybrid-engine(默认推荐)
    """
    pdfs = list(Path(pdf_dir).glob('*.pdf'))
    
    for pdf in pdfs:
        cmd = [
            'mineru',
            '-p', str(pdf),
            '-o', f'{output_dir}/{pdf.stem}',
            '-m', 'auto',          # 自动选择模式
            '-d', str(gpu_id),      # GPU设备
            '--lang', 'en,zh'      # 多语种
        ]
        
        try:
            result = subprocess.run(
                cmd, capture_output=True, timeout=300, text=True
            )
            if result.returncode != 0:
                log_error(pdf, result.stderr)
        except subprocess.TimeoutExpired:
            log_error(pdf, 'Timeout after 300s')

3.4.3 Nougat 与公式还原

Nougat 是 Meta 开发的基于 ViT 的 PDF 解析器,特别擅长公式还原:

# Nougat 直接将PDF转为Markdown+LaTeX
# 输入:paper.pdf
# 输出:paper.mmd(Mathpix Markdown)

# 命令行用法
# nougat path/to/file.pdf -o output_dir

# 一个解析后的样例:
"""
# Attention Is All You Need

## Abstract

We propose a new simple network architecture, the Transformer, 
based solely on attention mechanisms, dispensing with recurrence 
and convolutions entirely.

$$\text{Attention}(Q, K, V) = \text{softmax}\left(\frac{QK^T}{\sqrt{d_k}}\right)V$$

where $Q \in \mathbb{R}^{n \times d_k}$ ...
"""

踩坑经验:Nougat 对扫描版PDF(图片型)效果差,需要先用 OCR 处理。MinerU 的 auto 模式可以自动切换。

3.5 采集架构图

工业级数据采集系统的典型架构:

┌─────────────────────────────────────────────────────────────┐
│                    采集调度层 (Airflow)                     │
│   定时任务 → CC快照监控 → GitHub爬取 → arXiv同步 → ...     │
└──────────────────────────────┬──────────────────────────────┘
                               │
                               ▼
┌─────────────────────────────────────────────────────────────┐
│                    下载队列层 (Kafka)                       │
│   topic: cc.warc.url, github.repo.url, arxiv.id, ...       │
└──────────────────────────────┬──────────────────────────────┘
                               │
              ┌────────────────┼────────────────┐
              ▼                ▼                ▼
        ┌──────────┐    ┌──────────┐    ┌──────────┐
        │ Worker 1 │    │ Worker 2 │    │ Worker N │
        │  (S3下载) │    │  (S3下载) │    │  (S3下载) │
        └────┬─────┘    └────┬─────┘    └────┬─────┘
             │               │               │
             └───────────────┼───────────────┘
                             ▼
┌─────────────────────────────────────────────────────────────┐
│                    解析集群 (Ray/Spark)                     │
│   WARC → HTML抽取 → 纯文本                                  │
│   PDF  → MinerU/Nougat → Markdown+LaTeX                     │
└──────────────────────────────┬──────────────────────────────┘
                               │
                               ▼
┌─────────────────────────────────────────────────────────────┐
│                    原始文本存储 (S3/HDFS)                  │
│   parquet 格式,按 source/date/hash 分区                    │
│   schema: source, url, html, text, lang, parsed_at, ...    │
└─────────────────────────────────────────────────────────────┘

3.5.1 关键设计原则

  1. 解耦:下载、解析、存储三层独立扩缩容
  2. 幂等:每个URL/文件可重试,避免重复处理
  3. 可追溯:保留WARC原始字节,方便重新解析
  4. 背压:下游处理慢时,上游自动暂停下载

3.6 解析时序图

单条数据从下载到产出的时序:

时间轴 →

下载层:    [S3 GET WARC.gz]──────────[stream 1MB chunks]
                                        │
解析层:                                  ▼
                          [warcio迭代器]──[record 1]──[record 2]──...
                                              │
                                              ▼
                                          [trafilatura]
                                              │
                                              ▼
                                       [质量检查]
                                          │
                                          ▼
                                   [语种识别 fastText]
                                          │
                              ┌───────────┴───────────┐
                              ▼                       ▼
                          [zh文本]                 [en文本]
                              │                       │
                              ▼                       ▼
                         [S3: zh/raw]            [S3: en/raw]
                       parquet分区              parquet分区

3.6.1 性能基准

在 32 vCPU + 64GB 内存节点上:

阶段吞吐单节点日处理量
WARC 下载200 MB/s~17 TB
HTML 抽取500 docs/s~43M 文档
PDF 解析(MinerU GPU)5 docs/s~430K 文档
完整流水线300 docs/s~26M 文档

PDF 解析是瓶颈—— MinerU 单GPU 5 docs/s,处理100万篇arXiv论文需要约 50 GPU·天。

3.7 GitHub 代码采集

代码数据的特殊处理:

3.7.1 采集策略

# GitHub API 限制:5000 req/h,所以要用 GH Archive + 仓库快照
# 推荐用 Google BigQuery 的 GH Archive 数据集

# 高质量代码仓库筛选标准
REPO_FILTERS = {
    'min_stars': 10,          # 至少10 star
    'min_commits': 5,         # 至少5次提交
    'exclude_fork': True,     # 排除纯fork
    'exclude_template': True, # 排除模板仓库
    'exclude_size_kb': 1,    # 排除小于1KB的
    'exclude_size_mb': 10,   # 排除大于10MB的(避免单文件巨无霸)
    'languages': ['Python', 'Java', 'C++', 'JavaScript', 'Go', 'Rust']
}

# 文件级过滤
FILE_FILTERS = {
    'exclude_paths': [
        'node_modules/', 'vendor/', '.git/',
        'test/', 'tests/', '__pycache__/',
        'docs/', 'examples/'  # 这些通常质量低
    ],
    'exclude_extensions': ['.min.js', '.lock', '.svg', '.png'],
    'exclude_licenses': ['GPL-3.0', 'AGPL-3.0']  # 视团队合规而定
}

3.7.2 代码数据许可证

许可证是否可用(参考)
MIT✅
Apache-2.0✅
BSD-3-Clause✅
GPL-3.0❌(传染性)
AGPL-3.0❌(网络传染)
无 LICENSE 文件❌(默认”All rights reserved”)

3.8 arXiv 与学术数据

3.8.1 arXiv 采集

arXiv 提供 bulk access(S3上公开):

# arXiv S3 bucket
aws s3 ls s3://arxiv/pdf/ --recursive | head

# 完整下载约 2.5 TB,含 240万+ 篇论文
# 推荐方式:用 arxiv-oai-mcp 工具或 kaggle 的 arxiv dataset

3.8.2 LaTeX 源码处理

arXiv 大部分论文有 LaTeX 源码,比 PDF 解析更准确:

def parse_arxiv_latex(source_tarball):
    """
    从arXiv tarball提取LaTeX源码,转纯文本
    比从PDF解析质量高10倍以上
    """
    with tarfile.open(source_tarball) as tar:
        # 找到主tex文件
        tex_files = [f for f in tar.getnames() if f.endswith('.tex')]
        main_tex = find_main_tex(tex_files)
        
        # 提取内容
        content = tar.extractfile(main_tex).read().decode('utf-8')
        
        # 去除LaTeX命令,保留文本
        # 推荐用 pylatexenc 或 panflute
        from pylatexenc.latex2text import LatexNodes2Text
        text = LatexNodes2Text().latex_to_text(content)
        
        return text

踩坑经验:arXiv 的 LaTeX 源码格式极其混乱(每个作者风格不同)。建议先尝试 LaTeX 源码,失败再回退到 PDF(Nougat/MinerU)。

3.9 踩坑经验总结

坑1:CC单快照不够,必须跨快照去重

某团队用1个快照(100TB)训练,发现大量数据是同一个网站的不同时间快照。必须跨多个快照做去重,且时间戳靠后的优先。

坑2:HTML抽取把广告当正文

某团队用 html2text 抽取,结果模型生成的内容里全是”点击这里”、“立即购买”。必须用 trafilatura 或 resiliparse 做严格的正文/模板分离。

坑3:PDF表格被解析成乱序文本

某团队用 pdfminer 解析财报PDF,表格被拆成一坨。复杂版式PDF必须用 MinerU/Marker,简单文本PDF才用 pdfminer。

坑4:GitHub代码含测试数据

某团队从 GitHub 抓代码,没过滤 test/ 目录,结果训练数据里混入了大量 JSON/CSV 测试数据。模型生成代码时经常吐出一坨测试数据。代码采集必须做路径过滤。

坑5:解析后的文本长度过滤太激进

某团队过滤掉了所有<500字符的文本,结果丢失了大量列表页、表格、定义。长度过滤推荐 <200 字符,而不是<500。

3.10 本课总结

  • 七大主流数据源:CC/GitHub/arXiv/Wiki/Books/StackExchange/OpenWebMath
  • CC 三种格式:WARC(首选)、WAT(链接分析)、WET(简化但解析差)
  • HTML抽取:trafilatura(首选)+ resiliparse(fallback)
  • PDF解析:MinerU(最强通用)/ Nougat(论文公式)/ Marker(综合)
  • 架构原则:下载-解析-存储三层解耦,可独立扩缩容
  • 性能瓶颈:PDF解析是最大开销,单GPU只能处理5 docs/s

课后练习

  1. 用 warcio 流式解析1个CC的WARC.gz文件,统计其中的语种分布
  2. 对比 trafilatura 和 resiliparse 在100篇中文网页上的抽取质量
  3. 用 MinerU 解析1篇 arXiv 论文,检查公式还原准确率
  4. 设计你团队的代码采集流水线,包含许可证过滤

下一章我们将进入语种识别与质量过滤——CommonLingua、fastText、Gopher规则、FineWeb-Edu模型打分,把”垃圾数据”过滤掉的核心环节。