网友作品 #000199 · 提交于 2026/08/26
语雀导出转Markdown工具:lakebook批量无损转换

作品简介
语雀知识库导出后得到的是私有格式(lakebook 压缩包 + Lake 格式 JSON),不能直接当 Markdown 用。本工具把这些原始导出文件完整、批量地转换成标准 Markdown:直接解析源头数据 body_asl 字段,而不是先渲染成 HTML 再转换——后者会因中途报错静默漏掉大量文档。实测 3 个真实知识库共 375 篇文档全部转换成功、0 篇失败,无 Lake 标记残留。
核心亮点
- 不丢内容:自研 Lake 解析器逐元素、逐 card 类型精确映射 Markdown,覆盖标题、引用与 alert 提示块、多级列表、任务清单、代码块、GFM 表格(含合并单元格降级)、视频/附件卡片、语雀文档内链等
- 不丢结构:按语雀知识库目录树(TOC)还原文件夹层级,生成带可点击链接的 README 目录索引;目录树外的孤儿文档归入「未分类」
- 资源本地化:图片附件并发下载到本地 assets 文件夹,链接自动改写相对路径,离线可看;单个资源失败自动保留远程链接,不影响整体转换
- 源形态兼容:支持已解压文件夹、多知识库父目录、lakebook/zip/tar 压缩包三种源(自动探测 tar 格式,含 zip-slip 路径穿越防护与中文文件名编码修复)
- 过程可控:后台线程转换不卡界面,分阶段进度汇报(转换文档 x/y、下载资源 x/y),支持随时取消;所有失败弹窗提示 + 写日志,绝不静默
- 开箱即用:单文件 exe(约 44MB)无需 Python 环境;输出文件名自动安全化(非法字符替换、保留名规避、超长截断、同名加序号),不区分大小写登记已占用路径,彻底避免覆盖
- 质量保障:26 个单元测试 + ruff/mypy 零告警 + 真实数据端到端验证 + 打包后冒烟测试
技术与工具
- Python 3.12(uv 管理环境);界面用 NiceGUI(Native 模式),文件对话框改用 tkinter 进程内直调 Windows 原生窗体
- BeautifulSoup4 + html5lib 解析、PyYAML 解析目录树、httpx + 线程池并发下载资源、PyInstaller 打包 onefile exe
- pytest + ruff + mypy 质量流水线;开发使用 AI 编程工具搭配国内顶级模型
开发过程
- 关键决策一:解析 body_asl 而非 body_html——前者是 Lake 源头数据,之前用 HTML 工具转换「漏很多内容」的根因就在渲染产物丢元素
- 关键决策二:lakebook 后缀有欺骗性,实为 tar 格式,实现上先按 zip 解、失败再按 tar 解,同一段代码天然支持普通 zip/tar 源
- 关键决策三:NiceGUI 原生文件对话框在 onefile 打包后走跨进程消息队列不稳定,改用 tkinter 直调 Win32 原生窗体,打包后 100% 可用
- 作者的感受:尝试新 UI(NiceGUI)会翻车,不如最朴素的 GUI 稳,「要好看就要多花时间与 token」
适用场景
- 语雀官方不给 Markdown 格式导出,需要把知识库迁移到其他 Markdown 平台的用户
- 希望离线查看、本地归档语雀导出文档的使用者
原始提交内容(点开查看,可复制全文去问 AI)
语雀转 Markdown(YuqueToMarkdown)使用与技术指南
把语雀导出的 lakebook / JSON 文档完整转换为 Markdown 的 Windows 桌面工具 版本:1.0 | 打包形态:单文件 exe(约 44 MB,无需安装 Python)
一、工具简介
语雀知识库导出后得到的是私有格式(lakebook 压缩包 + Lake 格式 JSON),不能直接当 Markdown 用。本工具把这些原始导出文件完整、批量转换成标准 Markdown,解决的核心痛点:
- 不丢内容:直接解析语雀原始 Lake 格式(
body_asl字段),而不是先渲染成 HTML 再转换——后者会因中途报错静默漏掉大量文档 - 不丢结构:按语雀知识库的目录树(TOC)还原文件夹层级,并生成 README 目录索引
- 资源本地化:可一键把图片、附件下载到本地
assets文件夹,链接自动改写为相对路径 - 双击就能用:打包成单文件 exe,拿到任何 Windows 电脑都能直接运行,不依赖 Python 环境
实测数据:3 个真实知识库共 375 篇文档全部转换成功,0 篇失败,无 Lake 标记残留。
二、快速上手(三步走)
第 1 步:选择源
界面上有两种选法,可以混用:
| 按钮 | 作用 |
|---|---|
| 选文件夹… | 选择已解压的知识库文件夹(也可以选包含多个知识库的父目录) |
| 选压缩包… | 直接选择 .lakebook / .zip / .tar 压缩包(可按住 Ctrl 多选) |
选好的路径会出现在输入框里,多个源用分号 ; 分隔。也可以直接手动粘贴路径进来。
输入框下方会实时提示识别结果,例如:发现 3 个知识库;2 个压缩包(转换时自动解压)。
源的三种典型形态都支持:
- 单个知识库文件夹(里面有
$meta.json+ 一堆文档 JSON) - 包含多个知识库的父目录(每个子文件夹是一个知识库)
- 语雀官方导出的 lakebook 压缩包(tar 格式,无需先解压)
第 2 步:选择输出文件夹
点「浏览…」选一个保存结果的位置。每个知识库会在输出目录下生成独立的子文件夹。
第 3 步:开始转换
点「开始转换」,进度条和状态栏实时更新,运行日志区显示关键节点。转换完成后:
- 点「打开输出文件夹」直接查看结果
- 出问题时点左侧「打开日志目录」查看详细日志(按日期分文件)
三、功能详解
3.1 输出目录结构
以转换 EasyShu图表插件.lakebook 为例:
输出目录/
EasyShu图表插件/ lakebook 内部的 hash 目录名会自动改回压缩包名
README.md 目录索引(按 TOC 层级生成,含可点击链接)
欢迎语.md
快速上手/
安装指南.md
常见问题.md
assets/ 图片附件(开启下载时生成)
xxx.png
要点:
- 文档夹层级完全按语雀目录树还原;不在目录树里的“孤儿文档”归入
未分类/ - 文件名经过 Windows 安全化处理:非法字符替换、保留名(CON/NUL 等)规避、超长截断、同名自动加序号
名称 (2).md - lakebook 解压出来的顶层目录是一串 hash(如
0a223e3b…),工具会自动改用压缩包文件名作为输出文件夹名
3.2 资源下载开关
| 状态 | 行为 |
|---|---|
| 开启(默认) | 文档中的图片、附件下载到 assets/,Markdown 里链接改为本地相对路径,离线可看 |
| 关闭 | 不下载,保留语雀远程链接(需要网络且资源未失效时才能查看) |
下载是并发执行的(多线程),单个资源失败不影响整体——失败项自动保留远程链接,转换结束后日志会汇总 成功数/总数。
3.3 Lake 格式转换覆盖范围
| 类别 | 支持的内容 |
|---|---|
| 基础块 | 标题 h1–h6、段落、引用块(含 NOTE/TIP/WARNING 等 alert 标记)、水平线 |
| 列表 | 有序 / 无序列表,支持多级嵌套、任务清单 |
| 代码 | 代码块(保留语言标记)、行内代码 |
| 富文本 | 粗体、斜体、删除线、链接、转义字符 |
| 媒体 | 图片、视频卡片(转链接)、附件卡片(转下载链接,含文件大小) |
| 表格 | GFM 表格(含表头、对齐、合并单元格降级处理) |
| 其他 | 标签(label)、语雀文档内链、第三方嵌入卡片(转链接或占位说明)、alert 提示块 |
3.4 转换过程可观测、可取消
- 后台线程执行转换,界面不卡死
- 进度分阶段汇报:
转换文档 12/310下载资源 45/120 - 「取消」按钮可随时中止(已完成写入的文档保留)
- 所有按钮操作失败都会弹窗提示 + 写日志,绝不静默
四、技术架构
4.1 技术栈
| 层次 | 选型 |
|---|---|
| 界面 | NiceGUI(Native 模式,pywebview 双进程窗口) |
| 文件对话框 | tkinter(Windows 原生对话框,进程内直调) |
| HTML/Lake 解析 | BeautifulSoup4 + html5lib |
| 目录树 | PyYAML(解析 $meta.json 内嵌的 tocYml) |
| 资源下载 | httpx + ThreadPoolExecutor 并发 |
| 打包 | PyInstaller(onefile + windowed,内置 PNG 图标) |
| 质量保障 | pytest(26 个用例)+ ruff + mypy |
| 环境管理 | uv(Python 3.12) |
4.2 模块分层
src/yuque2md/
lake_parser.py # Lake 格式 Markdown 的解析器(纯函数,无 IO)
toc.py # $meta.json 目录树(TocNode)
downloader.py # 资源并发下载 + 文件名安全化
core.py # 转换编排:扫描/解压/规划路径/调用解析器/生成索引
ui.py # NiceGUI 界面 + 事件循环 + 后台线程通信
main.py # 入口:日志、端口、native 窗口启动
logging_setup.py # 按日期分文件、单文件 5MB、保留 30 天
resources.py # 打包后的资源路径处理
核心调用链:
UI 线程 convert_sources(源列表)
目录源 scan_books()
压缩包源 extract_archive()(ziptar 自动探测)
逐知识库 convert_book()
load_toc() 目录树
convert_lake_to_markdown() 单篇解析
AssetDownloader 资源下载(可选)
README 索引生成
UI 与后台线程之间用 queue.Queue 传事件(progress / done / error),界面定时器轮询刷新,避免跨线程直接操作 UI。
4.3 关键设计决策
** 为什么解析 body_asl 而不是 body_html**
语雀导出的每个文档 JSON 里同时有 body_html 和 body_asl 两个字段。body_asl 是 Lake 格式(HTML 变体 + <card> 扩展元素),是源头数据;body_html 是渲染产物,某些元素会丢失或转义异常。之前用 HTML 工具转换“漏很多内容”,根因就在这里。本工具直接实现 Lake 解析器,逐元素、逐 card 类型精确映射到 Markdown。
** lakebook 格式探测**
.lakebook 后缀具有欺骗性——它是 tar 格式(魔数验证),不是 zip。实现上先按 zip 解,抛 BadZipFile 再降级按 tar 解,两者都失败才报错。同一段代码因此也天然支持普通 .zip 和 .tar 源。解压统一在临时目录进行,带 zip-slip 路径穿越防护和中文文件名编码修复(cp437UTF-8/GBK)。
** 文件对话框为什么用 tkinter 而不是 pywebview**
NiceGUI Native 官方推荐 app.native.main_window.create_file_dialog(),但它走跨进程消息队列(主进程发请求 webview 子进程弹窗 队列回传),在 onefile 打包后的 exe 里该链路不稳定,可能永远等不到回包——表现为点击按钮无任何反应。tkinter 对话框在进程内直接调用 Win32 原生窗体(用户看到的就是 Windows 自带的文件/文件夹选择窗体),打包后 100% 可用,是桌面程序的标准做法。详见《踩坑总结-NiceGUI原生对话框与lakebook解析.md》。
** 输出路径规划**
- 目录源本身是知识库 直接输出到输出根目录;是父目录 每个知识库一个子文件夹
- 压缩包源 一律输出到
输出目录/压缩包名/ _UsedPaths集合登记所有已占用路径(不区分大小写),彻底避免同名覆盖
4.4 质量保障
- 26 个单元测试:Lake 解析器各元素覆盖、TOC 构建、目录规划、lakebook(tar)/zip/混合源转换、文件名安全化、多路径输入解析
- ruff + mypy 零告警:全量类型注解,
from __future__ import annotations风格 - 真实数据端到端验证:3 个知识库 375 篇全部成功后才打包发布
- 打包后冒烟测试:验证 exe 三进程架构(引导进程 服务进程 webview 窗口进程)正常启动
4.5 运行日志
日志位于 exe 同目录的 logs/ 下,按日期命名(app-2026-08-26.log),单文件超 5MB 自动分卷,保留 30 天。格式含 pid/tid/模块名,方便定位多线程问题。
五、开发与打包
# 开发环境(项目根目录执行)
uv sync # 还原依赖
uv run pytest -q # 跑测试(26 个用例)
uv run ruff check src tests # lint
uv run mypy src # 类型检查
uv run yuque2md # 开发模式运行(带控制台)
# 打包 onefile exe
.\scripts\build_windows.ps1 -BuildMode onefile
# 产物:dist\YuqueToMarkdown.exe(约 44 MB)
修改代码后建议流程:改 src/ pytest 验证 打包 冒烟测试(启动 exe 确认窗口出现) 交付。
六、常见问题
Q:启动 exe 后要等几秒才出窗口? onefile 模式每次启动要先把内嵌资源解压到临时目录,属正常现象。
Q:某些图片下载失败? 语雀部分附件链接需要登录才能访问。失败项自动保留远程链接,不影响文档本身转换,日志汇总里能看到成功/总数。
Q:转换结果里个别文档只有标题?
源文档本身内容为空(如仅建了标题的占位文档),转换行为正确。可打开源 JSON 核实 body_asl 长度为 0。
Q:能选 .7z 压缩包吗? 不能,Python 标准库只支持 zip/tar。7z 专有格式请先用 7-Zip 解压成文件夹再选文件夹。