最受欢迎的 HTML 转 Markdown 库竟然不会转换表格

最后更新于 August 17, 2026
最受欢迎的 HTML 转 Markdown 库竟然不会转换表格
AI 摘要
在元数据快照时,turndown 是四个受测库里 GitHub 星标最多的一个,拥有 11,386 个 stars。可在四个共享的 HTML 测试样本上,它用默认核心设置输出了 0 个 Markdown 表格,而且在相同输入下所用 token 比 markdownify 多 24.6%。四个工具都完整保住了所有内容探针。差别完全出在结构如何被处理,以及这些结构在下游会带来什么成本。要把内容喂给模型,或者保存页面中的结构化内容,先从 markdownify 开始。它在这套计数器下和 markitdown 的表格行数打平,token 数又处在最低一档,许可证是 MIT,安装体积只有 1.8 MiB。

在元数据快照时,turndown 是四个受测库里 GitHub 星标最多的一个,拥有 11,386 个 stars。可在四个共享的 HTML 测试样本上,它用默认核心设置输出了 0 个 Markdown 表格,而且在相同输入下,所用 token 比 markdownify 多 24.6%

四个库都完整保住了所有内容探针。差别完全出在结构是怎么被处理的,以及这些结构会在下游给你带来什么成本。

测量了什么,又是基于谁的样本

这套研究原本就有一份针对 markitdown 的测试包,里面包含 5 个 HTML 样本,以及预先登记好的探针字符串——既包括要确认是否保留的精确文本,也包括用于检测页面杂项内容的模板字符串。综合对比使用的是 四个被所有四个转换器共同覆盖的样本:一个书店目录、一个引语网站、一个曲棍球统计表,以及 Wikipedia 上关于网页抓取的文章。第五个样本 Nothing but tables 只作为“表格密集型”诊断样本使用,不纳入这次 24.6% 的综合统计。

四个工具分别是:turndown 7.2.4(Node)、markdownify 1.2.3、html2text 2025.4.15,以及 markitdown(直接使用其公开结果)。页面包括:书店目录、引语网站、曲棍球统计表和 Wikipedia 的网页抓取词条。

下面所有指标名称都与 markitdown 自己的工件字段一一对应,因此这些结果可以直接并排比较,而不用去纠结两套名词是不是在说同一件事。

表格

Measured results chart: Token output vs Markdown table rows

转换器正文探针输出字符数Token(o200k)每 token 字节数Markdown 表格行数链接数
turndown16/1695,18826,2363.630611
markdownify16/1676,86821,0623.6536599
html2text16/1676,45221,1763.6132545
markitdown16/1676,99521,3363.6136598

四个共享样本——也就是 markitdown 同样跑过的那四个。完整的按样本拆分数据见 fiveway-scores.json。Token 统计采用 o200k_base;markitdown 的表格行数则是根据它自己保存的 Markdown,再用同一计数器重新统计得出的。

内容保留率打平。 四个样本里的 16 个正文探针,全部被四个转换器完整保留。也就是说,如果你唯一关心的是“文字会不会丢”,这四个答案都是否定的。

结构表现不打平。 在这四个共享样本上,markdownify 和 markitdown 都输出了 36 行 Markdown 表格;html2text 输出 32 行;turndown 则一行都没有。单独的表格专用诊断样本中,markdownify 输出 62 行,html2text 输出 59 行;这些行数没有计入上面的综合结果。

turndown 的 token 成本高出 24.6%,而原因并不是表格。起初我也以为是表格,但按样本拆开后,情况并不是这样——下面会展开。

turndown 如何处理表格

下面以曲棍球统计表这个样本为例,展示同一批行内容的三种输出方式。

turndown:

System diagram: Core Table Paths Diverge

Team Name

Year

Wins

Losses

Boston Bruins

1990

44

24

markdownify:

| Team Name | Year | Wins | Losses | ... |
| --- | --- | --- | --- | --- |
| Boston Bruins | 1990 | 44 | 24 | ... |

html2text:

Team Name  |  Year  |  Wins  |  Losses  | ...
---|---|---|---|---
Boston Bruins  |  1990  |  44  |  24  | ...

turndown 转换后,每个值都还在,这也是它在探针上能拿到 16/16 的原因。真正没有保留下来的,是 每个值对应哪一列。你看 turndown 的输出时,44 只是单独一行数字;除非你自己按位置去数,而且还得假设单元格不会空,否则根本无法知道它是波士顿队的胜场数。而这个样本里偏偏就有空单元格,所以单纯按位置数也行不通。

对模型来说,这意味着它面对的不是一张还能回答问题的表,而是一串只能靠猜的数字。

这更像是一个已知边界,而不是缺陷——turndown 的核心功能本来就不处理表格,想要表格需要额外装 turndown-plugin-gfm。但默认安装并不包含它,而 11,386 个 stars 也说明很多人确实是在直接用默认配置。

token 差距真正从哪来

上面那段话里,我一开始以为 24.6% 的 token 差距,是因为表格被压平后变成了更多字符。后来按样本拆开一看,根本不是。

样本turndown token ÷ markdownify token
引语网站(无表格)0.99×
书店目录1.06×
曲棍球统计表(一个大表)1.37×
Wikipedia(主要是正文,含 9 行表格)1.29×
只有表格0.78×

在那个“只有表格”的样本里,turndown 反而便宜了 22%——因为 Markdown 的管道符和表头分隔线本身也要占 token,而 turndown 根本不输出这些东西。也就是说,仅仅把表格压平,并不必然带来 token 惩罚。

Wikipedia 这个样本占总量的 74%,其中只有 9 行表格。它那 15,378 字符的差距,显然不可能全是表格造成的。真正的来源是这段内容:

(function(){var className="client-js vector-feature-language-in-header-enabled…
.mw-parser-output cite.citation{font-style:inherit;word-wrap:break-word}…
(RLQ=window.RLQ||[]).push(function(){mw.config.set({"wgHostname":"mw-web…

turndown 不会剥离 <script><style> 的内容,而 markdownify 和 html2text 都会。 如果只统计只会出现在这些元素内部的标记,turndown 在所有样本里带出了 10 个 script 标记和 84 个 style 标记;另外两个则一个都没有。在 Wikipedia 页面上,MediaWiki 内联 JavaScript 配置和 CSS 的 8 行内容就占了 14,644 个字符,也就是整个差距的 95%(见 script-style-stripping.json)。

这才是我真正会采取行动的结论。表格被压平,是一个你看得见的结构问题;而 Markdown 里混进 JavaScript 配置块,则是纯成本,完全没有信息价值。在真实页面里,这类内容往往比本次对比中的其他差异都更大。

html2text 写出的表格,可能让你认不出来

html2text 的表格行数是 32,而 markdownify 和 markitdown 都是 36;我最初写的计数器只给它算了 1 行

那是我的计数规则有问题,不是库有问题。html2text 会输出 Team Name | Year | Wins 这种 没有首尾竖线 的表格写法——这在 Markdown 里很常见,但对要求 ^\|.*\|$ 的正则来说是看不见的。我当时就是写了这个正则,跑了一遍,然后差点得出“html2text 不支持表格”的结论。

实际上它是支持的。修正后的计数器改用了一个启发式规则:只要是一串连续包含竖线的行,并且中间有一行分隔线,就当作表格。按这个规则,html2text 就从 1 行变成了 32 行。不过这毕竟不是完整的 Markdown 解析器,所以这些行数应该理解为“在明确计数规则下得到的测量结果”,而不是绝对通用的渲染结果。

如果你会拿自己的正则去二次处理 Markdown,这点尤其值得注意:这四个库里,有两个会输出外层竖线,有一个不会。

很少有人提起的许可证问题

转换器许可证安装体积冷启动导入Stars最近一次发布
turndownMIT3 个 npm 包,8.8 MiB0.056 s11,3862026-04-03
markdownifyMIT5 个包,1.8 MiB0.046 s2,2352026-06-30
html2textGPL-3.0-or-later1 个包,0.2 MiB0.077 s2,1682025-04-15
markitdown以其包元数据为准本次安装未测未测

install-and-import.json。每个库都被安装到了各自空白环境中。许可证信息来自三处交叉确认:注册表元数据、GitHub 仓库,以及已安装包自身的 METADATA 文件,其中写明 License-Expression: GPL-3.0-or-later

在这次安装对比里,最轻量的库——1 个包,0.2 MiB——是 GPL-3.0-or-later。它是否会影响项目,要看软件如何组合以及如何分发。把它当成一个许可选择的检查点更合适;这篇文章不是法律意见。markitdown 之所以在这里显示为未测,是因为这次工件里没有记录它的安装和许可证信息。

这个取舍很容易被忽略,因为许可证恰恰是那种不会出现在基准测试里的属性。

不过三者都比同类里的文章抽取库轻得多——那些通常要 21 到 70 MiB。转换器和抽取器不是一回事,依赖预算里最好分开算。

在这张表成立前,我必须先修正两个干扰项

上面的数字其实是第三版。前两版之所以不对,是因为有两个很容易复现、也很容易被忽略的问题。

五个样本对四个样本。 markitdown 只跑了这五个文件中的四个,而另外三个工具都跑了全部五个。如果按各自跑过的集合分别汇总,markdownify 会得到 98 行表格,而 markitdown 只有 36 行,看起来就像能力差距。但如果只看 同样的四个样本,结果就是 36 对 36,完全打平。第五个样本正好还是那个表格最密集的,所以这个干扰项朝着最糟糕的方向放大了差异,几乎把后来者与现有工具的差距夸大了近三倍。

System diagram: Make the Comparison Comparable

两个计数器,一列结果。 markitdown 公开的 md_table_rows 来自它自己的代码,而我当时并没有读过那段代码。如果拿这个数字直接和我自己的计数器比,实际上比较的可能是两个计数规则,而不是两个转换器。它的 Markdown 输出存放在磁盘上,所以正确做法是用同一套计数器把四个工具都重新跑一遍。等我这么做之后,markitdown 重新计数得到的结果,和它公开的数据在每个样本上都完全一致(0、0、27、9)。定义其实是一致的;只是我在核对之前并不能知道这一点。

这两类错误,在最终输出里都看不出来。它们都会产生一个看起来很自信、实际上却错得离谱的表格。

谁适合用哪个

要喂给模型,或者要把页面里的结构化内容保存下来? 先从 markdownify 开始。按这套计数器,它和 markitdown 的表格行数打平,token 数又处在最低的一档,许可证是 MIT,安装体积只有 1.8 MiB。正式标准化前,先用你自己的页面样式验证一下。

你的依赖预算按 KB 计算,而且你不打算分发软件? 选 html2text。1 个包,0.2 MiB,表格也能保住。先确认 GPL 相关问题,并注意它最近一次发布是在 2025 年 4 月。

你本来就在 Node 技术栈里? 用 turndown,但要配上 turndown-plugin-gfm,并且在交给它之前先把 HTML 里的 <script><style> 去掉,因为 turndown 不会替你处理。这两个遗漏会让 token 多出四分之一,也会把表格结构直接抹掉,而且只有看输出才能发现。

你已经在做其他文档格式转换? markitdown 还能处理 PDF、Office 等等,而且它的 HTML 输出在这类专用转换器里也有竞争力。只用一个依赖,而不是两个,这本身就有价值。

托管 API 适合放在哪

上面这四个工具都只处理你已经拿到的 HTML。它们都不会去抓页面、渲染 JavaScript,也不会应对反爬机制——而在很多真实目标站点上,这一半工作才更难。

我们在 Thunderbit 的开发者技术栈正好覆盖了那一侧。POST /distill 传入 URL 后,会返回干净、可直接喂给 LLM 的 Markdown,抓取和渲染都已经处理好;POST /extract 则会基于你提供的 JSON Schema 返回 AI 结构化 JSON,输出形态又是另一种——更像行数据,而不是还要再解析的 Markdown 表格。这两个接口也都可以通过 MCP 服务器和 CLI(npx @thunderbit/thunderbit-cli)访问。价格见 Thunderbit 定价页

更诚实地说:如果你已经拿到了 HTML,并且只是想要 Markdown,markdownify 是免费的,而且干得不错——这张表也告诉你它的竞品里哪些同样能胜任。如果你要抓页面,或者你要的是结构化行数据而不是散文式内容,那就是另一类采购决策了。

如果你想看更广一点的选型,我们的 网页抓取 API 总览 介绍了托管方案,开源爬虫总指南 则覆盖了自托管方案。Python 中如何把 HTML 转成 Markdown 是更实用的上手教程,而 llms.txt 想标准化的到底是什么 则解释了这个方向未来会怎么走。

试试 Thunderbit 做网页数据提取

结论

就这四个样本的工作负载来说,markdownify 是最稳妥的默认选择:在这套共享计数器下,它与 markitdown 的表格行数一样,token 数又与其他高效转换器非常接近,许可证是 MIT,安装体积只有 1.8 MiB。

这次发现的重点,是“流行度”和“实测表现”之间的落差。turndown 是个很优秀的库,但很多人装了它,却没装让它能处理表格的插件;代价就是 token 多出四分之一,表格结构也直接消失。直到你真的去统计,这些数字都不会自动出现在任何地方。

如果只带走一句话:在把输出交给模型之前,先看看你的转换器是怎么处理表格的。四个里有三个做得还算合理,最流行的那个却不是,除非你额外告诉它该怎么做。

试试 Thunderbit 做网页数据提取 Get Started Free

常见问题

turndown 真的不支持表格吗? 它的核心确实不支持。表格能力来自单独的 turndown-plugin-gfm 包,而直接 npm install turndown 并不会自动包含它。没有这个插件时,每个单元格都会被保留成自己的段落——值都在,但行列关系没了。四个样本综合下来,它输出了 0 行 Markdown 表格,而且相同内容下比 markdownify 多用了 24.6% 的 token。

为什么一开始看起来 html2text 没有表格? 因为我的计数器要求表格必须有首尾竖线,而 html2text 并不输出这种格式。Team Name | Year | Wins 是合法 Markdown,而且渲染也没问题;只是它和 | Team Name | Year | 属于不同风格。修正后的计数器会像解析器那样,识别由连续含竖线行和分隔线组成的表格,所以 html2text 从 1 行变成了 32 行。如果你自己用正则再处理 Markdown,这个差异就会直接坑到你。

html2text 的 GPL 许可证会是个真问题吗? 要看你怎么组合和分发这套软件。GPL-3.0-or-later 可能带来 MIT 不会有的义务,所以如果你要把它用于可分发产品,最好先让负责许可的人参与判断。这是合规检查点,不是法律意见;许可证身份已经通过注册表元数据、仓库和已安装包的 METADATA 三方确认。

这些 token 数对其他页面也有意义吗? 24.6% 的差距,主要来自 turndown 保留了 <script><style> 内容,所以它会随着页面里这类内容的多少而变化——现代 CMS 页面里可能很重,静态页面里则接近于零。表格会把差距往另一边拉:在那个“只有表格”的样本里,turndown 反而便宜了 22%,因为它不输出任何管道符结构。四个工具的每 token 字节数都落在 3.61 到 3.65 之间,所以输出密度基本一致,差异只在总量。如果这个数字会影响预算,最好还是拿你自己的语料跑一遍。

这里没有测试什么? 真实世界的多样性——毕竟四个样本就是四个样本。嵌套列表、定义列表、脚注和数学公式。格式不规范的 HTML,而这通常正是转换器最容易分歧的地方。把 Markdown 再回转成 HTML 的往返测试。docling,它也有同样的样本文件,但公开结果没有报告这些字段,所以这里没有列出,而不是被估算。还有配置:html2text 用的是 body_width=0,因为它默认的 78 会强制每行硬换行,那样这张表里的字符数和 token 数都会完全不同。

Ke
Ke
Thunderbit 首席技术官 | 高级数据科学家与机器学习专家 Ke Shen 拥有近十年的机器学习和数据科学经验,毕业于哥伦比亚大学,曾任 Walmart Labs 高级数据科学家。他在 Python、R、Java 和统计学方面拥有深厚且备受同行认可的专业能力,并分享如何将复杂的 AI 算法从理论落地到生产级架构的实战经验。
目录
Thunderbit · AI 网页数据代理

一键 内提取任意页面数据

深受 250,000+ 用户信赖
提供免费方案
从网页到表格
描述你的需求——Thunderbit 的 AI 代理会帮你抓取并导出到 Excel、Google Sheets、Airtable 或 Notion。可免费开始。
Chrome Store Rating
PRODUCT HUNT#1 Product of the Week