
简介这是一款面向为知笔记用户的 Markdown 导出插件基于 JavaScript 开发专为需要批量迁移或整理 Markdown 笔记的群体设计。它解决了原生笔记平台对 Markdown 支持不彻底、导出格式受限的问题支持为知笔记 4.5 及以上版本。插件内置多项实用配置可自定义导出的 Markdown 文件与图片目录路径图片支持粘贴并导出为相对路径新增 showRootDir 配置项用于控制 categories 标签是否作为根目录同时支持自定义 !--- 前后包裹语法将配置信息写入导出文件头部兼顾灵活性与易用性。整套资源共 11 个文件包含核心 JavaScript 脚本、ini 配置文件、Markdown 说明文档以及 6 张演示图片压缩包仅 170KB轻量便携。已有 1080 人浏览学习。对经常使用为知笔记撰写技术文档、需要规范化导出 Markdown 的用户而言这款插件可大幅简化导出流程配合说明文档与截图示例上手很快。 用为知笔记记了七八年笔记沉淀了几千篇文档说句掏心窝的话真正让我有安全感的不是它在云端存了多少数据而是我能不能把这些笔记干干净净地搬出来。为知笔记的Markdown编辑体验一直在线但导出这件事官方做得有点拧巴——会员功能才有完整导出导出来还经常带着一堆HTML样式残留图片路径乱七八糟换个编辑器打开全是问题。这个痛点我忍了很久后来干脆自己动手做了一个小工具也就是这篇要讲的ExportToMd一个把为知笔记里的Markdown笔记批量、干净地导出为本地.md文件的插件/脚本。文章会覆盖我的设计思路、数据格式踩坑、核心代码实现、完整实操步骤以及在我自己笔记库上跑出来的问题排查记录。如果你也是为知笔记的重度用户并且开始担心笔记进去出不来这篇文章应该能帮你省下不少折腾时间。不需要你懂太多底层原理照着做就能把笔记库安全地复制一份到本地。1. 项目背景与核心思路拆解1.1 为知笔记导出Markdown的三个痛点先说结论不是为知笔记不能导出而是导出的结果对Markdown用户不够友好。我用自己的账号实测过几种官方路径问题集中在三块。第一导出被会员功能卡住。完整导出、批量导出这些能力在主账号上没问题但如果是普通账号或子账号不少导出选项是灰的。用户只是想备份自己的笔记还得先判断会员权益够不够这件事本身就有点反直觉。第二导出的HTML文件夹杂大量样式噪音。为知笔记底层用HTML存储内容即使你编辑时写的是Markdown保存之后的正文也是一段已经渲染过的HTML。官方导出的.html或.md里经常塞进内联样式、加粗标签、span包裹甚至还有编辑器自己的class名。用Typora、VS Code这类工具打开排版就变了代码块可能出现双换行表格也可能乱掉。第三图片和附件没有统一的出口。为知笔记的笔记正文里图片引用路径不是标准的本地路径而是类似wiz://wiz_document/xxx.png这样的内部协议。直接导出HTML图片要么不显示要么散落在多个难以理解的目录里。对我这种习惯把笔记当资产的人来说图片丢一张都心疼。所以我的需求很具体把为知笔记的笔记正文从HTML还原成干净的Markdown文本同时把图片、附件一并复制到本地目录并让新Markdown文件里的链接指向这些本地文件。1.2 为什么选本地数据库 脚本工具而不是官方导出中间的取舍过程简单记录一下大家做类似工具时可以少走弯路。第一个方案是调用官方开放API通过云端获取笔记内容再转Markdown。优点是跨设备、不需要读本地文件缺点是API的笔记内容同样是拼接后的HTML图片处理绕不开云端下载而且频繁调用要考虑频控数据量大的时候非常被动。第二个方案是直接解析本地数据。为知笔记客户端会把笔记库同步到本地正文、附件都落在一个可见的数据目录里。我只需要用只读方式连接本地数据库读取HTML正文再转成Markdown即可。这个方案不依赖网络、不占用官方接口额度速度也完全由本地磁盘决定几千篇笔记跑下来也就几分钟。第三个方案是做纯客户端插件挂在为知笔记的界面上提供一个右键菜单导出为Markdown。这个体验最好但插件的运行环境和API限制比较依赖版本通用性没有脚本强。我最终的实现是脚本为主、插件思路做辅助核心导出逻辑独立成一个Python模块界面层后续再适配。从结果看本地数据库 Python脚本是性价比最高的路径可控性强、可批量处理、出错好排查而且只要数据目录完整导出的质量基本不依赖云端状态。2. 插件整体设计与基础数据认知2.1 为知笔记本地数据是怎么存的在设计工具之前得先搞清楚为知笔记的数据落盘结构。不同版本可能略有差异但总体思路是一致的你的数据目录下每个笔记或笔记分组会对应一个GUID命名的文件夹文件夹里存放笔记的元信息、附件资源和相关的数据库文件。我用客户端自带的打开数据目录功能定位到了本机笔记库的根目录。在这个目录下能看到两类关键文件一类是.db/.db-wal/.db-shm组成的SQLite数据库另一类是以GUID命名的大量子文件夹。笔记正文通常不在纯文本文件里而是存在SQLite数据库的表中。常见表结构包含文档基本信息GUID、标题、创建时间、修改时间和正文内容字段。正文内容字段存的是渲染后的HTML字符串不是原始Markdown源码。这一点非常重要后面所有转换工作都围绕HTML还原来做。实际操作中不要凭着字段名猜直接执行PRAGMA table_info(表名)看真实结构最靠谱。不同版本的链接方式和字段命名可能不一样但SQLite本身支持只读打开不会破坏原数据。import sqlite3 db_path rD:/WizNote/data/index.db conn sqlite3.connect(ffile:{db_path}?modero, uriTrue) cur conn.cursor() cur.execute(PRAGMA table_info(WizDocument)) for row in cur.fetchall(): print(row)如果你不确定库文件名在数据目录下找.db后缀的文件一个个试一下能列出表的就是笔记主库。注意同时存在多个.db文件时优先看体积大、表名包含Document的那个。2.2 导出流程设计与工具架构明确了存储结构ExportToMd的完整流程就清晰了整体分五步。第一步定位数据库用只读方式建立连接避免影响正在运行的为知客户端。第二步读取笔记基础信息包括标题、GUID、创建时间、标签等用于生成文件名和frontmatter。第三步从正文字段取出HTML用转换器还原为Markdown。第四步解析HTML里的图片引用到对应GUID目录里找到真实图片文件复制到导出目录的assets文件夹并重写Markdown中的图片路径。第五步把Markdown内容和元信息写入目标.md文件保持与笔记库一致的目录层级。这个流程的好处是每一步都独立出了问题可以单独调试。比如图片丢失不需要重新跑一遍全库只要针对单个笔记执行解析HTML并复制资源这一部分就行。工具的整体架构我拆成了三个模块db_reader.py负责数据库连接与查询converter.py负责HTML到Markdown的转换和路径重写export_core.py负责调度和写文件。模块化对后续扩展很关键等我以后想加一个按标签导出或导出到Notion的功能只需要在调度层加逻辑不需要动转换核心。3. 核心实现细节从HTML到干净Markdown3.1 读取笔记列表先解决数据库权限与编码第一次连接数据库的时候我直接用了普通的sqlite3.connect(db_path)结果程序报错提示数据库被锁定。原因是客户端正在运行SQLite默认连接拿不到写锁。解决办法很简单用URI模式的只读连接。from pathlib import Path db_file Path(rD:/WizNote/data/index.db) conn sqlite3.connect(ffile:{db_file.as_posix()}?modero, uriTrue) conn.text_factory lambda b: b.decode(utf-8, errorsreplace)第二行里的text_factory是我踩坑之后加上的。不同系统、不同版本下笔记标题可能混有历史遗留编码不统一处理后面写文件名时容易抛UnicodeDecodeError。用errorsreplace虽然会遮蔽个别乱码但至少不会让整个导出流程中断。查询笔记列表时建议只取必要的字段不要一次性把所有HTML正文都加载进内存。几千篇笔记的HTML加起来可能有大几十MB全塞进内存再处理速度快不了多少反而容易撑爆内存。我按LIMIT分批处理每批200篇跑完一批写一批。3.2 HTML转Markdown的硬骨头这一步是整个工具的核心也是最容易翻车的地方。我从一开始就放弃了用正则表达式手动解析HTML因为为知笔记生成的HTML标签嵌套复杂正则根本写不完整。我选用了成熟的HTML转Markdown库并在它基础上做了不少定制。import html2text h html2text.HTML2Text() h.body_width 0 h.ignore_images False h.ignore_tables False h.unicode_snob True h.single_line_break False markdown_text h.handle(html_content)几个关键参数解释一下。body_width 0表示不强制换行很多编辑器默认每80个字符就折行会让列表和表格里出现多余换行single_line_break False则确保普通段落之间的换行不会被合并成一整段。unicode_snob True是为了保留符号本身而不是转成乱七八糟的HTML实体。代码块是为知笔记转换时最需要额外处理的地方。为知笔记的代码块在HTML里通常带有class属性标明语言类型比如language-python、language-javascript。转换器默认只能生成围栏代码块语言标注经常丢掉。我在转换后写了一个后缀处理函数把HTML里提取到的语言类名映射成Markdown代码块的语言标识。import re def restore_lang_from_html(html, markdown): fenced_blocks re.findall(rpre[^]*class[\]([^\]*language-([\w-]))[\][^]*, html) if fenced_blocks and \n in markdown: for full_class, lang in fenced_blocks: markdown markdown.replace(\n, f{lang}\n, 1) return markdown这段代码只处理了最常见的围栏代码块场景。如果你的笔记里有大量行内代码或混合代码块可以再深入定制但核心思路是一样的先做通用转换再回填语言标注。表格也是容易翻车的点。为知笔记编辑器的表格HTML结构比较标准html2text能转成管道语法表格。但注意如果表格单元格内容特别复杂比如包含列表或代码转换结果可能会断裂。我的处理策略是先转换再校验表格行数是否一致不一致就保留HTML原文并加注释标记提醒人工处理。宁可在导出结果里留一个此表格转换失败的提示也不要静默输出错误格式。3.3 图片与附件不要让笔记变成断链孤儿说实话文本转换再难也是语法层面的问题顶多不好看图片引用处理不好那才是真灾难。为知笔记HTML里的图片src往往是wiz://wiz_document/图片名.png这种内部协议或者相对路径。直接放进Markdown里任何外部编辑器都打不开。我的处理逻辑是解析HTML中的img标签取出文件名然后在当前笔记GUID对应的本地目录里按文件名查找真实图片文件找到后复制到导出目录的assets文件夹并把src改写成相对路径。from pathlib import Path import re, shutil def process_images(html, note_guid, note_dir, asset_dir): asset_dir.mkdir(parentsTrue, exist_okTrue) def replace_src(match): full_tag match.group(0) src match.group(1) filename Path(src).name source_file None # 优先在当前笔记目录找 potential note_dir / filename if potential.exists(): source_file potential else: # 退而求其次在整个数据目录里按文件名找 for candidate in note_dir.parent.rglob(filename): source_file candidate break if source_file is None: return full_tag # 没找到就不动原标签留待人工处理 target asset_dir / filename if not target.exists(): shutil.copy2(source_file, target) return fimg srcassets/{filename} alt{Path(filename).stem} return re.sub(rimg[^]src[\]([^\])[\][^]*, replace_src, html)这段代码里有个隐蔽的坑如果多个笔记引用了相同文件名的图片而它们其实是不同内容直接按文件名复制会互相覆盖。我在实战中的解决办法是在文件名前面加一个短哈希前缀再更新Markdown引用。图片处理完后再走一遍HTML到Markdown的转换这样Markdown里的图片路径就已经是本地相对路径了。顺序上先处理图片再转换比先转换再处理路径要稳因为转换器可能对wiz://协议里的特殊字符做转义干扰后续路径匹配。3.4 元数据收尾frontmatter与文件名一份只在本地自嗨的Markdown文件其实不写元数据也问题不大。但一旦想迁移到静态博客、Notion、Obsidiantag和日期就显得很重要了。我导出时会在文件头部写一段YAML frontmatter保存标题、标签、创建时间、修改时间和原始GUID。import yaml from datetime import datetime metadata { title: title, tags: [tag.strip() for tag in tag_string.split(,) if tag.strip()], created: datetime.fromtimestamp(created_ts).isoformat() if created_ts else None, updated: datetime.fromtimestamp(updated_ts).isoformat() if updated_ts else None, source: fwiz://wiz_document/{guid}, } with open(output_path, w, encodingutf-8) as f: f.write(---\n) f.write(yaml.dump(metadata, allow_unicodeTrue, sort_keysFalse)) f.write(---\n\n) f.write(markdown_text)文件名我强烈建议不要直接用笔记标题。为知笔记里标题经常带/、:、?这些在Windows文件系统里非法的字符。我统一处理为GUID前8位作为前缀标题过滤非法字符作为可读部分例如3f1a2b4c_使用ExportToMd导出笔记.md。好处是文件名稳定、不重名、方便回溯原始笔记。4. 完整实操在自己电脑上跑通ExportToMd4.1 环境准备与依赖安装工具依赖Python 3.8以上主要用三个库sqlite3标准库、html2text、pyyaml。安装命令很简单pip install html2text pyyaml如果你电脑上有多个Python版本建议用python3 -m pip install指定当前解释器。我本人在Windows上用Python 3.11跑的Linux也验证过代码没有用到平台特有路径所以跨平台问题不大。然后需要确定你的为知笔记数据目录。打开客户端进入账号设置或存储设置一般能看到数据存储位置或打开数据目录按钮。记下这个路径后面命令行参数要用。4.2 配置路径并执行导出准备工作做完直接运行脚本。我把导出参数都做成命令行参数了方便多次调用python export_to_md.py \ --db D:/WizNote/data/index.db \ --note-root D:/WizNote/data \ --out ./wiz_export \ --batch-size 200参数含义分别是数据库文件路径、笔记数据根目录、导出目标目录、每批处理数量。--note-root主要用于图片查找兜底因为有些图片不在当前笔记GUID文件夹里而是在公共资源目录这时需要回退全库搜索。第一次跑建议先用小批量试验比如加一个--limit 20参数只导出前20篇笔记检查没问题再全量导出。全量导出过程中屏幕上会打印当前进度类似[1234/3456] 完成: 使用ExportToMd导出笔记.md方便实时了解是否卡住。4.3 导出结果与验证清单导出完成后目录结构大概是这个样子的wiz_export/ ├── 3f1a2b4c_使用ExportToMd导出笔记.md ├── 0d9e8f77_数据备份方案整理.md └── assets/ ├── a1b2c3_架构图.png ├── d4e5f6_流程图.png └── ...拿到导出目录后务必做一轮验证。我列一个自检清单全过了再算成功。用Typora或VS Code打开几个Markdown文件确认标题、列表、表格显示正常。逐一点击文章里的图片确认图片能正常打开而且路径确实是相对路径。抽查代码块看语言高亮是否生效。随机找一篇带标签的笔记确认frontmatter里的tag字段非空。检查文件名是否出现?、/等非法字符如果有说明清理逻辑没覆盖全需要补过滤规则。这套清单让我在第一次全量导出时就发现了一批问题后面逐个解决省了不少返工时间。5. 常见问题与排查技巧实录5.1 数据库被占用打不开最常见的错误就是连不上SQLite库。解决办法上面提过用modero只读连接并且确保程序不会尝试对数据库做任何写操作。有一个细节容易忽略如果数据库有WAL模式只读连接也可能需要访问-wal文件所以脚本运行时不要把-db-wal文件挪走。如果还是提示锁检查是不是同时开了两个脚本实例。SQLite的锁机制比较保守双实例并发读也会冲突所以我把脚本设计成单实例运行用了一个简单的portalocker锁但如果你只是自己用记住别开两个命令行窗口同时跑就行。5.2 图片导出后仍然不显示图片处理最大的坑是文件名相同但内容不同。我前面提到加短哈希前缀就是为解决这个。另外还有一种情况为知笔记的图片存在专属资源目录文件名经过MD5重命名和HTML里引用的文件名不一致。这时按文件名直接找会找不到。排查建议是先打开一张不显示的图片对应的HTML原文看看src到底长什么样再去数据目录里手动搜索一遍确认真实文件名。如果确认是MD5重命名就需要在解析时把原始文件名映射关系从数据库附件表里取出来再按附件表的值搜索实际文件。这属于高级调整普通用户遇到不多但一旦遇到定位思路很重要。5.3 代码块语言标注丢失如果你发现导出后的Markdown代码块都是空的围栏三个反引号后面没有语言名说明restore_lang_from_html函数没有正确匹配到pre classlanguage-xxx结构。先检查HTML原文里的class名格式有的版本可能是lang-python而不是language-python还有的可能在code标签上而不是pre标签上。我在自己的笔记库上就遇到过后一种情况write了一个通用正则同时匹配pre和code上的语言类覆盖率达到95%以上。剩下的个别笔记语言标注确实缺失但不影响阅读只能说编辑器里没有语法高亮而已。这里想提醒一句语言标注丢失不影响Markdown内容本身不用为了这一个瑕疵反复重跑全库浪费时间。等工具版本迭代以后针对特定GUID做单篇修复重新导出即可。5.4 标签和目录结构错乱为知笔记的标签概念和普通文件夹有两个维度笔记属于某个目录文件夹同时可以打多个标签。导出时我默认把文件夹层级映射为Markdown文件的子目录这是最符合直觉的做法。但有些用户习惯把文件夹当标签用导出后就会出现同一个主题分散在多个目录的问题。如果你希望按标签归类调整一下导出逻辑把tags字段作为一级分类目录文件夹路径作为文件名前缀。比如标签名/文件夹名_标题.md。这个改动不复杂就是在写文件路径时改一下拼接顺序。我自己的选择是保持目录结构优先因为文件夹层级更稳定标签偶尔会改动。还有一个坑数据库里的标签字段可能包含多个标签用中文逗号或英文逗号隔开。没有统一分隔符的话yaml解析会报错。我在读取时做了规范化把所有常见分隔符统一替换成英文逗号再按逗号拆分。最后再分享一点个人体会整套ExportToMd工具我前后改了三版从最早暴力正则解析HTML到现在的html2text 图片重定位 frontmatter稳定结构核心收获是笔记工具的迁移真正难的不是格式转换而是对底层存储结构的理解。只要搞懂了数据是怎么存的导出工具其实就是一个读出来、转换、写下去的过程。建议你也给为知笔记做一个完整的本地备份哪怕暂时不离开这个软件手里有一份干净的Markdown版本心里会踏实很多。尤其是用了很多年、积累了大量图片附件的朋友早晚会需要一次数据自由。如果你后面想扩展可以往这几个方向做加一个自动为笔记生成Obsidian内部链接的插件导出的Markdown之间互相引用或者写一个定时任务每周自动增量备份再或者对接静态博客框架一键把导出目录发布成网站。这个工具的核心逻辑已经足够稳扩展起来很顺手。本文还有配套的精品资源点击获取