网页正文提取实战指南:从正则到Readability的完整路线

发布时间:2026/10/20 4:36:17

网页正文提取实战指南:从正则到Readability的完整路线 不管你是做调研、扒资料、写报告还是给大模型喂语料十有八九会撞上同一个需求把网页上的正文干干净净地提取出来。复制粘贴吧页面上全是广告、导航、推荐位用爬虫硬抓吧返回的 HTML 里标签套标签看着就头大。我这两年处理过的页面没有一万也有八千从新闻门户到文档站再到论坛帖文本提取这事其实有章可循。今天把这套从零基础纯手工到写脚本自动化的路子完整盘一遍每个方案都给你能直接用的思路和代码保证看完就能上手。1. 先搞清楚你要的文本到底是什么很多人在第一步就跑偏了。所谓网页文本内容至少能分成三种完全不同的东西纯展示型文本、结构化数据文本、正文主体文本。纯展示型文本就是页面上你能直接看到的所有字包括菜单、广告、页脚、评论区统统算进去。拿来做舆情监控或者全站存档够用但拿来做分析噪音大得能淹死人。结构化数据文本是从 HTML 里抽出来的带语义的内容比如商品的价格、标题、库存状态新闻的发布时间、作者、正文。这类数据的价值在于能直接进数据库能做字段映射。正文主体文本才是大多数人真正想要的去掉所有导航和广告后那篇新闻文章、那个博客帖子、那份技术文档的核心内容。判断标准很简单——这段文字从页面里抠出来之后单独贴到记事本里依然读得通、读得完整那就算是合格的正文提取。还有个经常被忽略的点文本的载体不只有 HTML。有些页面用 JS 渲染HTML 里屁都没有得等浏览器跑完脚本才有内容有些是 PDF 直接嵌在页面里还有不少做了懒加载滚动到哪儿才加载哪儿的内容。后面我会专门讲怎么应付这几类脏数据。2. 正文提取的三种主流技术路线先说个实用结论复杂的页面用不上复杂的技术绝大多数网页用最简单的字符串处理反而最稳。2.1 教科书级的地板方案正则表达式别一听正则就头疼提取正文只需要两个固定套路。第一个套路是剥标签把 HTML 里的标签全删了只留可见字符。第二个套路是描边界找到正文区块的起始和结束标记只取中间那段。先看剥标签import re html open(page.html, encodingutf-8).read() # 去掉 script 和 style 块这些里面全是代码和样式不是正文 html re.sub(r(script|style|noscript)[^]*.*?/\1, , html, flagsre.S | re.I) # 去掉所有其他标签只看文本 text re.sub(r[^], \n, html) # 清理空行和首尾空格 lines [line.strip() for line in text.splitlines() if line.strip()] text \n.join(lines) print(text[:2000])这套代码五分钟内就能写完对付静态页面够用但副作用很明显菜单和广告文本也会被捞进来得靠下面的边界法来圈定范围。边界法的核心思路是找特征串。比如某文档站的文章正文全在div idcontent_body和/div之间那你就可以写start html.find(idcontent_body) end html.find(/div, start) content html[start:end]听起来朴素但很多项目就是这么干的。唯一需要提防的是 HTML 嵌套——你找到的第一个/div很可能只是内层某个子块的结尾导致正文截一半。所以用边界法时必须配合标签配对逻辑或者直接数层级深度否则早晚踩坑。2.2 正规军方案解析库提取正经做项目还是建议用解析库Python 生态里 BeautifulSoup 和 lxml 是双巨头。BeautifulSoup 的优势是容错性极强面对烂到掉渣的 HTML 也能撑住不崩。lxml 则是性能怪兽解析速度比前者快几倍适合批量抓取。实际操作时我一般用 BeautifulSoup 做小规模验证一旦要跑几万个页面立刻切 lxml。核心操作就几步先定位正文容器再把容器里的标签递归遍历最后把文本拼起来。from bs4 import BeautifulSoup soup BeautifulSoup(html, lxml) # 先按 class 或 id 找容器找不到就退回 body container soup.find(article) or soup.find(div, class_content) or soup.body for tag in container.find_all([script, style, iframe]): tag.decompose() # 所有文本节点直接提取段落之间用换行隔开 paragraphs container.find_all([p, h1, h2, h3, li, blockquote]) text \n.join(p.get_text(stripTrue) for p in paragraphs) print(text)用解析库有个小讲究不要对全页get_text()一定要先缩容器。全页提取会把页头和页脚的版权信息全部带进来后面清洗成本比提取本身还高。2.3 无脑省力方案现成算法库如果你不想自己琢磨选择器甚至有专门干这件事的开源算法。这里面最有名的就是 python-readability它是火狐阅读模式的算法实现。用法非常简单from readability import Document doc Document(html) content doc.summary() # 返回提取后的 HTML 片段 title doc.short_title()它背后跑的是语义打分算法给每个节点按文本密度、标签类型、链接占比打分得分高的块判定为正文。做得好的地方在于它会自动剔除版权声明、页脚、分享按钮这类权重低的区域。实测对于博客类和新闻类页面准确率在八成以上剩下的靠微调options参数或者叠加正则兜底。如果追求更高的准确率还有一个方向是用训练好的模型做网页主体抽取。这类项目有很多泰语丹麦语中文的效果都还不错但部署门槛明显高一档得装依赖、跑模型只有量级大到人工清洗扛不住的时候我才会考虑动用这招。3. 动手实操一步步实现一个文本提取脚本光说不练假把式。下面我带你搭一个真正能跑的脚本目标是把任意新闻页的标题、发布时间、正文一次性提出来。3.1 准备阶段拿到干净的 HTML 源码第一步不是写正则而是拿 HTML。很多人上来就requests.get(url)然后发现拿到的 HTML 里什么都没有——因为目标站是 JS 渲染的。判断方法很简单看返回的 HTML 里有没有正文内容没有就说明得换方案。静态页面直接上神器 requests 就行import requests headers { User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0 Safari/537.36 } resp requests.get(url, headersheaders, timeout10) resp.encoding resp.apparent_encoding # 让 requests 自动猜编码 html resp.textresp.apparent_encoding这一行特别容易被忽略但没有它中文网页大概率乱码。requests 默认按 HTTP 头里的 charset 解码很多老站根本不写这个头或者写错必须按页面内容检测。遇到 JS 渲染的页面常规手段是上浏览器自动化工具让它等脚本跑完再抓内容。我用得最多的是 Playwright它比老式的 Selenium 更轻、更快而且对现代框架支持更好。from playwright.sync_api import sync_playwright with sync_playwright() as p: browser p.chromium.launch() page browser.new_page() page.goto(url, wait_untilnetworkidle) # 等所有请求结束 html page.content() browser.close()这里有个参数坑wait_untilnetworkidle建议配合超时时间一起用因为有些页面有轮询接口可能永远不会空闲要设置timeout15000强制返回。不要把自动化的浏览器当普通爬虫使它是最后手段能静态就不动浏览器否则采集速度会慢一个数量级。3.2 提取阶段结合固定特征与算法兜底拿到 HTML 之后别直接调用现成库先把固定特征找出来。比如很多新闻网站的正文容器 class 都带article-content或者post_body这种字样页面结构几十年不换。用固定的选择器提一次再挂一层算法库做兜底是效率和鲁棒性的最佳平衡点。一个参考实现from bs4 import BeautifulSoup from readability import Document def extract(url): # 第一步拉 HTML前面写的代码略 html fetch(url) # 第二步尝试固定选择器 soup BeautifulSoup(html, lxml) node ( soup.find(article) or soup.find(div, class_lambda c: c and content in c) or soup.find(div, idlambda v: v and content in v) ) if node: for tag in node.find_all([script, style]): tag.decompose() return node.get_text(\n, stripTrue) # 第三步固定选择器失效交给算法库兜底 doc Document(html) body BeautifulSoup(doc.summary(), lxml).get_text(\n, stripTrue) return body这套组合拳的好处是既保住了那八成结构稳定的站点的提取准确性又不会因为改版导致全盘崩溃。定期巡检目标站的结构把失效的容器选择器换掉就能保持长期稳定。3.3 收尾阶段清洗噪声文本提取出来后文本还是可能有杂质。最常见的是这几类版权与备案信息通常含Copyright版权所有ICP备字样直接按关键词过滤。图片链接和 Alt 文本alt 是用来描述图片的文字不属于正文得剔除。社交分享按钮文本比如分享到微信新浪微博这种按钮名会顺着get_text混进来。重复段落和空行有些页面的正文渲染两次清洗时做一次去重合并。清洗时我习惯写一个黑名单文件按行扫描过滤noise_keywords [版权所有, Copyright, ICP备, 分享到, 点击查看, 阅读原文] def clean(text): lines [] for line in text.splitlines(): if not line.strip(): continue if any(k.lower() in line.lower() for k in noise_keywords): continue if line not in lines: # 连续重复 lines.append(line) return \n.join(lines)别小看这步。前一阵帮朋友爬一份行业报告页面正文总共两千字结果提取出来的文本里混了三百多字的分享按钮和推荐位清洗前后差异巨大。所以别急着进入下游先花十分钟确认输出干净了。4. 高频疑难场景动态页面、登录墙与反爬4.1 动态页面怎么处理判断一个页面是动态还是静态有一个笨办法直接用curl拉源码搜索页面里该出现的正文关键词。搜得到就是静态搜不到就是动态。动态页面分两种。一种是有接口的页面首次加载时会用 AJAX 请求 JSON再渲染成界面。这种情况下直接抓接口而不是页面。怎么找接口打开浏览器的开发者工具切到 Network 面板刷新页面过滤 XHR/Fetch 请求找到返回里带正文文字的那条那往往就是数据源直接请求它拿到的还是干净的 JSON比解析 HTML 舒服太多。另一种动态页面是纯客户端渲染的比如某些博客用前端框架搭的站HTML 里只有一堆空白div完全靠 JS 跑数据。这种没辙老老实实上 Playwright 或者 Selenium等渲染完成再取页面内容。另外记得设置无头模式时加上参数headlessTrue不然还得装桌面环境。4.2 登录墙怎么处理遇到必须登录才能看正文的站点优先看有没有免登录的移动版页面或者 RSS 订阅源。很多 PC 版页面加密、移动版却免费这种属于站方自己开了口子绕过去不仅不违规还更省资源。实在绕不过去就在自动化浏览器里提前登录把登录后的 Cookie 保存下来后续请求直接带上。# 保存 Cookie 示例 import json cookies page.context.cookies() with open(cookies.json, w) as f: json.dump(cookies, f) # 后续通过 context.add_cookies() 恢复登录态这里有个提醒很多登录态的 Cookie 有有效期并且服务端会校验操作频次。拿到的账号和 Cookie 别在几秒钟内大量请求否则 IP 会被站点标记用户也会受影响。我在做采集任务时单账号并发一般控制在每秒一到两个请求再大就得换多账号轮换。4.3 反爬机制怎么应对反爬这东西做提取脚本的人躲不开。最常见的三种请求频率异常封 IP、缺少浏览器头标识被识别、JS 加密生成动态令牌。基础应对是改 User-Agent、加 Referer、伪装成普通浏览器访问。进阶一点的是用代理 IP 池但质量参差不齐很多代理比原 IP 更容易被封。我一般建议的顺序是先调慢速率、伪装请求头解决八成问题再考虑用浏览器自动化模拟真人操作最后才上代理池。绝大多数内容站的采集难度远没有传说中那么夸张别一上来就堆重型武器。5. 提取结果的后续处理去重、转格式与文本切分5.1 文本去重不只是删重复行爬完一批页面你会发现内容高度重合同一篇新闻被转载了十次虽然域名不同、页面不同正文却一字不差。这时候如果直接拿去用语料冗余度极高训练出来的模型也容易被带偏。逐字逐句比对不现实比较常用的做法是计算文本指纹。思路是取正文前若干字符加上标题凑一个字符串做哈希哈希相同就视为重复内容保留第一篇。import hashlib def fingerprint(title, content): # 取正文前500字作为指纹基础标题权重更高 sample title content[:500] return hashlib.md5(sample.encode(utf-8)).hexdigest()更精细一点的可以把正文切词后按词频抽关键词做指纹对付某些只改标题不改正文的转载站效果明显更好。5.2 转格式从纯文本到结构化文件提取出来的文本大多被存成.txt但不能下载后直接交付。如果你打算做后续分析建议一步到位存成 JSON 或 Markdown附带元信息来源链接、抓取时间、标题、作者、正文、字数。存 JSON 方便程序读取存 Markdown 方便人阅读和二次编辑。如果你处理的是日语、多语言混合的内容记得统一编码为 UTF-8并且写文件时指定encodingutf-8。这一点 Windows 上特别容易踩默认用gbk写文件然后你写出来的数据在别的机器上打开全变乱码一脸懵。5.3 文本切分给大模型喂料的准备工作如果你是为了给大模型做长文输入这一步非常关键。模型有上下文长度限制几百 K 的长文不可能整个塞进去需要先切块。切块粒度取决于下游任务。做摘要一般按标题语义切每个二级标题下一段话为一组做问答检索按固定长度切重叠区设十几到几十个字避免句子横切切断。def chunk_text(text, size500, overlap50): result [] start 0 while start len(text): end min(start size, len(text)) result.append(text[start:end]) start end - overlap if end len(text) else len(text) return result这样切完之后每块基本保持语义完整扔给下游任务不会断章取义。实际运行时可以按文本总长度调节 size 和 overlap没有绝对的标准值多跑几次看看感觉。6. 实操总结与常见错误提醒网页文本提取这事儿技术难点不在工具而在理解目标页面的结构规律。正则、BeautifulSoup、Readability这三板斧解决九成需求剩下的一成是靠耐心和边界情况处理能力。几个印象深刻的坑这里一次性说出来一是别对全页面用find_all(p)。有些页面的导航栏也用p标签全页提取会把导航文字当正文清洗起来极烦。正确做法是先定位正文容器再在容器范围内做标签搜索。二是别天真地以为 BeautifulSoup 什么烂 HTML 都能修。确实它能容忍不少不规范写法但遇到文件里夹杂大量 CDATA 块或者特殊转义字符串解析结果照样稀疏错乱。这时候要退回 lxml 的html.fromstring或者干脆先做一轮字符串预处理把特殊标记替换掉。三是批量提取必须要有失败收集机制。爬一千个页面总有几个超时、几个结构不同、几个返回 403。脚本里必须把失败的页面记录下来存到单独的失败队列里跑完集中排查。否则一次任务悄悄丢了几十页数据下游分析还在傻傻做着你根本察觉不到。实际项目里我习惯让脚本每处理一百个页面打一行日志方便全局监控。还有一个小技巧是把提取逻辑单独搁到一个模块里主流程只管调用不做页面结构判断。这样哪天某个站改版了只改提取模块不用动整个工程。不同领域、不同目标站提取策略可能截然不同。但核心思想终身受用先理解页面、再选工具、最后才写代码。多花十分钟在观察上省下的是数小时的调试。这行的经验全都是一次次踩坑攒出来的。
延伸阅读

更多相关文章

2026/10/20 4:31:17

快慢指针找环全解析:从数学推导到代码实现

搞过链表算法的人,一定绕不开“快慢指针找环”这个经典题。我第一次理解“重置快/慢指针”这套操作时,确实觉得它有点魔法:两个指针明明已经在环里相遇了,把其中一个放回链表头部,然后都改成一步一步走,再相…

2026/10/20 4:31:17

无人机岔路口车辆巡检数据集:YOLO格式目标检测与落地实践

最近打开数据集分享页,看到这样的标题:"无人机岔路口车辆巡检数据集 城市交通流监测识别 自动驾驶车辆感知检测 低空航拍目标识别 交通违章识别 无人机数据集YOLO第10560期"。如果只看前半段,这像是一套定位很清楚的航拍交通数据&a…

2026/10/20 4:31:17

rea技术解析:React状态响应、实时事件分析与资源弹性分配实战

1. 从“rea”这个标题说起:一个被低估的缩写背后藏着什么第一次看到“rea”这个标题的时候,我脑子里蹦出来的第一反应是——这大概率又是一个被缩写玩坏了的项目名。做技术的人都有个通病,喜欢把什么都缩成三四个字母,仿佛名字越短…

2026/10/20 7:56:34

基于SpringBoot的保险保单分析系统开题答辩全流程指南

上个月我走完了一场开题答辩,题目是《基于SpringBoot的保险保单分析系统设计与实现》。从收到开题通知到答辩结束,前后准备了小一个月,中间踩了不少坑。今天把完整过程整理出来——标题里说的“全过程”不只是现场那十几分钟,而是…

2026/10/20 7:56:34

PHP+Excel通用课表查询系统v3.8:部署、查询逻辑与性能优化

简介:这份资源是面向Web开发初学者、计算机专业学生及毕业设计实践者的PHP课表查询系统源码包,基于PHP与Excel文件格式构建,用于高效管理和查询课程表信息,适用于学校、培训机构或个人学习计划管理场景。压缩包共19个文件&#xf…

2026/10/20 7:56:34

Oracle ASM命令行添加聚合磁盘实战:从语法到重平衡验证

干这行十来年,ASM磁盘组的管理几乎每周都会碰到。今天想聊聊“Oracle ASM 命令行方式添加聚合磁盘”这件事。很多人看到“聚合磁盘”会愣一下,因为Oracle官方文档里其实没有这个标准名词,它更多是实践里的说法:把多块物理裸盘交给…

2026/10/20 7:56:34

单目结构光三维重建:Matlab实现互补格雷码与多频外差相位展开

做结构光三维重建的,手里基本都攒过一套“投影仪相机”的实验台。这套代码就是把台上最核心的几个环节——条纹编码、图像采集、相位解算、相位展开、三角化重建——全部用Matlab串起来。项目同时实现了互补格雷码加四步相移、多频外差加四步相移两条相位展开路线&a…

2026/10/20 7:51:34

机器学习复习地图:从损失函数到评估指标的系统梳理

简介:《唐宇迪-机器学习课程资料》是一套面向机器学习初学者与进阶者的综合学习包,内容覆盖Python编程基础、常用库实战与主流算法理论,可帮助学习者从零搭建知识体系并掌握实际建模能力。整个资源以RAR压缩包形式发布,大小66.11M…

2026/10/19 18:06:35

金融AI落地指南:从调研报告到技术选型与工程实践

简介:这份PDF报告聚焦2025年金融服务业中国AI现状与趋势,面向金融科技从业者、AI战略规划人员及关注行业数字化转型的研究者,帮助读者把握生成式AI在银行、支付、交易等场景中的落地路径与投资方向。资源包内含1个PDF文件,大小约1…

2026/10/20 1:05:37

单片机C运行时从启动到多任务:中断安全与共享资源冲突

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/20 1:05:37

GCC编译四步详解:预处理、编译、汇编、链接实战

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/20 1:05:37

FIW线是什么?FEP注塑线与普通氟塑料线的区别及应用指南

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

还想了解更多?直接咨询顾问

免费诊断 + 免费方案 + 透明报价。

全国咨询热线400-8866-253
免费获取方案
☎咨询二维码 ☎ ↑