【免费下载链接】markitdown
markitdown:Cli;Lib; Java
markitdown 是一个把文档转成 Markdown 的开源工具,其 Java 实现(markitdown4j)支持 PDF、Word、Excel、PPT、HTML、图片、音频、压缩包等 12 种文档转换器。本文带你走读两个核心源码:DocumentConverter接口与ConverterRegistry注册表,看懂 12 个转换器是如何被统一注册、按 MIME 类型路由并参与优先级选择的。
一、整体架构:一次转换要经过什么?
在 Java 模块markitdown-java中,转换引擎的入口是 MarkItDownEngine.java。一次文件转换的完整链路只有 5 步:
- 校验文件:存在性、是否普通文件、大小上限、可读性
- 识别类型:
FileTypeDetector.detectMimeType()探测出 MIME 类型 - 查找转换器:
ConverterRegistry.getConverter(mimeType)按优先级选出一个 - 执行转换:优先流式转换,否则走路径转换
- 返回结果:统一的
ConversionResult(含 Markdown 内容、元数据、警告)
整个设计遵循"策略模式 + 注册表":每种文档格式是一个独立策略(Converter),注册表负责"谁该上场"。
二、DocumentConverter 接口:所有转换器的统一契约
📄 接口源码:DocumentConverter.java
这个接口只有 6 个方法,却定义了 12 个转换器共同遵守的规则:
| 方法 | 类型 | 作用 |
|---|---|---|
convert(Path, ConversionOptions) | 必须实现 | 把磁盘上的文件转成 Markdown |
convert(InputStream, String, ConversionOptions) | default | 流式转换,默认直接抛异常(见 L42-L49) |
supportsStreaming() | default | 是否支持流式转换,默认false |
supports(String mimeType) | 必须实现 | 声明"我能处理哪些 MIME 类型" |
getPriority() | default | 优先级,默认 0,数值越大越先被选中 |
getName() | 必须实现 | 人类可读的唯一名称,用于注册表去重 |
两个容易忽略的细节:
- 流式转换是"选择性加入"的:
convert(InputStream, ...)的默认实现会抛出"Stream-based conversion is not supported by ..."异常,转换器只有在明确支持时才需要覆写它,配合supportsStreaming()返回true。 - 优先级机制:当多个转换器声明支持同一个 MIME 类型时,
getPriority()数值大者胜出。
三、ConverterRegistry:线程安全的转换器注册表
📄 注册表源码:ConverterRegistry.java
注册表内部只有两个ConcurrentHashMap(见 L35-L36):
nameToConverter:转换器名称 → 实例,保证名称唯一mimeTypeToConverters:MIME 类型 → 排序后的转换器列表,惰性缓存
3.1 注册:名称冲突直接拒绝
registerConverter()(L53-L65)是synchronized方法,重复注册同名转换器会立即抛出IllegalArgumentException: Converter with name '...' is already registered——这让"名字冲突"这种低级错误在启动阶段就暴露。
3.2 查找:按优先级排序 + MIME 缓存
getConverter(mimeType)(L91-L113)的查找逻辑:
- 先查 MIME 缓存,命中直接取列表第 0 个
- 未命中则遍历所有已注册转换器,用
supports(mimeType)过滤 - 按
getPriority()降序排序,取第一个 - 排序结果写入缓存,后续同 MIME 的查找零开销
此外还有配套方法:getAllConverters(mimeType)返回全部匹配项(供批量场景)、getConverterByName(name)按名查找、getSupportedMimeTypes()基于 FileTypeDetector.java 的已知 MIME 集合推断支持范围。
💡 注意:
unregisterConverter()移除转换器后会清空整个 MIME 缓存(L73-L81),避免读到过期结果——这是缓存一致性的关键细节。
四、12 个内置转换器一览
📁 转换器目录:converters/
所有转换器都实现DocumentConverter接口,各自声明名称、优先级和 MIME 支持:
| 转换器 | 优先级 | 流式 | 支持的 MIME 类型 | 源码 |
|---|---|---|---|---|
| PdfConverter | 100 | ✗ | application/pdf | PdfConverter.java |
| DocxConverter | 100 | ✗ | OOXML document、application/msword | DocxConverter.java |
| DocConverter | 100 | ✗ | application/msword | DocConverter.java |
| PptxConverter | 100 | ✗ | OOXML presentation、application/vnd.ms-powerpoint | PptxConverter.java |
| PptConverter | 100 | ✗ | application/vnd.ms-powerpoint | PptConverter.java |
| XlsxConverter | 100 | ✗ | OOXML sheet、application/vnd.ms-excel等 3 种 | XlsxConverter.java |
| XlsConverter | 100 | ✗ | application/vnd.ms-excel | XlsConverter.java |
| HtmlConverter | 100 | ✗ | text/html、application/xhtml+xml | HtmlConverter.java |
| ImageConverter | 80 | ✗ | image/*(前缀匹配) | ImageConverter.java |
| TextConverter | 50 | ✓ | text/plain、text/markdown、text/csv、application/json、XML 等 6 种 | TextConverter.java |
| ZipConverter | 50 | ✗ | application/zip、application/x-zip-compressed | ZipConverter.java |
| AudioConverter | 30 | ✗ | audio/*(前缀匹配) | AudioConverter.java |
三个值得注意的设计:
- MIME 重叠 + 优先级竞争:如
application/msword同时被 DocxConverter 和 DocConverter 声明,此时靠优先级(同为 100 时按注册表内部顺序)决定;image/*、audio/*则用前缀匹配一次覆盖整族格式。 - TextConverter 是唯一支持流式转换的内置转换器,它一个类就覆盖了纯文本、Markdown、CSV、JSON、XML 六种 MIME 类型。
- 优先级体现取舍:PDF/Office 类给 100(核心格式),图片 80,文本与压缩包 50,音频 30(依赖外部转写能力,相对次要)。
五、注册流程:createDefaultRegistry 如何装配 12 个转换器
📄 注册入口:MarkItDownEngine.java
MarkItDownEngine的无参构造默认调用createDefaultRegistry(),它依次注册 11 个普通转换器:
registry.registerConverter(new PdfConverter()); registry.registerConverter(new DocxConverter()); // ... 共 11 个最后注册 ZipConverter 时,源码展示了一个很有意思的委托模式(L474-L488):
ZipConverter zipConverter = new ZipConverter(); zipConverter.setDelegate(new ZipConverter.DocumentConverterDelegate() { ... }); registry.registerConverter(zipConverter);Zip 压缩包里的成员文件(如a.zip/doc.pdf)需要被"解开再转一次"。这里没有写死任何格式逻辑,而是通过 ZipConverter.java 中的DocumentConverterDelegate函数式接口,把"解压后每个成员如何转换"委托给一个复用同一注册表的新引擎实例——本质上实现了递归转换:外层 zip 转成"内部各文件 Markdown 的合集 + 摘要"。
六、一次转换的完整调用链
把前面的知识串起来,看doConvert()(MarkItDownEngine.java#L182-L235)的核心决策:
converterRegistry.getConverter(mimeType)查不到转换器时,不会抛异常,而是返回一个带警告信息的失败结果:No converter found for MIME type '...',并附上当前支持的全部类型- 拿到转换器后:
- 输入是流且
supportsStreaming() == true→convert(InputStream, ...) - 输入是文件 →
convert(Path, ...) - 流输入但转换器不支持流式 → 抛
ConversionException
- 输入是流且
- 结果统一封装为 ConversionResult.java:包含
markdownContent、metadata、warnings、fileSize、originalFileName
异步与批量场景下,convertAsync()/convertParallel()只是把同步流程丢进ForkJoinPool,选型逻辑完全相同——注册表是唯一的路由中枢。
七、如何自定义一个新转换器?
得益于接口 + 注册表设计,扩展只需两步:
// 1. 实现接口:声明名称、优先级、MIME 支持,实现 convert(Path, ...) // 2. 注册进引擎 markItDownEngine.registerConverter(new MyConverter());需要遵守的约定只有 3 条:
- 名称唯一,重复注册会直接抛异常
supports()声明尽量精确,MIME 重叠时靠getPriority()分出先后- 仅当你确实能脱离文件系统工作时,才覆写
convert(InputStream, ...)并让supportsStreaming()返回true
更多背景可参考项目文档:development.md(软件设计与模块职责)、configuration.md(配置项说明)、markitdown.example.yml(配置示例)。
小结
markitdown4j 用最小接口 + 注册表就搭出了一个可扩展的文档转换框架:
DocumentConverter:6 个方法定义契约,default 方法让"流式、优先级"成为可选能力ConverterRegistry:双 Map 结构,名称唯一性检查 + MIME 优先级排序 + 惰性缓存,线程安全createDefaultRegistry():一处集中装配 12 个转换器,Zip 委托模式优雅解决递归转换MarkItDownEngine:校验 → 探测 → 路由 → 转换,五步走完全链路
这套"策略 + 注册表 + 委托"的组合,值得任何需要"按类型分发处理器"的 Java 项目借鉴。
【免费下载链接】markitdown
markitdown:Cli;Lib; Java
相关推荐
Docling 架构解析:DocumentConverter、Backend 与 Pipeline 如何协同完成文档转换
Docling 架构解析:DocumentConverter、Backend 与 Pipeline 如何协同完成文档转换 本篇围绕 Docling 官方架构文档
人工智能计算机视觉OCRRAG学术论文 AI 流水线实战:academic-research-skills 从文献到定稿
学术论文 AI 流水线实战:academic research skills 从文献到定稿 你一定遇到过这个瞬间:打开 AI 帮你写的参考文献,发现有一条引用根
AI 技能科研AI 应用AI 评测markitdown4j 支持格式完整清单:PDF、DOCX、XLSX、HTML 等 13 种文档转 Markdown 一次转换到位
markitdown4j 支持格式完整清单:PDF、DOCX、XLSX、HTML 等 13 种文档转 Markdown 一次转换到位 markitdown4j
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考