网页数据如何加工成可用的模型语料
面向微调与 RAG,文章介绍从原始网页构建干净、结构化数据集的五阶段流程,并提供各阶段代码。重点涉及目标页面发现、内容筛选,以及垂直领域采集的规模化问题。
AI 深度解读
做文档问答或领域模型,网页抓下来只是起点。导航、重复转载、过期页面和代码块丢失,都可能让后面的微调或检索增强生成(RAG)偏离目标。原文给出了一条五阶段流水线:发现来源、提取内容、清洗去重、质量验证、存储导出,并用 Firecrawl 演示接入方式。
它最有用的提醒是:把网页变成 Markdown,不等于拿到了可用数据。不过,作者没有提供模型效果或工具对照实验。「数据质量比模型架构重要」更适合作为工程上的优先级建议,不能当成已经证明的普遍结论。
先决定抓哪些页面,再考虑怎么抓
作者把来源发现放在第一步。文档站要找齐内容页,新闻站则要筛出文章地址,排除导航、广告和无关的用户内容。
这里有个取舍:浅抓很多站点,内容更丰富;深抓一个站点,领域知识更连贯。作者认为多数微调项目更适合后者,后面又建议混合 5-10 个高质量来源。两者可以结合:先明确领域边界,再从少量相关来源中深入采集。
Firecrawl 的 map 会结合 sitemap 和链接发现页面。下面这段代码解决的是「先看清候选 URL」,还没有开始全文采集:
from firecrawl import Firecrawl
firecrawl = Firecrawl(api_key="fc-YOUR_API_KEY")
res = firecrawl.map(url="https://docs.example.com", limit=500)
for link in res.links:
print(link.url, link.title)
要注意 limit=500 限制的是这次发现的页面数量,不能据此判断站点内容已经找全。作者建议先检查列表,手动删掉登录页、标签归档和分页重复地址,再花采集额度。
这一步也决定了后续清洗的难度。把范围圈准,通常比抓完后再判断每页有没有用更省事。
正文提取解决了格式问题,还没解决质量问题
提取阶段至少要处理 JavaScript 渲染、页眉页脚等模板内容、作者和日期等元数据,以及正文与侧栏、评论的边界。登录和无限滚动也会增加难度,但原文没有交代这些情况如何逐一解决。
作者演示的 firecrawl.crawl 使用 limit=50,通过 scrape_options={"formats": ["markdown"]} 请求 Markdown,并从 job.status、job.completed、job.total 查看进度,从 job.data 读取页面。
单页则使用 firecrawl.scrape(url, formats=["markdown"])。需要只保留主要内容时,scrape 接受 only_main_content=True,在 crawl 中则把它放进 scrape_options。作者明确提醒:这只是基于 DOM 的启发式判断,应该抽查 20 页。
对技术文档来说,抽查不能只看文字是否通顺,还要看代码块、标题层级和示例上下文是否完整。正文选错了,后面再精细地切块也补不回来。
已有 URL 列表时,可以使用批量端点并行处理数百或数千页,但原文没有给出具体 API 名称和调用代码。
去重为什么要放在生成向量之前
清洗包括语言筛选、处理 HTML 残留和格式噪声、过滤低质量页面,以及文档级和段落级去重。作者特别强调去重,因为许可证、Cookie 提示和相关文章模块会反复占用 token。
下面的实现先用哈希排除完全相同的文本,再比较词片段集合,判断内容是否近似:
import hashlib, re
def shingles(text, n=5):
"""Word n-grams, lowercased and punctuation-stripped."""
words = re.findall(r"\w+", text.lower())
return {" ".join(words[i:i+n]) for i in range(len(words) - n + 1)}
def dedupe(texts, n=5, threshold=0.8):
seen_hashes, seen_sh, kept = set(), [], []
for t in texts:
exact = hashlib.blake2b(t.encode(), digest_size=16).hexdigest()
if exact in seen_hashes: # bit-identical copy
continue
seen_hashes.add(exact)
sh = shingles(t, n)
# near-duplicate: Jaccard overlap with anything already kept
if any(len(sh & prev) / max(1, len(sh | prev)) >= threshold
for prev in seen_sh):
continue
seen_sh.append(sh)
kept.append(t)
return kept
关键是 threshold=0.8:两个集合的交集占并集达到这个比例,就舍弃后来的文本。n=5 表示每个片段由连续 5 个词组成。作者认为这组参数适合文档站,论坛或聊天数据可以尝试更低阈值,但要求先用有标注的样本调参。
这段代码的近似比较是 O(n²)。作者给出的规模判断是:几千篇还能用,几十万篇就应改用 MinHash/LSH,或借助 datasketch。
它也没有自动完成段落级去重:传入整篇文档,就比较整篇文档。中文文本通常没有词间空格,\w+ 的结果也不能直接视为中文分词。这两点决定了代码适合作为起步样例,不能原封不动覆盖所有语料。
RAG 接入同样绕不过去重。原文推荐 langchain-firecrawl 中的 FirecrawlLoader,它封装 v2 SDK;示例使用 mode="crawl",调用 loader.load(),读取 page_content。作者提醒,langchain_community 里的旧 FireCrawlLoader 仍调用 v1 SDK 方法,会与文中所用的当前 API 不兼容。原文没有给出精确包版本,照做时仍需核对依赖。
加载器把网页变成文档对象,并不能证明文档已经适合生成向量。
我的判断
我认同这条流水线的顺序,尤其是先筛 URL、再抽查正文、最后批量处理。与每站手写爬虫相比,现成提取工具能减少重复接入工作;但原文没有提供成功率、耗时、成本或微调效果的对照数据,不能据此认定 Firecrawl 更优。
来源也需要交代:文章由 a9n9(Visybl Inc)运行的自主 AI agent002 撰写,并包含推广分成链接。它可以作为流程指南,产品选择部分则应结合自己的样本验证。
质量验证也不能只看长度、标题和代码块。原文还列出了语言置信度与领域相关性,却没有说明评分规则;文档是否过期、版本是否冲突,也没有展开。微调和 RAG 的下游需求不同,不能因为共用前半段采集流程,就沿用同一套验收标准。
导出方面,原文建议使用带 text 字段的 JSONL、大规模数据使用 Parquet,或接入 LangChain document loaders,同时记录来源、时间和参数。这个要求很实际:答案出错时,至少能追查它来自哪批数据。
能拿来做什么
- 给前端组件库或 Node 项目搭文档问答时,先抓 10-20 页,检查正文边界、代码完整性和元数据,再扩大范围。
- 用文档、转载、论坛引用各做一组标注样本,验证
n=5、threshold=0.8的误删与漏删情况,再决定是否调整分词、阈值或比较粒度。 - 在生成向量之前完成去重,并记录 URL、抓取时间、采集参数和数据集版本;采集时同时核对
robots.txt与速率限制,便于后续重建和排查。