HtmlAgilityPack 提取正文需先清理 DOM:移除 script、style、comment 节点,再用 XPath 精确选取 body 内非脚本/样式的文本节点,过滤空白与超短文本,归一化空白符并显式指定 UTF-8 编码。

用 HtmlAgilityPack 提取正文文本(C#)
直接调用 DocumentNode.InnerText 会把所有标签、脚本、样式、注释里的文本全塞进来,包括 <script> 里的一串 JSON、<style> 里的 CSS 规则,甚至注释里的调试说明——这不是“正文”,是噪音。
真正可用的正文提取,得先清理 DOM 树再取文本。推荐做法是:
- 用
SelectNodes("//body//text()[not(ancestor::script) and not(ancestor::style) and not(ancestor::comment())]")精确选中 body 内非脚本/样式的纯文本节点 - 过滤掉空白行和超短文本(比如单个标点或空格),用
node.InnerText.Trim().Length > 2做基本清洗 - 对剩余文本做归一化:把连续空白符(含换行、制表)替换成单个空格,再
Trim() - 注意编码:加载 HTML 时显式指定
Encoding.UTF8,否则中文可能乱码
htmlspecialchars 不是用来提取文本的,别误用
有人看到 PHP 里有 htmlspecialchars 就以为它能“提取”文本,其实它只是转义——把 <p>Hello</p> 变成 <p>Hello</p>,输出仍是 HTML 字符串,不是纯文本内容。
如果你的目标是“从 HTML 源码拿到干净的可读文字”,htmlspecialchars 是上游预处理步骤(比如防止 XSS),不是提取手段。真要提取,PHP 应该用 DOMDocument + getElementsByTagName('body') 或更稳妥的 strip_tags 配合白名单:
立即学习“前端免费学习笔记(深入)”;
将调研报告、方案对比、数据分析、需求梳理、会议纪要、设计说明、代码评审、学习笔记等任务产物输出为可在浏览器直接打开的单文件 HTML,替代 Markdown 作为最终交付物。Markdown 在信息密度、视觉清晰度和可分享性上已跟不上 AI 产物的丰富度,HTML 能同时承载排版、表格、SVG 等。
-
strip_tags($html, ['p', 'br', 'h1', 'h2'])可保留换行和标题结构,但不推荐用于正文提取——它不区分<script>和<p>,只要在白名单外就删光 - 更安全的做法是先用
DOMDocument::loadHTML()解析,再遍历body子节点,跳过script、style、noscript等节点,只收集TEXT_NODE类型内容
为什么正则匹配 <p>.*?</p> 总是失败
HTML 不是正则友好型语言:嵌套、自闭合、属性引号、换行位置都不可控。写 <p>(.+?)</p> 会漏掉没有 <p> 包裹的段落(比如直接用 <div> 或 <section>),也会被跨行或含属性的标签打断(如 <p class="intro">)。
真实页面里正文分散在多个语义容器中,可靠路径是:
- 优先找
<article>或<main>——现代网页通常把正文放这里 - fallback 到
<div id="content">、<div class="post-body">这类常见 class/id(需提前配置白名单) - 绝对避免依赖标签名顺序或固定层级,比如假设“第三个
<p>就是正文第一段”——爬虫一换模板就崩
提取后仍需人工校验的三个硬伤
再好的解析器也解决不了语义层面的歧义。以下情况必须加规则干预或人工复核:
- 广告文案混在正文流里(如微信公众号文末“关注我们”卡片)——需基于 CSS class 或 XPath 位置排除
- 表格内文本被拉成一行(
table→tr→td的文本连在一起)——需要识别表格结构并加换行/制表符 - JavaScript 渲染的动态内容(如评论区、无限滚动加载的段落)——
HtmlAgilityPack和DOMDocument都拿不到,必须换 Puppeteer 或 Playwright
正文提取不是“解析完就完事”的环节,而是数据清洗链路的第一关;没做上下文过滤的提取结果,大概率带着噪声进下游 NLP 或训练流程。


















