
做爬虫和内容聚合的人大概率都在同一个地方卡过明明拿到了整页HTML但真正想要的“正文”却淹没在导航、广告、推荐列表和底部版权信息里。正则写了几个小时换一个网站就废用肉眼找节点改版一次就白干。这也是我最初接触article-extractor的原因——一个专门用来从网页里自动提取核心正文的开源库。它做的事很聚焦给你一段杂乱的网页源码它把标题、作者、正文、发布时间、配图拆出来以结构化数据返回给你。这篇文章我会从原理到实战把它的用法、参数、坑和排查思路一次讲透适合正在做爬虫、阅读增强工具、离线存档或者大数据抓取的读者。1. article-extractor 初印象从一团乱麻里抽出正文1.1 这个库到底解决什么问题很多人第一次看到article-extractor就理所当然地以为它是一个“爬虫框架”其实不是。它只负责网页解析和正文提取这一环至于你要不要并发下载、要不要管理Cookie、要不要处理JS渲染那是其他工具的事。它的输入可以是一个URL也可以是一段已经拿到的HTML字符串输出则是结构化的文章信息通常包括标题、正文、作者、发布日期、图片列表等。这种设计思路很符合“Unix哲学”一件事只做好一件事。在你已有的爬虫链路上它就是一个解耦良好的替换层。之前你可能用一堆xpath硬编码去匹配不同站点的正文容器现在只需要调一个统一接口它会在内部根据页面的结构特征自己判断哪一块才是正文。有一个容易混淆的点市面上名字相似的库不少比如Node生态里的article-extractor、Python生态里的newspaper、boilerpy3、readability-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安装过程会自动拉取几个核心依赖包括lxml、requests、html2text等。其中lxml负责把HTML解析成DOM树requests负责拉取URLhtml2text负责把HTML正文转换成干净的Markdown或纯文本。如果你的环境里有OpenSSL版本比较老的问题可能还需要单独升级一下urllib3和certifi否则请求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_urlresp.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( urlhttps://example.com/long-read, languagezh, min_text_length300, link_density_threshold0.35, use_shortest_nodeFalse, keep_article_htmlTrue, include_imagesTrue, )languagezh告诉提取器正文以中文为主。不同语言在标点和停用词上差异较大明确语言能提高断句和段落边界的识别质量。min_text_length300候选节点少于300个字符的直接忽略。新闻站可以调高论坛页面建议调低。link_density_threshold0.35链接文字占整段文本比例超过35%的节点会降权。这个值太高容易把导航区当正文太低会漏掉一些本身自带很多引用链接的文章。use_shortest_nodeFalse某些实现当候选分数接近时会倾向选择更短的节点。对于深度长文把这个参数关掉更偏向保留完整内容。keep_article_htmlTrue返回清洗过的HTML而不是只返回纯文本。有排版需求的场景建议开启。include_imagesTrue提取正文中的图片地址方便做封面图。这些参数不是越多越好关键是你清楚每个参数背后影响的到底是什么。文本密度阈值调节的是算法对“正文最少要多少字”的判断链接密度阈值调节的是对“导航嫌疑”的敏感度。两个参数一起调基本能应付大多数误判问题。4.2 对不同类型站点的调优策略不同类型页面的结构差异非常大我不太建议一套参数走天下。根据我的经验至少要把站点分成三类来调整页面类型典型特征推荐策略新闻/博客文章标题清晰、正文集中、段落多默认参数即可适当提高min_text_length避免抓到摘要区论坛/问答帖子正文分散、楼层结构复杂降低min_text_length关闭use_shortest_node必要时按回复区块二次提取文档/技术手册页面导航列很宽正文有代码块降低链接密度阈值开启Markdown转换让代码块不被当成广告删掉举例来说技术文档站点的侧边栏往往是一个巨大的二级导航列表里面每一行都是链接。这个区域的链接密度极高算法很容易识别并排除。但如果文章本身是“教程列表”正文里也有大量指向其他章节的链接这时候提取器可能会误伤正文。我的做法是先用默认参数跑一遍如果发现正文被截断就把链接密度阈值从0.35提高到0.5左右再跑一遍对比输出文本是否明显变长。4.3 自定义提取规则白名单与黑名单有的库允许传入自定义CSS选择器作为对算法结果的修正。比如你知道某个新闻站的文章正文永远都在div.main-content里那么直接告诉提取器优先考虑这个区域结果会稳定很多result extract( urlhttps://specific-site.com/news, custom_selectordiv.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(encodingutf-8) result extract(html_content, source_urlhttps://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_asciiFalse, indent2))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(urlhttps://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里其实有没出现再考虑渲染。解决方案有三个方向用selenium或playwright渲染后再取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里加入广告节点选择器图片全部丢失图片是懒加载真实地址在>