当前位置: 首页 > news >正文

Typora to Obsidian 迁移助手 (Typora-to-Obsidian-Migration-Helper)

本脚本基于历史文章中模块程序组合而成,能够实现可控自动化迁移笔记,自用。

一个交互式的、基于状态机模式的 Python 脚本,旨在帮助用户安全、高效地将 Typora 笔记库迁移至 Obsidian。它将多个繁琐的手动步骤整合为一个自动化的、可控的流程。

📖 简介

当您决定将主力笔记应用从 Typora 切换到 Obsidian 时,通常会面临几个棘手的问题:

  1. 图片链接不兼容:Typora 使用标准的 Markdown 相对路径链接 (![](folder/image.png)),而 Obsidian 推荐使用更简洁、更强大的双链格式 (![[image.png]])。

  2. 附件管理混乱:Typora 倾向于为每个笔记或每个子文件夹创建一个独立的附件文件夹(默认为 Database),导致附件散落在笔记库的各个角落,难以统一管理。

  3. 笔记命名缺乏时间戳:为笔记文件名添加创建或修改日期前缀,有助于在 Obsidian 中进行更有效的时间线排序和回顾。

本工具旨在一次性解决以上所有问题,通过一个引导式的命令行界面,让迁移过程变得简单而透明。
image

✨ 主要特性

  • 🤖 状态机驱动:整个流程由清晰的状态机控制,确保每一步都按预定顺序执行。

  • 🛡️ 交互式确认:在执行每一项核心操作(重命名、链接转换、附件整理)后,程序会暂停并等待用户确认,给予用户充足的时间检查日志,确保万无一失。

  • 📂 单文件运行:无需安装任何第三方库,仅依赖 Python 标准库,下载即用。

  • 三合一核心功能

    1. 文件名批量重命名:自动为所有 .md 文件名添加其“最后修改日期”作为前缀(格式:YYYYMMDD-笔记名.md)。

    2. 图片链接格式转换:智能识别并转换 Typora 风格的图片链接为 Obsidian 兼容的双链格式。

    3. 附件库整合与清理:将所有散落的附件文件夹(默认为 Database)内的文件,统一移动到一个新的、位于根目录的附件库中(DatabaseNew),并自动处理重复文件和清理空文件夹。

🚀 如何使用

✅ 准备工作

  1. 安装 Python:确保您的电脑上已安装 Python 3.6 或更高版本。

  2. 下载脚本:将 main.py 脚本文件下载到您的电脑任意位置。

  3. ‼️ 最重要的一步:备份! 在运行此脚本前,请务必、务必、务必手动完整备份您的整个笔记库文件夹! 本脚本的操作是不可逆的,备份是您唯一的“后悔药”。

🏃‍♀️ 运行流程

  1. 打开终端

    • 在 Windows 上,打开 命令提示符(CMD)PowerShell

    • 在 macOS 或 Linux 上,打开 终端(Terminal)

  2. 运行脚本:使用 python 命令来执行脚本。

    python /path/to/your/main.py
    

    (请将 /path/to/your/main.py 替换为您存放 main.py 的实际路径)

  3. 按照提示操作:脚本启动后,会引导您完成以下步骤:

    • 确认备份:首先会要求您确认是否已完成备份。

    • 输入路径:提示您输入要处理的笔记库根文件夹的绝对路径。可以直接从文件管理器复制路径并粘贴。

    • 分步执行与确认

      1. 执行文件名重命名 -> 打印日志 -> 等待您按 Enter 确认

      2. 执行图片链接转换 -> 打印日志 -> 等待您按 Enter 确认

      3. 执行附件库整理 -> 打印日志 -> 等待您按 Enter 确认

    • 完成:所有步骤确认后,程序会打印最终的操作总览并退出。

🔬 执行结果详解

假设您的原始笔记库结构如下:

MyNotes/
├── 日常笔记/
│   ├── 一篇关于猫的笔记.md
│   └── 一篇关于猫的笔记.assets/
│       └── cat_photo.png
└── 技术分享/├── K8s学习.md└── K8s学习.assets/└── k8s_arch.png

运行脚本并完成所有步骤后,您的笔记库结构将变为:

MyNotes/
├── 日常笔记/
│   └── 20251001-一篇关于猫的笔记.md  <-- 文件名已修改
└── 技术分享/
│   └── 20250928-K8s学习.md        <-- 文件名已修改
│
├── K8s学习.assets/               <-- 旧的空附件文件夹已被自动删除
├── 一篇关于猫的笔记.assets/        <-- 旧的空附件文件夹已被自动删除
│
├── DatabaseNew/                    <-- 新创建的统一附件库
│   ├── cat_photo.png               <-- 附件已移入
│   └── k8s_arch.png                <-- 附件已移入
│
└── Database重复文件手动处理/         <-- (如果存在重名附件)

同时,笔记文件内的内容也会发生变化:

  • 修改前 (一篇关于猫的笔记.md):

    这是一只可爱的猫。
    ![](一篇关于猫的笔记.assets/cat_photo.png)
    
  • 修改后 (20251001-一篇关于猫的笔记.md):

    这是一只可爱的猫。
    ![[cat_photo.png]]
    

最终,终端会显示类似这样的摘要报告:

======================================================================
🎉 全部流程执行完毕 - 操作总览 🎉
======================================================================
📄 文件名重命名:   成功重命名 2 个 .md 文件。
🔗 图片链接转换:   成功转换 2 个链接。
🗂️ 附件库整理:- 成功移动 2 个附件到新的库中。- 发现 0 个重复附件,已隔离处理。
======================================================================✅ 所有迁移任务已成功完成!程序退出。

⚠️ 注意事项

  • 本脚本会直接修改您指定文件夹内的文件,所有操作都是不可逆的。请务必在运行前进行备份。

  • 在每一步确认前,请仔细检查终端打印的日志,确保操作符合您的预期。

  • 如果中途输入 'n' 退出,已经完成的步骤不会撤销。

  • 如果在附件整理步骤中出现重名文件,它们会被移动到 Database重复文件手动处理 文件夹中,需要您手动检查和决定保留哪一个。如果没有任何重名文件,该文件夹会被自动删除。

📜 许可证

本项目采用 MIT License 授权。

main.py

点击查看代码

import os
import sys
import datetime
import re
import shutil
from enum import Enum, auto# --- 状态定义 ---
class State(Enum):"""定义程序的所有可能状态"""WAITING_FOR_BACKUP_CONFIRMATION = auto()GETTING_PATHS = auto()PROCESSING_RENAME = auto()PROCESSING_CONVERSION = auto()PROCESSING_ORGANIZATION = auto()FINALIZED = auto()ABORTED = auto()class MigrationAssistant:"""一个采用状态机模式的、交互式的 Typora 到 Obsidian 迁移工具。整合了文件名重命名、链接转换和附件整理三大功能。"""def __init__(self):"""初始化状态机"""self.state = State.WAITING_FOR_BACKUP_CONFIRMATIONself.target_folder_path = Noneself.asset_source_folder_name = "Database" # 默认附件文件夹名# --- 新增: 用于最终统计 ---self.renamed_files_count = 0self.converted_links_count = 0self.moved_files_count = 0self.duplicate_files_count = 0def run(self):"""启动并运行状态机"""while True:if self.state == State.WAITING_FOR_BACKUP_CONFIRMATION:self._handle_backup_confirmation()elif self.state == State.GETTING_PATHS:self._handle_getting_paths()elif self.state == State.PROCESSING_RENAME:self._handle_rename()elif self.state == State.PROCESSING_CONVERSION:self._handle_conversion()elif self.state == State.PROCESSING_ORGANIZATION:self._handle_organization()elif self.state == State.FINALIZED:self._display_final_summary()print("\n✅ 所有迁移任务已成功完成!程序退出。")breakelif self.state == State.ABORTED:print("\n❌ 操作已由用户终止。程序退出。")breakdef _wait_for_user_confirmation(self, prompt):"""暂停程序,打印提示信息,并等待用户确认。返回 True 表示继续,返回 False 表示中止。"""response = input(f"\n{prompt} ")if response.lower() == 'n':return Falsereturn Truedef _display_final_summary(self):"""在程序结束前打印最终的操作总览。"""print("\n" + "=" * 70)print("🎉 全部流程执行完毕 - 操作总览 🎉")print("=" * 70)print(f"📄 文件名重命名:   成功重命名 {self.renamed_files_count} 个 .md 文件。")print(f"🔗 图片链接转换:   成功转换 {self.converted_links_count} 个链接。")print(f"🗂️ 附件库整理:")print(f"  - 成功移动 {self.moved_files_count} 个附件到新的库中。")print(f"  - 发现 {self.duplicate_files_count} 个重复附件,已隔离处理。")print("=" * 70)# --- 状态处理逻辑 ---def _handle_backup_confirmation(self):"""处理备份确认状态"""print("=" * 70)print("欢迎使用 Typora 到 Obsidian 迁移助手")print("警告: 此工具将直接修改您的文件,操作不可逆。")print("在开始前,请务必手动完整备份您的整个笔记库文件夹!")print("=" * 70)confirm = input("我确认已经手动备份了我的笔记库文件夹 (y/n): ")if confirm.lower() == 'y':self.state = State.GETTING_PATHSelse:self.state = State.ABORTEDdef _handle_getting_paths(self):"""处理路径获取状态"""while True:path = input("请输入您的笔记库文件夹的绝对路径并按 Enter: ")cleaned_path = path.strip().strip('"')if os.path.isdir(cleaned_path):self.target_folder_path = cleaned_pathprint(f"✔️ 目标文件夹已确认为: {self.target_folder_path}")self.state = State.PROCESSING_RENAMEbreakelse:print(f"错误: 您输入的路径 '{cleaned_path}' 不是一个有效的文件夹,请重新输入。")def _handle_rename(self):"""处理文件名重命名状态"""print("\n" + "=" * 70)print("🚀 步骤 1/3: [文件名重命名]")print("将为所有 .md 文件名添加 '最后修改日期' 前缀 (格式: YYYYMMDD-文件名.md)")print("=" * 70)self.renamed_files_count = self._batch_rename_md_files(self.target_folder_path)print("\n" + "-" * 70)print("下一步将执行:")print("🚀 步骤 2/3: [图片链接转换]")print("将 Typora 格式的图片链接 (如 `![](folder/image.png)`) 转换为 Obsidian 格式 (`![[image.png]]`)")print("-" * 70)if self._wait_for_user_confirmation("检查以上日志,按 Enter 键继续,或输入 'n' 退出。"):self.state = State.PROCESSING_CONVERSIONelse:self.state = State.ABORTEDdef _handle_conversion(self):"""处理链接转换状态"""print("\n" + "=" * 70)print("🚀 步骤 2/3: [图片链接转换]")print("将 Typora 格式的图片链接 (如 `![](folder/image.png)`) 转换为 Obsidian 格式 (`![[image.png]]`)")print("=" * 70)self.converted_links_count = self._convert_image_links(self.target_folder_path)print("\n" + "-" * 70)print("下一步将执行:")print("🚀 步骤 3/3: [附件库整理]")print("将散落在各处的附件文件夹内容统一整理到一个新的附件库中。")print("-" * 70)if self._wait_for_user_confirmation("检查以上日志,按 Enter 键继续,或输入 'n' 退出。"):self.state = State.PROCESSING_ORGANIZATIONelse:self.state = State.ABORTEDdef _handle_organization(self):"""处理附件整理状态"""print("\n" + "=" * 70)print("🚀 步骤 3/3: [附件库整理]")print("将散落在各处的附件文件夹内容统一整理到一个新的附件库中。")print("=" * 70)prompt = f"默认的源附件文件夹名为 '{self.asset_source_folder_name}',是否需要更改?(直接按 Enter 使用默认值,或输入新名称): "new_name = input(prompt)if new_name.strip():self.asset_source_folder_name = new_name.strip()print(f"✔️ 源附件文件夹名已更新为: '{self.asset_source_folder_name}'")moved, duplicates = self._organize_database_folders(self.target_folder_path, self.asset_source_folder_name)self.moved_files_count = movedself.duplicate_files_count = duplicatesif self._wait_for_user_confirmation("检查以上日志,按 Enter 键完成全部流程。"):self.state = State.FINALIZEDelse:self.state = State.ABORTED# --- 核心功能逻辑 (从原脚本整合) ---def _batch_rename_md_files(self, root_folder):"""(源自 rename_md.py)"""print(f"\n开始扫描文件夹: '{root_folder}'")renamed_count = 0scanned_count = 0for dirpath, _, filenames in os.walk(root_folder):for filename in filenames:if filename.endswith('.md'):scanned_count += 1if re.match(r'^\d{8}-', filename):print(f"-> 跳过 (已是目标格式): {filename}")continueold_filepath = os.path.join(dirpath, filename)try:modification_timestamp = os.path.getmtime(old_filepath)modification_date = datetime.datetime.fromtimestamp(modification_timestamp)date_prefix = modification_date.strftime('%Y%m%d')new_filename = f"{date_prefix}-{filename}"new_filepath = os.path.join(dirpath, new_filename)os.rename(old_filepath, new_filepath)print(f"✅ 重命名: '{filename}' -> '{new_filename}'")renamed_count += 1except Exception as e:print(f"❌ 处理 '{filename}' 时发生错误: {e}")print("\n--- [文件名重命名] 操作摘要 ---")print(f"共扫描到 {scanned_count} 个 .md 文件。")print(f"成功重命名 {renamed_count} 个文件。")return renamed_countdef _convert_image_links(self, directory):"""(源自 typora_to_obsidian.py)"""image_extensions = ['png', 'jpg', 'jpeg', 'gif', 'bmp', 'svg', 'webp']pattern_str = r'!\[.*?\]\((?:.*/)?(.*?\.({}))\)'.format('|'.join(image_extensions))image_pattern = re.compile(pattern_str, re.IGNORECASE)total_replacements = 0print(f"\n开始扫描文件夹: '{directory}'")for root, _, files in os.walk(directory):for filename in files:if filename.endswith('.md'):file_path = os.path.join(root, filename)try:with open(file_path, 'r', encoding='utf-8') as f:content = f.read()new_content, num_replacements = image_pattern.subn(r'![[\1]]', content)if num_replacements > 0:with open(file_path, 'w', encoding='utf-8') as f:f.write(new_content)print(f"处理文件: {file_path}")print(f"  -> 成功转换 {num_replacements} 个链接。")total_replacements += num_replacementsexcept Exception as e:print(f"处理文件 {file_path} 时出错: {e}", file=sys.stderr)print("\n--- [图片链接转换] 操作摘要 ---")if total_replacements > 0:print(f"总共成功转换了 {total_replacements} 个链接。")else:print("未在任何文件中发现需要转换的 Typora 格式图片链接。")return total_replacementsdef _organize_database_folders(self, root_folder, source_folder_name):"""(源自 organize_db.py)"""consolidate_dir = os.path.join(root_folder, f"{source_folder_name}迁移后文件夹")duplicate_dir = os.path.join(root_folder, f"{source_folder_name}有重复文件需要手动处理")try:os.makedirs(consolidate_dir, exist_ok=True)os.makedirs(duplicate_dir, exist_ok=True)print(f"✔️ 目标文件夹创建/确认成功:\n  - {consolidate_dir}\n  - {duplicate_dir}")except OSError as e:print(f"❌ 错误: 无法创建目标文件夹,请检查权限。 {e}", file=sys.stderr)return 0, 0moved_count = 0duplicate_count = 0print(f"\n--- 开始阶段 1: 移动文件 (源文件夹: '{source_folder_name}') ---")for dirpath, _, filenames in os.walk(root_folder):if os.path.basename(dirpath) == source_folder_name:print(f"\n正在处理文件夹: {dirpath}")for filename in filenames:source_path = os.path.join(dirpath, filename)dest_path_consolidate = os.path.join(consolidate_dir, filename)if not os.path.exists(dest_path_consolidate):try:shutil.move(source_path, dest_path_consolidate)print(f"  -> 移动到 '{os.path.basename(consolidate_dir)}': {filename}")moved_count += 1except Exception as e:print(f"  ❌ 移动失败 (权限问题?): {filename} - {e}")else:dest_path_duplicate = os.path.join(duplicate_dir, filename)try:shutil.move(source_path, dest_path_duplicate)print(f"  ⚠️ 发现重复, 移动到 '{os.path.basename(duplicate_dir)}' 文件夹: {filename}")duplicate_count += 1except Exception as e:print(f"  ❌ 移动重复文件失败: {filename} - {e}")print(f"\n--- 开始阶段 2: 清理空的 '{source_folder_name}' 文件夹 ---")deleted_folders_count = 0non_empty_folders = []for dirpath, _, _ in os.walk(root_folder, topdown=False):if os.path.basename(dirpath) == source_folder_name:try:if not os.listdir(dirpath):os.rmdir(dirpath)print(f"  🗑️ 删除空文件夹: {dirpath}")deleted_folders_count += 1else:non_empty_folders.append(dirpath)except OSError as e:print(f"  ❌ 删除文件夹失败: {dirpath} - {e}")non_empty_folders.append(dirpath)print("\n--- [附件库整理] 操作摘要 ---")print(f"移动到 '{os.path.basename(consolidate_dir)}' 的文件总数: {moved_count}")print(f"因重名而移动到 '{os.path.basename(duplicate_dir)}' 的文件总数: {duplicate_count}")print(f"成功删除的空 '{source_folder_name}' 文件夹数量: {deleted_folders_count}")if duplicate_count == 0:try:if not os.listdir(duplicate_dir):os.rmdir(duplicate_dir)print(f"\n✔️ 因没有重复文件,已自动删除空的 '{os.path.basename(duplicate_dir)}' 文件夹。")except OSError as e:print(f"  ❌ 尝试自动删除空的重复文件夹失败: {e}")if non_empty_folders:print("\n!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!")print(f"!!! 警告: 以下 '{source_folder_name}' 文件夹未能清空或删除,请手动检查。")for path in non_empty_folders:print(f"  - {path}")print("!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!")else:print(f"\n所有 '{source_folder_name}' 文件夹均已成功处理并清理。")return moved_count, duplicate_countif __name__ == "__main__":app = MigrationAssistant()app.run()
http://www.hskmm.com/?act=detail&tid=22313

相关文章:

  • 64. 最小路径和
  • 题解:P1020 [NOIP 1999 提高组] 导弹拦截
  • 哈希表专题
  • Meta基础设施演进与AI技术革命
  • 完整教程:Spring AI整合聊天模型DeepSeek
  • 2025 年焚烧炉厂家 TOP 企业品牌推荐排行榜!权威甄选实力与口碑俱佳的江苏焚烧炉 / 无锡焚烧炉推荐这十家公司!
  • 2025 年防腐涂料厂家 TOP 企业品牌推荐排行榜,乙烯基、环氧煤沥青、环氧防腐涂料、防腐涂料地坪 、防腐涂料水池推荐这十家公司!
  • 2025双氧水厂家权威推荐榜:优质供应与专业定制实力之选
  • Win环境下包管理工具
  • MX Round 11 解题报告
  • 用 C# 打造企业资产管理系统雏形——从控制台到完整模块设计 - 详解
  • java开发之微信机器人的二次开发
  • 10.1刷题计划一
  • 笔记本电脑重装系统后找不到5G WIFI无线网或蓝牙模块消失的解决方案
  • 菜鸟坚持记录-开头篇
  • AI+传统工作流:Photoshop/Excel的智能插件开发指南 - 实践
  • Typora 笔记迁移 Obsidian 图片附件库批量移动方法,适用于笔记整理。
  • 2025年确有专长培训权威推荐榜:专业资质与特色诊疗口碑之选
  • 开源 C# 快速构建(五)自定义控件--仪表盘
  • 2025中医师承培训、考试、认证机构权威推荐榜:名师传承与临床实践口碑之选
  • 电子文件分类整理与双向同步 2025年10月1日
  • C++版搜索与图论算法 - 详解
  • 62. 不同路径
  • 达成设计卓越:全面解析 IC 设计中的验证之道
  • Typora 笔记迁移 Obsidian 图片链接转换
  • Java 运行 Word 文档标签并赋值:从基础到实战
  • 词云组件
  • 2025 年超声波清洗机品牌最新权威推荐排行榜:龙门式 / 悬挂式 / 全自动等多类型设备厂家 TOP3 精选,助力企业精准选购
  • 树的统一迭代法
  • 2025 年冷却塔品牌最新推荐排行榜:玻璃钢冷却塔、闭式冷却塔、方型逆流式冷却塔优质厂家 TOP3 精选,赋能企业选购