教程概述
前言
出这个帖子的原因,因为最早我用的是 oneonte 所以我的笔记全都在这里, 后来发现 oneonte 没有 ai 功能,也就是微软的副驾驶,因为我没有购买,而且公司也越来越多人用 wps。 所以打算换个笔记软件,可以总结文本软件,发现了思源笔记,整体用起来非常好用,但是同步功能让我很费脑子, 购买了缤纷云,充值后就非常好用了,
后来因为一些原因,我购买了微软 365,并且有微软的副驾驶 AI, 所以打算重新迁移回 oneonte,但是导出的 Markdown 格式让我无法迁移, 格式不互通, 中间花费 3 天时间,研究出了此方法。
如无必要,请继续使用思源笔记,因为折腾起来太麻烦了。 我刚开始使用思源笔记时候,就直接购买了功能特性,也是支持开发者,希望思源越来越好。
本教程帮助用户将思源笔记(SiYuan Notes)内容迁移到 Microsoft OneNote,步骤包括:
- 环境准备
- 安装 Python 及依赖
- 注册 Microsoft Azure 应用(获取 API 权限)
- 配置迁移脚本
- 执行迁移
- 注意事项
1. 环境准备
确保电脑满足以下条件:
-
操作系统:Windows 10/11
-
Python:3.10 或以上
-
OneNote:已安装并可登录 Microsoft 账户
-
思源笔记:导出的.zip 文件压缩包

- 将其解压到本地文件夹(后续这个文件夹会用到)
-
网络:可以访问 Microsoft Azure
2. 安装 Python 与依赖
2.1 安装 Python
- 访问 Python 官网
- 下载适用于 Windows 的 Python 3.10+
- 安装时勾选 Add Python to PATH
- 安装完成后,在命令行执行:
python --version
确认 Python 安装成功
2.2 安装依赖(可写自动化脚本)
保存以下代码为 install_dependencies.bat:
@echo off
echo Installing Python dependencies...
REM 升级 pip
python -m pip install --upgrade pip
REM 安装 requests
pip install requests
REM 安装解析库
pip install beautifulsoup4 lxml
echo Dependencies installed successfully!
pause
运行方法:直接打开这个 bat 文件
install_dependencies.bat
3. 注册 Microsoft Azure 应用(获取 OneNote API 权限)
-
登录
-
在左侧菜单选择 Azure Active Directory → 应用注册 → 新注册
-
填写应用信息:
- 名称:
SiYuanToOneNote(可自定义) - 支持账户类型:按照图片设置
- 重定向 URI:留空
- 名称:
-

-
-
点击 注册
-
记录 应用(客户端)ID ,后续脚本中会用到
-
配置 API 权限:
-
点击 API 权限 → 添加权限 → Microsoft Graph → 委托的权限
-
搜索框搜索
Notes.ReadWrite,点击 添加权限 -
添加权限后,点击代表默认目录 授予管理员同意
-
-
点击 证书与机密 → 新建客户端密钥,记录密钥值(只显示一次)
-
点击身份验证,添加 URL 到移动和桌面应用程序,内容填写:http://localhost

- 点击设置,按照我的设置方法

以上信息在迁移脚本中使用,用于 OAuth2 登录和访问 OneNote API
4. 配置迁移脚本
桌面新建 txt 文本,复制以下代码,粘贴进去,保存为(.py)文件 注意查看代码内的配置选项

主要配置这两项。
import os
import re
import base64
import msal
import requests
import markdown
from bs4 import BeautifulSoup
# ---------------- 配置 ----------------
CLIENT_ID = "XXXXXXXXXXXXXXXXXXXXXXXXXX" # 微软应用ID
TENANT_ID = "consumers" # 个人账户必须用 "consumers"
SCOPES = ["Notes.ReadWrite"]
MD_ROOT = r"XXXXXXXXXXXXXXXXXXXXXXXXXXXX" # 思源导出的 Markdown 根目录
NOTEBOOK_NAME = "思源迁移" # OneNote 笔记本名,不要和已有重名
GRAPH_API = "https://graph.microsoft.com/v1.0"
# ---------------- 认证 ----------------
def get_token():
app = msal.PublicClientApplication(CLIENT_ID, authority=f"https://login.microsoftonline.com/{TENANT_ID}")
result = app.acquire_token_interactive(scopes=SCOPES)
if "access_token" not in result:
raise Exception(f"获取访问令牌失败: {result}")
return result["access_token"]
# ---------------- OneNote 操作 ----------------
def get_or_create_notebook(token, notebook_name):
url = f"{GRAPH_API}/me/onenote/notebooks"
resp = requests.get(url, headers={"Authorization": f"Bearer {token}"})
resp.raise_for_status()
for nb in resp.json().get("value", []):
if nb["displayName"] == notebook_name:
return nb["id"]
resp = requests.post(url, headers={"Authorization": f"Bearer {token}", "Content-Type": "application/json"},
json={"displayName": notebook_name})
resp.raise_for_status()
return resp.json()["id"]
def get_or_create_section(token, notebook_id, section_name):
url = f"{GRAPH_API}/me/onenote/notebooks/{notebook_id}/sections"
resp = requests.get(url, headers={"Authorization": f"Bearer {token}"})
resp.raise_for_status()
for sec in resp.json().get("value", []):
if sec["displayName"] == section_name:
return sec["id"]
resp = requests.post(url, headers={"Authorization": f"Bearer {token}", "Content-Type": "application/json"},
json={"displayName": section_name})
resp.raise_for_status()
return resp.json()["id"]
def create_page(token, section_id, title, content_html, resources=None, is_subpage=False):
"""
在 OneNote 创建页面,支持插入图片(作为 multipart 上传)
"""
url = f"{GRAPH_API}/me/onenote/sections/{section_id}/pages"
html = f"""
<!DOCTYPE html>
<html>
<head>
<title>{title}</title>
{"<meta name='isSubPage' content='true'/>" if is_subpage else ""}
</head>
<body>
<h1>{title}</h1>
{content_html}
</body>
</html>
"""
# 如果没有图片/资源,直接发 xhtml
if not resources:
resp = requests.post(url,
headers={"Authorization": f"Bearer {token}",
"Content-Type": "application/xhtml+xml"},
data=html.encode("utf-8"))
else:
# multipart 请求,带图片
multipart = {
"Presentation": ("presentation.html", html, "application/xhtml+xml")
}
for rid, (fname, fcontent, mime) in resources.items():
multipart[rid] = (fname, fcontent, mime)
resp = requests.post(url,
headers={"Authorization": f"Bearer {token}"},
files=multipart)
if not resp.ok:
print("创建页面失败:", resp.text)
return resp
# ---------------- Markdown 转 HTML,并处理图片 ----------------
def md_to_html(md_path):
try:
with open(md_path, "r", encoding="utf-8") as f:
text = f.read()
# markdown 转 html
html = markdown.markdown(text, extensions=["extra", "tables", "fenced_code"])
soup = BeautifulSoup(html, "html.parser")
resources = {}
# 处理 <img> 标签,把本地图片转成 OneNote 资源
for idx, img in enumerate(soup.find_all("img")):
src = img.get("src")
if not src:
continue
img_path = os.path.join(os.path.dirname(md_path), src)
if os.path.exists(img_path):
rid = f"image{idx}"
with open(img_path, "rb") as f:
data = f.read()
mime = "image/png" if src.lower().endswith("png") else "image/jpeg"
resources[rid] = (os.path.basename(src), data, mime)
# 替换 src 为 oneNote:image resource
img["src"] = f"name:{rid}"
return str(soup), resources
except Exception as e:
print(f"读取 {md_path} 出错: {e}")
return "", {}
# ---------------- 遍历文件夹并上传 ----------------
def process_directory(token, notebook_id, root_path):
for section_name in os.listdir(root_path):
section_path = os.path.join(root_path, section_name)
if not os.path.isdir(section_path):
continue
section_id = get_or_create_section(token, notebook_id, section_name)
print(f"创建分区: {section_name}")
process_pages(token, section_id, section_path, level=0)
def process_pages(token, section_id, folder_path, level=0):
entries = sorted(os.listdir(folder_path))
for entry in entries:
entry_path = os.path.join(folder_path, entry)
if os.path.isdir(entry_path):
folder_md = os.path.join(entry_path, f"{entry}.md")
if os.path.exists(folder_md):
content_html, resources = md_to_html(folder_md)
create_page(token, section_id, entry, content_html, resources, is_subpage=(level > 0))
print("创建页:", entry)
process_pages(token, section_id, entry_path, level=level + 1)
elif entry.endswith(".md"):
title = os.path.splitext(entry)[0]
content_html, resources = md_to_html(entry_path)
create_page(token, section_id, title, content_html, resources, is_subpage=(level > 0))
print("创建页:", title)
# ---------------- 主程序 ----------------
def main():
token = get_token()
notebook_id = get_or_create_notebook(token, NOTEBOOK_NAME)
process_directory(token, notebook_id, MD_ROOT)
if __name__ == "__main__":
main()
确保脚本可访问思源笔记导出的本地文件目录
5. 执行迁移
- 将思源笔记导出的文件放在脚本目录下
- 打开命令行,切换到目录:
cd C:\Users\YourName\Documents\SiYuanMigration
- 执行脚本:
python siyuan_to_onenote.py

- 完成后登录 OneNote 检查笔记是否已成功同步
6. 注意事项
- 备份数据:迁移前请备份思源笔记数据
- 分批迁移:大数据量笔记建议分批处理
- 编码问题:确保笔记文件 UTF-8 编码
- Azure 权限:确保应用权限正确,并完成授权
- 网络:迁移需要网络访问 OneNote API
- 目前此代码仅支持个人微软账户,不支持公司账户
-
因为 oneonte 默认只支持,分区-页, 没有子页,子子页,如果需要有很多子子子子子页的用户,迁移后需要手动分类,
-
这个是你想要的。
-
实际迁移后的样子
-












欢迎来到这里!
我们正在构建一个小众社区,大家在这里相互信任,以平等 • 自由 • 奔放的价值观进行分享交流。最终,希望大家能够找到与自己志同道合的伙伴,共同成长。
注册 关于