ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

网页转Markdown利器caveman:RAG语料清洗与无头浏览器实践

网页转Markdown利器caveman:RAG语料清洗与无头浏览器实践 如果你最近在搭RAG知识库或者准备给LLM整理训练语料大概率逃不过一件事把网页变成干净文本。我去年有段时间每天都在干这事试过正则、试过BeautifulSoup、试过各种“阅读模式”最后在GitHub上翻到一个叫caveman的小工具一下子省了我不少时间。caveman不是什么重型框架也没有复杂的分布式架构从名字到用法都带着一股“原始人”气质但干起活来却意外地精细。你给它一个URL它给你一篇干干净净、层级清楚、代码块完整的Markdown文档正文是正文导航是导航广告是广告不会混在一起。这篇就围绕caveman这个工具聊聊它解决的痛点、核心原理、实操方式以及我在批量处理网页转语料时踩过的那些坑。1. 为什么我在语料清洗这条路上最后选了caveman先说清楚我不是专业爬虫工程师大部分时间在做算法和数据管道。但凡是接触过LLM数据准备的人应该都有过这种体验最花时间的不是训练模型而是把网上的内容变成能用的文本。网上现成的抓取方案不少但实际跑一轮之后你会发现每个方案都有让人挠头的地方caveman恰好补上了这些缺口。1.1 传统网页抓取的三个常见痛点第一个痛点是正则和CSS选择器的脆弱性。用BeautifulSoup写规则去抓取今天能用明天网站改个类名就全废。很多文章页看着规整实际上一大堆导航、侧栏、推荐阅读全都挂在同一个嵌套结构里写选择器时要么误伤正文要么漏掉关键段落。维护这套规则的成本远远超过我当初省下来的时间。第二个痛点是动态渲染。现在稍微像样一点的网站正文经常是JS异步加载出来的用requests直接拉回来的HTML往往只有骨架真正的文章内容在后面才“长”出来你拿回去清洗得到的是一堆没有实体的空壳。第三个痛点是格式还原。就算你把正文提取出来了代码块、表格、标题层级这些结构信息也经常丢失。对LLM来说代码块被拆成一行行普通文本、表格被压成竖线串损失的不只是格式更是语义。RAG检索时召回质量会肉眼可见地变差。这三个痛点叠在一起就是我在语料整理这条路上一直不顺的原因。市面上的通用抽取算法解决了一部分但离“直接可用”还差得远。1.2 caveman不是爬虫它是“网页转Markdown处理器”很多人在第一次听说caveman时会把它当成又一个爬虫框架这个理解不准确。爬虫解决的是“如何大规模获取网页”caveman解决的是“如何把单个网页变成高质量的Markdown文本”。它更像一个处理车间而不是运输车队。你给它URL它内部完成浏览器渲染、视觉分块、正文识别、Markdown输出这一整条链路最后交给你一份可以直接喂给RAG或训练管道的干净文档。caveman这个命名也很妙。开发者社区里早就有一个词叫“caveman debugging”指的是用最原始的print语句调试代码。听起来落后但在某些场景下print大法就是比复杂调试器好使。caveman工具的设计哲学有类似的味道不搞复杂的规则配置不迷信某种万能AI模型而是用最朴素的“人眼怎么看页面就怎么切页面”的思路做正文抽取结果反而比花哨的方案更可靠。对于正在构建知识库、需要批量整理文档、或者做离线阅读存档的人来说caveman提供了一个非常实在的选项。2. caveman核心拆解从URL到干净Markdown它内部到底做了什么我习惯用“三层理解法”来读懂这种工具。caveman从输入到输出大致可以拆成渲染层、解析层、输出层三个环节每一环都有它对应解决的具体问题。2.1 第一步让浏览器先“演”一遍网页传统抓取思路是直接分析HTML源代码caveman的做法更接近真实用户的行为打开一个无头浏览器把页面完整加载一遍。这个区别至关重要。现代网页中正文内容往往由JavaScript动态填充直接抓取到的源码里可能只有空div和加载动画。无头浏览器会等页面真正渲染完成包括执行脚本、加载异步数据、触发页面内部的各种初始化逻辑。这一步就规避了动态渲染带来的最大麻烦。你可以这么理解普通抓取像隔着毛玻璃看屋里有什么能看到轮廓但看不清楚无头浏览器渲染则像是推门进去把房间里的所有家具都看一遍。当然代价是速度变慢、资源占用更高这也是为什么caveman更适合对质量要求高的精处理场景而不是全互联网无差别海量抓取。2.2 第二步用视觉密度找正文而不是用CSS选择器这是caveman最核心的部分。传统抽取算法通常会去做一些启发式规则比如优先选择包含大量标签的容器或者匹配某个文章节点的CSS路径。这在结构稳定的老式博客上很好用但在布局复杂、样式多变的网站上很容易翻车。caveman的思路是“视觉优先”先把渲染出来的页面切成一个个视觉块然后根据文本密度、区块位置、内容连续性等信息模拟人眼的注意力分布判断哪一块才是真正的正文区域剔除导航、广告、推荐列表这些干扰元素。从工程实现的角度看这套思路就是“Context-Aided Visual Extraction”的含义所在。它把“位置”和“内容特征”结合在了一起。简单来说正文区块通常有较高的文本密度、连续的行内元素、较少的外链而侧栏和页眉页脚则是链接密集、文本碎片化。caveman通过综合分析这些特征而不是依赖某一个固定规则来划定正文范围。实测下来它对那种“正文埋在多个嵌套容器里”的复杂页面容错率明显高过传统方案。2.3 第三步结构化输出把HTML还原成Markdown识别出正文之后下一步是把HTML的语义结构转换成Markdown的对应结构。这一步看起来简单实际上坑很多。HTML里的h1到h6要映射成Markdown的#到######要转成围栏式代码块表格要重建管道对齐语法图片要处理相对路径和URL。caveman在这一步做得比较仔细尤其是代码块的识别和保留对技术文档类页面非常友好。还有一点值得提如果一个页面里包含评论区或“相关文章”区块这些部分很容易被视觉解析阶段误纳入正文范围。caveman在输出时会有一定程度的过滤策略但如果你对这种边缘情况的要求非常高后期仍需要做样本级别的抽检和规则微调。这个我会在第6节详细说。3. 方案横向对比Trafilatura、Readability、BeautifulSoup与caveman怎么选我整理这套工具的时候参考了目前社区里最常用的几个方案包括老牌的Trafilatura、Readability.js加上手写BeautifulSoup还有caveman。它们各有各的适用场景没有银弹。方案核心原理JS渲染支持输出格式适合场景维护成本BeautifulSoup 手写规则依赖CSS/XPath选择器不支持自定义单一固定站点、结构长期稳定高Readability.js基于DOM结构启发式评分较好文本提取非完整Markdown博客、新闻类文章页中Trafilatura基于文本块密度和元数据启发式不支持文本/XML纯静态新闻、学术页面低caveman无头浏览器渲染 视觉分块支持标准化Markdown动态渲染站点、技术文档、语料精处理低维护在工具内部3.1 四种工具的定位差异从实用角度看这几个工具根本不是同一层的东西。BeautifulSoup是解析库是一个“积木”你需要自己组装出一整套爬虫逻辑Readability是一套启发式抽取算法适合快速从文章页拿到可读文本但对复杂页面和后处理支持有限Trafilatura在批处理元数据时很高效非常适合学术文档和新闻站的大批量处理而caveman靠浏览器渲染和视觉分块对动态页面、现代前端框架上做的处理明显更稳输出的是现成的Markdown省去了二次加工。这里有个容易被忽视的点Trafilatura虽然速度很快但不执行JavaScript遇到Vue、React、Next.js这类动态框架渲染的页面时抓到的可能就是一片空白。而caveman因为有无头浏览器这层“外挂”对这些页面有天然的适应性。支付的时间成本是单页处理速度更慢、资源开销更大所以在数据量规模达到百万级时你也得考虑是不是该把caveman只用在关键页面上而不是全面铺开。3.2 不同场景下的选型建议如果是快速抓一两个静态博客Readability完全够用没必要上caveman。如果是要批量处理几千个新闻页面且结构差异不大Trafilatura的性价比很高。但如果你要建的是RAG知识库内容来自大量技术文档、API参考、项目文档这些页面普遍有动态渲染、代码块密集、目录层级复杂的特点caveman的“先渲染再看再转Markdown”的流程反而是最省心的。我自己的实践结论是把caveman用在“质量敏感”的语料精处理环节把Trafilatura这类轻量工具用在“量大但要求不高”的粗筛环节这样搭配效率最高。4. 实操把caveman跑起来完成一次网页转Markdown说再多原理不如动手跑一遍。下面是我实际用过的一整套流程包括安装、单页转换、批量处理和最后的输出目录设计你照着做基本不会跑偏。4.1 准备工作两种安装方式caveman的一个特点是依赖无头浏览器环境所以最省事的方式是用Docker跑避免在本机装一堆浏览器依赖。我的建议是如果你的机器已经有Docker直接用镜像方式docker pull ghcr.io/yqnn/caveman:latest docker run --rm \ -v $(pwd):/data \ ghcr.io/yqnn/caveman:latest \ https://example.com/docs/page.html \ -o /data/page.md如果不想用Docker也可以在本地Python环境里安装依赖后运行。以我的经验本地方式更方便调试但首次安装时需要在系统里装好Chrome或Chromium对应版本。需要注意项目版本迭代过程中命令名和参数可能会有调整动手前先看一眼项目README里的Usage部分以那个为准。4.2 单页转换与参数说明单页转换是基本用法命令长这样python caveman.py \ --url https://example.com/docs/getting-started \ --output docs/getting-started.md \ --timeout 30 \ --wait-for network_idle几个参数的作用给新朋友解释一下。--timeout是页面加载超时时间单位是秒。如果页面加载时间超过这个值caveman会放弃本次转换避免进程卡死。--wait-for是等待条件我建议设为network_idle意思是等网络空闲后再开始解析正文这对那些加载后还会偷偷发起一堆请求的页面很有用。如果你不设置--wait-for碰上稍慢的站点大概率会抓到半成品。输出文件如果内容较长caveman会保留页面原有的标题层级并且代码块会以围栏格式原样保存。比如# Getting Started ## Installation bash npm install example-package 这样的输出对LLM来说是相当理想的输入标题能体现层级语义代码块不会被切成碎片表格也会被转换为管道语法。拿这种数据去做RAG切片时能保留更好的上下文。4.3 批量处理用URL清单一口气抓全站单页转完了接下来是批量。我通常的做法是维护一个urls.txt每行一个URL然后写一个简单的循环调用。不过这里有个技巧不要一次性把一千个URL塞给caveman而是分批跑。每批50个左右中间停几秒既能避免本地资源被占满也方便出问题时定位到具体是哪个URL。while read url; do filename$(echo $url | md5sum | cut -d -f1) timeout 60 python caveman.py \ --url $url \ --output docs/$filename.md \ --wait-for network_idle sleep 2 done urls.txt文件名用URL的MD5值是为了避免URL中的斜杠和特殊字符破坏目录结构。这种方式的另外一个好处是天然去重URL不变MD5不变重复更新时直接覆盖同名文件不会产生一堆重复副本。完整跑完后docs/目录下就是你整理好的本地语料库。我习惯把对应的源URL写进生成的Markdown文件头部作为元数据这样后续溯源审计时方便。5. 语料质量与规模化如何保证转出来的Markdown真能用批量转出来的文件不是直接就能用的我踩过不少质量坑。这节聊一聊在规模化处理时怎么从流程上保证数据质量。5.1 投入前的URL清单筛选抓取之前先花时间筛URL是性价比最高的事。不要觉得这一步浪费时间它决定了你后面所有处理环节的信噪比。我一般会在URL清单这一步做三件事去掉明显是标签页、分类页或搜索结果页的链接去掉URL里带#comment、#respond这类锚点的地址如果源站有sitemap.xml优先用站点地图里的内容链接而不是靠爬虫从首页延伸。原因很简单LLM语料要的是信息密度高的页面列表页和分页页即使转换成功也会产生大量低质量噪声。5.2 转换后的质量检查清单这里我整理了一张检查表每次批处理之后我会抽样一部分结果进行快速评估。虽然是抽样但在数据管道里坚持做能拦截绝大多数脏数据。检查项说明检查方式正文是否为空或过短渲染失败最直接的表现统计文件行数少于20行的标记是否混入导航/页脚页眉菜单、版权声明的字样是否出现在正文关键词搜索“home/about/隐私”等代码块是否完整代码块是否被截断语言标注是否存在统计反引号对表格是否变形是否存在大量单行竖线检查管道语法完整度是否残留HTML标签输出特殊字符转义异常正则搜索[a-z]图片是否失效图片URL是否是本地完整路径检查![]()语法抽样比例我通常设在5%左右如果发现问题再针对性地扩展检查范围。看起来好像多了一步工作实际上省下来的时间远比找bug的时间多。5.3 去重与数据指纹语料里最容易被忽视的问题网页转语料还有一个隐蔽的坑同一个内容可能出现在多个URL下或者同一篇文章在不同时间被更新导致重复和近似重复。对于LLM训练语料重复数据会导致模型对特定表述过拟合对于RAG知识库重复片段会稀释检索结果的精度。我处理这个问题时会为每篇生成的Markdown算一个内容指纹最简单的方式是计算正文文本的MD5稍微高级一点的做法是用MinHash一类算法对n-gram集合做相似度估算。在caveman批量处理流程里加指纹非常简单cat docs/*.md | head -c 8192 | md5sum截取前8KB文本算哈希已经能在很大程度上判断内容是否近似。如果两个文件指纹一致我保留更新时间更新的那个删掉旧版本。这一步虽然不起眼但对语料质量的提升立竿见影。6. 实战中踩过的坑与排查实录最后聊聊具体问题。caveman整体上很省心但毕竟要在各种奇怪的网页上工作实战中还是会遇到若干典型问题。我把最常见的情况列出来再分享几个我印象特别深的排查案例。6.1 常见问题速查表症状可能原因快速检查方式建议处理输出文件为空或只有标题页面加载超时手动用浏览器打开URL确认响应增大--timeout设置--wait-for network_idle输出频繁中断目标站点反爬检查是否有验证码或429响应降低并发拉长间隔更换User-Agent总是只拿到首页SPA页面路由问题确认URL是否经过前端路由检查页面是否需要点击或延迟加载数据Markdown里有残留HTML标签页面结构异常搜索div或span定位到具体页面做二次清洗编码乱码源站响应头不规范查看原始HTML的charset手动用--encoding参数指定字符集图片路径是相对路径页面未格式化base URL打开原文地址确认图片归属输出后统一处理图片URL前缀6.2 三个让我印象深刻的排查案例第一个案例是单页应用。当时我有一个开源项目的文档站入口是React应用正文内容全部由前端路由和异步请求加载。直接用URL访问首页只会看到一个骨架屏。caveman默认等待一段时间后开始解析结果我得到了一篇只有文档目录标题、没有正文内容的空壳。后来我把等待条件改成network_idle并调大超时问题才解决。这里的关键认识是对于SPA工具默认渲染策略不一定适配你需要主动告诉它“等所有请求都跑完再动手”。第二个案例是评论区被当成正文抓了进来。有一个教程网站文章末尾挂着很长的用户讨论区。视觉密度极高文本也连续caveman很容易把这些内容识别为正文的一部分。如果你是在构建RAG知识库评论区混入正文会让检索结果变脏。这种情况没有特别自动的处理办法我最后是用一份“区块黑名单”URL模式来过滤并且在后期抽检时对特定网站的页面做了额外的后处理。你如果对某一两个固定源站抓取频繁值得为它们写一点定制清理逻辑。第三个案例是表格转换翻车。有一次批量处理一批API文档里面的参数表格非常多。caveman输出后我抽查发现部分表格变成了只有一列另一列的内容诡异失踪。排查下来是页面本身用了嵌套表格和rowspan布局转换逻辑对这种不规则结构无能为力。最后我在后处理阶段采用了表格修复脚本把单行文本按分隔符重新对齐。对于要求特别高的表格型文档我建议输出后用工具做一次表格结构校验不要默认工具输出的Markdown一定正确。6.3 几条独家避坑经验经历过这么多轮清洗之后我再分享几个大概率别人没提过的细节。第一批量处理时千万别关闭重定向跟随。很多网页在URL后面加了参数后会重定向到新地址如果你生成的文件名用的是URL的MD5重定向后的两个地址可能生成两个不同文件名结果同一个页面被抓两次还看不出重复。我建议文件名基于“最终落地URL”而不是你清单里的原始URL。第二不要把--timeout设得特别满。设满意味着每个超时页面都会让你干等一次一万条URL跑下来光等超时就能耗掉几个小时。我一般设置在10到15秒之间超时的单独存到一个failed.txt跑完后统一处理重试。第三一定要保留源URL在文件头。不论你是用YAML front matter还是HTML注释把源地址和抓取时间写进文件里后续做数据审计、去重、更新时都能省很多事。用caveman的过程其实也是我调整工具观的过程。以前我总想着用一个统一的规则库解决所有页面后来发现越是复杂多变的环境越需要“先理解再抽取”的策略。工具叫caveman反而提醒我回到最本质的问题上去只要结果够干净方法朴素一点没什么不好。如果你正在整理语料、构建知识库不妨把它加进你的流水线里试试尤其是那些动态渲染、结构混乱的页面它会给你不小的惊喜。
返回列表