把Obsidian笔记复制到hexo博客


把Obsidian笔记复制到hexo博客

方案1,ob插件(推荐)

Obsidian 自定义插件——Hexo 格式转换插件

方案2,python程序

界面

打包

通过网盘分享的文件:把Obsidian的md笔记复制到hexo博客.exe 链接: https://pan.baidu.com/s/1JGSQWPuEMOPTCckRARUnDQ?pwd=kmpa 提取码: kmpa

代码

import tkinter as tk
from tkinter import filedialog, messagebox
import ttkbootstrap as ttk
from ttkbootstrap.constants import *
import tkinterdnd2 as tkdnd
import re
import os
import shutil
import json
from pathlib import Path

# 配置文件路径(保存历史记录)
CONFIG_FILE = "md_image_extractor_config.json"

class MDImageExtractor:
    def __init__(self, root):
        self.root = root
        self.root.title("MD文档图片提取工具")
        self.root.geometry("800x500")
        
        # 初始化变量
        self.md_file_path = tk.StringVar()  # MD文件路径
        self.save_path = tk.StringVar()     # 保存路径
        self.save_path_history = []         # 保存路径历史记录
        self.remove_prefix = tk.BooleanVar(value=True)  # 是否去掉数字前缀
        
        # 加载历史记录
        self.load_history()
        
        # 创建UI界面
        self.create_widgets()
        
    def load_history(self):
        """加载保存路径的历史记录"""
        try:
            if os.path.exists(CONFIG_FILE):
                with open(CONFIG_FILE, "r", encoding="utf-8") as f:
                    config = json.load(f)
                    self.save_path_history = config.get("save_path_history", [])
                    # 去重并保留最新的5条
                    self.save_path_history = list(dict.fromkeys(self.save_path_history))[:5]
                    # 如果有历史记录,默认选中第一条
                    if self.save_path_history:
                        self.save_path.set(self.save_path_history[0])
        except Exception as e:
            messagebox.showwarning("警告", f"加载历史记录失败:{str(e)}")
            self.save_path_history = []
    
    def save_history(self):
        """保存路径历史记录到配置文件"""
        current_path = self.save_path.get().strip()
        if current_path and current_path not in self.save_path_history:
            self.save_path_history.insert(0, current_path)
            # 只保留最新的5条
            self.save_path_history = self.save_path_history[:5]
        
        try:
            with open(CONFIG_FILE, "w", encoding="utf-8") as f:
                json.dump({"save_path_history": self.save_path_history}, f, ensure_ascii=False, indent=2)
        except Exception as e:
            messagebox.showwarning("警告", f"保存历史记录失败:{str(e)}")
    
    def create_widgets(self):
        """创建界面组件"""
        # 主容器
        main_frame = ttk.Frame(self.root)
        main_frame.pack(fill=BOTH, expand=YES, padx=20, pady=20)
        
        # 1. MD文件选择区域
        md_frame = ttk.LabelFrame(main_frame, text="MD文档选择")
        md_frame.pack(fill=X, pady=(0, 15))
        
        # MD文件输入框(支持拖拽)
        self.md_entry = ttk.Entry(md_frame, textvariable=self.md_file_path)
        self.md_entry.pack(side=LEFT, fill=X, expand=YES, padx=(0, 10))
        
        # 注册拖拽事件
        self.md_entry.drop_target_register(tkdnd.DND_FILES)
        self.md_entry.dnd_bind("<<Drop>>", self.on_drop_file)
        
        # MD文件选择按钮
        md_btn = ttk.Button(md_frame, text="选择文件", command=self.select_md_file, bootstyle=PRIMARY)
        md_btn.pack(side=RIGHT)
        
        # 2. 保存路径选择区域
        save_frame = ttk.LabelFrame(main_frame, text="结果保存位置")
        save_frame.pack(fill=X, pady=(0, 15))
        
        # 保存路径输入框
        self.save_entry = ttk.Entry(save_frame, textvariable=self.save_path)
        self.save_entry.pack(side=LEFT, fill=X, expand=YES, padx=(0, 10))
        
        # 保存路径选择按钮
        save_btn = ttk.Button(save_frame, text="选择文件夹", command=self.select_save_path, bootstyle=PRIMARY)
        save_btn.pack(side=RIGHT)
        
        # 历史记录下拉框(可选)
        if self.save_path_history:
            history_listbox = tk.Listbox(save_frame, height=1)
            for path in self.save_path_history:
                history_listbox.insert(END, path)
            history_listbox.pack(side=RIGHT, padx=(10, 0))
            history_listbox.bind("<<ListboxSelect>>", self.on_history_select)
        
        # 3. 去掉数字前缀选项
        prefix_check = ttk.Checkbutton(
            main_frame,
            text="去掉文件名前面的数字前缀(如:150笔记A.md → 笔记A.md)",
            variable=self.remove_prefix
        )
        prefix_check.pack(pady=(0, 10))
        
        # 4. 开始提取按钮
        extract_btn = ttk.Button(
            main_frame, 
            text="开始提取", 
            command=self.start_extract, 
            bootstyle=SUCCESS
        )
        extract_btn.pack(pady=20)
        
        # 4. 日志显示区域
        log_frame = ttk.LabelFrame(main_frame, text="提取日志")
        log_frame.pack(fill=BOTH, expand=YES)
        
        # 日志文本框
        self.log_text = tk.Text(log_frame, height=10, font=("Consolas", 9), wrap=tk.WORD)
        self.log_text.pack(side=LEFT, fill=BOTH, expand=YES)
        
        # 滚动条
        log_scroll = ttk.Scrollbar(log_frame, orient=VERTICAL, command=self.log_text.yview)
        log_scroll.pack(side=RIGHT, fill=Y)
        self.log_text.config(yscrollcommand=log_scroll.set)
    
    def on_drop_file(self, event):
        """处理文件拖拽事件"""
        try:
            # 获取拖入的文件路径(tkinterdnd2 返回的数据格式)
            file_path = event.data
            # 去除可能的引号和花括号
            file_path = file_path.strip("{}").strip('"').strip("'")
            
            # 只接受md文件
            if file_path.lower().endswith(".md"):
                self.md_file_path.set(file_path)
                self.log(f"已拖入MD文件:{file_path}")
            else:
                messagebox.showwarning("警告", "请拖入.md格式的文件!")
        except Exception as e:
            self.log(f"拖入文件失败:{str(e)}")
    
    def select_md_file(self):
        """选择MD文件"""
        file_path = filedialog.askopenfilename(
            title="选择MD文档",
            filetypes=[("Markdown文件", "*.md"), ("所有文件", "*.*")]
        )
        if file_path:
            self.md_file_path.set(file_path)
            self.log(f"已选择MD文件:{file_path}")
    
    def select_save_path(self):
        """选择保存文件夹"""
        folder_path = filedialog.askdirectory(title="选择保存文件夹")
        if folder_path:
            self.save_path.set(folder_path)
            self.save_history()
            self.log(f"已选择保存路径:{folder_path}")
    
    def on_history_select(self, event):
        """选择历史保存路径"""
        listbox = event.widget
        if listbox.curselection():
            index = listbox.curselection()[0]
            selected_path = listbox.get(index)
            self.save_path.set(selected_path)
            self.log(f"选择历史保存路径:{selected_path}")
    
    def log(self, msg):
        """日志输出"""
        self.log_text.insert(END, f"{msg}\n")
        self.log_text.see(END)
        self.root.update_idletasks()
    
    def extract_images_from_md(self, md_path):
        """从MD文件中提取图片名称(匹配![](图片名)格式)"""
        try:
            with open(md_path, "r", encoding="utf-8") as f:
                content = f.read()
            
            # 正则匹配![](图片名称)格式
            pattern = r"!\[\[(.*?)\]\]"
            image_names = re.findall(pattern, content)
            
            self.log(f"从MD文件中提取到 {len(image_names)} 张图片引用")
            return image_names
        except Exception as e:
            self.log(f"读取MD文件/提取图片失败:{str(e)}")
            return []
    
    def start_extract(self):
        """开始提取MD文件和图片"""
        # 清空日志
        self.log_text.delete(1.0, END)
        
        # 1. 验证输入
        md_path = self.md_file_path.get().strip()
        save_path = self.save_path.get().strip()
        
        if not md_path:
            messagebox.showwarning("警告", "请选择MD文档!")
            return
        
        if not save_path:
            messagebox.showwarning("警告", "请选择保存文件夹!")
            return
        
        if not os.path.exists(md_path):
            messagebox.showwarning("警告", f"MD文件不存在:{md_path}")
            return
        
        if not os.path.exists(save_path):
            # 尝试创建保存文件夹
            try:
                os.makedirs(save_path)
                self.log(f"创建保存文件夹:{save_path}")
            except Exception as e:
                messagebox.showerror("错误", f"创建保存文件夹失败:{str(e)}")
                return
        
        # 2. 提取图片名称
        image_names = self.extract_images_from_md(md_path)
        
        # 3. 获取MD文件相关信息
        md_file_name = os.path.basename(md_path)
        md_file_dir = os.path.dirname(md_path)
        md_file_name_no_ext = os.path.splitext(md_file_name)[0]
        md_file_ext = os.path.splitext(md_file_name)[1]
        
        # 去掉数字前缀(如果勾选)
        if self.remove_prefix.get():
            original_name = md_file_name_no_ext
            # 使用正则表达式去掉开头的数字
            md_file_name_no_ext = re.sub(r'^\d+', '', md_file_name_no_ext)
            if original_name != md_file_name_no_ext:
                self.log(f"已去掉数字前缀:{original_name} → {md_file_name_no_ext}")
            # 更新文件名
            md_file_name = md_file_name_no_ext + md_file_ext
        
        # 4. 图片源文件夹(同级目录的附图文件夹)
        image_source_dir = os.path.join(md_file_dir, "附图")
        if not os.path.exists(image_source_dir):
            self.log(f"警告:附图文件夹不存在 - {image_source_dir}")
            # 如果没有图片引用,直接复制MD文件
            if not image_names:
                self.log("无图片引用,仅复制MD文件")
            else:
                messagebox.showwarning("警告", f"附图文件夹不存在:{image_source_dir}")
                return
        
        # 5. 目标文件夹准备
        # MD文件目标路径
        md_target_path = os.path.join(save_path, md_file_name)
        # 图片目标文件夹(MD同名文件夹)
        image_target_dir = os.path.join(save_path, md_file_name_no_ext)
        if not os.path.exists(image_target_dir):
            os.makedirs(image_target_dir)
            self.log(f"创建图片目标文件夹:{image_target_dir}")
        
        # 6. 复制MD文件并修改图片引用格式
        try:
            shutil.copy2(md_path, md_target_path)
            self.log(f"成功复制MD文件:{md_target_path}")
            
            # 修改图片引用格式:![](图片名) -> ![](图片名)
            with open(md_target_path, "r", encoding="utf-8") as f:
                content = f.read()
            
            # 替换 Obsidian 格式为标准 Markdown 格式,并将图片名称中的空格替换为 %20
            def replace_image_name(match):
                image_name = match.group(1)
                image_name = image_name.replace(' ', '%20')
                return f"![]({image_name})"
            
            modified_content = re.sub(r"!\[\[(.*?)\]\]", replace_image_name, content)
            
            # 写回文件
            with open(md_target_path, "w", encoding="utf-8") as f:
                f.write(modified_content)
            
            self.log(f"已修改图片引用格式为标准Markdown格式")
        except Exception as e:
            self.log(f"复制MD文件失败:{str(e)}")
            messagebox.showerror("错误", f"复制MD文件失败:{str(e)}")
            return
        
        # 7. 复制图片文件
        copied_count = 0
        missing_images = []
        for img_name in image_names:
            img_source_path = os.path.join(image_source_dir, img_name)
            img_target_path = os.path.join(image_target_dir, img_name)
            
            if os.path.exists(img_source_path):
                try:
                    shutil.copy2(img_source_path, img_target_path)
                    self.log(f"成功复制图片:{img_name}")
                    copied_count += 1
                except Exception as e:
                    self.log(f"复制图片 {img_name} 失败:{str(e)}")
                    missing_images.append(img_name)
            else:
                self.log(f"图片不存在:{img_source_path}")
                missing_images.append(img_name)
        
        # 8. 结果总结
        self.log("="*50)
        self.log(f"提取完成!")
        self.log(f"- MD文件已复制到:{md_target_path}")
        self.log(f"- 成功复制图片:{copied_count} 张")
        if missing_images:
            self.log(f"- 缺失/复制失败的图片:{len(missing_images)} 张")
            self.log(f"  缺失列表:{', '.join(missing_images)}")
        
        # 保存历史记录
        self.save_history()
        
        messagebox.showinfo("成功", f"提取完成!\n成功复制 {copied_count} 张图片\nMD文件已保存到:{md_target_path}")

if __name__ == "__main__":
    # 初始化界面样式(使用 tkinterdnd2 的 Tk)
    root = tkdnd.Tk()
    root.title("MD文档图片提取工具")
    root.geometry("800x500")
    
    # 应用 ttkbootstrap 主题
    style = ttk.Style()
    style.theme_use("flatly")
    
    app = MDImageExtractor(root)
    root.mainloop()

文章作者: 摸鱼
版权声明: 本博客所有文章除特別声明外,均采用 CC BY 4.0 许可协议。转载请注明来源 摸鱼 !
  目录