把Obsidian笔记复制到hexo博客
方案1,ob插件(推荐)
方案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""
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()