1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
| import os
import re
import requests
import hashlib
from urllib.parse import urlparse
import tkinter as tk
from tkinter import filedialog, messagebox, ttk
import threading
from PIL import Image
import io
import time
class MarkdownImageDownloader:
def __init__(self):
self.root = tk.Tk()
self.root.title("Markdown图片下载工具")
self.root.geometry("600x400")
# 创建界面组件
self.setup_ui()
# 存储处理统计信息
self.processed_files = 0
self.downloaded_images = 0
self.skipped_images = 0
def setup_ui(self):
# 主框架
main_frame = ttk.Frame(self.root, padding="10")
main_frame.grid(row=0, column=0, sticky=(tk.W, tk.E, tk.N, tk.S))
# 配置网格权重
self.root.columnconfigure(0, weight=1)
self.root.rowconfigure(0, weight=1)
main_frame.columnconfigure(1, weight=1)
# 选择目录标签和按钮
ttk.Label(main_frame, text="Markdown文件目录:").grid(row=0, column=0, sticky=tk.W, pady=5)
self.dir_path = tk.StringVar()
ttk.Entry(main_frame, textvariable=self.dir_path, width=50).grid(row=0, column=1, sticky=(tk.W, tk.E), padx=5)
ttk.Button(main_frame, text="浏览...", command=self.browse_directory).grid(row=0, column=2, padx=5)
# 图片目录设置
ttk.Label(main_frame, text="图片保存目录:").grid(row=1, column=0, sticky=tk.W, pady=5)
self.image_dir_path = tk.StringVar()
ttk.Entry(main_frame, textvariable=self.image_dir_path, width=50).grid(row=1, column=1, sticky=(tk.W, tk.E), padx=5)
ttk.Button(main_frame, text="浏览...", command=self.browse_image_directory).grid(row=1, column=2, padx=5)
# 选项设置
options_frame = ttk.LabelFrame(main_frame, text="选项", padding="5")
options_frame.grid(row=2, column=0, columnspan=3, sticky=(tk.W, tk.E), pady=10)
options_frame.columnconfigure(1, weight=1)
self.use_relative_path = tk.BooleanVar(value=True)
ttk.Checkbutton(options_frame, text="使用相对路径", variable=self.use_relative_path).grid(row=0, column=0, sticky=tk.W)
self.overwrite_existing = tk.BooleanVar(value=False)
ttk.Checkbutton(options_frame, text="覆盖已存在图片", variable=self.overwrite_existing).grid(row=0, column=1, sticky=tk.W)
# 进度条
ttk.Label(main_frame, text="进度:").grid(row=3, column=0, sticky=tk.W, pady=5)
self.progress = ttk.Progressbar(main_frame, mode='indeterminate')
self.progress.grid(row=3, column=1, columnspan=2, sticky=(tk.W, tk.E), pady=5)
# 日志文本框
ttk.Label(main_frame, text="处理日志:").grid(row=4, column=0, sticky=tk.W, pady=5)
self.log_text = tk.Text(main_frame, height=15, width=70)
self.log_text.grid(row=5, column=0, columnspan=3, sticky=(tk.W, tk.E, tk.N, tk.S), pady=5)
# 滚动条
scrollbar = ttk.Scrollbar(main_frame, orient=tk.VERTICAL, command=self.log_text.yview)
scrollbar.grid(row=5, column=3, sticky=(tk.N, tk.S))
self.log_text['yscrollcommand'] = scrollbar.set
# 按钮框架
button_frame = ttk.Frame(main_frame)
button_frame.grid(row=6, column=0, columnspan=3, pady=10)
ttk.Button(button_frame, text="开始处理", command=self.start_processing).pack(side=tk.LEFT, padx=5)
ttk.Button(button_frame, text="清空日志", command=self.clear_log).pack(side=tk.LEFT, padx=5)
ttk.Button(button_frame, text="退出", command=self.root.quit).pack(side=tk.RIGHT, padx=5)
# 配置主框架权重
main_frame.rowconfigure(5, weight=1)
def browse_directory(self):
directory = filedialog.askdirectory(title="选择Markdown文件所在目录")
if directory:
self.dir_path.set(directory)
# 自动设置图片目录为Markdown目录下的images文件夹
self.image_dir_path.set(os.path.join(directory, "images"))
def browse_image_directory(self):
directory = filedialog.askdirectory(title="选择图片保存目录")
if directory:
self.image_dir_path.set(directory)
def log_message(self, message):
"""添加消息到日志"""
self.log_text.insert(tk.END, message + "\n")
self.log_text.see(tk.END)
self.root.update_idletasks()
def clear_log(self):
"""清空日志"""
self.log_text.delete(1.0, tk.END)
def download_image(self, url, folder_path, filename=None):
"""下载图片到指定文件夹"""
try:
response = requests.get(url, stream=True, timeout=15)
if response.status_code == 200:
# 确定文件名
if not filename:
parsed_url = urlparse(url)
original_filename = os.path.basename(parsed_url.path)
# 清理文件名
if original_filename and '.' in original_filename:
name, ext = os.path.splitext(original_filename)
# 移除可能存在的查询参数
ext = ext.split('?')[0]
# 限制扩展名为常见图片格式
if ext.lower() not in ['.jpg', '.jpeg', '.png', '.gif', '.webp', '.bmp']:
# 从Content-Type获取扩展名
content_type = response.headers.get('content-type', '')
if 'jpeg' in content_type or 'jpg' in content_type:
ext = '.jpg'
elif 'png' in content_type:
ext = '.png'
elif 'gif' in content_type:
ext = '.gif'
elif 'webp' in content_type:
ext = '.webp'
else:
ext = '.jpg' # 默认使用jpg
# 创建友好文件名(限制长度)
clean_name = re.sub(r'[^\w\-_]', '_', name)
if len(clean_name) > 50:
clean_name = clean_name[:50]
filename = f"{clean_name}{ext}"
else:
# 从Content-Type获取扩展名
content_type = response.headers.get('content-type', '')
if 'jpeg' in content_type or 'jpg' in content_type:
ext = '.jpg'
elif 'png' in content_type:
ext = '.png'
elif 'gif' in content_type:
ext = '.gif'
elif 'webp' in content_type:
ext = '.webp'
else:
ext = '.jpg'
# 使用URL的MD5哈希作为文件名
url_hash = hashlib.md5(url.encode()).hexdigest()[:8]
filename = f"image_{url_hash}{ext}"
# 确保文件夹存在
os.makedirs(folder_path, exist_ok=True)
# 完整文件路径
filepath = os.path.join(folder_path, filename)
# 避免重复下载(除非设置了覆盖)
if os.path.exists(filepath) and not self.overwrite_existing.get():
self.log_message(f"图片已存在,跳过: {filename}")
self.skipped_images += 1
return filepath
# 保存图片
with open(filepath, 'wb') as f:
for chunk in response.iter_content(1024):
f.write(chunk)
self.log_message(f"下载成功: {filename}")
self.downloaded_images += 1
return filepath
else:
self.log_message(f"下载失败: HTTP状态码 {response.status_code} - {url}")
return None
except Exception as e:
self.log_message(f"下载图片失败: {url}, 错误: {e}")
return None
def process_markdown_file(self, md_file_path, image_dir):
"""处理单个Markdown文件中的网络图片"""
self.log_message(f"处理文件: {os.path.basename(md_file_path)}")
with open(md_file_path, 'r', encoding='utf-8') as f:
content = f.read()
# 正则表达式匹配Markdown图片
pattern = r'!\[(.*?)\]\((.*?)\)'
matches = re.findall(pattern, content)
changed = False
for alt_text, url in matches:
if url.startswith(('http://', 'https://')):
local_path = self.download_image(url, image_dir)
if local_path:
# 确定使用的路径类型
if self.use_relative_path.get():
# 使用相对路径
relative_path = os.path.relpath(local_path, os.path.dirname(md_file_path))
replacement_path = relative_path.replace('\\', '/') # 确保使用正斜杠
else:
# 使用绝对路径
replacement_path = local_path.replace('\\', '/')
content = content.replace(f"]({url})", f"]({replacement_path})")
changed = True
# 如果有更改,写回文件
if changed:
# 创建备份文件
backup_path = md_file_path + '.bak'
with open(backup_path, 'w', encoding='utf-8') as f:
f.write(content)
# 重命名备份文件为原文件
os.replace(backup_path, md_file_path)
self.log_message(f"已更新: {os.path.basename(md_file_path)}")
else:
self.log_message(f"无需更新: {os.path.basename(md_file_path)}")
self.processed_files += 1
def batch_process_markdown_files(self):
"""批量处理目录中的所有Markdown文件"""
root_dir = self.dir_path.get()
image_dir = self.image_dir_path.get()
if not root_dir or not os.path.exists(root_dir):
messagebox.showerror("错误", "请选择有效的Markdown文件目录")
return
# 创建图片目录
os.makedirs(image_dir, exist_ok=True)
# 重置统计信息
self.processed_files = 0
self.downloaded_images = 0
self.skipped_images = 0
# 查找所有Markdown文件
md_files = []
for root, dirs, files in os.walk(root_dir):
# 跳过图片目录
if os.path.abspath(root) == os.path.abspath(image_dir):
continue
for file in files:
if file.endswith('.md'):
md_files.append(os.path.join(root, file))
total_files = len(md_files)
self.log_message(f"找到 {total_files} 个Markdown文件")
# 处理每个文件
for i, md_file in enumerate(md_files):
self.process_markdown_file(md_file, image_dir)
# 显示处理结果
self.log_message("\n处理完成!")
self.log_message(f"已处理文件: {self.processed_files}/{total_files}")
self.log_message(f"下载图片: {self.downloaded_images}")
self.log_message(f"跳过图片: {self.skipped_images}")
messagebox.showinfo("完成", f"处理完成!\n已处理文件: {self.processed_files}\n下载图片: {self.downloaded_images}")
def start_processing(self):
"""开始处理文件(在线程中运行)"""
if not self.dir_path.get():
messagebox.showerror("错误", "请选择Markdown文件目录")
return
# 禁用开始按钮,防止重复点击
self.root.children['!frame'].children['!frame2'].winfo_children()[0].config(state=tk.DISABLED)
# 启动进度条
self.progress.start()
# 在线程中运行处理过程
thread = threading.Thread(target=self.batch_process_markdown_files)
thread.daemon = True
thread.start()
# 检查线程是否完成
self.check_thread(thread)
def check_thread(self, thread):
"""检查处理线程是否完成"""
if thread.is_alive():
# 线程仍在运行,稍后再次检查
self.root.after(100, lambda: self.check_thread(thread))
else:
# 线程已完成,停止进度条并启用按钮
self.progress.stop()
self.root.children['!frame'].children['!frame2'].winfo_children()[0].config(state=tk.NORMAL)
def run(self):
"""运行应用程序"""
self.root.mainloop()
# 运行应用程序
if __name__ == "__main__":
app = MarkdownImageDownloader()
app.run()
|