告别手动导出:一键将思源笔记自动同步到 Git 仓库
你是否也曾像我一样,在思源笔记中精心维护着一份指南或知识库,却在将其同步到 GitLab/GitHub 时感到烦恼?每次更新,都意味着一系列重复枯燥的操作:手动导出 -> 找到 Markdown 和图片 -> 复制粘贴到 Git 仓库 -> git add -> git commit -> git push。
这个过程不仅耗时,而且极易出错。今天,我将与你分享如何通过一个 Python 脚本,将这整个流程彻底自动化,实现真正的“一键同步”,让你从此专注于内容创作。
最终目标
我们的目标是创建一个脚本,当它运行时,会自动完成以下所有事情:
- 调用思源笔记 API:自动请求思源笔记将指定文档(包含所有文本和图片)导出为一个
.zip压缩包。 - 定位并移动文件:智能地找到思源笔记在本地生成的导出文件。
- 更新仓库:将新的
.zip包移动到本地 Git 仓库,清空旧内容,然后解压新内容。 - 自动版本控制:自动执行 Git 命令,将所有变更提交并推送到远程仓库。
准备工作
在开始之前,请确保你已准备好以下环境:
- 思源笔记桌面客户端:脚本运行时,它必须处于运行状态。
- Python 3:下载并安装 Python。
- Git:下载并安装 Git,并确保你的仓库已配置好远程推送权限。
- 本地 Git 仓库:将你的目标 GitLab/GitHub 仓库克隆到本地。
- 安装
requests库:在终端中运行pip install requests。
步骤一:获取你的个人凭证
我们的脚本需要一些关键信息才能与你的思源笔记和仓库进行交互。
-
思源笔记 API Token:
- 打开思源笔记客户端,进入 设置 -> 关于 -> API Token。
- 点击“复制”按钮,这个 Token 就是脚本访问你笔记的钥匙。
-
文档 ID (Block ID) :
- 在笔记中找到你想导出的那篇文档。
- 右键点击文档标题,选择 复制 -> 块链接。
- 你会得到类似
siyuan://blocks/20251029171326-62jlw08的链接,其中20251029171326-62jlw08就是我们需要的文档 ID。
-
思源笔记临时导出目录:
- 这是最关键的一步!经过探索发现,思源笔记通过 API 导出时,并不是直接返回文件流,而是在本地的一个临时目录生成文件。
- 你需要找到这个目录。通常它位于你的思源笔记工作空间的
temp/export文件夹下。在我的电脑上,这个路径是D:\SiYuanDoc\temp\export。请根据你的实际情况找到并记录下这个路径。
步骤二:构建自动化脚本
现在,让我们把所有东西组合成一个强大的 Python 脚本。下面的代码整合了我们所有的发现,并包含了详细的注释和强大的错误处理机制。
将以下代码保存为 siyuan_sync.py:
import requests
import os
import zipfile
import subprocess
import shutil
from datetime import datetime
import urllib.parse# --- 1. 请在这里配置你的个人信息 ---
SIYUAN_API_TOKEN = "在这里粘贴你的API Token"
SIYUAN_DOC_ID = "在这里粘贴你的文档ID"
GIT_REPO_PATH = r"D:\path\to\your\local\git\repo" # 使用 r"" 原始字符串避免转义问题
SIYUAN_API_URL = "http://127.0.0.1:6806"
# 关键:你实际的思源笔记临时导出目录
SIYUAN_EXPORT_BASE_PATH = r"D:\SiYuanDoc\temp\export" # --- 2. 脚本核心功能 ---def export_from_siyuan():"""调用exportMd API,解析JSON响应,并从思源临时目录移动ZIP文件。"""export_url = f"{SIYUAN_API_URL}/api/export/exportMd"headers = {"Authorization": f"Token {SIYUAN_API_TOKEN}"}payload = {"id": SIYUAN_DOC_ID}print("正在向思源笔记请求导出...")try:response = requests.post(export_url, json=payload, headers=headers)response.raise_for_status()json_data = response.json()if json_data.get("code") != 0:print(f"错误:思源笔记API返回错误。")print(f" 错误码: {json_data.get('code')}")print(f" 错误消息: {json_data.get('msg')}")return None, Falserelative_zip_path = json_data["data"]["zip"]print(f"API返回相对路径: {relative_zip_path}")# 将URL编码的文件名解码成系统可识别的文件名global decoded_filenamedecoded_filename = urllib.parse.unquote(os.path.basename(relative_zip_path))full_source_zip_path = os.path.join(SIYUAN_EXPORT_BASE_PATH, decoded_filename)dest_zip_path = os.path.join(GIT_REPO_PATH, "siyuan_export.zip")if not os.path.exists(full_source_zip_path):print(f"错误:在思源导出目录中未找到文件: {full_source_zip_path}")print("请检查 SIYUAN_EXPORT_BASE_PATH 配置是否正确。")return None, Falseprint(f"正在从 '{full_source_zip_path}' 移动到 '{dest_zip_path}'...")shutil.move(full_source_zip_path, dest_zip_path)print(f"成功获取ZIP文件到Git仓库目录。")return dest_zip_path, Trueexcept requests.exceptions.RequestException as e:print(f"错误:无法连接到思源笔记。请确保思源笔记正在运行。详细信息: {e}")return None, Falseexcept KeyError:print(f"错误:API响应格式不正确。收到的响应: {response.text}")return None, Falsedef unzip_and_organize(zip_path):"""解压文件并整理到Git仓库根目录。"""temp_extract_path = os.path.join(GIT_REPO_PATH, "temp_extract")try:print("正在清理旧文件...")for item in os.listdir(GIT_REPO_PATH):if item in ['.git', os.path.basename(zip_path), 'temp_extract']:continueitem_path = os.path.join(GIT_REPO_PATH, item)if os.path.isdir(item_path):shutil.rmtree(item_path)else:os.remove(item_path)print("旧文件清理完毕。")print(f"正在解压 {zip_path}...")with zipfile.ZipFile(zip_path, 'r') as zip_ref:zip_ref.extractall(temp_extract_path)exported_content_path = ""unzipped_folder_name = os.path.splitext(decoded_filename)potential_path = os.path.join(temp_extract_path, unzipped_folder_name)if os.path.isdir(potential_path):exported_content_path = potential_pathelse:for item in os.listdir(temp_extract_path):path = os.path.join(temp_extract_path, item)if os.path.isdir(path): exported_content_path = path; breakif not exported_content_path:print("错误:未在ZIP包中找到预期的导出文件夹。"); return Falsefor item in os.listdir(exported_content_path):src_path = os.path.join(exported_content_path, item)dest_path = os.path.join(GIT_REPO_PATH, item)shutil.move(src_path, dest_path)print("文件解压和整理完成。"); return Trueexcept Exception as e:print(f"解压过程中发生错误: {e}"); return Falsefinally:if os.path.exists(zip_path): os.remove(zip_path)if os.path.exists(temp_extract_path): shutil.rmtree(temp_extract_path)def git_push():"""在Git仓库目录执行add, commit, push命令。"""print("正在执行Git操作...")try:base_cmd = ["git", f"--git-dir={os.path.join(GIT_REPO_PATH, '.git')}", f"--work-tree={GIT_REPO_PATH}"]status_result = subprocess.run(base_cmd + ["status", "--porcelain"], capture_output=True, text=True, check=True)if not status_result.stdout.strip():print("Git仓库内容无变化,无需提交。"); returnsubprocess.run(base_cmd + ["add", "."], check=True)commit_message = f"Automated update from Siyuan Note on {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}"subprocess.run(base_cmd + ["commit", "-m", commit_message], check=True)subprocess.run(base_cmd + ["push"], check=True)print("成功推送到GitLab仓库!")except Exception as e:print(f"Git操作失败: {e}")# 在全局作用域声明变量以供函数间共享
decoded_filename = ""if __name__ == "__main__":zip_file_path, export_ok = export_from_siyuan()if export_ok:unzip_ok = unzip_and_organize(zip_file_path)if unzip_ok:git_push()
步骤三:跑起来看看
-
填写配置:打开
siyuan_sync.py文件,仔细填写顶部的四项个人配置信息。 -
确保思源在运行:这是必须的!
-
运行脚本:打开终端,进入脚本所在目录,执行命令:
python siyuan_sync.py
更进一步:设置定时任务
- 在 Windows 上:使用“任务计划程序”创建一个新任务,设置好触发时间和要执行的
python siyuan_sync.py命令。 - 在 macOS 或 Linux 上:使用
cron。在终端运行crontab -e,添加类似0 22 * * * /usr/bin/python3 /path/to/your/siyuan_sync.py的行,即可实现每天晚上10点自动同步。
