news 2026/10/20 4:36:17

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

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
网页正文提取实战指南:从正则到Readability的完整路线

不管你是做调研、扒资料、写报告,还是给大模型喂语料,十有八九会撞上同一个需求:把网页上的正文干干净净地提取出来。复制粘贴吧,页面上全是广告、导航、推荐位;用爬虫硬抓吧,返回的 HTML 里标签套标签,看着就头大。

我这两年处理过的页面没有一万也有八千,从新闻门户到文档站再到论坛帖,文本提取这事其实有章可循。今天把这套从"零基础纯手工"到"写脚本自动化"的路子完整盘一遍,每个方案都给你能直接用的思路和代码,保证看完就能上手。

1. 先搞清楚你要的"文本"到底是什么

很多人在第一步就跑偏了。所谓网页文本内容,至少能分成三种完全不同的东西:纯展示型文本、结构化数据文本、正文主体文本。

纯展示型文本,就是页面上你能直接看到的所有字,包括菜单、广告、页脚、评论区,统统算进去。拿来做舆情监控或者全站存档够用,但拿来做分析,噪音大得能淹死人。

结构化数据文本,是从 HTML 里抽出来的带语义的内容,比如商品的价格、标题、库存状态,新闻的发布时间、作者、正文。这类数据的价值在于能直接进数据库,能做字段映射。

正文主体文本,才是大多数人真正想要的:去掉所有导航和广告后,那篇新闻文章、那个博客帖子、那份技术文档的核心内容。判断标准很简单——这段文字从页面里抠出来之后,单独贴到记事本里依然读得通、读得完整,那就算是合格的正文提取。

还有个经常被忽略的点:文本的载体不只有 HTML。有些页面用 JS 渲染,HTML 里屁都没有,得等浏览器跑完脚本才有内容;有些是 PDF 直接嵌在页面里;还有不少做了懒加载,滚动到哪儿才加载哪儿的内容。后面我会专门讲怎么应付这几类脏数据。

2. 正文提取的三种主流技术路线

先说个实用结论:复杂的页面用不上复杂的技术,绝大多数网页用最简单的字符串处理反而最稳。

2.1 教科书级的地板方案:正则表达式

别一听正则就头疼,提取正文只需要两个固定套路。第一个套路是"剥标签",把 HTML 里的标签全删了,只留可见字符。第二个套路是"描边界",找到正文区块的起始和结束标记,只取中间那段。

先看剥标签:

import re html = open('page.html', encoding='utf-8').read() # 去掉 script 和 style 块,这些里面全是代码和样式,不是正文 html = re.sub(r'<(script|style|noscript)[^>]*>.*?</\1>', '', html, flags=re.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 id="content_body">和</div>之间,那你就可以写:

start = html.find('id="content_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(strip=True) 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, headers=headers, timeout=10) resp.encoding = resp.apparent_encoding # 让 requests 自动猜编码 html = resp.text

resp.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_until='networkidle') # 等所有请求结束 html = page.content() browser.close()

这里有个参数坑:wait_until='networkidle'建议配合超时时间一起用,因为有些页面有轮询接口,可能永远不会空闲,要设置timeout=15000强制返回。不要把自动化的浏览器当普通爬虫使,它是最后手段,能静态就不动浏览器,否则采集速度会慢一个数量级。

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', id=lambda v: v and 'content' in v) ) if node: for tag in node.find_all(['script', 'style']): tag.decompose() return node.get_text('\n', strip=True) # 第三步:固定选择器失效,交给算法库兜底 doc = Document(html) body = BeautifulSoup(doc.summary(), 'lxml').get_text('\n', strip=True) 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,等渲染完成再取页面内容。另外记得设置无头模式时加上参数headless=True,不然还得装桌面环境。

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,并且写文件时指定encoding='utf-8'。这一点 Windows 上特别容易踩:默认用gbk写文件,然后你写出来的数据在别的机器上打开全变乱码,一脸懵。

5.3 文本切分:给大模型喂料的准备工作

如果你是为了给大模型做长文输入,这一步非常关键。模型有上下文长度限制,几百 K 的长文不可能整个塞进去,需要先切块。

切块粒度取决于下游任务。做摘要,一般按标题语义切,每个二级标题下一段话为一组;做问答检索,按固定长度切,重叠区设十几到几十个字,避免句子横切切断。

def chunk_text(text, size=500, overlap=50): 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。脚本里必须把失败的页面记录下来,存到单独的失败队列里,跑完集中排查。否则一次任务悄悄丢了几十页数据,下游分析还在傻傻做着,你根本察觉不到。实际项目里我习惯让脚本每处理一百个页面打一行日志,方便全局监控。

还有一个小技巧是把提取逻辑单独搁到一个模块里,主流程只管调用,不做页面结构判断。这样哪天某个站改版了,只改提取模块,不用动整个工程。

不同领域、不同目标站,提取策略可能截然不同。但核心思想终身受用:先理解页面、再选工具、最后才写代码。多花十分钟在观察上,省下的是数小时的调试。这行的经验,全都是一次次踩坑攒出来的。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/20 4:35:58

消息队列选型:Redis、RabbitMQ与Kafka的权衡与实战

这个消息队列选型&#xff0c;我纠结了三个月&#xff0c;最后被一张图点醒很多人一提到消息队列&#xff0c;第一反应就是Kafka&#xff0c;好像不用Kafka就显得架构不够高级。但如果你真的经历过一个业务系统因为选了不合适的消息中间件&#xff0c;在流量高峰期被折腾得欲仙…

作者头像 李华
网站建设 2026/10/20 4:35:52

IP协议从入门到排障:地址、子网掩码、路由与配套技术全解析

1. 从一个让人抓狂的排障现场说起上周帮朋友处理一个内网服务访问异常的问题&#xff0c;现象很典型&#xff1a;一台新上的应用服务器能ping通网关&#xff0c;能访问同网段的其他机器&#xff0c;但就是连不上跨网段的一台数据库。查了防火墙、查了端口、查了服务状态&#x…

作者头像 李华
网站建设 2026/10/20 4:35:40

context-mode:用一份YAML配置,彻底解决多项目上下文切换与状态混乱

如果你同时开着十几个项目标签页&#xff0c;经常在终端里从 A 仓库切到 B 仓库&#xff0c;还要记得重新加载环境变量、切换工作目录、换一套代码风格配置&#xff0c;甚至得手动把项目背景贴给 AI 对话工具&#xff0c;那你大概率也会有同样的感觉&#xff1a;真正浪费时间的…

作者头像 李华
网站建设 2026/10/20 4:35:38

开源AI全家桶实测:图像修复到音频分离,替代正版还早

最近这几天&#xff0c;我的设计群和AI工具群几乎被同一条消息刷屏&#xff1a;有人用开源模型拼出了一套“AI版Adobe全家桶”&#xff0c;从图像修复到视频剪辑再到音频处理&#xff0c;每样都有对应的功能模块&#xff0c;而且完全免费。视频演示里的效果确实惊艳&#xff0c…

作者头像 李华
网站建设 2026/10/20 4:35:38

Image-to-WebDev Arena评测机制与Opus 5.5登顶解析

最近更新的Image-to-WebDev Arena榜单在圈子里的讨论度比我预想的高很多&#xff0c;核心原因是用了新模型家族里一个叫Opus 5.5 (Max)的版本以1749分登顶&#xff0c;直接把原来榜单前排的模型都压了一头。我们团队刚好一直在做设计稿到网页原型的自动化&#xff0c;对这个竞技…

作者头像 李华
网站建设 2026/10/20 4:35:26

深度学习行人口罩佩戴检测实战:基于YOLOv8的完整毕设项目实现

毕业设计项目&#xff1a;深度学习行人口罩佩戴检测做毕设的时候选了这个方向的同学&#xff0c;十有八九是看中了它“既落地又容易出效果”。行人口罩佩戴检测&#xff0c;本质上是一个目标检测任务&#xff0c;属于计算机视觉里最成熟、上手路径最清晰的方向之一。它不需要你…

作者头像 李华