第 2 课

在 Python 里批量转换、接视觉大模型、自己写插件

用 MarkItDown 的 convert、convert_local、convert_stream 加 StreamInfo、convert_response 转换文件、内存数据和网页,写一个批量转换目录的脚本,用假客户端看清 llm_client 和 llm_prompt 怎样把图片交给视觉大模型,处理 FileNotFoundError 和 UnsupportedFormatException,再照官方示例插件写一个 .srt 字幕插件并用 --use-plugins 启用。

图文25 分钟MarkItDown 官方文档 ↗

课程目录第 2 / 3 课

学习位置仅保存在当前浏览器,有效期 180 天。

01 / 图文教材

图文教材

本课对照的官方页面:MarkItDown README 的 Python API 部分 、官方示例插件 markitdown-sample-plugin 、markitdown-ocr 插件说明 。测试环境:2026-11-05,Linux x86_64 云主机,Python 3.13.5,markitdown 0.1.9。接入真实视觉大模型的部分没有实测,用一个假的客户端代替,课内注明了。

上一课在命令行里转换了一个个文件。这一课在 Python 里使用 MarkItDown:读取转换结果、转换内存里的数据、批量转换一个目录、给图片接上大模型描述,最后自己写一个插件,让 MarkItDown 认识新格式。下面的脚本都放在 ~/markitdown-lab/l2/ 里运行,用的是上一课下载到 ~/markitdown-lab/files/ 的测试文件。

mkdir -p ~/markitdown-lab/l2 && cd ~/markitdown-lab/l2
source ../.venv/bin/activate

第 1 步:最基本的调用

新建 basics.py:

from markitdown import MarkItDown

md = MarkItDown(enable_plugins=False)
result = md.convert("../files/test.xlsx")
print(type(result).__name__, result.title)
print(result.markdown.splitlines()[0])
print(len(result.markdown), "chars")

local = md.convert_local("../files/test.docx")
print("docx title:", local.title)

运行 python basics.py,对照结果:

DocumentConverterResult None
## Sheet1
808 chars
docx title: None

convert() 返回一个 DocumentConverterResult 对象,正文在 .markdown 里,.title 只有网页这类有标题的来源才有值。convert() 能接受本地路径、网址或文件流;convert_local() 只接受本地文件。

第 2 步:转换内存里的数据和自己下载的网页

数据不在磁盘上时(比如从数据库或接口拿到的字节),用 convert_stream()。流必须是二进制模式,并用 StreamInfo 告诉它扩展名。如果你自己用 requests 下载网页,可以把响应对象交给 convert_response()。新建 stream.py:

import io
import requests
from markitdown import MarkItDown, StreamInfo

md = MarkItDown()

# 1) 内存里的字节流:用 StreamInfo 告诉它扩展名
data = open("../files/test.pdf", "rb").read()
info = StreamInfo(extension=".pdf")
r1 = md.convert_stream(io.BytesIO(data), stream_info=info)
print("stream:", r1.markdown.split()[1])

# 2) 自己发请求,再交给 convert_response
resp = requests.get("https://example.com/", timeout=20)
r2 = md.convert_response(resp)
print("response title:", r2.title)

对照结果:

stream: Introduction
response title: Example Domain

传给 convert_stream() 的应是二进制流,例如 io.BytesIO,或用 "rb" 模式打开的文件。

第 3 步:批量转换一个目录

新建 batch.py,把 files/ 里的 Office 文件和 PDF 全部转成 .md,放进 md/ 目录:

from pathlib import Path
from markitdown import MarkItDown

md = MarkItDown()
src, out = Path("../files"), Path("md")
out.mkdir(exist_ok=True)
for f in sorted(src.iterdir()):
    if f.suffix not in {".docx", ".xlsx", ".pptx", ".pdf"}:
        continue
    text = md.convert_local(f).markdown
    (out / (f.stem + f.suffix.replace(".", "_") + ".md")).write_text(text)
    print(f"{f.name:12} -> {len(text):6} chars")

对照结果:

test.docx    ->   4651 chars
test.pdf     ->   5194 chars
test.pptx    ->   2047 chars
test.xlsx    ->    808 chars

输出文件名里保留了原扩展名(如 test_docx.md),这样同名的 .docx 和 .pdf 不会互相覆盖。

第 4 步:让大模型描述图片

给 MarkItDown() 传入 llm_client 和 llm_model 后,转换图片文件和 PowerPoint 时会把图片交给大模型,生成一段描述。官方 README 用的是 OpenAI 客户端:

from markitdown import MarkItDown
from openai import OpenAI

client = OpenAI(max_retries=5)
md = MarkItDown(llm_client=client, llm_model="你的视觉模型名",
                llm_prompt="用中文描述这张图,并抄出图里的文字。")
print(md.convert("../files/test_llm.jpg").markdown)

上面这段需要 API Key 和可用的视觉模型,测试机没有接真实模型(未实测)。官方示例里的模型名是 gpt-4o,你可以换成自己账号里当前可用的视觉模型。按官方说明,大模型描述目前只用于 PowerPoint 和图片文件;max_retries=5 让 OpenAI 客户端遇到可重试的错误时最多尝试 6 次。如果最后仍然失败,MarkItDown 会改用其他转换器,全部失败时才抛出 FileConversionException。

为了在没有 Key 的情况下看清 MarkItDown 发给模型的是什么,可以写一个假客户端。MarkItDown 只调用 client.chat.completions.create(model=..., messages=...),并读取 choices[0].message.content。新建 llm_stub.py:

from types import SimpleNamespace
from markitdown import MarkItDown

class FakeCompletions:
    def create(self, model, messages):
        parts = messages[0]["content"]
        print("model =", model)
        print("prompt =", parts[0]["text"])
        print("image =", parts[1]["image_url"]["url"][:23] + "...")
        msg = SimpleNamespace(content="(这里会是模型写的图片描述)")
        return SimpleNamespace(choices=[SimpleNamespace(message=msg)])

client = SimpleNamespace(chat=SimpleNamespace(completions=FakeCompletions()))
md = MarkItDown(llm_client=client, llm_model="your-vision-model",
                llm_prompt="用中文描述这张图,并抄出图里的文字。")
print(md.convert("../files/test_llm.jpg").markdown)

对照结果:

model = your-vision-model
prompt = 用中文描述这张图,并抄出图里的文字。
image = data:image/jpeg;base64,...

# Description:
(这里会是模型写的图片描述)

可以看到:图片被编码成 base64 的 data URI,和你的提示词一起发给模型;模型返回的文字放在 # Description: 下面。不传 llm_prompt 时,默认提示词是 Write a detailed caption for this image.。

如果要从 PDF、Word、PowerPoint、Excel 里嵌入的图片中识别文字,官方另有 markitdown-ocr 插件(pip install markitdown-ocr),也用同样的 llm_client 和 llm_model 参数。本课没有实测这个插件。注意:它的 README 里写的命令行参数 --llm-client、--llm-model,在 markitdown 0.1.9 的命令行里并不存在(运行 markitdown --help 可以确认),请改用上面的 Python 写法。

第 5 步:处理出错的情况

新建 safety.py:

from markitdown import MarkItDown

md = MarkItDown()
try:
    md.convert_local("https://example.com/")
except Exception as e:
    print(type(e).__name__)

open("note.bin", "wb").write(bytes(range(256)))
try:
    md.convert_local("note.bin")
except Exception as e:
    print(type(e).__name__)

对照结果:

FileNotFoundError
UnsupportedFormatException

convert_local() 不会去下载网址。如果你的程序要处理用户传来的路径,用它比 convert() 更安全,可以避免程序被引导去访问任意网址。认不出格式的文件会抛出 UnsupportedFormatException,批量转换时记得捕获这个异常。

第 6 步:写一个插件,让它认识字幕文件

MarkItDown 默认不认识 .srt 字幕,会把它当纯文本原样输出。官方用 markitdown-sample-plugin 演示了插件的写法:一个 Python 包,通过 markitdown.plugin 入口点注册,提供 __plugin_interface_version__ 和 register_converters()。照着这个结构写一个字幕插件:

cd ~/markitdown-lab
mkdir -p markitdown-srt/src/markitdown_srt && cd markitdown-srt

新建 pyproject.toml:

[project]
name = "markitdown-srt"
version = "0.1.0"
dependencies = ["markitdown>=0.1.9,<0.2"]

[project.entry-points."markitdown.plugin"]
srt = "markitdown_srt"

[build-system]
requires = ["hatchling"]
build-backend = "hatchling.build"

新建 src/markitdown_srt/__init__.py:

import re
from typing import Any, BinaryIO
from markitdown import (MarkItDown, DocumentConverter,
                        DocumentConverterResult, StreamInfo)

__plugin_interface_version__ = 1
TIME = re.compile(r"(\d\d:\d\d:\d\d),\d+ --> ")

class SrtConverter(DocumentConverter):
    def accepts(self, file_stream: BinaryIO, stream_info: StreamInfo,
                **kwargs: Any) -> bool:
        return (stream_info.extension or "").lower() == ".srt"

    def convert(self, file_stream: BinaryIO, stream_info: StreamInfo,
                **kwargs: Any) -> DocumentConverterResult:
        text = file_stream.read().decode(stream_info.charset or "utf-8")
        lines = []
        for block in text.strip().split("\n\n"):
            rows = block.strip().splitlines()
            m = TIME.match(rows[1]) if len(rows) > 2 else None
            if m:
                lines.append(f"- **{m.group(1)}** " + " ".join(rows[2:]))
        return DocumentConverterResult(markdown="\n".join(lines))

def register_converters(markitdown: MarkItDown, **kwargs):
    markitdown.register_converter(SrtConverter())

accepts() 决定这个转换器接不接这个文件,convert() 返回转换结果。再准备一个两条字幕的样例 demo.srt:

1
00:00:01,000 --> 00:00:03,500
大家好,欢迎来到昆仑学堂。

2
00:00:04,000 --> 00:00:07,200
今天我们用 MarkItDown
把字幕转成 Markdown。

用可编辑模式安装插件,然后确认 MarkItDown 能找到它:

pip install -e .
markitdown --list-plugins

对照结果:

Installed MarkItDown 3rd-party Plugins:

  * srt             	(package: markitdown_srt)

Use the -p (or --use-plugins) option to enable 3rd-party plugins.

插件默认不启用。对比一下加不加 --use-plugins 的区别:

markitdown demo.srt | head -2
markitdown --use-plugins demo.srt

对照结果:

1
00:00:01,000 --> 00:00:03,500
- **00:00:01** 大家好,欢迎来到昆仑学堂。
- **00:00:04** 今天我们用 MarkItDown 把字幕转成 Markdown。

前两行是没启用插件时的原样输出,后两行是插件转出来的列表。在 Python 里启用插件的写法是 MarkItDown(enable_plugins=True)。

小结

  • MarkItDown().convert() 返回结果对象,正文在 .markdown。
  • 内存数据用 convert_stream() 加 StreamInfo;自己下载的网页用 convert_response();只想读本地文件时用 convert_local()。
  • 传入 llm_client 和 llm_model 后,图片会交给大模型描述,提示词可以用 llm_prompt 改。
  • 插件是一个普通 Python 包:注册 markitdown.plugin 入口点,实现 accepts() 和 convert(),使用时加 --use-plugins。