连 .doc 都不支持,MarkItDown 凭什么拿下 179K Star
- MarkItDown 定位服务于 LLM 输入管线而非高保真文档转换,因此敢于舍弃 .doc 等老格式支持,换取工程上的简洁
- 格式识别采用三路信号融合:StreamInfo 元信息、mimetype 交叉补全、magika 机器学习内容嗅探,不做仲裁只做排序兼容
- 转换器调度用双层循环暴力匹配加优先级常量构建降级链,并用断言锁定流位置契约,保证试探无副作用
- 转换器内藏针对性手艺:OMML 公式预转 LaTeX、MasterFormat 编号合并补丁、配合 Cloudflare 的 Accept 请求头约定
- 项目以文档边界克制扩张:拒收 Web 服务与前端、OCR 走插件机制、MCP 仅暴露单一工具,核心库保持纯粹
上个月帮朋友搭一个 RAG 知识库,他甩给我一堆文件,PDF、Word、PPT、Excel,还有一个 2005 年的 .doc。我说这不简单,MarkItDown 一把梭。结果跑到那个 .doc 的时候直接报了 UnsupportedFormatException。
我当时的第一反应是,这可是微软的项目啊,Word 是他们家的,老格式反而不支持?
后来去翻 issue 区才发现,.doc 支持的请求从 2024 年 12 月就挂在那了,编号 #23,14 条评论,到今天还开着。你猜官方是什么态度,明确的「知道了,不打算做」。
这就很有意思了。一个连自家老格式都不支持的转换工具,凭什么拿到 179,750 个 Star、13,237 个 Fork。带着这个疑问,我把它的源码拆了一遍,结论是,MarkItDown 从第一天起就没想做一个「全能文档转换器」,它只伺候一个客户,LLM。
为什么是 Markdown
先看官方 README 里的一段原话,大致意思是,主流 LLM 比如 GPT-4o 天生「说」Markdown,你不提示它,它的回复里也全是 Markdown,说明训练语料里这类文本多到它已经内化了这种结构。
这段话定了整个项目的调性。MarkItDown 不是给人做高保真排版转换的,它输出的是给模型吃的结构化文本。README 里也明说了,输出结果虽然人看着还行,但项目的目标是文本分析管线,不是人类消费级的高保真转换。
这个定位直接决定了后面所有的工程取舍。
比如转换 DOCX,它根本没自己解析 Word 的 XML,而是走了一条两段管线。第一段用 mammoth 库把 .docx 转成 HTML,第二段再用自家的 HtmlConverter 把 HTML 转成 Markdown。你看 converters 目录下的 DocxConverter,代码里就一句核心调用,mammoth.convert_to_html 拿到 HTML 之后,直接丢给 self._html_converter.convert_string。
你想想看,微软写了 30 年 Word,比谁都清楚 OOXML 的水有多深,与其自己硬啃,不如复用社区已经打磨好的 mammoth,自己只管最后一公里的 HTML 到 Markdown。
一台猜格式的机器
真正让我觉得值得写这篇文章的,是它的格式识别设计。
你接到一个文件流,怎么知道它是 PDF 还是 Word?大多数人会说看扩展名。但扩展名会骗人,mimetype 也会骗人,服务器返回的 Content-Type 一半是瞎填的。
MarkItDown 的做法是把三个信号全用上,还不轻信任何一个。
第一个信号是调用方给的元信息,文件名、扩展名、URL,打包成一个叫 StreamInfo 的 dataclass。第二个信号是 Python 标准库 mimetypes 做交叉补全,有扩展名没 mimetype 就补 mimetype,反过来也一样。第三个信号最狠,它调了 Google 的 magika 库,那是一个机器学习模型,直接读文件内容的字节特征来判断真实类型,Chrome 浏览器判断下载文件类型用的同款技术。
三路信号汇总的关键代码在 _get_stream_info_guesses 这个方法里。它不会强行选出「对的那个」,而是做兼容性检查,如果 magika 的判断和你声称的扩展名对得上,就合并成一个猜测;对不上,它把两个猜测都留着,先试你的,再试 magika 的。
注意这个设计,不仲裁,只排序。
文本文件还有第四层处理,它会读流的前 64 KB,用 charset_normalizer 猜编码。一个 GBK 的中文 txt,文件名上什么都看不出来,全靠这一步兜底。
双层循环与一条降级链
格式猜出来了,接下来选转换器。这里是整个项目最精巧的部分。
MarkItDown 注册了二十多个转换器,调度不是 if-else 链,而是两层循环暴力匹配。外层遍历所有的 StreamInfo 猜测,内层遍历所有转换器,每个转换器先回答一个问题,accepts 这个文件吗,答是就试转,转失败不抛异常,记下来,换下一个。
听起来很笨,但配合两个优先级常量就变得很聪明。源码里定义了 PRIORITY_SPECIFIC_FILE_FORMAT 等于 0.0,PRIORITY_GENERIC_FILE_FORMAT 等于 10.0,数字小的先试。PDF、DOCX 这些有明确格式的转换器是 0.0,PlainTextConverter 和 HtmlConverter 这种接近万能适配器的是 10.0。
这样降级链就出来了。一个其实是 PDF 的文件,就算扩展名被改成了 .txt,PdfConverter 在第一轮就会通过内容嗅探认领它。一个啥都不认识的文本流,前面二十个转换器都摇头,最后 PlainTextConverter 把它当纯文本接住。
实在没有任何转换器接,才抛 UnsupportedFormatException。如果有转换器试了但失败,抛的是另一个异常 FileConversionException,里面带着每一次 FailedConversionAttempt 的完整记录,谁试的、错在哪,方便你排查。
这里还有个容易被忽略的契约设计。每个转换器的 accepts 方法被严格要求不能移动文件流的读取位置,_convert 主循环里直接放了断言,accepts 前后流的位置必须一致,不一致就崩给你看。为什么这么较真,因为一个流要被二十个转换器依次试探,任何一个偷偷往前读了几个字节,后面的转换器拿到的就是残缺数据,这种 bug 极难排查。用断言把隐式契约变成显式约束,这是给扩展性上保险。
转换器里藏着不少手艺
虽然核心调度只有八百多行,但单个转换器里能看到很多针对性的手艺。
DOCX 那条管线里有个预处理步骤 pre_process_docx,干的事情相当细分。Word 里的公式是 OMML 格式,mammoth 不认识,MarkItDown 就在预处理阶段用 BeautifulSoup 把公式节点挖出来,查着一张 LaTeX 映射表逐个转译,行内公式包一层美元符号,块级公式包两层,再塞回文档流里交给 mammoth。所以数学试卷的 Word 文档转出来,公式还是能读的。
PDF 转换器里藏着一个更冷门的补丁。有个正则 PARTIAL_NUMBERING_PATTERN,专门匹配以 .1 .2 .10 开头的行。这是建筑工程行业 MasterFormat 标准的编号方式,某些 PDF 提取器会把编号和正文拆成两行,MarkItDown 就写了合并逻辑把它们缝回去。你能想象吗,179K Star 的明星项目里,有人专门为建筑行业的招标文件格式打了补丁。这种边角料手艺,恰恰是它在真实场景里被大量使用的证据。
HTTP 请求头也有个小彩蛋。MarkItDown 发请求时主动声明 Accept 是 text/markdown 优先,HTML 其次。这是在配合 Cloudflare 去年推的 Markdown for Agents 约定,支持的服务器会直接返回 Markdown 版本,省一步 HTML 转换。一个转换工具在替整个 Agent 生态探路。
拒绝变成平台
翻这个仓库的演进史,能看到一条清晰的克制路线。
项目已经长成了 monorepo,四个包,核心库 markitdown、markitdown-mcp、markitdown-ocr、还有一个示例插件包。OCR 能力就是通过插件机制挂出去的,默认不装,你要处理扫描件再自己加。整个仓库的结构画出来是这样。
核心转换能力全部沉淀在 markitdown 主包里,mcp 和 ocr 都是围着它转的外围包,谁也不侵入谁。
插件系统用的是 Python 标准的 entry_points 机制,读取 markitdown.plugin 这个分组的入口点,第三方包装好发到 PyPI 就能被自动发现,而且插件默认关闭,要显式传 enable_plugins=True 才加载。安全考量在源码注释里写得很直白。
更硬核的是它的贡献政策。README 里有一节明确列了不收的东西,Web 服务、REST API、托管转换服务、网页前端、桌面和移动应用,全都不收。官方的态度是这些项目很有价值,但请独立建仓。微软在用文档边界防止这个库膨胀成平台,转换内核保持纯粹。
MCP 包倒是值得单独说一句,整个服务就暴露一个工具 convert_to_markdown,接收一个 URI 返回 Markdown。就这么薄的一层,却让 MarkItDown 成了 Claude、Cursor 这类 Agent 编辑器的标配外挂。
那些没人告诉你的坑
拆完架构,说几个 README 不会主动告诉你的事。
最先撞上的就是老格式,全家不支持。.doc、.ppt、.xls 这些 2007 之前的二进制格式,ACCEPTED_FILE_EXTENSIONS 里只有 .docx 一族。前面说的 issue #23 挂了快两年不是没人管,是官方优先级明确不在这。如果你的历史文档库里有大量 97-2003 格式,先跑一遍 libreoffice 转档再来。
DOCX 的坑更隐蔽,锅在依赖层。issue #1282 里有人报错 KeyError 值是 w:ilvl,我特意在 MarkItDown 源码里全文搜了这个字符串,一次都没出现。这个错是 mammoth 抛的,某些列表样式不规范的文档会触发,从 2025 年 6 月挂到现在。两层架构的代价在这里显形,你在核心库层面根本修不了依赖的 bug。
安全面比看起来大,这条要重点说。README 顶部有一个 IMPORTANT 提示,大意是 MarkItDown 会以当前进程的全部权限做 I/O。什么意思,你调 convert 传一个字符串进去,它会解析 scheme,是 http 就真的发网络请求,是 file 就真的读本地文件。如果你的服务把用户输入直接丢给 convert,等于亲手递了一个 SSRF 加任意文件读取的原语。官方给的建议是用最窄的入口,只调 convert_stream 或者 convert_local,别用全知的 convert。这个提示放在 README 最顶上,说明他们踩过或者见过真实案例。
还有版本号,别被 Star 数唬住了。179K Star 的项目,版本还停在 0.1.8b1,项目类注释里自己都写着 In preview。
社区热度和产品成熟度,是两回事。
还有一个数字值得玩味。贡献榜前两位,afourney 贡献了 110 次,gagb 70 次,第三名直接掉到 9 次。这基本是两个人的项目,巧合的是两人都来自微软,主导者 afourney 的名字出现在 MIT 协议文件里。大厂开源的标准画像,公司背书加两个全职维护者,活跃度倒是真不虚,我查的那周光 9 月 3 日一天就有 20 个合并提交。
给 LLM 造工具的一个范本
拆完这个仓库,我带走的不只是一个好用的工具,而是一种可以复用的设计模式。
我把它叫做「猜三次再动手」。面对不可信的输入信号,不设计一个权威的裁判,而是收集多路独立信号,排个优先级顺序,挨个试。每次试探都保证无副作用,流的位置用断言锁死,失败的尝试记录在案而不是中途崩溃。这套东西在 MarkItDown 里是文件格式识别,挪到你的系统里,可以是用户意图识别、数据源探测、任何输入不可信的场景。
另一个启发是目标客户越窄,工程越敢做减法。MarkItDown 敢不支持 .doc,敢用 mammoth 躺平,敢拒绝所有 Web 前端贡献,都是因为它的客户只有一个,LLM 的输入管线。这个客户要的是结构完整、token 干净,不是像素级保真。想清楚你的工具为谁优化,才知道哪些「功能缺失」其实是「需求不存在」。
如果你在做 RAG,做 Agent 的文件理解,或者只是想给自己的模型应用加个文档入口,MarkItDown 值得放进依赖清单,也值得翻开它的 _markitdown.py 读读那八百行调度代码,那是整个项目真正值钱的部分。

评论互动