拆解 MarkItDown:一周收获 26K 星的项目竟然如此简单

MarkItDown 是微软开源的一款将任何格式的文件转换为 markdown 纯文本的工具。

这个工具的用途主要是让 AI 方便对内容进行索引和文本分析。

此工具声称能处理下面这些类型的文件

  • PDF
  • PowerPoint
  • Word
  • Excel
  • 图片 (EXIF 元数据和 OCR)
  • 语音 (EXIF 元数据和字幕)
  • HTML
  • 文本格式 (CSV, JSON, XML)
  • 压缩文件 (逐个批量处理)

这个工具是用 Python 开发的。最简单的使用方法,类似这样

from markitdown import MarkItDown
md = MarkItDown()
result = md.convert("test.xlsx")
print(result.text_content)

构造 MarkItDown 实例时也可以传入大模型参数,

from markitdown import MarkItDown
from openai import OpenAI

client = OpenAI()
md = MarkItDown(llm_client=client, llm_model="gpt-4o")
result = md.convert("example.jpg")
print(result.text_content)

仅仅就一个简单的 HTML 到 markdown 的转换,其实就有不少细节。看到这里,我的小脑袋里禁不住有这样一些问题:

  • 如何能支持这么多源文件格式?
  • 大模型在项目里是什么作用?
  • 转换的质量如何?特别是 HTML 里嵌入的代码、图片、音视频,能否美观的在 markdown 里呈现?

项目的代码规模

将项目下载后,用 loc 看

Language Files Lines Blank Comment Code
HTML 3 2448 185 47 2216
Python 6 1924 328 163 1433
Markdown 4 238 74 0 164
Toml 1 83 9 0 74
XML 1 1 0 0 1
Total 15 4694 596 210 3888

这里面除了 Python 之外,其它基本上是测试和说明文件。关键代码才几个。

src
└── markitdown
    ├── __about__.py
    ├── __init__.py
    ├── __main__.py
    ├── _markitdown.py
    └── py.typed

核心逻辑,全部在 _markitdown.py 里。

转换方法详解

由于核心代码不长,才 1500 行,非常适合交给 Gemini 2.0 去分析。

下面是 Gemini 2.0 Flash Experimental 分析代码后给出的回答

1. 它是如何支持如此多的源文件格式的?

该工具通过多种技术的结合来实现其广泛的文件格式支持:

  • 使用转换器类的模块化设计:

    • 它采用模块化方法,为每种文件类型定义单独的转换器类(例如, PlainTextConverterHtmlConverterPdfConverterDocxConverterImageConverter 等)。
    • 每个转换器类都继承自一个公共基类 ( DocumentConverter),并实现特定于其格式的 convert 方法。这使得将来添加新的转换器变得容易。
  • 文件扩展名和 MIME 类型检测:

    • 它利用 Python 的 os.path.splitextmimetypes 库从扩展名和内容类型推断文件类型。
    • 它还使用 puremagic 来检查文件内容(魔术字节)以猜测文件类型,当扩展名缺失或不可靠时,这尤其有用,对于从 URL 检索的文件尤其如此。
  • 外部库:

    • mammoth:将 DOCX 文件转换为 HTML。

    • markdownify:将 HTML 转换为 Markdown。

    • pandas:从 Excel (XLSX) 文件中读取数据到数据帧。

    • pdfminer:从 PDF 文件中提取文本。

    • pptx:从 PowerPoint (PPTX) 文件中读取数据。

    • bs4 (BeautifulSoup):解析 HTML 和 XML。

    • speech_recognition:转录音频文件。

    • pydub:读取和转换音频文件。

    • youtube_transcript_api:从 YouTube 视频中提取转录。

    • Exiftool: 脚本还使用 exiftool 命令行工具(如果系统存在)从图像和音频文件中提取元数据。

    • 它利用几个外部库来处理特定于格式的解析和转换:

  • 针对复杂格式的特定处理:

    • HTML: 使用 BeautifulSoup 解析 HTML,提取相关内容,并利用 markdownify 转换为 Markdown,同时去除 Javascript 和复杂的样式。

    • RSS/Atom: 使用 minidom 解析 XML,并通过提取和格式化每个帖子的标题和内容来创建 Markdown 输出。

    • Jupyter Notebook (.ipynb): 从 JSON 内容中提取 Markdown 和代码单元格并转换为 Markdown。

    • YouTube: 尝试提取视频标题、描述和转录,并将其格式化为 Markdown。

    • Zip: ZipConverter 将文件提取到临时文件夹,并递归处理文件夹中的每个文件。

    • 它针对更复杂的格式定制了转换器,例如:

  • 错误处理和回退:

    • MarkItDown.convert 方法遍历转换器,尝试每个转换器,直到找到合适的转换器。如果所有尝试都失败,它会记录错误并引发异常。

本质上,该工具通过以下方式支持多种格式:

  1. 通过 MIME 类型、扩展名和魔术字节的组合来识别源格式。
  2. 将每个文件的处理委托给专门为给定文件格式设计的专用转换器。
  3. 使用外部库和工具来完成文件解析的繁重工作。
  4. 优雅地回退,并且仅在尝试所有方法后才报告失败。

2. 大型语言模型 (LLM) 在该项目中的作用是什么?

该代码目前利用 LLM 进行 图像字幕/描述

  • 图像描述:

    • ImageConverter 类具有一个 _get_llm_description 方法,该方法调用外部 LLM 客户端( llm_client,在构造函数中配置)来生成图像的文本描述。
    • 图像被编码为 base64 字符串,嵌入到数据 URL 中,并与提示一起发送到 LLM(提示可以自定义,但默认为“为这张图片写一个详细的标题。”)。
    • 然后,LLM 的文本响应包含在 Markdown 输出中。
  • 它如何在 MarkItDown 中使用:

    • 构造函数接受 llm_clientllm_model 参数,这些参数随后传递给 ImageConverter
    • 这是一个可选功能,仅当在创建 MarkItDown 实例时配置了这些参数才使用。

要点:

  • LLM 集成目前 仅限于图像描述
  • LLM 被视为通过客户端调用的 外部服务
  • 系统允许将自定义提示发送到 LLM。
  • 代码使用 LLM 客户端生成描述,而不是实际执行转换逻辑。

3. 转换质量如何?HTML 中嵌入的代码、图像和音频/视频能否在 Markdown 中很好地表示?

转换质量因源格式的复杂性而异:

  • 总体良好:

    • 文本: 纯文本、基本 HTML、PDF、DOCX 和 XLSX 文件通常可以很好地转换为 Markdown,保留基本的文本格式、标题、列表和表格(如果适用)。
    • 元数据: 图像和音频文件的 EXIF 元数据被正确提取并显示。
  • HTML 转换:

    • 该脚本在删除 scriptstyle 标签以降低复杂性方面做得很好。
    • 该脚本会转义 URI,这意味着链接应该是安全的,并且下划线将正确呈现。
    • 默认情况下, markdownify 包括所有图像,但 _CustomMarkdownify 会删除数据 URI,因为大量的嵌入数据会减慢 LLM 的速度。
  • 代码块:

    • 转换器使用标准 ``` markdown 渲染带有语法突出显示的代码块。
  • 局限性:

    • 生成的 Markdown 主要关注内容,而不是复杂的布局或演示。不支持复杂的 Markdown 扩展。

    • PowerPoint 旨在支持表格和图表(以基本格式),但复杂的图表样式可能无法正确表示。

    • 如果图像是 PPTX 的一部分,它们将通过占位符名称显示。

    • HTML、XLSX 和 PPTX 中的表格会转换为基本的 Markdown 表格格式,但可能无法完全表示具有复杂样式或格式的表格。

    • 音频通过元数据和转录转换为 Markdown(如果环境已设置为执行转换)。但是,结果将仅是文本,而不是实际的音频文件。

    • 视频(例如 YouTube)通过元数据和转录转换为 Markdown(如果环境已设置为执行转换)。但是,结果将仅是文本,而不是实际的视频文件。

    • 没有对 HTML 中嵌入的视频的特定支持。

    • 图像路径在 Markdown 中保留为相对路径。它无法直接嵌入图像(PPTX 的缩略图除外)。

    • PPTX 幻灯片中的图像的处理方式是仅包含图像的 alt 文本和一个占位符名称(例如, ![alt text](imagename.jpg))。如果需要保留图像,则必须手动解析占位符名称。

    • 对于标准图像,可以提取元数据、EXIF 以及通过 LLM 进行的描述(如果使用)。

    • HTML/CSS 复杂性: 虽然 markdownify 可以将大多数标准 HTML 转换为 Markdown,但复杂的布局、样式和交互元素可能无法完全保留。诸如 CSS Grid、JavaScript 驱动的元素和动画之类的东西将不会呈现。同样重要的是要提到,复杂文件类型的大多数转换逻辑都依赖于在生成 markdown 之前转换为 HTML,并且 HTML 的复杂性可能会导致次优的结果。

    • 图像:

    • 音频/视频:

    • 表格:

    • PowerPoint:

    • 没有高级格式:

  • 它们在 Markdown 中的表示程度如何?

    • HTML 中嵌入的代码表示为标准的 Markdown 代码围栏。
    • 图像使用标准的 Markdown 语法引用,但实际文件不会嵌入。
    • 音频和视频文件通过其元数据和(在某些情况下)转录引用,但实际的音频/视频不会嵌入到生成的 Markdown 文本中。

大模型在里面起到的作用

使用多模态的大模型将图片转换成描述图片的提示词,关键在下面这个函数。说穿了,就是一句提示词

Write a detailed caption for this image.

def _get_llm_description(self, local_path, extension, client, model, prompt=None):
    if prompt isNoneor prompt.strip() == "":
        prompt = "Write a detailed caption for this image."

    data_uri = ""
    with open(local_path, "rb") as image_file:
        content_type, encoding = mimetypes.guess_type("_dummy" + extension)
        if content_type isNone:
            content_type = "image/jpeg"
        image_base64 = base64.b64encode(image_file.read()).decode("utf-8")
        data_uri = f"data:{content_type};base64,{image_base64}"

    messages = [
        {
            "role": "user",
            "content": [
                {"type": "text", "text": prompt},
                {
                    "type": "image_url",
                    "image_url": {
                        "url": data_uri,
                    },
                },
            ],
        }
    ]

    response = client.chat.completions.create(model=model, messages=messages)
    return response.choices[0].message.content

项目工程优点

微软开源的 Python 项目值得学习的地方

  • 使用了社区最新的构建打包方案, pyproject.toml 没错,Python 这种解释型编程语言也需要构建了。

可以简单的把这个文件的作用理解为 JavaScript 项目里的 package.json,以一种工具中立的方式说明项目的元数据、依赖等等。

  • 启用了 Python 的类型检查 (type hinting)

相当于 TypeScript。类型检查可以让弱类型的 Python 项目在规模变大后不容易失控。

  • 有测试 CI

使用 hatch test 。测试用例可以让多人贡献项目时,保证质量。

小结

核心代码 1500 行,一周收获 26K 星。

拆解后,发现全是调包。

项目工程质量不错,也为以后继续迭代功能留足了空间。

可以说这是一个“有想法,先上线,再优化”的典型实践案例了。你有什么想法呢,欢迎在评论区留言探讨。

原文阅读