ARTICLE DETAIL

资讯详情

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

网页正文抽取与Markdown转换:大模型内容预处理实战指南

网页正文抽取与Markdown转换:大模型内容预处理实战指南 1. paperclip是什么为什么这个热词值得关注最近在AI应用圈里paperclip这个词出镜率很高。很多人第一次听到会愣一下这不就是回形针吗确实字面意思就是回形针。但在当前语境下它已经被赋予了一个非常实用的技术隐喻。把一张网页“夹”下来整理成干干净净、结构完整的文本再喂给大语言模型——这个动作就像用回形针把散落的纸张夹成一叠方便携带和阅读。我一直觉得这个比喻特别传神。因为大模型消费网页内容的方式和人类完全不一样。人打开一个网页眼睛会自动忽略导航栏、广告位、推荐流、弹窗引导直接落在正文上。但大模型没有这种“视觉注意力”你给它什么它就读什么。如果你把整个HTML原封不动丢给它它会花大量上下文去理解那些样式标签、脚本代码、无意义的class名——不仅浪费token还容易干扰它对内容主题的判断。所以所谓paperclip式处理核心就一句话从杂乱网页里精准提取正文转成Markdown或结构化文本让大模型用最小的上下文读到最核心的信息。这篇分享就是围绕这个目标展开的完整实操方案。我会从技术选型、核心原理、具体代码到高频排障把我实测过的处理管线完完整整拆给你看。适合谁看如果你正在做RAG检索增强生成、本地知识库构建、AI自动摘要、网页内容聚合工具或者只是想让GPT类模型读某个网页后回答得更准这篇都能直接落地参考。不需要你有很深的爬虫功底但我会默认你懂基本的Python和HTML概念。2. 方案设计与技术选型不要一上来就写爬虫很多朋友拿到“网页转文本喂给大模型”这个需求第一反应是写爬虫requests抓HTML正则抽内容再清洗。我不否认这是条路但它的问题在于——网页结构千变万化正则表达式会变成一场无休止的打地鼠游戏。你今天适配了这个博客明天换个新闻站又得重写。所以第一步得先把方案选型想明白。2.1 三条主流路线的对比我在实际项目里评估过三类方案各有各的适用场景。第一类是用开源正文抽取库代表是trafilatura、readability-lxml、newspaper3k。这类库的核心思路是解析DOM树通过文本密度、标签特征、链接比例等信号自动识别正文区域。优点是本地运行、免费、可定制适合批量处理。缺点是对极复杂的动态页面无能为力需要配合渲染工具使用。第二类是用在线转码服务比如有些平台提供URL转Markdown的API。你传一个链接它返回清洗后的Markdown。优点是省事不用自己维护解析逻辑。缺点也很明显付费、有调用频率限制、中文站点适配不一定好而且内容经过第三方服务器敏感场景不能这么干。第三类是自己写解析器用BeautifulSoup配合CSS选择器针对固定站点做精细化抽取。优点是精度最高缺点是需要针对每个站点单独写规则维护成本高。它适合目标站点固定的场景不适合做通用工具。2.2 为什么我优先选trafilatura这类“正文抽取”路线我自己的选择是通用场景用trafilatura做主体搭配readability-lxml做兜底极少数特殊站点才写自研规则。Trafilatura最打动我的一点是它对“正文边界”的判定能力。很多抽取工具只能找到“最可能是正文的节点”但trafilatura会进一步处理标题、段落、列表、表格的关系输出结构相对完整。它内部实现了一套基于文本密度和标签权重的打分机制可以理解为给DOM树上的每个节点算一个“内容分”然后把分数最高的连续区域切成正文。这套机制最典型的成功案例是新闻类和博客类页面。这类页面的特征非常明显正文部分是密集的段落文本链接密度低而导航栏和侧边栏恰好相反链接多、文本碎。算法抓住这个统计学规律就能在绝大多数场景下做出正确判断。生活化点说它不太需要“看懂”页面只需要“感觉”出哪里像正文——就像你逛商场看到哪个区域人流密集、大家都在认真看那大概率是商品区而不是过道。2.3 哪些场景必须自研解析器当然trafilatura不是万能的。我遇到过一个典型的反例某个产品文档站点正文被拆成了几十个折叠面板每个面板只有一两句话。这种页面文本密度极低统计方法完全失效萃取结果东一块西一块。遇到这种站点就认怂直接针对它写一个专用解析器用明确的CSS选择器把每一块内容捞出来再拼装。这也提醒我们工具是帮你解决80%通用场景的剩下20%长尾需求靠的是你自己的灵活补充。3. 核心细节解析正文抽取与Markdown化的关键参数方案定了接下来就是细节。Trafilatura虽然开箱即用但参数调得好不好输出质量天差地别。这一节我把我常用的参数、背后的原理以及容易忽略的坑一个个说清楚。3.1 正文抽取密度打分与区域识别先理解trafilatura的工作原理否则调参就是瞎调。它大致分四步第一步把HTML解析成DOM树。第二步遍历节点计算每个节点的特征值包括文本长度、链接文本占比、标点符号密度、段落标签数量等。第三步根据这些特征给每个节点打分并且用“文本/链接比”做惩罚项——如果一个节点里链接文本太多分数会被大幅压低。第四步找出分数最高的连续区域向上下扩展边界直到遇到明显的分隔特征比如页脚、大片空白或另一个高链接密度区域。这个流程里有一个关键参数值得单独说favor_precision。默认情况下trafilatura追求的是“尽量别漏”所以抽取结果偶尔会把正文底部的“相关阅读”“讨论区”也带进来。打开favor_precisionTrue后算法会更保守宁可少抽一点也要保证抽出来的都是正文。做喂给大模型用的场景我强烈建议打开这个开关。因为少一段内容损失不大但混入一堆推荐链接会让模型对“这篇文章到底在讲什么”产生误判。还有个参数是include_comments默认是False这很正确。评论区对大模型来说价值极低还容易让模型把网友观点当成作者观点必须关掉。3.2 Markdown输出保留结构比“纯文本”更有价值抽取完正文之后输出格式怎么选trafilatura支持txt、csv、json、xml、markdown等多种输出。我实测下来喂给大模型Markdown格式的综合效果最好。原因在结构信息。Markdown的标题层级、列表、表格、加粗都会让模型更容易理解文章的骨架。我用同样的网页分别转成纯文本和Markdown再让模型做摘要Markdown版本的回答在条理性和重点把握上明显更好。打个比方纯文本像一叠没有目录的资料Markdown像带目录和标题层级的手册。模型处理后者时注意力分配会更合理。具体来说我通常这样调用import trafilatura downloaded trafilatura.fetch_url(https://example.com/article) result trafilatura.extract( downloaded, output_formatmarkdown, favor_precisionTrue, include_commentsFalse, include_tablesTrue, include_formattingTrue, )这里include_tablesTrue值得单说。很多人做网页转文本时会忽略表格但技术文档和数据分析类文章里表格往往是信息密度最高的部分。trafilatura会把表格转成Markdown表格语法模型读到的是规整的列对齐格式而不是一串被空格强行分开的数字理解难度完全不一样。3.3 元信息与去噪规则除了正文我还会把元信息单独提取出来。标题、作者、发布时间、站点名称这些信息对模型理解上下文很重要。比如模型回答“这篇文章是什么时候发布的”如果元信息没提取它就答不上来。trafilatura提供extract_metadata接口可以一次性拿到这些字段meta trafilatura.extract_metadata(downloaded) print(meta.title) # 文章标题 print(meta.author) # 作者 print(meta.date) # 发布日期 print(meta.sitename) # 站点名称我需要提醒一个坑有些页面会把发布时间藏在非常隐蔽的标签里或者带时区偏移extract_metadata偶尔会拿到None。我的做法是拿到元信息后做一层校验缺了什么再用正则从原始HTML里二次匹配。别指望一个库把所有脏活干完多一层兜底管线才稳。去噪规则这块我一般会在抽取后追加一轮自定义清洗去掉空行过多的冗余换行、清理掉Markdown里孤立的图片引用如果模型不需要看图、把连续三个以上的换行压缩成一个。这些操作可以用正则做成本极低但能显著提升文本整洁度。4. 实操过程一小时搭一个网页转Markdown处理管线理论部分讲得不少了接下来上真家伙。我带你把一条完整的处理管线从零搭出来。这条管线我在多个项目里复用结构简单但应对日常需求足够了。4.1 环境准备我假设你用的是Python 3.9以上版本。先装依赖pip install trafilatura就这一个库够了剩下都用标准库。有个小提示如果你要处理的页面很多建议顺手装个lxmltrafilatura底层解析时对lxml的依赖会让速度有明显提升。4.2 核心实现代码及参数说明我直接给一段完整的处理函数你复制就能用import trafilatura import re from urllib.parse import urlparse def url_to_markdown(url, min_output_size300): 抓取网页并转为干净Markdown # 第一步抓取 downloaded trafilatura.fetch_url(url) if downloaded is None: raise ValueError(f抓取失败: {url}) # 第二步元信息提取 meta trafilatura.extract_metadata(downloaded) # 第三步正文抽取转Markdown content trafilatura.extract( downloaded, output_formatmarkdown, favor_precisionTrue, include_commentsFalse, include_tablesTrue, include_formattingTrue, ) if content is None or len(content) min_output_size: # 备选方案一降低精度要求重试 content trafilatura.extract( downloaded, output_formatmarkdown, include_commentsFalse, include_tablesTrue, ) if content is None or len(content) 100: raise ValueError(f正文抽取失败或内容过短: {url}) # 第四步后处理清洗冗余换行 content re.sub(r\n{3,}, \n\n, content).strip() # 第五步组装最终文本 parts [] if meta and meta.title: parts.append(f# {meta.title}) if meta and meta.sitename: parts.append(f 来源站点{meta.sitename}) if meta and meta.date: parts.append(f 发布时间{meta.date}) if meta and meta.author: parts.append(f 作者{meta.author}) parts.append() parts.append(content) return \n.join(parts), meta这段代码里我做了几层容错。min_output_size用来过滤抽取失败的场景如果拿到的正文不足300字我会降低精度要求再试一次。为什么是300因为正常文章哪怕再短也至少有几百字的描述性内容。如果抽取结果连100字都不到那基本可以断定这个页面不适合用通用方法处理早点抛异常走人工规则反而高效。有一点你可能注意到了我在组装文本时把元信息放在了正文前面。这样做的目的是让模型在第一眼就看到来源、时间、作者这些背景信息相当于给它一个“阅读前提前”。实测这种排版方式比把元信息塞在正文末尾效果更好。4.3 批量处理与超长内容分块单条管线有了批量需求通常会跟着来。最常见的场景是你有一堆URL要全部转成Markdown入库。这时不要用循环串行抓取太慢了。我给个简单的并发版本from concurrent.futures import ThreadPoolExecutor, as_completed import json urls [ https://example.com/articles/1, https://example.com/articles/2, https://example.com/articles/3, # ... 更多URL ] results {} def safe_process(url): try: content, meta url_to_markdown(url) return url, {content: content, title: meta.title if meta else None} except Exception as e: return url, {error: str(e)} with ThreadPoolExecutor(max_workers8) as executor: futures {executor.submit(safe_process, u): u for u in urls} for future in as_completed(futures): url, data future.result() results[url] data # 保存结果 with open(output.json, w, encodingutf-8) as f: json.dump(results, f, ensure_asciiFalse, indent2)这里线程数取8是个折中。太少了浪费带宽太多了容易被目标站点限流。实际上如果你要抓的站点比较介意爬虫建议把线程数降到3-4并且在请求间加随机延迟做一个有礼貌的采集者。批量处理完还有个躲不开的问题超长内容怎么喂给大模型。文章长了一套上下文窗口放不下必须分块。我用的分块策略是按Markdown的标题层级来切先按##分一块太长再按###分还太长就按段落硬切。为什么优先考虑标题切分因为保持语义完整性对模型理解非常重要在标题处切断每一块还能保持相对独立的话题完整性。硬切段落是最不得已的方案因为一段话被拦腰截断后模型看到的完全是两个无法理解的碎片。分块时建议给每块设一个合理的token上限。按我的经验以4000到6000 token为上限比较合适既能保证信息密度又不会超出大多数模型的上下文窗口。注意token和字符不是一回事中文字符和英文字符的token换算比例差别很大中文大约1个字符接近0.6到1个token具体要看模型的分词器。拿不准就缩保守一点。5. 常见问题与排查技巧实录这条管线我跑了几个月在不同站点上踩过不少坑。挑几个最有代表性的问题和排查思路整理成速查表你遇到类似情况可以少走弯路。5.1 问题速查表现象常见原因排查与解决抽取结果是None页面是SPA内容靠JS动态渲染用Playwright渲染后再传给trafilatura见下文说明抽到的内容只有几十字正文区域被识别错误或页面本身就一个登录框先肉眼打开页面确认再考虑调低min_output_size重试中文乱码requests自动猜测编码失败直接从HTML的charset或http-equiv里拿编码部分站点会在URL参数中带编码信息表格转换后错位原页面的表格嵌套了复杂样式或多级表头接受不完美关键数据手动补或用pandas的read_html做二次处理内容里有大段“相关推荐”页面把推荐流放在正文节点内部打开favor_precisionTrue若无效则用CSS选择器手动裁剪抓取时返回403目标站有反爬策略换带UA的请求头减少并发加随机延迟必要时用代理池抽取结果正常但Markdown里全是孤立的链接页面本身是链接聚合页不是文章判断页面上正文密度极低时改用其他方案如直接提取所有链接标题做列表元信息date是None发布时间藏在JSONLD结构里加一层json.loads解析页面里的application/ldjson脚本5.2 几个值得养成的习惯先说动态渲染。现在有大量站点是用React、Vue这类框架做的首屏HTML只是一个空壳真正的正文要等JS执行完才出现。trafilatura抓到的就是空壳自然抽不出东西。我的处理方式是先用Playwright把页面渲染成完整的HTML再喂给trafilaturafrom playwright.sync_api import sync_playwright def fetch_rendered_html(url): with sync_playwright() as p: browser p.chromium.launch(headlessTrue) page browser.new_page() page.goto(url, wait_untilnetworkidle, timeout30000) html page.content() browser.close() return html注意wait_untilnetworkidle这个参数。它的意思是等网络连接基本稳定后再取页面内容能覆盖大多数异步加载场景。但有些页面有一堆定时轮询接口永远不会真正空闲这时就要设个超时上限别让单个页面把整个队列卡死。第二个习惯是拿到抽取结果后永远先抽样检查再批量入库。不要迷信库的输出一定正确。我自己的流程是先跑10个URL肉眼扫一遍抽取结果确认正文覆盖率和噪声水平都达标再放开到全量。这个习惯救过我很多次尤其是网站改版之后原本正常的抽取可能一夜之间就崩了。第三个习惯是为你的处理管线设计一个“人工覆盖通道”。总有些页面是算法搞不定的。与其反复调参死磕不如把这些URL列入例外名单走人工复制粘贴的流程。你可能会觉得这样不够自动化但实际运行下来的效率和稳定性远高于为1%的页面投入80%的维护精力。关于输入页面的问题还有一些比较偏门的场景比如目标URL是PDF文件。trafilatura只处理HTML对PDF无能为力。我的做法是在抓取前先检查URL后缀或Content-Type发现是PDF就改用pdfplumber或PyMuPDF提取文字。这个判断只需要几行代码却能让你的管线瞬间多覆盖一大类文档型内容。6. 这个热点背后的工程思路还能怎么扩展最后说点我个人的真实体会。围绕“paperclip”这个热词大家看到的可能是一股技术小浪潮但在我看来它真正点破的事情是大模型应用的质量瓶颈很多时候不在模型本身而在喂给模型的数据形态。我们花了很多时间调prompt、选模型却少有人愿意在数据预处理上下笨功夫。但实测下来把一份网页内容从“适合人眼阅读”转成“适合模型消费”的结构化文本对回答质量的提升往往比换一个更大的模型还要明显。这在RAG场景里尤其突出——你喂给它的检索片段如果本身是杂乱的那后续再好的生成能力也难以力挽狂澜。如果你已经在跑类似的管线我建议你往这几个方向继续扩展一是把抽取结果做成带指纹的缓存同URL不去重复抓取节省资源二是对抽取出的Markdown做向量化时把标题和元信息单独编码检索时能按标题过滤三是把这条管线接入定时任务做成自动化的“网页监控摘要推送”。这些都是从“能用”到“好用”的跨越每一步都不复杂但组合起来价值很大。最后再分享一个小技巧给模型喂网页内容时可以在Markdown末尾追加一行“你即将阅读的是一篇来自某站点的文章请忽略一切非正文内容”。这个小提示在少数模型上能起到注意力聚焦的作用成本只是几个token值得一试。这篇分享就到这里。如果你也在折腾类似的内容预处理管线希望这些踩坑经验能让你少走几步弯路。抓取、抽取、清洗、转换——这套工序听起来平淡无奇但它恰好是当前很多AI应用最扎实的地基。
返回列表