diff --git a/README_zh.md b/README_zh.md new file mode 100644 index 000000000..12ec14e76 --- /dev/null +++ b/README_zh.md @@ -0,0 +1,465 @@ +# MarkItDown - 中文README + +
+ +[![PyPI](https://img.shields.io/pypi/v/markitdown.svg)](https://pypi.org/project/markitdown/) +![PyPI - Downloads](https://img.shields.io/pypi/dd/markitdown) +[![Built by AutoGen Team](https://img.shields.io/badge/Built%20by-AutoGen%20Team-blue)](https://github.com/microsoft/autogen) + +**将各种文档格式转换为 Markdown 的轻量级 Python 工具** + +
+ +--- + +## ⚠️ 重要安全提示 + +> MarkItDown 执行 I/O 操作时具有当前进程的权限。类似于 `open()` 或 `requests.get()`,它将访问进程本身可以访问的资源。 +> +> **请在不受信任的环境中对输入进行消毒处理,并调用最窄的 `convert_*` 函数以满足您的使用场景(例如 `convert_stream()` 或 `convert_local()`)。** + +详见 [安全考虑](#安全考虑) 部分。 + +--- + +## 简介 + +MarkItDown 是微软开源的轻量级 Python 工具,用于将各种文件格式转换为 Markdown,专为与大语言模型(LLM)和相关文本分析管道配合使用而设计。 + +### 为什么选择 Markdown? + +Markdown 极其接近纯文本,标记最少,但仍能表示重要的文档结构。主流 LLM(如 OpenAI 的 GPT-4o)原生"理解"Markdown,并且经常在回复中不加提示地使用 Markdown。这表明它们在大量 Markdown 格式的文本上进行了训练,并且理解得很好。此外,Markdown 约定也是高度 token 高效的。 + +### 支持的格式 + +MarkItDown 目前支持从以下格式转换: + +| 类别 | 支持的格式 | +|------|-----------| +| **办公文档** | PDF, Word (DOCX), PowerPoint (PPTX), Excel (XLSX/XLS) | +| **网页** | HTML, Wikipedia, YouTube (字幕), Bing 搜索结果 | +| **媒体** | 图片 (EXIF 元数据 + OCR), 音频 (EXIF 元数据 + 语音转录) | +| **文本格式** | CSV, JSON, XML, Jupyter Notebook (IPYNB) | +| **其他** | ZIP 文件 (遍历内容), EPUB 电子书, Outlook 消息 (MSG), RSS 订阅 | + +--- + +## 前置要求 + +MarkItDown 需要 Python 3.10 或更高版本。建议使用虚拟环境以避免依赖冲突。 + +### 创建虚拟环境 + +**标准 Python 安装:** +```bash +python -m venv .venv +source .venv/bin/activate +``` + +**使用 uv:** +```bash +uv venv --python=3.12 .venv +source .venv/bin/activate +# 注意:在此虚拟环境中安装包时请使用 'uv pip install' 而非 'pip install' +``` + +**使用 Anaconda:** +```bash +conda create -n markitdown python=3.12 +conda activate markitdown +``` + +--- + +## 安装 + +### 从 PyPI 安装 + +安装所有可选依赖(推荐): +```bash +pip install 'markitdown[all]' +``` + +或仅安装特定格式的依赖: +```bash +pip install 'markitdown[pdf, docx, pptx]' +``` + +### 从源码安装 + +```bash +git clone git@github.com:microsoft/markitdown.git +cd markitdown +pip install -e 'packages/markitdown[all]' +``` + +--- + +## 使用方法 + +### 命令行使用 + +#### 基本转换 + +```bash +# 转换文件并输出到 stdout +markitdown path-to-file.pdf + +# 转换文件并保存到指定文件 +markitdown path-to-file.pdf -o document.md + +# 使用重定向 +markitdown path-to-file.pdf > document.md + +# 从 stdin 读取 +cat path-to-file.pdf | markitdown +``` + +#### 命令行选项 + +```bash +# 查看版本 +markitdown --version + +# 查看帮助 +markitdown --help + +# 提供文件扩展名提示(当从 stdin 读取时) +markitdown --extension .pdf + +# 提供 MIME 类型提示 +markitdown --mime-type "application/pdf" + +# 提供字符编码提示 +markitdown --charset UTF-8 + +# 保留 data URI(如 base64 编码的图片) +markitdown --keep-data-uris path-to-file.docx +``` + +#### 使用 Azure 文档智能 + +```bash +# 使用 Azure Document Intelligence 进行更精确的转换 +markitdown path-to-file.pdf -o document.md -d -e "" +``` + +#### 插件管理 + +```bash +# 列出已安装的插件 +markitdown --list-plugins + +# 使用插件进行转换 +markitdown --use-plugins path-to-file.pdf +``` + +### Python API 使用 + +#### 基本用法 + +```python +from markitdown import MarkItDown + +md = MarkItDown(enable_plugins=False) # 设置为 True 以启用插件 +result = md.convert("test.xlsx") +print(result.text_content) +``` + +#### 转换结果对象 + +```python +result = md.convert("document.pdf") + +# 获取转换后的 Markdown 内容 +print(result.markdown) # 推荐使用 +print(result.text_content) # 已弃用的别名 + +# 获取文档标题(如果有) +print(result.title) +``` + +#### 多种输入源 + +```python +from markitdown import MarkItDown, StreamInfo +import io +import requests + +md = MarkItDown() + +# 1. 本地文件路径 +result = md.convert("/path/to/document.pdf") +result = md.convert_local("/path/to/document.pdf") + +# 2. URL +result = md.convert("https://example.com/document.pdf") +result = md.convert_uri("https://example.com/document.pdf") + +# 3. 文件 URI +result = md.convert("file:///path/to/document.pdf") + +# 4. Data URI +data_uri = "data:text/plain;base64,SGVsbG8sIFdvcmxkIQ==" +result = md.convert(data_uri) + +# 5. requests.Response 对象 +response = requests.get("https://example.com/document.pdf") +result = md.convert(response) +result = md.convert_response(response) + +# 6. 二进制流 +with open("document.pdf", "rb") as f: + result = md.convert_stream(f) + +# 或使用 BytesIO +pdf_data = io.BytesIO(...) +result = md.convert_stream( + pdf_data, + stream_info=StreamInfo(extension=".pdf") +) +``` + +#### 使用 LLM 进行图像描述 + +```python +from markitdown import MarkItDown +from openai import OpenAI + +client = OpenAI() +md = MarkItDown( + llm_client=client, + llm_model="gpt-4o", + llm_prompt="自定义提示词(可选)" +) + +# 转换包含图片的文件,LLM 将生成图片描述 +result = md.convert("example.pptx") +print(result.text_content) + +# 转换图片文件 +result = md.convert("example.jpg") +print(result.text_content) +``` + +#### 使用 Azure 文档智能 + +```python +from markitdown import MarkItDown + +md = MarkItDown(docintel_endpoint="") +result = md.convert("test.pdf") +print(result.text_content) +``` + +--- + +## 可选依赖详解 + +MarkItDown 使用可选依赖来激活各种文件格式支持。 + +### 可用的依赖组 + +| 依赖组 | 包含的功能 | +|--------|-----------| +| `[all]` | 所有可选依赖(推荐) | +| `[pptx]` | PowerPoint 文件支持 | +| `[docx]` | Word 文件支持 | +| `[xlsx]` | Excel 2007+ 文件支持 | +| `[xls]` | 旧版 Excel 文件支持 | +| `[pdf]` | PDF 文件支持 | +| `[outlook]` | Outlook 消息 (.msg) 支持 | +| `[audio-transcription]` | WAV/MP3 音频转录 | +| `[youtube-transcription]` | YouTube 字幕获取 | +| `[az-doc-intel]` | Azure 文档智能支持 | + +### 安装示例 + +```bash +# 仅安装 PDF 和 Word 支持 +pip install 'markitdown[pdf, docx]' + +# 安装所有依赖 +pip install 'markitdown[all]' +``` + +--- + +## 插件系统 + +### 什么是插件 + +MarkItDown 支持第三方插件扩展其功能。插件通过 Python 的 `entry_points` 机制自动发现。 + +### markitdown-ocr 插件 + +`markitdown-ocr` 插件为 PDF、DOCX、PPTX 和 XLSX 转换器添加 OCR 支持,使用 LLM Vision 从嵌入图片中提取文本。 + +#### 安装 + +```bash +pip install markitdown-ocr +pip install openai # 或任何 OpenAI 兼容的客户端 +``` + +#### 使用 + +```python +from markitdown import MarkItDown +from openai import OpenAI + +md = MarkItDown( + enable_plugins=True, + llm_client=OpenAI(), + llm_model="gpt-4o", +) +result = md.convert("document_with_images.pdf") +print(result.text_content) +``` + +> 如果未提供 `llm_client`,插件仍会加载,但 OCR 会被静默跳过,转而使用标准的内置转换器。 + +### 查找和开发插件 + +- 在 GitHub 上搜索标签 `#markitdown-plugin` 查找可用插件 +- 参考 `packages/markitdown-sample-plugin` 开发自己的插件 + +--- + +## Docker 使用 + +### 构建镜像 + +```bash +docker build -t markitdown:latest . +``` + +### 运行容器 + +```bash +# 转换本地文件 +docker run --rm -i markitdown:latest < ~/your-file.pdf > output.md +``` + +--- + +## 安全考虑 + +### 输入消毒 + +**不要直接将不受信任的输入传递给 MarkItDown。** 如果输入的任何部分可能由不受信任的用户或系统控制(例如在托管或服务器端应用程序中),则必须在调用 MarkItDown 之前对其进行验证和限制。 + +根据您的环境,这可能包括: +- 限制文件路径范围 +- 限制 URI 方案和网络目标 +- 阻止访问私有、回环、链路本地或元数据服务地址 + +### 使用合适的 API + +优先选择最符合您使用场景的狭窄转换 API: + +| API | 用途 | 安全性 | +|-----|------|--------| +| `convert()` | 最宽泛,接受本地文件、远程 URL、流 | ⚠️ 最宽泛 | +| `convert_local()` | 仅本地文件 | ⚠️ 仍需路径验证 | +| `convert_stream()` | 仅二进制流 | ✅ 最可控 | +| `convert_response()` | 仅 requests.Response | ✅ 您控制请求 | + +**示例:** +```python +# ❌ 危险:用户输入可能是恶意路径或 URL +md = MarkItDown() +result = md.convert(user_input) + +# ✅ 安全:使用狭窄的 API 并自己控制获取 +import requests + +# 对于 HTTP 资源 +response = requests.get( + url, + timeout=10, + allow_redirects=False # 控制重定向 +) +result = md.convert_response(response) + +# 对于本地文件(先验证路径) +import os + +# 确保路径在允许的目录内 +allowed_dir = "/allowed/directory" +full_path = os.path.abspath(os.path.join(allowed_dir, user_input)) + +if not full_path.startswith(allowed_dir): + raise ValueError("Invalid path") + +result = md.convert_local(full_path) +``` + +--- + +## 故障排除 + +### 常见问题 + +**Q: 转换 PDF 时出现 `MissingDependencyException`?** + +A: 确保安装了 PDF 依赖: +```bash +pip install 'markitdown[pdf]' +# 或 +pip install 'markitdown[all]' +``` + +**Q: 转换 DOCX 时数学公式不显示?** + +A: MarkItDown 支持将 OMML(Office Math Markup Language)转换为 LaTeX 格式。确保安装了完整依赖: +```bash +pip install 'markitdown[docx]' +``` + +**Q: 如何处理深度嵌套的 HTML?** + +A: MarkItDown 会自动处理。当 HTML 嵌套过深导致 `RecursionError` 时,它会回退到纯文本提取模式,并发出警告。 + +**Q: 如何保留图片的 base64 编码?** + +A: 使用 `keep_data_uris=True` 参数: +```python +# Python API +result = md.convert("document.docx", keep_data_uris=True) + +# 命令行 +markitdown --keep-data-uris document.docx +``` + +--- + +## 项目结构 + +``` +markitdown/ +├── packages/ +│ ├── markitdown/ # 核心包 +│ │ ├── src/markitdown/ +│ │ │ ├── _markitdown.py # 核心 MarkItDown 类 +│ │ │ ├── _base_converter.py # 转换器基类 +│ │ │ ├── converters/ # 各种格式转换器 +│ │ │ └── ... +│ │ └── tests/ # 测试文件 +│ ├── markitdown-ocr/ # OCR 插件 +│ ├── markitdown-sample-plugin/ # 示例插件 +│ └── markitdown-mcp/ # MCP 支持 +└── README.md +``` + +--- + +## 许可证 + +本项目采用 MIT 许可证。详见 [LICENSE](LICENSE) 文件。 + +--- + +## 商标 + +本项目可能包含项目、产品或服务的商标或徽标。Microsoft 商标或徽标的授权使用必须遵守 [Microsoft 商标和品牌指南](https://www.microsoft.com/en-us/legal/intellectualproperty/trademarks/usage/general)。使用第三方商标或徽标需遵守这些第三方的政策。 diff --git "a/\345\210\206\346\236\220\346\212\245\345\221\212.md" "b/\345\210\206\346\236\220\346\212\245\345\221\212.md" new file mode 100644 index 000000000..a1113d862 --- /dev/null +++ "b/\345\210\206\346\236\220\346\212\245\345\221\212.md" @@ -0,0 +1,909 @@ +# MarkItDown 项目分析报告 + +## 1. 项目概述 + +### 1.1 项目简介 +**MarkItDown** 是微软开源的一款轻量级 Python 工具,专门用于将各种文件格式转换为 Markdown 格式。该项目由 AutoGen 团队开发,主要设计用于与大语言模型(LLM)和相关文本分析管道配合使用。 + +### 1.2 项目特点 +- **Markdown 优先**:专注于保留文档结构(标题、列表、表格、链接等)转换为 Markdown +- **LLM 友好**:Markdown 格式非常适合 LLM 处理,因为: + - 接近纯文本,标记最少 + - 主流 LLM(如 GPT-4o)原生理解 Markdown + - 高度 token 高效 +- **插件化架构**:支持第三方插件扩展功能 + +### 1.3 项目许可证 +- **许可证类型**:MIT License +- **版权方**:Microsoft Corporation + +--- + +## 2. 项目架构分析 + +### 2.1 目录结构 + +``` +markitdown/ +├── packages/ +│ ├── markitdown/ # 核心包 +│ │ ├── src/markitdown/ +│ │ │ ├── __init__.py # 导出核心类和函数 +│ │ │ ├── __main__.py # CLI 入口 +│ │ │ ├── __about__.py # 版本信息 +│ │ │ ├── _markitdown.py # 核心 MarkItDown 类 +│ │ │ ├── _base_converter.py # 转换器基类 +│ │ │ ├── _stream_info.py # 流信息数据类 +│ │ │ ├── _exceptions.py # 异常定义 +│ │ │ ├── _uri_utils.py # URI 处理工具 +│ │ │ ├── converters/ # 各种格式转换器 +│ │ │ │ ├── __init__.py +│ │ │ │ ├── _pdf_converter.py +│ │ │ │ ├── _docx_converter.py +│ │ │ │ ├── _pptx_converter.py +│ │ │ │ ├── _xlsx_converter.py +│ │ │ │ ├── _html_converter.py +│ │ │ │ ├── _image_converter.py +│ │ │ │ ├── _audio_converter.py +│ │ │ │ ├── _zip_converter.py +│ │ │ │ ├── _epub_converter.py +│ │ │ │ ├── _csv_converter.py +│ │ │ │ ├── _wikipedia_converter.py +│ │ │ │ ├── _youtube_converter.py +│ │ │ │ ├── _rss_converter.py +│ │ │ │ ├── _ipynb_converter.py +│ │ │ │ ├── _bing_serp_converter.py +│ │ │ │ ├── _outlook_msg_converter.py +│ │ │ │ ├── _doc_intel_converter.py +│ │ │ │ └── _markdownify.py # 自定义 Markdown 转换 +│ │ │ └── converter_utils/ # 转换器工具 +│ │ │ └── docx/ # DOCX 特定工具 +│ │ ├── tests/ # 测试文件 +│ │ │ ├── test_files/ # 测试用文件 +│ │ │ ├── _test_vectors.py # 测试向量定义 +│ │ │ ├── test_module_vectors.py # 模块级测试 +│ │ │ ├── test_module_misc.py # 杂项模块测试 +│ │ │ ├── test_cli_vectors.py # CLI 测试 +│ │ │ └── test_cli_misc.py # 杂项 CLI 测试 +│ │ └── pyproject.toml # 包配置 +│ │ +│ ├── markitdown-ocr/ # OCR 插件包 +│ │ ├── src/markitdown_ocr/ +│ │ │ ├── __init__.py +│ │ │ ├── __about__.py +│ │ │ ├── _plugin.py # 插件注册 +│ │ │ ├── _ocr_service.py # OCR 服务 +│ │ │ ├── _pdf_converter_with_ocr.py +│ │ │ ├── _docx_converter_with_ocr.py +│ │ │ ├── _pptx_converter_with_ocr.py +│ │ │ └── _xlsx_converter_with_ocr.py +│ │ └── pyproject.toml +│ │ +│ ├── markitdown-sample-plugin/ # 示例插件包 +│ │ ├── src/markitdown_sample_plugin/ +│ │ │ ├── __init__.py +│ │ │ ├── __about__.py +│ │ │ └── _plugin.py # RTF 转换器示例 +│ │ └── pyproject.toml +│ │ +│ └── markitdown-mcp/ # MCP 相关包 +│ ├── src/markitdown_mcp/ +│ └── pyproject.toml +│ +├── README.md # 主 README +└── Dockerfile # Docker 配置 +``` + +### 2.2 核心类关系图 + +``` +┌─────────────────────────────────────────────────────────────────┐ +│ MarkItDown (主类) │ +├─────────────────────────────────────────────────────────────────┤ +│ 属性: │ +│ - _converters: List[ConverterRegistration] # 已注册的转换器列表 │ +│ - _requests_session: requests.Session # HTTP 会话 │ +│ - _magika: magika.Magika # 文件类型检测 │ +│ - _llm_client, _llm_model, _llm_prompt # LLM 相关配置 │ +├─────────────────────────────────────────────────────────────────┤ +│ 主要方法: │ +│ - convert() # 统一转换入口 │ +│ - convert_local() # 转换本地文件 │ +│ - convert_stream() # 转换流数据 │ +│ - convert_uri() # 转换 URI (http/https/file/data) │ +│ - convert_response() # 转换 requests.Response │ +│ - register_converter() # 注册新的转换器 │ +│ - enable_builtins() # 启用内置转换器 │ +│ - enable_plugins() # 启用插件 │ +└─────────────────────────────────────────────────────────────────┘ + │ + │ 包含 + ▼ +┌─────────────────────────────────────────────────────────────────┐ +│ ConverterRegistration (注册信息) │ +├─────────────────────────────────────────────────────────────────┤ +│ - converter: DocumentConverter # 转换器实例 │ +│ - priority: float # 优先级 (越小越高) │ +└─────────────────────────────────────────────────────────────────┘ + │ + │ 继承 + ▼ +┌─────────────────────────────────────────────────────────────────┐ +│ DocumentConverter (转换器基类) │ +├─────────────────────────────────────────────────────────────────┤ +│ 抽象方法: │ +│ - accepts(file_stream, stream_info, **kwargs) -> bool │ +│ # 判断是否接受该文件类型 │ +│ │ +│ - convert(file_stream, stream_info, **kwargs) │ +│ -> DocumentConverterResult │ +│ # 执行转换,返回转换结果 │ +└─────────────────────────────────────────────────────────────────┘ + │ + ┌─────────────────────┼─────────────────────┐ + │ │ │ + ▼ ▼ ▼ +┌─────────────────┐ ┌─────────────────┐ ┌─────────────────┐ +│ PdfConverter │ │ DocxConverter │ │ HtmlConverter │ +│ (PDF转换) │ │ (Word文档转换) │ │ (HTML转换) │ +├─────────────────┤ ├─────────────────┤ ├─────────────────┤ +│ 使用 pdfminer │ │ 使用 mammoth │ │ 使用 BeautifulSoup│ +│ 和 pdfplumber │ │ 转换为HTML │ │ 和 markdownify │ +│ 提取文本和表格 │ │ 再转Markdown │ │ 转换为Markdown │ +└─────────────────┘ └─────────────────┘ └─────────────────┘ + │ │ │ + └─────────────────────┼─────────────────────┘ + ▼ +┌─────────────────────────────────────────────────────────────────┐ +│ DocumentConverterResult (转换结果) │ +├─────────────────────────────────────────────────────────────────┤ +│ 属性: │ +│ - markdown: str # 转换后的 Markdown 内容 │ +│ - title: Optional[str] # 文档标题 (可选) │ +│ - text_content: str # 已弃用,markdown 的别名 │ +└─────────────────────────────────────────────────────────────────┘ +``` + +--- + +## 3. 数据处理流程 + +### 3.1 整体转换流程 + +``` +┌──────────────┐ ┌──────────────┐ ┌──────────────────┐ +│ 输入源 │───▶│ 文件类型识别 │───▶│ 选择合适转换器 │ +└──────────────┘ └──────────────┘ └──────────────────┘ + │ │ │ + │ │ ▼ + │ │ ┌──────────────────┐ + │ │ │ 执行转换 │ + │ │ │ (Converter) │ + │ │ └──────────────────┘ + │ │ │ + │ │ ▼ + │ │ ┌──────────────────┐ + │ │ │ 输出 Markdown │ + │ │ └──────────────────┘ + │ │ + ▼ ▼ +┌─────────────────────────────────────────────────────────────┐ +│ 输入源类型 │ +├─────────────────────────────────────────────────────────────┤ +│ 1. 本地文件路径 (str/Path) │ +│ └──▶ convert_local() │ +│ │ +│ 2. URI (http://, https://, file://, data:...) │ +│ └──▶ convert_uri() │ +│ │ +│ 3. requests.Response 对象 │ +│ └──▶ convert_response() │ +│ │ +│ 4. 二进制流 (BinaryIO) │ +│ └──▶ convert_stream() │ +└─────────────────────────────────────────────────────────────┘ +``` + +### 3.2 文件类型识别机制 + +MarkItDown 使用多重机制识别文件类型: + +#### 3.2.1 识别策略 + +```python +# 优先级从高到低: +# 1. 显式提供的扩展名 (file_extension 参数) +# 2. URL/文件名中的扩展名 +# 3. Content-Type 头部的 MIME 类型 +# 4. magika 库的内容检测 (基于文件内容) +# 5. mimetypes 库的扩展名映射 +``` + +#### 3.2.2 Magika 集成 + +项目使用 Google 的 `magika` 库进行文件类型检测: + +```python +# 位于 _markitdown.py:120 +self._magika = magika.Magika() + +# 检测逻辑 (位于 _get_stream_info_guesses 方法) +result = self._magika.identify_stream(file_stream) +if result.status == "ok" and result.prediction.output.label != "unknown": + # 获取检测到的: + # - mime_type: MIME 类型 + # - extensions: 可能的扩展名列表 + # - is_text: 是否为文本文件 +``` + +#### 3.2.3 StreamInfo 数据结构 + +```python +@dataclass(kw_only=True, frozen=True) +class StreamInfo: + """存储文件流的元数据信息""" + mimetype: Optional[str] = None # MIME 类型 + extension: Optional[str] = None # 文件扩展名 + charset: Optional[str] = None # 字符编码 + filename: Optional[str] = None # 文件名 + local_path: Optional[str] = None # 本地路径 + url: Optional[str] = None # 来源URL +``` + +### 3.3 转换器选择与执行流程 + +#### 3.3.1 转换器优先级系统 + +```python +# 优先级定义 (_markitdown.py:53-59) +PRIORITY_SPECIFIC_FILE_FORMAT = 0.0 # 特定格式 (最高优先级) +PRIORITY_GENERIC_FILE_FORMAT = 10.0 # 通用格式 (较低优先级) + +# 注册顺序 (后注册的同优先级转换器优先尝试) +# 位于 enable_builtins() 方法 +self.register_converter(PlainTextConverter(), priority=PRIORITY_GENERIC_FILE_FORMAT) +self.register_converter(ZipConverter(...), priority=PRIORITY_GENERIC_FILE_FORMAT) +self.register_converter(HtmlConverter(), priority=PRIORITY_GENERIC_FILE_FORMAT) +self.register_converter(RssConverter()) # priority=0.0 +self.register_converter(WikipediaConverter()) # priority=0.0 +self.register_converter(YouTubeConverter()) # priority=0.0 +self.register_converter(BingSerpConverter()) # priority=0.0 +self.register_converter(DocxConverter()) # priority=0.0 +self.register_converter(XlsxConverter()) # priority=0.0 +self.register_converter(XlsConverter()) # priority=0.0 +self.register_converter(PptxConverter()) # priority=0.0 +self.register_converter(AudioConverter()) # priority=0.0 +self.register_converter(ImageConverter()) # priority=0.0 +self.register_converter(IpynbConverter()) # priority=0.0 +self.register_converter(PdfConverter()) # priority=0.0 +self.register_converter(OutlookMsgConverter())# priority=0.0 +self.register_converter(EpubConverter()) # priority=0.0 +self.register_converter(CsvConverter()) # priority=0.0 +``` + +#### 3.3.2 转换执行流程 + +``` +┌─────────────────────────────────────────────────────────────────┐ +│ _convert() 核心方法 │ +└─────────────────────────────────────────────────────────────────┘ + │ + ▼ +┌─────────────────────────────────────────────────────────────────┐ +│ 1. 按优先级排序转换器 │ +│ sorted_registrations = sorted(self._converters, │ +│ key=lambda x: x.priority) │ +└─────────────────────────────────────────────────────────────────┘ + │ + ▼ +┌─────────────────────────────────────────────────────────────────┐ +│ 2. 遍历所有 stream_info 猜测 + [空 StreamInfo] │ +│ for stream_info in stream_info_guesses + [StreamInfo()]: │ +└─────────────────────────────────────────────────────────────────┘ + │ + ▼ +┌─────────────────────────────────────────────────────────────────┐ +│ 3. 遍历每个转换器: │ +│ for converter_registration in sorted_registrations: │ +│ converter = converter_registration.converter │ +└─────────────────────────────────────────────────────────────────┘ + │ + ▼ + ┌─────────────────────────────────┐ + │ 4. 调用 converter.accepts() │ + │ 判断是否接受该文件类型 │ + └─────────────────────────────────┘ + │ + ┌─────────────────┴─────────────────┐ + │ │ + ▼ ▼ + ┌──────────────┐ ┌──────────────┐ + │ 返回 True │ │ 返回 False │ + │ (接受) │ │ (不接受) │ + └──────────────┘ └──────────────┘ + │ │ + ▼ ▼ +┌─────────────────────────┐ 跳过此转换器 +│ 5. 调用 converter. │ +│ convert() 进行转换 │ +└─────────────────────────┘ + │ + ┌───────┴───────┐ + │ │ + ▼ ▼ +┌──────────┐ ┌──────────────┐ +│ 成功 │ │ 抛出异常 │ +│ 返回结果 │ │ 记录失败尝试 │ +└──────────┘ └──────────────┘ + │ │ + ▼ ▼ +┌──────────┐ 继续尝试下一个转换器 +│ 返回结果 │ +└──────────┘ +``` + +### 3.4 异常处理机制 + +#### 3.4.1 异常类层次结构 + +``` +MarkItDownException (基类) +│ +├── MissingDependencyException +│ # 当转换器缺少可选依赖时抛出 +│ # 例如:尝试转换 PDF 但未安装 pdfminer +│ +├── UnsupportedFormatException +│ # 当没有找到合适的转换器时抛出 +│ # 例如:尝试转换 .xyz 未知格式文件 +│ +└── FileConversionException + # 当找到转换器但转换失败时抛出 + # 包含所有失败尝试的详细信息 + # 属性: attempts (List[FailedConversionAttempt]) +``` + +#### 3.4.2 FailedConversionAttempt 类 + +```python +class FailedConversionAttempt(object): + """记录单次失败的转换尝试""" + def __init__(self, converter: Any, exc_info: Optional[tuple] = None): + self.converter = converter # 失败的转换器实例 + self.exc_info = exc_info # 异常信息 (type, value, traceback) +``` + +--- + +## 4. 模块协作机制 + +### 4.1 核心模块职责 + +| 模块 | 主要职责 | 关键文件 | +|------|---------|---------| +| **MarkItDown 主类** | 管理转换器注册、协调转换流程、处理输入源 | `_markitdown.py` | +| **转换器基类** | 定义转换器接口规范 | `_base_converter.py` | +| **流信息** | 存储文件元数据,支持类型识别 | `_stream_info.py` | +| **异常定义** | 统一异常类型,便于错误处理 | `_exceptions.py` | +| **URI 工具** | 解析各种 URI 格式 | `_uri_utils.py` | +| **PDF 转换器** | 提取 PDF 文本和表格 | `_pdf_converter.py` | +| **DOCX 转换器** | 转换 Word 文档 | `_docx_converter.py` | +| **HTML 转换器** | 转换 HTML 为 Markdown | `_html_converter.py` | +| **ZIP 转换器** | 递归处理压缩包内容 | `_zip_converter.py` | + +### 4.2 插件系统架构 + +#### 4.2.1 插件发现机制 + +MarkItDown 使用 Python 的 `entry_points` 机制发现插件: + +```python +# 位于 pyproject.toml (插件包) +[project.entry-points."markitdown.plugin"] +sample_plugin = "markitdown_sample_plugin" # 示例插件 +ocr = "markitdown_ocr" # OCR 插件 + +# 插件加载逻辑 (_markitdown.py:65-82) +def _load_plugins() -> Union[None, List[Any]]: + """懒加载插件""" + global _plugins + + if _plugins is not None: + return _plugins + + _plugins = [] + for entry_point in entry_points(group="markitdown.plugin"): + try: + _plugins.append(entry_point.load()) + except Exception: + warn(f"Plugin '{entry_point.name}' failed to load...") + + return _plugins +``` + +#### 4.2.2 插件接口规范 + +插件必须实现 `register_converters` 函数: + +```python +# 示例插件 (markitdown-sample-plugin/_plugin.py) +__plugin_interface_version__ = 1 # 插件接口版本 + +def register_converters(markitdown: MarkItDown, **kwargs): + """ + 在 MarkItDown 实例构造时调用,用于注册插件提供的转换器。 + """ + markitdown.register_converter(RtfConverter()) +``` + +#### 4.2.3 插件启用流程 + +```python +# 位于 _markitdown.py:232-250 +def enable_plugins(self, **kwargs) -> None: + """启用并注册插件提供的转换器""" + if not self._plugins_enabled: + # 加载插件 + plugins = _load_plugins() + + for plugin in plugins: + try: + # 调用插件的 register_converters 方法 + plugin.register_converters(self, **kwargs) + except Exception: + warn(f"Plugin '{plugin}' failed to register converters") + + self._plugins_enabled = True +``` + +### 4.3 转换器间协作 + +#### 4.3.1 HTML 转换器作为中间层 + +某些转换器将 HTML 作为中间格式: + +```python +# DocxConverter (_docx_converter.py:58-83) +def convert(self, file_stream, stream_info, **kwargs): + # 1. 使用 mammoth 将 DOCX 转换为 HTML + pre_process_stream = pre_process_docx(file_stream) + html_content = mammoth.convert_to_html( + pre_process_stream, + style_map=style_map + ).value + + # 2. 复用 HtmlConverter 将 HTML 转换为 Markdown + return self._html_converter.convert_string(html_content, **kwargs) +``` + +#### 4.3.2 ZIP 转换器递归处理 + +ZIP 转换器可以递归处理压缩包内的文件: + +```python +# ZipConverter (_zip_converter.py:87-116) +def convert(self, file_stream, stream_info, **kwargs): + with zipfile.ZipFile(file_stream, "r") as zipObj: + for name in zipObj.namelist(): + # 读取压缩包内的文件 + z_file_stream = io.BytesIO(zipObj.read(name)) + + # 构建 StreamInfo + z_file_stream_info = StreamInfo( + extension=os.path.splitext(name)[1], + filename=os.path.basename(name), + ) + + # 递归调用 MarkItDown 进行转换 + # 使用 _parent_converters 中的转换器 + result = self._markitdown.convert_stream( + stream=z_file_stream, + stream_info=z_file_stream_info, + ) +``` + +--- + +## 5. 技术依赖分析 + +### 5.1 核心依赖 (必需) + +| 依赖包 | 用途 | 版本要求 | +|--------|------|---------| +| `beautifulsoup4` | HTML 解析和操作 | 无 | +| `requests` | HTTP 请求 (获取远程资源) | 无 | +| `markdownify` | HTML 转 Markdown | 无 | +| `magika` | 文件类型检测 (Google 开源) | ~=0.6.1 | +| `charset-normalizer` | 字符编码检测 | 无 | +| `defusedxml` | 安全 XML 解析 | 无 | + +### 5.2 可选依赖 (按功能分组) + +| 功能组 | 依赖包 | 用途 | +|--------|--------|------| +| **PDF 处理** | `pdfminer.six`, `pdfplumber` | PDF 文本和表格提取 | +| **DOCX 处理** | `mammoth`, `lxml` | Word 文档转换 | +| **PPTX 处理** | `python-pptx` | PowerPoint 处理 | +| **XLSX 处理** | `pandas`, `openpyxl` | Excel 2007+ 处理 | +| **XLS 处理** | `pandas`, `xlrd` | 旧版 Excel 处理 | +| **Outlook** | `olefile` | Outlook .msg 文件处理 | +| **音频转录** | `pydub`, `SpeechRecognition` | WAV/MP3 语音转文本 | +| **YouTube** | `youtube-transcript-api` | YouTube 字幕获取 | +| **Azure 文档智能** | `azure-ai-documentintelligence`, `azure-identity` | 云端文档分析 | + +### 5.3 安装选项 + +```bash +# 安装所有可选依赖 (推荐) +pip install 'markitdown[all]' + +# 仅安装特定格式的依赖 +pip install 'markitdown[pdf,docx,pptx]' + +# 可用的可选依赖组: +# - [all] - 所有可选依赖 +# - [pptx] - PowerPoint +# - [docx] - Word +# - [xlsx] - Excel 2007+ +# - [xls] - 旧版 Excel +# - [pdf] - PDF +# - [outlook] - Outlook 消息 +# - [audio-transcription] - 音频转录 +# - [youtube-transcription] - YouTube 字幕 +# - [az-doc-intel] - Azure 文档智能 +``` + +--- + +## 6. 关键算法与实现细节 + +### 6.1 PDF 表格提取算法 + +#### 6.1.1 双引擎策略 + +PDF 转换器使用两种提取策略: + +```python +# 策略 1: pdfplumber (优先,更好的表格支持) +with pdfplumber.open(pdf_bytes) as pdf: + for page_idx, page in enumerate(pdf.pages): + # 检查是否为表单/表格样式内容 + page_content = _extract_form_content_from_words(page) + + if page_content is not None: + # 使用位置分析提取表格 + markdown_chunks.append(page_content) + else: + # 普通文本提取 + text = page.extract_text() + +# 策略 2: pdfminer (回退,更好的文本间距) +markdown = pdfminer.high_level.extract_text(pdf_bytes) +``` + +#### 6.1.2 无边界表格检测算法 + +```python +def _extract_form_content_from_words(page: Any) -> str | None: + """ + 通过分析单词位置提取无边界表格 + """ + # 1. 提取所有单词及其位置 + words = page.extract_words(keep_blank_chars=True, x_tolerance=3, y_tolerance=3) + + # 2. 按 Y 坐标分组 (行) + y_tolerance = 5 + rows_by_y: dict[float, list[dict]] = {} + for word in words: + y_key = round(word["top"] / y_tolerance) * y_tolerance + # ... 分组逻辑 + + # 3. 分析 X 坐标分布,识别列边界 + all_table_x_positions = [] + for info in row_info: + if info["num_columns"] >= 3 and not info["is_paragraph"]: + all_table_x_positions.extend(info["x_groups"]) + + # 4. 聚类分析确定全局列结构 + global_columns: list[float] = [] + for x in all_table_x_positions: + if not global_columns or x - global_columns[-1] > adaptive_tolerance: + global_columns.append(x) + + # 5. 将单词分配到对应列,生成 Markdown 表格 +``` + +### 6.2 HTML 转 Markdown 策略 + +#### 6.2.1 自定义 Markdownify 类 + +```python +# 基于 markdownify 库,自定义处理逻辑 +class _CustomMarkdownify(MarkdownConverter): + """ + 自定义的 HTML 转 Markdown 转换器 + - 处理深度嵌套 HTML 的递归限制 + - 特殊标签处理 (图片、链接、表格等) + """ +``` + +#### 6.2.2 深度嵌套 HTML 的回退机制 + +```python +# 位于 HtmlConverter.convert() 方法 +try: + # 尝试使用 markdownify 进行递归转换 + webpage_text = _CustomMarkdownify(**kwargs).convert_soup(body_elm) +except RecursionError: + # 当 HTML 嵌套过深时,回退到纯文本提取 + warnings.warn( + "HTML document is too deeply nested for markdown conversion " + "(RecursionError). Falling back to plain-text extraction." + ) + # 使用 BeautifulSoup 的 get_text() 迭代提取 + webpage_text = target.get_text("\n", strip=True) +``` + +### 6.3 LLM 集成机制 + +#### 6.3.1 图像描述生成 + +```python +# 位于 _llm_caption.py +def llm_caption( + image_stream: BinaryIO, + stream_info: StreamInfo, + client: Any, # OpenAI 兼容客户端 + model: str, # 模型名称 (如 "gpt-4o") + prompt: Optional[str] = None, +) -> str: + """ + 使用 LLM Vision 模型生成图像描述 + """ + # 默认提示词 + default_prompt = ( + "Provide a concise description of the image. " + "Focus on details that are not obvious from the surrounding context." + ) + + # 调用 LLM API + response = client.chat.completions.create( + model=model, + messages=[ + { + "role": "user", + "content": [ + {"type": "text", "text": prompt or default_prompt}, + {"type": "image_url", "image_url": {"url": data_url}} + ] + } + ], + max_tokens=300, + ) + + return response.choices[0].message.content +``` + +#### 6.3.2 PPTX 中的图像处理 + +```python +# 位于 PptxConverter.get_shape_content() +if self._is_picture(shape): + # 1. 尝试使用 LLM 生成描述 + llm_client = kwargs.get("llm_client") + llm_model = kwargs.get("llm_model") + + if llm_client is not None and llm_model is not None: + llm_description = llm_caption( + image_stream, + image_stream_info, + client=llm_client, + model=llm_model, + prompt=kwargs.get("llm_prompt"), + ) + + # 2. 同时获取嵌入的 alt 文本 + try: + alt_text = shape._element._nvXxPr.cNvPr.attrib.get("descr", "") + except Exception: + pass + + # 3. 组合生成 Markdown 图片语法 + alt_text = "\n".join([llm_description, alt_text]) or shape.name +``` + +--- + +## 7. 安全考虑 + +### 7.1 安全警告 + +项目 README 明确指出: + +> MarkItDown 执行 I/O 操作时具有当前进程的权限。类似于 `open()` 或 `requests.get()`,它将访问进程本身可以访问的资源。 + +### 7.2 安全建议 + +#### 7.2.1 输入验证 + +```python +# ❌ 危险做法:直接传递用户输入 +md = MarkItDown() +result = md.convert(user_input) # user_input 可能包含恶意路径 + +# ✅ 安全做法:验证和限制输入 +# 1. 限制文件路径范围 +# 2. 限制 URI 方案 +# 3. 阻止访问私有/回环/链路本地地址 +``` + +#### 7.2.2 使用狭窄的 API + +```python +# 选择合适的转换方法 + +# 最宽泛 (接受本地文件、远程 URL、流) +md.convert(source) + +# 仅本地文件 +md.convert_local(path) + +# 仅流 (最可控) +md.convert_stream(stream, stream_info=...) + +# 处理 HTTP 响应 (自己控制请求) +response = requests.get(url, timeout=10) +result = md.convert_response(response) +``` + +#### 7.2.3 CVE 修复示例 + +```python +# 测试用例 test_doc_rlink() 检测 CVE-2025-11849 +# 防止 DOCX 文件中的 rlink (关系链接) 被滥用读取任意文件 + +# 修复策略: +# 1. 阻止读取外部关系文件 +# 2. 不将目标文件内容嵌入输出 +``` + +--- + +## 8. 二次开发要点 + +### 8.1 创建自定义转换器 + +```python +from markitdown import DocumentConverter, DocumentConverterResult, StreamInfo +from typing import BinaryIO, Any + +class MyCustomConverter(DocumentConverter): + """ + 自定义转换器示例 + """ + + ACCEPTED_EXTENSIONS = [".myext"] + ACCEPTED_MIME_PREFIXES = ["application/my-format"] + + def accepts( + self, + file_stream: BinaryIO, + stream_info: StreamInfo, + **kwargs: Any, + ) -> bool: + """判断是否接受该文件类型""" + mimetype = (stream_info.mimetype or "").lower() + extension = (stream_info.extension or "").lower() + + # 检查扩展名 + if extension in self.ACCEPTED_EXTENSIONS: + return True + + # 检查 MIME 类型 + for prefix in self.ACCEPTED_MIME_PREFIXES: + if mimetype.startswith(prefix): + return True + + return False + + def convert( + self, + file_stream: BinaryIO, + stream_info: StreamInfo, + **kwargs: Any, + ) -> DocumentConverterResult: + """执行转换""" + # 读取文件内容 + content = file_stream.read() + + # 执行转换逻辑... + markdown = self._do_convert(content) + + # 返回结果 + return DocumentConverterResult( + markdown=markdown, + title="可选标题" + ) + + def _do_convert(self, content: bytes) -> str: + """实际的转换逻辑""" + # 实现你的转换逻辑 + return "# 转换结果\n\n..." +``` + +### 8.2 创建插件包 + +```python +# 1. 实现插件代码 (my_plugin/_plugin.py) +from markitdown import MarkItDown + +__plugin_interface_version__ = 1 + +def register_converters(markitdown: MarkItDown, **kwargs): + """注册插件提供的转换器""" + from ._my_converter import MyCustomConverter + + # 注册转换器 (priority=0.0 与内置转换器同优先级) + markitdown.register_converter(MyCustomConverter()) + + # 或者使用自定义优先级 + # markitdown.register_converter(MyCustomConverter(), priority=5.0) + +# 2. 配置 pyproject.toml +[project.entry-points."markitdown.plugin"] +my_plugin = "my_plugin" +``` + +### 8.3 扩展现有转换器 + +```python +# 例如:增强 PDF 转换器 +from markitdown.converters import PdfConverter +from markitdown import DocumentConverterResult + +class EnhancedPdfConverter(PdfConverter): + """增强版 PDF 转换器""" + + def convert(self, file_stream, stream_info, **kwargs): + # 调用基类转换 + result = super().convert(file_stream, stream_info, **kwargs) + + # 添加自定义后处理 + enhanced_markdown = self._enhance_markdown(result.markdown) + + return DocumentConverterResult( + markdown=enhanced_markdown, + title=result.title + ) + + def _enhance_markdown(self, markdown: str) -> str: + """自定义后处理逻辑""" + # 例如:添加额外的格式处理 + return markdown +``` + +--- + +## 9. 总结 + +### 9.1 项目优势 + +1. **模块化设计**:清晰的转换器接口,易于扩展 +2. **插件架构**:通过 entry_points 实现动态扩展 +3. **类型检测智能**:结合 magika、mimetypes、扩展名多重检测 +4. **LLM 集成**:支持 Vision 模型进行图像描述和 OCR +5. **安全意识**:明确的安全警告和最佳实践建议 + +### 9.2 技术栈特点 + +- **纯 Python**:无需编译,易于部署 +- **可选依赖**:按需安装,减少依赖体积 +- **现代工具**:使用 hatch 构建,pyproject.toml 配置 +- **测试覆盖**:完善的测试向量和单元测试 + +### 9.3 适用场景 + +1. **LLM 应用**:将文档转换为 LLM 友好的 Markdown 格式 +2. **文档处理管道**:作为文档预处理步骤 +3. **RAG 系统**:构建检索增强生成系统的数据源 +4. **批量转换**:批量处理多种格式的文档