首页
/ MemPalace 格式覆盖详解:`mine --mode extract` 如何将 PDF/DOCX/PPTX/XLSX/RTF/EPUB 无损入库

MemPalace 格式覆盖详解:`mine --mode extract` 如何将 PDF/DOCX/PPTX/XLSX/RTF/EPUB 无损入库

2026-09-06 11:00:29作者:钟日瑜

MemPalace 3.3.6 引入了第三个挖掘器 format_miner.py,让 mempalace mine --mode extract 能把二进制办公文档(PDF、DOCX、PPTX、XLSX、RTF、EPUB)在读取时转换为文本并注入向量宫殿,而不修改磁盘上的任何源文件。读完本文,你将掌握该特性的完整用法、按格式路由的转换原理、13 类边缘情况的处理契约、公开 API 与 CLI 接线方式,并能直接在自己的文档目录上运行 extract 模式挖掘。以下内容基于 格式覆盖设计文档 及其对应的 format_miner.py 实现、CLI 入口测试清单 交叉印证。

一、它是什么:第三个挖掘器,按内容类型分治

MemPalace 遵循“每种内容类型一个 miner”的模式。三种模式共享同一条 mempalace mine 命令,由 --mode 参数分发:

mempalace mine <dir>                  → miner.py        (项目文件:代码、文档、笔记)
mempalace mine <dir> --mode convos    → convo_miner.py  (聊天导出)
mempalace mine <dir> --mode extract   → format_miner.py (二进制办公文档)   ← 新增

cli.py 中,cmd_mine 的实际分发逻辑与设计文档完全一致:mode == "extract" 时延迟导入 mine_formats,并透传 format_dirpalace_pathwingagentlimitdry_run 六个参数。--mode 的 argparse 定义位于 cli.py,取值为 ["projects", "convos", "extract"],help 文本明确提示 extract 模式需要 mempalace[extract] 依赖集。

各格式与其对应的转换库如下:

扩展名 转换库 Python 版本要求 许可证
.pdf MarkItDown ≥ 3.10 MIT
.docx MarkItDown ≥ 3.10 MIT
.pptx MarkItDown ≥ 3.10 MIT
.xlsx MarkItDown ≥ 3.10 MIT
.epub MarkItDown ≥ 3.10 MIT
.rtf striprtf ≥ 3.6 MIT

二、按格式路由:为什么 .rtf 不走 MarkItDown

这是该特性最关键的实证发现:在 2026-05-19 对本地混合格式测试目录的实测中,MarkItDown 0.1.5 并不转换 .rtf——它会把原始 RTF 控制码源码({\rtf1\ansi\ansicpg1252...)原样返回。若不加路由修正,52 个 RTF 文件将全部以不可读的控制码形式入库。

因此 format_miner.pyextract_text 内部做了按扩展名的分发:

is_rtf = p.suffix.lower() == ".rtf"
if is_rtf:
    text = _extract_via_striprtf(p)      # striprtf:纯 Python、约 150 行、专为 RTF 设计
else:
    text = _extract_via_markitdown(p)    # MarkItDown 处理其余五种格式

设计原则保持不变:转换发生在读取时,绝不动源文件——改变的只是每种格式选用的转换库。测试 test_rtf_routes_to_striprtf_not_markitdowntest_non_rtf_does_not_touch_striprtf 分别断言了 RTF 只走 striprtf、非 RTF 只走 MarkItDown,且大写扩展名 .RTF 同样会被正确路由(扩展名匹配统一经 Path.suffix.lower() 小写化,见 format_miner.pySUPPORTED_FORMATS 定义)。

三、可选依赖:按需安装,缺失时给出安装指令

MarkItDown 与 striprtf 都是可选运行时依赖,只挖掘项目文件或聊天导出的用户不需要安装它们。pyproject.tomlextract extra 的完整声明比设计文档中的建议版更进一步——MarkItDown 的导入包本身不足以完成转换,必须带各格式的子 extra(拉取 pdfminer-six、mammoth、python-pptx、openpyxl 等):

extract = [
    "striprtf>=0.0.27",
    # 裸 markitdown 只够 import,转换需要各格式子 extra;
    # EPUB 由基础包覆盖(无 [epub] 子 extra),RTF 由 striprtf 覆盖。
    "markitdown[docx,pdf,pptx,xlsx]>=0.1.5; python_version >= '3.10'",
]

要点:

  • 环境标记 python_version >= '3.10' 使 Python 3.9 用户自动只获得 striprtf,RTF 仍可提取,其余格式被静默跳过;
  • 缺少 MarkItDown(pdf/docx/pptx/xlsx/epub 任一)→ 状态 SKIP_NO_MARKITDOWN,日志输出 pip install markitdown
  • 缺少 striprtf(任何 .rtf)→ 状态 SKIP_NO_STRIPRTF,日志输出 pip install striprtf
  • 若 MarkItDown 已装但缺某个格式的子 extra(如 markitdown[pdf]),MarkItDown 会抛出 MissingDependencyExceptionextract_text 通过异常类型名(而非 isinstance)识别后返回单独的状态 SKIP_MISSING_FORMAT_DEPS,让用户看到可执行的安装信号,而不是笼统的 SKIP_EXTRACTION_ERROR

四、架构设计:读取时转换,存储层零改动

与 3.3.6 的姊妹特性虚拟行号(见 virtual-line-numbering.md)同出一源:转换在读取时完成,存储层原样不动

磁盘上的 PDF →(字节读入内存)→ MarkItDown → Markdown 文本 → 现有分块器 → ChromaDB 抽屉
  • 磁盘上的 PDF 只被读取,从不被修改
  • 不向用户文件系统写入任何中间 .md 文件;
  • 转换完全在内存中完成,文本一旦进入现有分块器,后续处理与其他入库路径完全一致;
  • Closet / Drawer / Hall 机制全部照常工作——format miner 产出的抽屉形状与其他 miner 相同。

五、为什么新建一个 miner,而不是改 miner.py

设计文档给出了按重要性排序的三条理由:

  1. 匹配既有的“按内容类型分 miner”模式:已有 miner.pyconvo_miner.py,第三个 miner 是同一形状的自然延伸,阅读代码库的人可立即识别;
  2. 评审面更小format_miner.py 是纯新增文件,评审者孤立地读一个新文件;miner.pyconvo_miner.py 的稳定代码路径零改动;
  3. 故障隔离更干净:format miner 若有 bug,从物理上不可能影响既有 miner。

权衡结论在 2026-05-19 的设计评审中敲定:新 miner 模式在架构与评审面两个维度上都胜出。

六、13 类边缘情况:每个路径都有可审计的状态码

每个边缘情况都映射到一个具体的 ExtractionStatus 枚举值(format_miner.py),调用方与评审者可以精确审计到底走了哪条路径;13 个案例全部有 tests/test_format_miner.py 中的专门测试(该文件共 71 个测试函数,覆盖全部边缘案例与编排器行为):

# 案例 状态码 行为
1 MarkItDown 未安装(任一非 RTF 格式) SKIP_NO_MARKITDOWN 记录 pip install markitdown 指令;跳过文件
2 文件过大(默认 > 500 MB,调用方可覆盖) SKIP_TOO_LARGE 记录大小与阈值;跳过
3 iCloud 云端独占文件(.icloud 后缀或 st_flags dataless 位) SKIP_CLOUD_ONLY 在任何可能触发物化的 I/O 之前检测;跳过
4 加密 / 受密码保护的 PDF SKIP_ENCRYPTED 捕获消息匹配 encrypt/decrypt/password/protected 的异常;跳过
5 空文件(零字节) SKIP_EMPTY 静默跳过
6 权限被拒 SKIP_PERMISSION 捕获 PermissionError;记录日志;跳过
7 断裂符号链接(目标缺失) SKIP_BROKEN_SYMLINK 通过 is_symlink() + not exists() 检测;跳过
8 提取文本中的脏编码 (恢复) decode_robust() 依次回退 UTF-8 → CP1252 → UTF-8-with-replace;绝不抛异常
9 Windows 路径语义 (无专门状态) 全程使用 pathlib.Path;后缀匹配大小写不敏感;接受 strPath 输入
10 转换库内部崩溃(畸形文件) SKIP_EXTRACTION_ERROR 在文件边界捕获通用 Exception;记录文件名 + 异常类型 + 消息尾部;跳过
11 网络 / 同步盘超时 SKIP_NETWORK_TIMEOUT 捕获 TimeoutError;跳过
12 未识别的扩展名 SKIP_UNRECOGNIZED 廉价的后缀集合检查;跳过
13 striprtf 未安装(任一 .rtf SKIP_NO_STRIPRTF 记录 pip install striprtf 指令;跳过。2026-05-19 加入,起因是实测发现 MarkItDown 0.1.5 不支持 RTF

另有 SKIP_UNREADABLE 作为 stat 阶段 OSError 的兜底状态(不在原始 13 项之内,为防御完备性补加——见 extract_text 的文件 stat 块)。加密检测的具体实现在 _ENCRYPTED_PATTERNS:一条不区分大小写的正则,使加密判定与底层抛出异常的具体库无关。

一个重要的运行时细节:瞬态跳过不写哨兵

SKIP_NO_MARKITDOWNSKIP_NO_STRIPRTFSKIP_MISSING_FORMAT_DEPSSKIP_NETWORK_TIMEOUT 这四种状态被归入 _TRANSIENT_MISSING_DEP_STATUSES 集合(format_miner.py)。编排器在调用哨兵注册前会检查该集合:瞬态环境性跳过不会写入 file_already_mined 哨兵。原因是哨兵会把文件永久标记为“已挖掘”——如果用户日后补装了缺失的依赖或网络恢复,下一次挖掘必须能重新拾起这些文件。持久性跳过(如加密、空文件)则正常写哨兵,避免每次重挖都重复扫描。

七、明确的边界:本 miner 不解决什么

按设计文档,以下是有据可查的限制而非 bug:

  • 针对特定文档类型的定制 PDF 解析器——MarkItDown 尽力而为,接受其能力边界;
  • 扫描版图片 PDF 的 OCR——独立关注点,留给后续版本的可选特性;
  • DRM 锁定文件——不绕过、也不应尝试;
  • 病理性损坏文件——它们就是坏的,SKIP_EXTRACTION_ERROR 加一条清晰的日志行是负责任的结局。

这些限制通过逐文件的跳过码日志上报,用户始终知道哪些文件没进来、为什么。

八、API 参考:两个函数 + 一个枚举 + 两个辅助函数

所有公共符号经 __all__ 导出(format_miner.py)。

extract_text(path, max_file_size=DEFAULT_MAX_FILE_SIZE) -> tuple[Optional[str], ExtractionStatus]

把一个文件转换为文本,返回 (text, status)OKtext 是提取出的 Markdown,所有跳过场景为 None。纯函数——path 处的源文件永不被修改。path 同时接受 Pathstr,内部经 Path(path).expanduser() 解析 ~/foo.pdf 风格的输入。大小上限默认 DEFAULT_MAX_FILE_SIZE = 500 * 1024 * 1024(500 MB,定义处),与其他两个 miner 的默认值一致;调用方可通过参数为合法的大文件(如扫描书)提高上限。

scan_formats(directory) -> list[Path]

递归遍历目录,返回按路径排序的支持文件列表。跳过规则见 scan_formats

  • 隐藏 / 构建目录:复用共享的 palace.SKIP_DIRS 常量(与 miner.py、convo_miner.py 同步,含 .git.venv__pycache__ 等);
  • OS 元数据文件:.DS_StoreThumbs.dbdesktop.ini
  • 符号链接:防止循环链接与同一文件经多路径重复处理(与其他两个 miner 对齐);
  • 扩展名不在 SUPPORTED_FORMATS 中的文件。

排序结果保证重挖时处理顺序确定,便于复现 bug 报告。目录不存在时返回空列表而非抛错。

ExtractionStatus(Enum)

上文 13 个文档化值外加 SKIP_UNREADABLESKIP_MISSING_FORMAT_DEPS。每个成员配有人类可读的字符串值(如 "skip:no_markitdown")用于日志;测试断言 .name 以保稳定。

decode_robust(raw: bytes) -> str

与终端会话规范化器中的辅助函数同策略(实现):先试 UTF-8(干净路径),失败后试 CP1252(处理旧 Office 文档中 0x91–0x9F 的智能引号字节),最终以 errors='replace' 的 UTF-8 兜底——任何字节都不会丢失,只会被显式化为替换符。函数永不抛异常,空输入返回空串。

is_icloud_dataless(path: Path) -> bool

两个检测信号(实现):

  1. 字面 .icloud 后缀——iCloud 卸载文件的占位约定;
  2. macOS st_flags 的 dataless 位(0x40000000)——尽力而为,非 macOS 平台优雅降级为 False(通过 getattr(path.lstat(), "st_flags", 0) 取值,OSError 时返回 False)。

返回 Trueextract_text 会在任何可能触发 iCloud 物化拉取的 I/O 之前短路到 SKIP_CLOUD_ONLY——因为让 MarkItDown 阻塞等待 iCloud 物化可能挂起数分钟。

九、CLI 集成:两处 argparse 编辑 + 一个 elif 分支

mempalace/cli.pycmd_mine 的改动极小,实际代码位于 cli.py

def cmd_mine(args):
    ...
    try:
        if args.mode == "convos":
            from .convo_miner import mine_convos
            mine_convos(...)
        elif args.mode == "extract":                       # ← 新增
            from .format_miner import mine_formats          # ← 新增
            mine_formats(
                format_dir=args.dir,
                palace_path=palace_path,
                wing=args.wing,
                agent=args.agent,
                limit=args.limit,
                dry_run=args.dry_run,
            )
        else:
            from .miner import mine
            mine(...)

argparse 侧仅是在 --mode 的 choices 中加入 "extract"定义)。这就是全部 CLI 改动。

十、编排器 mine_formats:锁、幂等与元数据

文档只列了 API 契约,而 mine_formats 的实现揭示了完整的入库流水线,值得补充说明:

  1. 配置贯通:加载 MempalaceConfig,把用户的 chunk_size / chunk_overlap / min_chunk_size 透传给 miner.chunk_text,格式模式的挖掘同样尊重用户的分块调参;
  2. 房间路由:读取目标目录的 mempalace.yaml(rooms 列表),无配置时回退到单一 documents 房间;每份文件经 detect_room 路由(文件夹匹配 → 文件名匹配 → 内容关键词评分 → 兜底 general),与 miner.py 语义一致;
  3. 幂等与锁mine_lock 锁源文件后重查 file_already_mined(check_mtime=True, extract_mode="format")——extract_mode="format" 把幂等检查限定在格式模式抽屉集合内,同一 source_file 上 convo / project miner 的抽屉不会误判;源文件 mtime 变化会触发重新挖掘;
  4. 清除 + 批量写入:先 delete 该源文件的旧抽屉,再按 DRAWER_UPSERT_BATCH_SIZE = 1000 分批 upsert;写入前经 assert_no_collisions 校验;
  5. 抽屉元数据ingest_mode="extract"extract_mode="format" 使格式挖掘的抽屉在宫殿中可与其他模式区分;另含 wingroomsource_filechunk_indexadded_byfiled_athall(内容关键词路由)、entitiessource_mtimecontent_dateline_start/line_end 等;
  6. 健壮性:每个文件的处理包在独立 try/except 中,单个坏文件不会打断整个挖掘;KeyboardInterrupt 在编排层捕获,Ctrl-C 时仍打印摘要——抽屉 ID 是确定性的(make_drawer_id_from_chunk),重挖 upsert 到相同行,部分进度天然安全;
  7. 收尾校验:挖掘结束后执行 FTS5 完整性检查(_validate_palace_fts5_after_mine),并清理 hook 派生挖掘遗留的 PID 文件。

dry_run=True 时完成遍历 + 提取 + 分块,但不打开集合、不 upsert,仅打印“将归档”的内容。

十一、实证冒烟测试:52 RTF + 11 PDF 的真实数据

设计文档记录了 2026-05-19 在本地混合格式目录(52 个 .rtf + 11 个 .pdf,真实书信与研究/架构文档,无合成夹具)上的端到端结果(Python 3.13,MarkItDown 0.1.5 + striprtf 0.0.27):

Supported formats: ['.docx', '.epub', '.pdf', '.pptx', '.rtf', '.xlsx']
scan_formats: found 63 supported files
格式 文件数 结果 提取字符数
RTF(经 striprtf) 52 52/52 OK 1,350,679
PDF(经 MarkItDown) 11 10 OK,1 SKIP_ENCRYPTED 1,340,869

唯一跳过的 PDF 是真实的受密码保护文件——边缘案例 4(SKIP_ENCRYPTED)经由异常消息匹配器在真实数据上正确触发,设计行为得到实证验证。防回归断言:零个原始 \rtf1 / \ansi 控制码泄漏进提取文本。若没有按格式路由修正,52/52 的 RTF 都会以不可读的控制码形式入库;修正后每个 RTF 都产出干净的纯文本。这是被 mock 单元测试无法捕获、却促成了 SKIP_NO_STRIPRTF 状态与按格式分发的集成缺陷。

十二、向后兼容与 3.3.6 范围外的事项

兼容性承诺(均可从仓库确认):

  • 既有 miner 零改动miner.pyconvo_miner.py 未被触碰;
  • 无新增必选依赖:MarkItDown 与 striprtf 是可选 extra,安装方式为 pip install mempalace[extract]pyproject.toml 中已按上文的子 extra + 环境标记声明);
  • 无磁盘格式变化:无论哪个 miner 产出,抽屉 / closet / hall 形状完全相同;
  • 无迁移步骤:已有宫殿的用户直接运行 mempalace mine --mode extract ~/docs/,新抽屉落入同一宫殿、同一 wing、同一房间约定;
  • 大文件上限:默认 500 MB,extract_text 接受 max_file_size 覆盖,供扫描书等合法大文件场景使用。

3.3.6 范围外的真实未来特性:超大 PDF(> 500 MB)的流式提取(MarkItDown 默认整体读入内存,pypdf 逐页流式是自然扩展)、音视频转写(Whisper)、扫描 PDF 的 OCR(Tesseract / 云 OCR)、迁移导入器(Notion / Obsidian / Mem0 导出格式)。这些各有独立 scope,不属于“通过 MarkItDown 增量扩展格式覆盖”的 3.3.6 发布范围。

小结

mempalace mine --mode extract 的完整价值链条可以概括为:scan_formats 确定性遍历 → extract_text 按格式分发到 MarkItDown / striprtf 并以 13+ 状态码全路径可审计 → 用户配置驱动的分块与房间路由 → 带锁、带哨兵、带 mtime 幂等的抽屉归档。所有环节都以“读取时转换、源文件零改动、可选依赖缺失时给指令而非崩溃”为底线。若你想在仓库中继续深入,建议按此顺序阅读:docs/format-coverage.md(设计与边界)、mempalace/format_miner.py(实现)、tests/test_format_miner.py(71 个测试逐一对应状态码)与 mempalace/cli.py(CLI 接线)。

登录后查看全文
热门项目推荐
相关项目推荐