把 markitdown 做成 Windows 桌面版:拖进文档就出 Markdown,附 233MB→64MB 打包裁剪全过程

先说结论

我把上游的 markitdown(MIT,Python 库 / CLI)封装成了一个 Windows 桌面应用 :把 Word、PDF、PPT、Excel 拖进窗口,点开始,拿到干净的 .md。

不需要装 Python,不需要开命令行,不需要记参数。

项目地址:github.com/it-kxw/mark... (MIT,自行取用)


为什么会做这个

上游 markitdown 是个相当扎实的库,API 设计也干净。但它只有两种用法:import markitdown 或者命令行。这两种用法都预设了一件事------你是个会装 Python 环境的人。

问题在于,真正需要"把 PDF 转成 Markdown"的人,往往不是开发者:

  • 产品同学要把竞品文档喂给大模型
  • 法务同事要把合同转成可编辑格式
  • 家里人问你这台电脑能不能"把那个 word 变成能粘贴的"

每次你都得出马,或者远程指导对方装 Python、配 pip 源、开 PowerShell、cd 到目录、敲参数。这件事的 ROI 低得离谱。

我去上游仓库翻了一下,发现官方把桌面 GUI 明确列为 out of scope ,但明确鼓励 以「独立项目 + 依赖 PyPI 上的 markitdown」的方式来做。这条路是通的,那就自己动手。

顺便说一句:这个工具全程本地转换,文档不出本机。市面上那些在线转换站,你得先把文件传上去------对一个正在被大模型投喂的文档来说,这个代价挺高的。


长什么样

三步,没有第四步:

支持的格式跟着上游走,PDF、Word、PowerPoint、Excel、CSV、HTML、EPUB、Outlook MSG、JSON、XML/RSS、Jupyter Notebook 都在内。

真正让我有底气写这篇文章的,是下面三个坑。


坑一:为什么放弃了 Electron

第一个念头当然是 Electron------界面好写,生态成熟。算了一下就放弃了。

转换内核是 Python 写的 markitdown。 用 Electron 的话,我得同时塞进一个 Node 运行时和一个 Python 运行时,打包体积直接起飞,冷启动也更慢。

所以选型收敛到 Python 原生 GUI,最终用 PySide6-Essentials(Qt 的官方 Python 绑定,LGPL-3.0,动态链接即可合规)。

方案 运行时 主要问题
Electron Node + 额外的 Python 双运行时,体积与启动都吃亏
Tauri Rust + 系统 WebView 体积小,但仍要外挂 Python,且 WebView2 不是所有机器都有
PySide6 纯 Python 与转换内核同进程语言,无额外运行时

选 PySide6 还有一个隐性好处:上游库抛出的异常、返回的数据结构,可以直接用,不需要跨进程序列化再翻译一遍。


坑二:QThread 做不出「真取消」

批量转换是耗时操作,所以必须有取消按钮。一开始我用的是教科书式的 QThread------然后发现取消按钮是个摆设。

现象:点取消,界面提示"正在取消",然后......等当前文件跑完才真的停。遇到一个 200 页的 PDF,用户要盯着进度条干等十几秒。

原因 :QThread 的 terminate() 会留下未释放的锁和句柄,属于 UB,没人敢用;而"优雅停止"只能靠协作式检查标志位------但转换是一次上游库的同步调用,中途没有检查点。 你只能在它返回之后才知道要停。

解法 :把转换挪进 multiprocessing(spawn) 进程池。进程可以硬杀。

python 复制代码
pool.terminate()   # 直接杀 worker,0.03 s 生效

代价是引入了新的问题:

  1. 子进程需要可序列化 ------用 spawn 而不是 fork,跨平台更稳,但要求入口做好保护;
  2. 孤儿进程------主程序被强杀(哪怕是启动那一瞬间)时,worker 会残留下来啃 CPU。

第 2 个问题的解法是加看门狗:子进程启动后盯着父进程 PID,父进程消失就自己退出。实测 1~2 秒内自退,任务管理器里不留残骸。

最终取消响应:0.03 秒。这个数字是实测的,不是理论值。


坑三:PySide6 和 pandas 的导入顺序冲突

这是整个项目里最隐蔽的一个坑,值得单独说。

症状:PySide6 先于 pandas 导入时,程序直接崩。

powershell 复制代码
python -c "from PySide6.QtWidgets import QApplication; import pandas"

报错长这样:

csharp 复制代码
AttributeError: '_SixMetaPathImporter' object has no attribute '_path'

根因 :shiboken6 的导入钩子内部会调用 inspect.getsource();而 inspect 触发模块查找时,会走到 six 注册的元路径导入器 _SixMetaPathImporter,它缺少 _path 属性------于是整个导入链断掉。

为什么打包后更隐蔽 :PyInstaller 的 pyi_rth_pyside6 运行时钩子会在你的代码之前先导入 PySide6,于是源码里跑得好好的,打包后一启动就崩。

解法 是在自己的包里做兼容垫片,给 six._importer 补上缺的属性:

python 复制代码
# _compat.py
import six
if not hasattr(six._importer, "_path"):
    six._importer._path = None

并且把"先 pandas 后 PySide6"的导入顺序作为第二道保险。这个修法与导入顺序无关,源码和冻结包同时生效。

如果你也撞上了,建议顺手给上游 PySide6 提个 issue------这个坑不该由每个使用者各修一遍。


坑四:安装包从 233MB 裁到 64MB

PyInstaller 一把梭出来的 one-folder 是 233MB。一个文档转换工具,这个体积说不过去。

裁剪原则:只减冗余,不减功能。 格式支持和未裁剪基线必须完全一致,同一套用例全过。

档位 one-folder 裁掉的东西
none 233 MB 基线,用于对比
safe(默认) 213 MB Qt 翻译(保留 en/zh_CN)、Pillow AVIF、QtNetwork、sqlite3、Qt TLS/网络信息插件
aggressive 192 MB 再裁 opengl32sw.dll(20MB 软件 OpenGL 回退)与 qdirect2d

依赖层面的取舍比裁文件更有效:

toml 复制代码
# 刻意不用 markitdown[all]:会拖进 Azure SDK 和约 31MB 的 SpeechRecognition
"markitdown[pdf,docx,pptx,xlsx,xls,outlook,youtube-transcription]>=0.1.8,<0.2.0"
"PySide6-Essentials>=6.8"   # 不用 PySide6-Addons,省约 39 MB

注意 magika/onnxruntime(内容类型嗅探)、pandas/numpy(Excel)、PDF 栈、Office 栈一个都没动------裁的是 UI 渲染用不到的东西,不是功能。

再往上走就是 Inno Setup 的活:per-user 安装、免 UAC,压出 64.32 MB 的安装包;免安装 zip 是 93.20 MB。

一个容易踩的细节:Inno Setup 的 .iss 脚本若含中文,必须存成 UTF-8 with BOM,否则编译器按 ANSI 读,中文全乱。


实测数据

Windows 11 x64,Python 3.12,PyInstaller 6.22.3:

指标 实测值
one-folder 产物 211.5 MB(safe 档)
安装包 / 免安装 zip 64.32 MB / 93.20 MB
冷启动(进程启动 → 窗口可交互) 0.947 s
常驻内存 160.2 MB(并发 2 峰值 438.4 MB)
取消响应 0.03 s
格式矩阵 13/13 夹具通过,输出与上游逐字节一致

最后一行是我最在意的:转换结果和上游逐字节一致。封装层不该偷偷改变任何行为,格式解析的正确性和缺陷都归上游。


还没做完的

写在文章里,免得有人下载后觉得被坑:

  • 没有代码签名 → 首次运行会弹 SmartScreen,要点「更多信息 → 仍要运行」。这是当前最大的信任问题,签名路线已定为 SignPath Foundation(对开源免费)。
  • 没有自动更新------涉及 UAC 与回滚,风险大于收益。
  • 没有跨平台------目前只在 Windows 11 x64 实测过。
  • 音视频转写、图片 OCR 未做,这些要么需要外部二进制,要么需要密钥存储。

最后

项目地址:github.com/it-kxw/mark...

MIT 许可,独立第三方项目,与 Microsoft 无关。转换能力全部来自上游 microsoft/markitdown。

如果你正好也被"帮别人装 Python"这件事烦过,可以试试。觉得有用的话点个 Star,遇到问题直接开 issue,我看到会回。

如果这篇文章里的某个坑帮到了你------尤其是那个 shiboken6 × six.moves 的导入冲突------那就是它最大的价值了。

相关推荐
启观川5 小时前
数据结构与算法-第 3 章 常用算法-贪心算法
数据结构·笔记·python·算法·贪心算法
南笙北梦5 小时前
本地开源大模型多轮对话显存泄漏排查与修复实录
python
小羊没烦恼!5 小时前
如何在IIS6,7中部署ASP.NET网站?
数据库·后端·python·web安全·asp.net
钱栈up6 小时前
扫完 3.5 万文件后,我发现了 521 组散落的同名文件
爬虫·python·数据分析
黄权光6 小时前
AI智能体项目实战记录(01):开发环境搭建之conda的安装与配置
python
Wokoo76 小时前
Selenium 核心 API 解析(三)
python·功能测试·selenium
在世修行6 小时前
智学文枢:技术选型
python·技术选型
用户8356290780516 小时前
如何使用 Python 将 PDF 转换为 PPTX 文件
后端·python
我命由我123457 小时前
Function Calling 模型
人工智能·python·学习·ai·学习方法·ai编程·ai写作