news 2026/9/11 16:18:16

网页正文提取原理与实战:article-extractor用法、调参与踩坑指南

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
网页正文提取原理与实战:article-extractor用法、调参与踩坑指南

做爬虫和内容聚合的人,大概率都在同一个地方卡过:明明拿到了整页HTML,但真正想要的“正文”却淹没在导航、广告、推荐列表和底部版权信息里。正则写了几个小时,换一个网站就废;用肉眼找节点,改版一次就白干。这也是我最初接触article-extractor的原因——一个专门用来从网页里自动提取核心正文的开源库。它做的事很聚焦:给你一段杂乱的网页源码,它把标题、作者、正文、发布时间、配图拆出来,以结构化数据返回给你。这篇文章我会从原理到实战,把它的用法、参数、坑和排查思路一次讲透,适合正在做爬虫、阅读增强工具、离线存档或者大数据抓取的读者。

1. article-extractor 初印象:从一团乱麻里抽出正文

1.1 这个库到底解决什么问题

很多人第一次看到article-extractor就理所当然地以为它是一个“爬虫框架”,其实不是。它只负责网页解析和正文提取这一环,至于你要不要并发下载、要不要管理Cookie、要不要处理JS渲染,那是其他工具的事。它的输入可以是一个URL,也可以是一段已经拿到的HTML字符串;输出则是结构化的文章信息,通常包括标题、正文、作者、发布日期、图片列表等。

这种设计思路很符合“Unix哲学”:一件事只做好一件事。在你已有的爬虫链路上,它就是一个解耦良好的替换层。之前你可能用一堆xpath硬编码去匹配不同站点的正文容器,现在只需要调一个统一接口,它会在内部根据页面的结构特征自己判断哪一块才是正文。

有一个容易混淆的点:市面上名字相似的库不少,比如Node生态里的article-extractor、Python生态里的newspaperboilerpy3readability-lxml。它们的目标几乎一致,但实现与侧重各有不同。这篇文章里讲到的article-extractor更强调轻量、易集成和跨语言,核心思路来自经典的“正文提取算法”,所以哪怕你最后不用这个库,只要理解了它背后的逻辑,迁移到其他同类工具也不会有障碍。

1.2 哪些场景会主动用到它

先说几个我真实接触过的使用场景,方便你对号入座:

  • 内容型爬虫:抓取新闻、博客、公众号文章,把网页正文转为干净文本后入库做搜索或者数据分析。
  • 阅读模式增强:类似浏览器里的“阅读模式”功能,把冗长页面清洗成适合阅读的排版。
  • 离线存档与分享:收藏文章时只保存正文和关键元数据,避免整个页面里外都是追踪脚本和无用标签。
  • 自然语言处理前的预处理:豆瓣短评、新闻语料、知乎回答等内容类数据,在清洗阶段用正文提取能大幅减少噪声。
  • 知识库构建:把分散在不同网站的技术文档、产品公告提取成统一格式,方便后续检索。

这些场景的共同特点是:面对的不是某一个固定网站,而是大量来源各异的页面。这时候硬编码选择器根本维护不过来,必须依赖算法层面的“结构通用性”。

2. 核心技术原理:它怎么知道哪块是正文

2.1 不是AI,而是“结构密度”的胜利

我第一次用这类工具时也产生过同样的疑惑:它怎么知道这一段是正文,而不是侧边栏的推荐文章?它是不是接了什么大模型?实际情况恰恰相反,多数传统提取算法用的都是人工设计的规则,核心思想可以理解成“看密度”。

整个页面被解析成DOM树后,每个节点都相当于一个候选容器。算法会为这些候选容器打分,分数跟几个信号挂钩:这段文字的文本长度、标点符号数量、段落标签数量、链接文字占这段文字的比例、是否处在页面中间位置。最像正文的容器,通常满足“文字够多、标点够密、链接够少、位置够居中”这些条件。

可以打一个比方:你把一堆人关在一个大厅里,有人在高声演讲,有人在四处走动发传单。演讲者周围聚集的人多且安静,发传单的人周围人群流动却没什么人停下来。正文就是这个“高声演讲的人”,导航和广告就是“发传单的人”。算法做的,就是在浏览器渲染出的HTML结构里找出那个被“持续阅读响应”包围的区域,也就是文字集中且交互元素稀少的节点。

2.2 关键信号:文本密度与链接密度

文本密度很容易理解:一段区域里的汉字或单词数量越多,它是正文的可能性越高。但单看文本长度不够,因为有些页面的“免责声明”和“版权信息”也是一大段字。这时候第二个信号更关键——链接密度。

链接密度指的是节点内部链接文字占所有文字的比例。正文通常是纯文字叙事,链接很少;而导航栏、标签云、相关推荐这类区域,几乎每句话都是一个超链接。所以算法会把链接密度高的候选节点直接降权甚至排除。这也是为什么很多提取工具对“评论区”的识别不太稳定——一条长评论往往文字很多、链接很少,结构上和正文太像。

除了这两个基础信号,一些实现还会加入标题标签的语义判断。比如页面里有<h1>标签,且这个<h1>的内容与<title>高度重合,那基本可以确定这就是文章标题;紧接着标题下面的第一个大段节点,大量情况下就是正文开头。这种规则补充了纯统计学方法的不足,让提取结果更符合人的阅读直觉。

2.3 清洗与后处理:把杂物踢出去

就算找到了正文容器,里面也未必干净。很多CMS模板会把“分享到微信”“本文章关键词”“相关阅读”这类东西直接塞进正文标签里。所以提取器通常还会有一步清洗流程:删除隐藏元素、剔除空白节点、去掉额外的div嵌套、合并零散的段落。

这一步对输出质量影响巨大。我见过不少裸写正则的爬虫,正则是能匹配到“正文区”,但截取出来的文本一行是正文、一行是分享按钮文字,噪音多到没法用。article-extractor内部做了标准化处理,输出的正文会尽量合并成连续段落,并把脚注、广告、脚本之类的东西挡在门外。

后处理还包括编码归一化和实体解码。网页里常见的&amp;&#39;这类HTML实体,如果不处理,最终文本里会出现一堆乱码符号。好的提取器会在输出前统一转成Unicode字符,这也是判断一个库是否成熟的小细节。

3. 快速上手:从安装到提取第一篇正文

3.1 安装与依赖

以Python生态下的一个典型实现为例,安装命令非常简单:

pip install article-extractor

安装过程会自动拉取几个核心依赖,包括lxmlrequestshtml2text等。其中lxml负责把HTML解析成DOM树,requests负责拉取URL,html2text负责把HTML正文转换成干净的Markdown或纯文本。如果你的环境里有OpenSSL版本比较老的问题,可能还需要单独升级一下urllib3certifi,否则请求https链接时容易报SSL证书错误。

如果是在Node.js环境,也可以找到同样名字的npm包,原理类似,底层通常用的是cheerio操作DOM。下面给出的示例以Python API为基准,核心概念在其他语言中完全通用,你只要把接口名调整一下就行。

3.2 最基础的三步调用

第一种用法是直接传URL,让库自己完成下载和解析:

from article_extractor import extract result = extract("https://example.com/tech/how-to-build-a-crawler") print(result.title) print(result.author) print(result.publish_date) print(result.text[:500])

返回的result对象一般都包含以下常用字段:

  • title:清理后的文章标题。
  • author:作者名,没找到时返回None
  • publish_date:发布时间,库内部会尝试解析多种日期格式。
  • text:纯文本正文,适合直接入数据库。
  • html:清洗后的正文HTML,适合保留多媒体格式。
  • images:正文里的图片地址列表。

如果你已经用别的爬虫框架拿到了HTML,不想让它再发一次网络请求,可以直接把HTML字符串传进去:

import requests from article_extractor import extract # 这里爬虫已经处理过登录态和代理,不需要提取器再请求 resp = requests.get("https://example.com/news/123") result = extract(resp.text, source_url=resp.url)

这里有一个非常重要的细节:传HTML时一定要带上source_url参数,除非你完全不需要相对路径转绝对路径。很多页面里的图片和链接都是相对路径,比如/uploads/1.jpg,如果缺少源地址,提取器不知道把https://example.com拼接到前面,最后得到的图片链接就是不可用的半成品。

3.3 命令行模式与输出格式

有时候你不想写代码,只想想快速看一个网页的提取效果。多数实现也提供了命令行工具:

# 把提取结果存成JSON article-extractor https://example.com/tech/article -o article.json # 只在控制台打印正文文本 article-extractor https://example.com/tech/article --format text

命令行输出比较好用的一点是,它会同时打印出候选节点数量、使用的提取耗时、正文长度等信息。调试阶段这些信息价值很高,你能直观看出算法是不是选错了节点。我自己的习惯是,新接入一个站点前先跑一遍命令行模式,把结果大致扫一眼,然后再决定要不要定制参数。

4. 参数与高级配置:把提取调到最稳

4.1 常用参数逐个说

实际使用中,默认参数已经能搞定七八成的网页,但剩下的两成你需要学会调参。下面以我常用的一组合法参数做说明:

result = extract( url="https://example.com/long-read", language="zh", min_text_length=300, link_density_threshold=0.35, use_shortest_node=False, keep_article_html=True, include_images=True, )
  • language="zh":告诉提取器正文以中文为主。不同语言在标点和停用词上差异较大,明确语言能提高断句和段落边界的识别质量。
  • min_text_length=300:候选节点少于300个字符的直接忽略。新闻站可以调高,论坛页面建议调低。
  • link_density_threshold=0.35:链接文字占整段文本比例超过35%的节点会降权。这个值太高容易把导航区当正文,太低会漏掉一些本身自带很多引用链接的文章。
  • use_shortest_node=False:某些实现当候选分数接近时会倾向选择更短的节点。对于深度长文,把这个参数关掉,更偏向保留完整内容。
  • keep_article_html=True:返回清洗过的HTML而不是只返回纯文本。有排版需求的场景建议开启。
  • include_images=True:提取正文中的图片地址,方便做封面图。

这些参数不是越多越好,关键是你清楚每个参数背后影响的到底是什么。文本密度阈值调节的是算法对“正文最少要多少字”的判断;链接密度阈值调节的是对“导航嫌疑”的敏感度。两个参数一起调,基本能应付大多数误判问题。

4.2 对不同类型站点的调优策略

不同类型页面的结构差异非常大,我不太建议一套参数走天下。根据我的经验,至少要把站点分成三类来调整:

页面类型典型特征推荐策略
新闻/博客文章标题清晰、正文集中、段落多默认参数即可,适当提高min_text_length,避免抓到摘要区
论坛/问答帖子正文分散、楼层结构复杂降低min_text_length,关闭use_shortest_node,必要时按回复区块二次提取
文档/技术手册页面导航列很宽,正文有代码块降低链接密度阈值,开启Markdown转换,让代码块不被当成广告删掉

举例来说,技术文档站点的侧边栏往往是一个巨大的二级导航列表,里面每一行都是链接。这个区域的链接密度极高,算法很容易识别并排除。但如果文章本身是“教程列表”,正文里也有大量指向其他章节的链接,这时候提取器可能会误伤正文。我的做法是先用默认参数跑一遍,如果发现正文被截断,就把链接密度阈值从0.35提高到0.5左右,再跑一遍,对比输出文本是否明显变长。

4.3 自定义提取规则:白名单与黑名单

有的库允许传入自定义CSS选择器,作为对算法结果的修正。比如你知道某个新闻站的文章正文永远都在div.main-content里,那么直接告诉提取器优先考虑这个区域,结果会稳定很多:

result = extract( url="https://specific-site.com/news", custom_selector="div.main-content", strip_selectors=[".ad-banner", "#recommend-list", ".footer-copyright"], )

这里custom_selector是白名单,提示算法从指定区域开始向下寻找最佳节点;strip_selectors是黑名单,表示不管算法怎么选,这些区域里的内容都必须删除。

这种“算法优先 + 规则兜底”的组合方式,是我在实际项目中最推荐的做法。纯算法的好处是免维护,坏处是有小概率抽风;纯写死规则的好处是稳定,坏处是每个站点都要单独写一遍。两者结合,既能让通用站点靠算法兜底,也能让核心站点享受规则带来的稳定性。

5. 实操过程实录:用真实网页测试提取效果

5.1 准备一组有代表性的测试页面

理论说完了,我拿一组真实场景来跑一遍。这里为了不涉及具体平台的版权和隐私问题,我用一组模拟页面来演示思路,但结构特征完全模拟了常见站点类型:

测试页页面结构特征期望提取出的正文
模拟新闻站顶部导航 + 多条广告位 + 图文正文新闻正文标题、作者、日期、正文
模拟博客站左侧分类 + 正文 + 底部相关文章博客正文,不包含相关推荐
模拟文档站左侧多级目录 + 正文含代码块正文保留代码块
模拟论坛页楼主楼层 + 多层回复 + 签名区只提取楼主楼层主内容

我写了一个简单脚本,循环读取本地HTML文件,逐项提取并对比人工标注的正文长度:

import json from article_extractor import extract from pathlib import Path test_cases = [ ("news.html", "新闻站"), ("blog.html", "博客站"), ("docs.html", "文档站"), ("forum.html", "论坛页"), ] results = {} for file_name, label in test_cases: html_content = Path(file_name).read_text(encoding="utf-8") result = extract(html_content, source_url="https://test-site.com/path/page") results[label] = { "title": result.title, "text_len": len(result.text), "text_start": result.text[:80].replace("\n", " ") } print(json.dumps(results, ensure_ascii=False, indent=2))

5.2 实际输出与结果点评

跑完的结论很直接:

  • 新闻站提取质量最高。因为新闻页面的语义化标签很规范,正文区域有明显的<article>标签,加上正文文本密度远高于四周,算法几乎没犹豫就选中正确容器。
  • 博客站也不错,但底部“相关文章”偶尔会被带进来一部分。后来检查发现是相关文章区域用了section标签并且内部文本量不低,我把strip_selectors加上.related-posts后解决。
  • 文档站默认参数表现一般,正文里的代码块被干掉了。原因是代码块文字与普通文本的标点特征差异太大,算法当成噪声清掉了。开启Markdown转换以后,代码块能保留下来,但依然需要手动调高min_text_length
  • 论坛页是最难的,默认提取结果抓到了包含所有楼层的大容器。这是结构问题,不是算法缺陷,因为论坛页没有一个天然的“单篇文章”容器。最终我改用自定义选择器指向楼主楼层,再配合黑名单删签名区,才拿到干净结果。

这个结果并不意外,它再次说明了同一件事:通用提取工具擅长做“整页正文识别”,但遇到结构复杂或多楼层页面时,不要指望让它自动理解“只看楼主”这种业务语义。你需要在工具基础上再加一层业务规则。

5.3 提取结果的后处理技巧

提取完成后不要直接用,建议至少要过一道清洗:

import re def clean_text(text: str) -> str: # 去掉空白字符过多的空行 text = re.sub(r"\n{3,}", "\n\n", text) # 去掉常见分享文案 for phrase in ["本文由XX编辑", "扫码分享", "点击关注"]: text = text.replace(phrase, "") # 按标点截断异常长的单行 lines = [line.strip() for line in text.splitlines() if line.strip()] return "\n".join(lines) result = extract(url="https://example.com/post") final_text = clean_text(result.text)

这一步的意义在于,提取器只能保证结构上干净,不能保证业务上干净。比如“阅读原文”“打开App查看”这种文字经常以正文形式出现,但对你未来的分析任务毫无价值。这些规则虽然简单,却能显著提升下游任务的质量。

6. 踩坑记录:那些查了三天才明白的问题

6.1 乱码与编码误判

这是最常见的坑。中文网页有的是UTF-8,有的是GBK,有些老站甚至还在用GB2312。如果直接用底层requests获取内容但没有正确解码,提取器拿到的就是一串乱码,文本密度算法也会因此失效。处理方式是优先依据响应头里的charset,或者在拿到二进制内容后调用apparent_encoding做编码探测,再决定用什么解码。

如果你用的是extract(url)这种一站式接口,库一般会帮你处理编码。但如果你先自己用requests抓HTML再传给extract,就必须在传参前确保解码正确。我踩过一次亏:一边用requests默认的ISO-8859-1解了码,一边又指望提取器能“自动修正”,结果输出全是菱形问号,还以为是库的Bug。

6.2 相对路径图片与链接失效

前面提到过source_url的重要性,这里再展开说一下。很多页面的正文里没有完整的绝对URL,图片是/upload/2024/1.png,链接是../article/123。如果提取器不知道页面原始URL,它就无法拼接。实际项目里,我通常会在提取后自己再做一遍URL补齐:

from urllib.parse import urljoin base_url = result.url or "https://example.com/page" absolute_images = [urljoin(base_url, img) for img in result.images]

这个小步骤看似简单,但少了它,后续做图片下载或者内容展示时会平白多出很多404错误。

6.3 动态渲染页面提取为空

现在越来越多站点是前端JS渲染内容,直接抓HTML时正文根本不在源码里,而是在某个接口里动态返回。article-extractor本质上处理的是静态HTML,拿不到渲染后的DOM,自然提取不出来。

遇到这种情况,我通常会先判断页面是不是真的纯JS渲染:找源码里正文关键词是否出现,出现就说明HTML里其实有;没出现再考虑渲染。解决方案有三个方向:用seleniumplaywright渲染后再取HTML、抓取数据接口手动拼文章、或者换用自带渲染能力的爬虫框架。我不建议在提取库层面硬扛渲染问题,它不该负责这块。

6.4 多页文章只提取到第一页

少数长文会被拆成十几个分页,每页只有几百字。由于min_text_length默认值可能高于单页文字量,算法会认为“这页不够像正文”,导致漏提取。

处理方案是:先用提取器拿到第一页正文,再解析正文末尾的“下一页”链接,循环抓取所有分页,最后拼接成完整文本。这个逻辑比调低min_text_length更可靠,因为调低阈值后误抓到导航区的概率会同步上升。分页拼接时要注意每页的文字开头和结尾可能重复了同一句过渡语,合并前先做一次去重。

6.5 两个正文容器被合并输出

有些页面把正文和评论区放在同一个<article>标签里,提取器会把他们都当成候选节点,输出里正文后面跟着一长串评论。此时没有银弹,最好的方式是用strip_selectors把评论区节点剔除。如果你的提取器不暴露这个参数,就用lxml先行处理,把评论区从DOM里删掉,再传给提取器。

7. 常见问题与排查技巧速查

为了省去你反复翻文档的时间,我把高频问题整理成一张速查表:

现象可能原因快速排查与解决
提取出的标题是空页面没有<title><h1>检查HTML源码,确认标题是否由JS动态写入
正文只有开头一句话候选节点长度不够,被min_text_length拦截调低min_text_length,观察文本长度变化
正文包含大量广告文字广告区与正文容器嵌套过深strip_selectors里加入广告节点选择器
图片全部丢失图片是懒加载,真实地址在>
版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/11 16:16:03

Dolphin Wii频道NAND启动失败:4步定位并修复3类报错

Dolphin Wii频道NAND启动失败&#xff1a;4步定位并修复3类报错 【免费下载链接】dolphin Dolphin is a GameCube / Wii emulator, allowing you to play games for these two platforms on PC with improvements. 项目地址: https://gitcode.com/GitHub_Trending/do/dolphin…

作者头像 李华
网站建设 2026/9/11 16:14:18

GLM-5.3多任务生成可运行程序实战指南

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

作者头像 李华
网站建设 2026/9/11 16:13:24

STM32F4充电桩固件调试与量产级验证指南

简介&#xff1a;本资源是一套基于STM32F4系列微控制器实现的小区级电动车充电桩嵌入式源码工程&#xff0c;面向嵌入式初学者、电力电子方向开发者及智能硬件工程师&#xff0c;解决从硬件驱动到充电控制逻辑落地的实际开发问题。压缩包共123个文件&#xff0c;含56个C源文件与…

作者头像 李华
网站建设 2026/9/11 16:12:35

Vue 3架构革新与Composition API实战解析

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

作者头像 李华
网站建设 2026/9/11 16:12:29

低功耗MCU端侧语音识别:从RT1050到智能穿戴设备

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

作者头像 李华
网站建设 2026/9/11 16:12:15

Typst 安装与配置:10 分钟从零到第一个 PDF

Typst 安装与配置&#xff1a;10 分钟从零到第一个 PDF 【免费下载链接】typst A markup-based typesetting system that is powerful and easy to learn. 项目地址: https://gitcode.com/GitHub_Trending/ty/typst 等排版引擎编译了整整 40 秒&#xff0c;只为了改一个…

作者头像 李华

关于博客

这是一个专注于编程技术分享的极简博客,旨在为开发者提供高质量的技术文章和教程。

订阅更新

输入您的邮箱,获取最新文章更新。

© 2025 极简编程博客. 保留所有权利.