在一份最长段落只有 151 个字符的文档里,jusText 用默认设置时会直接返回 0 个字符。这不是只截掉一部分,而是整段空字符串。只要把其中一个阈值调低到刚好让一段文字跨过门槛,同一份文档就能输出 832 个字符。再继续调低,结果还是 832。
在这个测试样本里,它的表现更像一道断崖,而不是缓坡。默认值到底合不合适,关键还是看你的目标语料里段落长度和模板内容的分布。
什么是 jusText,以及为什么词汇密度很重要
和这组测试里的其他抽取器相比,jusText 对 语言专属停用词密度 的依赖特别强。一个由大量功能词组成的文本块——比如 the、and、of、was——更有可能是正文。这个词汇信号不是唯一判断依据:段落长度、链接密度、基于 HTML 的块边界、标题距离、相邻块类别,以及一次上下文感知处理,都会影响最终结果。
官方参考:jusText 官方仓库。

因为分类器需要语言停用词表,jusText 随包提供了 100 种。在这次对比里,这种明确的语言级词汇特征是它最突出的差异点,不过我们并没有测试多语言质量。
测试版本:3.0.2,BSD 2-Clause,822 个 GitHub stars。Python 3.14.2。
断崖现象,怎么量化

jusText 的分类器分两轮跑。第一轮是上下文无关判断,把每个段落标成 good、bad、short 或 neargood。第二轮是上下文敏感判断:只有当 neargood 紧挨着一个已经是 good 的块时,才会被提升为 good。而段落要想自己单独拿到 good,必须超过 length_high,默认是 200 个字符。
在一份没有任何段落超过 200 字符的文档里,就不会出现“种子”块来触发提升,于是所有 neargood 块都会被当成模板内容,整页结果就空了。
我在这份最长段落为 151 字符的样本上扫了这个阈值:
length_high | 被判定为 good 的段落数 | 返回字符数 |
|---|---|---|
| 200(默认) | 0 | 0 |
| 150 | 8 | 832 |
| 120 | 8 | 832 |
| 100 | 8 | 832 |
| 80 | 8 | 832 |
justext-length-threshold.json。
在这个阈值样本里,只要有一段文字跨过门槛,8 个目标段落就会全部开始输出;继续放宽阈值,也不会再增加任何内容。这个轨迹说明,上下文轮会围绕已有的 good 块去提升相邻的 neargood;但这并不意味着在任意页面上,所有邻居都会无条件被提升。
在把结论归因给 length_high 之前,我还把 length_low 与四个值交叉测试,并与 max_link_density 的两个值组合:8 种组合,全部返回 0。改这两个参数都没能在这个样本里恢复输出。
放到整个 22 个样本的集合里,这个规律也成立:当 length_high=200 时,22 个里只有 2 个有输出;降到 150 时是 22 个里 9 个;降到 120 时是 22 个里 15 个。
这里有两点不要误读。第一,它并不是说 jusText 抽取效果差——在一页真实自然语言内容上,默认设置下它返回了 1,190 个字符的干净正文,因为真实新闻段落第一次就能超过 200 字符。第二,它也不是说默认值本身就是错的;它只是说明默认值假设段落够长,而你在运行前应该先判断自己的语料是否符合这个假设。
这组测试的另一面:默认设置下模板泄漏更高
在这组带标注的样本里,使用默认设置时,jusText 的模板内容泄漏率是最高的。
| 库 | 全部 22 个样本的正文召回率 | 模板泄漏率 | 内容 token 精确率 | 污染 token 数 |
|---|---|---|---|---|
| Readability | 1.0000 | 0.2353 | 0.9109 | 35 |
| trafilatura | 0.9865 | 0.0588 | 0.9411 | 4 |
| newspaper4k | 0.9865 | 0.0000 | 0.9452 | 0 |
| resiliparse | 0.9054 | 0.0588 | 0.9381 | 7 |
| jusText | 0.8378 | 0.4706 | 0.8760 | 74 |
| goose3 | 0.8243 | 0.0000 | 1.0000 | 0 |
sixway-scores.json。同一个样本集、同一个评分器,每个单位都用唯一标记标注,因此恢复结果时可以精确到子串匹配。

47% 的模板泄漏和 74 个污染 token——泄漏率是 Readability 的两倍,污染量更是它的两倍多。这还是在这组带标注的合成样本、默认设置下的诊断结果,不是一个稳定的全局产品排名。
观察到的泄漏和前面提到的断崖现象里的上下文轮是一致的。符合条件的 neargood 块,在周围块类别和距离规则允许时可以被提升;因此,正文旁边像简介或评论这样的“像正文”的内容,也可能跨过边界。这个样本结果展示的是哪些标注块发生了泄漏,而简化后的机制仍然取决于 jusText 的具体上下文规则。
它的召回率是 0.8378,排在倒数第三,而且所有损失都来自阈值:在一篇 129 字符的短文章里什么都没拿到,在一个包含十个短段落的页面里也什么都没拿到,在接近空白的文档里同样什么都没有。
语言停用词表清单
还有 99 个停用词表。
justext.get_stoplists() 会返回 100 种语言。这个分类器从设计上就是按语言参数化的,而不是把英文启发式规则翻译一遍而已;切换语言只需要一个参数:
import justext
paragraphs = justext.justext(html, justext.get_stoplist("Czech"))
text = "\n".join(p.text for p in paragraphs if not p.is_boilerplate)
trafilatura 和 goose3 也提供和语言相关的行为,但这次评测并没有对任何抽取器做非英文真值评分。jusText 内置的 100 份停用词表让它非常适合作为多语言评测候选;但仅凭词表数量,并不能证明它在这些语言上的抽取质量,也不能证明竞争者就一定覆盖得更差。
它的 API 只有两个函数和九个可调常量——length_low 70、length_high 200、stopwords_low 0.30、stopwords_high 0.32、max_link_density 0.20、max_heading_distance 200,再加上编码处理。简单、清晰,而且这些内容都写在函数签名里。
安装与速度
pip install justext 会拉取 3 个包——在对比中是最少的——以及 22.4 MiB,而且不到两秒就能装完。
官方参考:PyPI 上的 jusText。
| 库 | 包数量 | site-packages 占用 | 冷启动导入 | 提取 p50 |
|---|---|---|---|---|
| resiliparse | 5 | 21.0 MiB | 0.015 s | 0.06 ms |
| jusText | 3 | 22.4 MiB | 0.777 s | 0.56 ms |
| goose3 | 16 | 44.3 MiB | 2.181 s | 1.85 ms |
| newspaper4k | 22 | 47.5 MiB | 2.812 s | 2.69 ms |
| trafilatura | 17 | 69.9 MiB | 1.584 s | 0.51 ms |
install-and-import.json。每个库都运行在各自全新的空虚拟环境中。
3 个包和 22.4 MiB 在这组对比里算是很轻的依赖,而 0.56 ms 的中位提取时间在这个基准里也接近 trafilatura 的 0.51 ms。环境测量并没有按文件类型拆分包体积,所以不能判断磁盘占用里有多少来自停用词表。
维护情况,谨慎回答
这里使用的维护日期指标,是仓库最后一次提交和 PyPI 最后一次发布,二者都是 2025-02-25。那距离测试时已经过去 17 个月。总共 8 个 release,91 个 fork,9 个未关闭 issue,且仓库没有归档。
PyPI 的分类器只写到 Python 3.9。我在 3.14.2 上运行时,它仍然可以安装,导入耗时 0.777 秒,并且在 22 个样本中的 19 个上完成提取,没有抛出任何异常。
所以,元数据看起来比实际情况落后了 5 个 Python 版本,而现实情况是它能正常工作。这里真正有用的区别在于:安静的仓库说明的是 支持状态,并不自动等于 功能失效。对于一个算法本身只是公开的 2011 方法加上一组词表的库来说,“已经完成”本身就是合理状态——剩下可改的东西并不多,而且停用词表不像反爬策略那样容易腐化。
不过,安静仓库也意味着:如果你真碰到 bug,多半得自己修,或者 fork 一份。要把这一点和 9 个 open issue 一起看,因为这并不像是一个被大量未解决问题淹没的库。
内存,以及损坏 HTML 会带来什么影响
这里把两个运维层面的问题分开测量。
更大的压力测试背景见:十个库的内存与畸形 HTML 对比。
峰值常驻内存通过 /usr/bin/time -l 测量,每个单元都启动一个全新进程——导入基线表示库在“加载后空转”时的成本,而峰值则包含了文档本身。
| 库 | 运行时 | 导入基线 | 226 KB 峰值 | 10 MB 峰值 |
|---|---|---|---|---|
| html2text | python3.14 | 18.7 | 19.9 | 71.2 |
| pyquery | python3.14 | 30.3 | 33.9 | 172.5 |
| resiliparse | python3.14 | 20.5 | 25.1 | 225.1 |
| markdownify | python3.14 | 23.9 | 28.9 | 278.5 |
| goose3 | python3.14 | 44.1 | 52.4 | 398.5 |
| cheerio | node22 | 66.8 | 76.5 | 398.5 |
| justext | python3.14 | 30.3 | 36.6 | 431.2 |
| newspaper4k | python3.14 | 52.6 | 61.8 | 668.5 |
| trafilatura | python3.14 | 52.5 | 64.8 | 927.1 |
| turndown | node22 | 47.8 | 68.4 | 2947.1 |
memory-results.json。Python 和 Node 的基线彼此不能直接比较;因为解释器本身就包含在两边。
jusText 的导入基线是 30.3 MiB,在这个 10 MB 样本上的峰值是 431.2 MiB,大约是导入基线的 14.2 倍,也相当于输入大小的 43.1 倍绝对 RSS。单个样本、单个进程并不能说明通用的扩展曲线;Python 和 Node 的基线也仍然不能直接互比。
损坏 HTML。 十二份文档各自只破坏一处——未闭合标签、错误嵌套的行内元素、带空格却没加引号的属性、孤立的闭合标签、完全没有 <html>、重复属性、在标签中间被截断的文档、错误实体、未闭合的 <script>、虚假的字符集声明、包含标记的注释,以及 600 层嵌套——再加上 两个同尺寸的正常对照样本,因为“它什么都没返回”只有在它面对同尺寸的正常文档时也同样沉默,才能说明和损坏程度有关。
justext 在 14 个样本里 0 个抛错,在 13 个样本里什么都没返回,并且在所有损坏样本中只恢复了 0/33 个带标记的 sentinel(malformed-results.json)。那个正常的短对照样本同样返回 0 字符;正常的长对照样本则是唯一一个非空结果,返回了 1,333 个字符。12 个损坏样本全部保持空白,而未闭合 <script> 的情况不纳入 sentinel 存活评分。因此,这一轮只能说明解析器容错性——没有异常——但不能把沉默归因于 HTML 损坏,而不是前面已经测出的尺寸阈值。
优缺点
优点。 100 份语言停用词表,而且分类器确实是围绕这些词表设计的,而不是把英文规则简单翻译过去。对比中依赖包数量最少,只有 3 个包。中位提取只要 0.56 ms。9 个文档化且清晰的调参常量。BSD 2-Clause。虽然 PyPI 分类器只写到 3.9,但在 Python 3.14 上也能正常跑通。
缺点。 在这组合成测试的默认设置下,模板泄漏率最高:47%,污染 token 达 74 个。短段落样本在没有任何段落跨过 length_high 时直接返回空字符串。在这组样本上的召回率是 0.8378。仓库最后一次提交和发布距测试时已过去 17 个月,因此维护归属需要认真考虑。
谁适合用,谁不适合
建议评估 jusText 的场景是多语言语料,因为它的语言专属停用词表暴露得非常明确,而且覆盖面很广。这次测试确实统计了 100 份停用词表,但并没有测多语言质量。若你的部署场景适合 3 个依赖包,并且需要明确的阈值控制,它也值得考虑。
不建议使用 的场景是你按 token 喂模型,而且 74 个污染 token 直接就是成本;或者你的页面大多是短段落——产品简介、列表页、更新日志、FAQ 条目——除非你明确调整过 length_high。如果你因为合规或采购原因需要一个持续活跃维护的依赖,也不建议选它,即使代码本身能用,这也是真实约束。
如果你要用它, 不要直接照搬默认值或百分位规则,而是拿一份带标注的验证样本去调 length_high。把可能的阈值都扫一遍,同时测正文召回率和模板精确率;降低门槛能救回短正文,但也可能把不想要的相邻块一起抬上来。
托管 API 适合放在哪一层
jusText 处理的是你已经拿到的 HTML,就和这次对比中的所有库一样。它们都不会自己抓页面、渲染 JavaScript,也不会处理反爬层。
如果想看同一批样本在 6 个抽取器上的对比,请参见:六个库的抽取对比。
托管的抓取/渲染/抽取服务,包括我们自己的 Thunderbit,承担的是另一层责任边界。这里并没有对 Thunderbit 做基准测试。它和这篇文章里的库的区别在于:前者是对给定 HTML 做正文分类,后者是先获取 URL 再处理;本文并没有做同指标的质量对比。
实话实说,jusText 的多语言停用词表确实是一个真实而且免费的能力。如果你的问题是“如何抓取多语言页面”,而不是“如何在多语言页面里分类正文”,那你需要购买的是另一类方案。
更广泛的选型可以看我们的 网页抓取 API 总览 和 开源爬虫总篇。如果输出最终要交给模型处理,在 Python 中把 HTML 转成 Markdown 往往是最先丢失细节的地方。
要不要用 jusText?
如果你的语料是多语言的,或者段落长度分布适合显式调阈值,那它值得纳入候选。上线前先验证这两个条件。
内置的 100 份停用词表确实是它的设计亮点,但多语言准确率并没有在这次测试中验证。这个阈值断崖是可以调的;至于调低之后是否可接受,要看你在带标注样本上测出来的召回率和模板精确率。
在这组英文合成样本、默认设置下,newspaper4k 没有泄漏任何标注模板单元,并恢复了 0.9865 的正文单元。这让它成为这类工作负载的一个对比候选,但不代表它是通用替代方案。
试用 Thunderbit 进行网页数据提取 Get Started Free
常见问题
为什么 jusText 返回空字符串,而不是抽出一部分内容?
它的分类器分两轮工作。段落只有在超过 length_high(默认 200 字符)时,才能自己拿到 good;第二轮才会把相邻的 neargood 块往上提。如果没有任何段落跨过阈值,就没有“种子”块,所有候选都会退化成模板内容,于是结果就是空的。这是设计上的全有或全无,不是没找到部分答案。
jusText 是不是已经没人维护了? 最后一次仓库提交和最后一次 PyPI 发布都是 2025-02-25——距测试时 17 个月——而且 PyPI 分类器只写到 Python 3.9。但它确实能在 Python 3.14.2 上安装并运行,而且没有异常,9 个 open issue 也不算大量积压。把这些日期当作维护风险,而不是功能失效的证据;实际风险是,你可能需要自己修 bug。
为什么它比 Readability 泄漏更多模板内容? 在这组样本里,默认上下文规则把一些“像正文”的相邻块推过了输出边界。是否提升取决于块类别、距离和上下文,而不是见邻居就自动提。默认设置下测到的结果是 47% 的模板单位泄漏和 74 个污染 token。
如果要处理其他语言,怎么用?
justext.justext(html, justext.get_stoplist("German"))。get_stoplists() 会返回全部 100 种可用语言。停用词表是这个分类器的语言级词汇输入,但它同时还会用到段落长度、链接密度、HTML 切分、标题距离和相邻块上下文。改语言输入,并不等于自动验证了德语抽取质量。
这里没有测试什么?
完全没有测真实世界页面——这些都是带标注的受控样本。它最核心的卖点是多语言能力,但这里只统计了 100 份停用词表,并没有在非英文文本上评分。虽然 jusText 提供了 encoding、default_encoding 和 enc_errors 参数,但编码边界情况也没有测试。另外,max_heading_distance 和两个停用词比例阈值在整个测试中都保持默认值。


