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
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
| import os
import re
import subprocess
import time
import urllib.parse
from pathlib import Path
# ======== 配置参数(请根据实际情况修改)========
BLOG_ROOT = r"D:\Hugo\blog"
POSTS_DIR = os.path.join(BLOG_ROOT, "content", "posts")
IMAGES_DIR = os.path.join(BLOG_ROOT, "assets", "images") # 图片存储目录
CDN_BASE = "https://example.com/" # 您的图床基础URL
PICLIST_PATH = r"D:\Program Files\PicList\PicList.exe"
IMAGE_EXTENSIONS = ('.png', '.jpg', '.jpeg', '.gif', '.webp', '.svg') # 支持的图片格式
USE_YEAR_FOLDER = True # 是否按年分文件夹存储图片
# ===========================================
def is_in_code_block(line, in_code_block):
"""检测是否在代码块中(基于代码块标记)"""
if re.match(r'^\s*```', line):
return not in_code_block
return in_code_block
def contains_code_segment(line):
"""检测行内代码段(反引号包围的内容)"""
# 找出所有行内代码段的位置
code_segments = []
pos = 0
while pos < len(line):
start = line.find('`', pos)
if start == -1:
break
end = line.find('`', start + 1)
if end == -1: # 未闭合的反引号
break
code_segments.append((start, end))
pos = end + 1
return code_segments
def protect_code_content(line, in_code_block):
"""
保护代码内容不被修改
返回:处理后的行,以及新的代码块状态
"""
# 如果在代码块中,完全保护整行
if in_code_block:
return line, in_code_block, True
# 检测行内代码段
code_segments = contains_code_segment(line)
if not code_segments:
return line, in_code_block, False
# 保护行内代码段
protected_line = ""
last_pos = 0
for start, end in code_segments:
# 添加非代码内容
protected_line += line[last_pos:start]
# 添加受保护的代码段(原样保留)
protected_line += line[start:end+1]
last_pos = end + 1
# 添加剩余内容
protected_line += line[last_pos:]
return protected_line, in_code_block, True
def convert_relative_paths_to_absolute():
"""将文章中的图片相对路径转换为绝对路径"""
print("\n将相对路径转换为绝对路径...")
updated_files = 0
# 遍历所有Markdown文件
for md_file in os.listdir(POSTS_DIR):
if not md_file.endswith('.md'):
continue
file_path = os.path.join(POSTS_DIR, md_file)
print(f"处理文章: {md_file}")
# 读取文章内容
with open(file_path, "r", encoding="utf-8", errors="replace") as f:
content = f.read()
original_content = content
# 逐行处理内容
lines = content.split('\n')
new_lines = []
in_code_block = False
changes_made = False
for line in lines:
# 更新代码块状态
in_code_block = is_in_code_block(line, in_code_block)
# 保护代码内容
protected_line, in_code_block, is_protected = protect_code_content(line, in_code_block)
if not is_protected:
# 匹配所有非HTTP开头的图片路径
pattern = r'!\[([^\]]*?)\]\(\s*([^http][^\s\)]+)\s*\)'
matches = re.findall(pattern, protected_line)
for alt_text, img_path in matches:
# 跳过已经是绝对路径的图片
if img_path.startswith('http') or os.path.isabs(img_path):
continue
# 构建相对于文章目录的绝对路径
article_dir = os.path.dirname(file_path)
abs_img_path = os.path.normpath(os.path.join(article_dir, img_path))
# 替换为绝对路径(使用原始字符串避免转义问题)
# 使用re.escape处理特殊字符,但避免Windows路径中的反斜杠问题
escaped_img_path = re.escape(img_path)
# 使用原始字符串替换
new_line = re.sub(
r'!\[\s*' + re.escape(alt_text) + r'\s*\]\(\s*' + escaped_img_path + r'\s*\)',
r' + r')',
protected_line
)
if new_line != protected_line:
protected_line = new_line
print(f" 转换路径: {img_path} → {abs_img_path}")
changes_made = True
new_lines.append(protected_line)
# 保存更新
if changes_made:
new_content = '\n'.join(new_lines)
with open(file_path, "w", encoding="utf-8") as f:
f.write(new_content)
updated_files += 1
print(f"✅ 完成路径转换! 更新了 {updated_files} 个文件")
return updated_files
def process_posts():
"""处理所有文章:更新图片引用为图床URL,并收集图片"""
image_mapping = {} # 存储图片路径到文件名的映射
updated_files = 0 # 记录更新的文件数量
print(f"\n扫描文章目录: {POSTS_DIR}")
# 获取当前年份(如果启用年份目录)
current_year = time.strftime("%Y") if USE_YEAR_FOLDER else ""
# 遍历所有Markdown文件
for md_file in os.listdir(POSTS_DIR):
if not md_file.endswith('.md'):
continue
file_path = os.path.join(POSTS_DIR, md_file)
print(f"\n处理文章: {md_file}")
# 读取文章内容
with open(file_path, "r", encoding="utf-8", errors="replace") as f:
content = f.read()
# 备份原始内容
original_content = content
# 查找文章中的所有本地图片引用
local_images = set()
pattern = r'!\[[^\]]*?\]\(([^\s\)]+)\)'
for match in re.findall(pattern, content):
# 跳过HTTP链接
if match.startswith('http'):
continue
# 获取图片路径
img_path = os.path.normpath(match)
# 检查图片文件是否存在
if os.path.exists(img_path):
local_images.add(img_path)
else:
print(f" ⚠️ 图片文件不存在: {img_path}")
if not local_images:
print(" 未找到有效的本地图片引用")
continue
# 收集需要处理的图片
for img_path in local_images:
# 获取原始文件名(不重命名)
img_name = os.path.basename(img_path)
# 构建图床URL - 根据配置添加年份目录
if USE_YEAR_FOLDER:
# 使用URL安全的拼接方式
cdn_url = urllib.parse.urljoin(CDN_BASE, f"{current_year}/")
cdn_url = urllib.parse.urljoin(cdn_url, urllib.parse.quote(img_name))
else:
cdn_url = urllib.parse.urljoin(CDN_BASE, urllib.parse.quote(img_name))
# 记录映射关系
image_mapping[img_path] = (img_name, current_year) if USE_YEAR_FOLDER else img_name
print(f" 发现图片: {img_name} ({img_path})")
print(f" 图床URL: {cdn_url}")
# 逐行处理内容,保护代码块和行内代码
lines = content.split('\n')
new_lines = []
in_code_block = False
changes_made = False
for line in lines:
# 更新代码块状态
in_code_block = is_in_code_block(line, in_code_block)
# 保护代码内容
protected_line, in_code_block, is_protected = protect_code_content(line, in_code_block)
if not is_protected and local_images:
# 对每个图片进行替换
for img_path in local_images:
# 使用原始字符串避免转义问题
escaped_img_path = re.escape(img_path)
# 尝试匹配原始路径
if re.search(escaped_img_path, protected_line):
img_name = os.path.basename(img_path)
# 构建正确的CDN URL
if USE_YEAR_FOLDER:
cdn_url = urllib.parse.urljoin(CDN_BASE, f"{current_year}/")
cdn_url = urllib.parse.urljoin(cdn_url, urllib.parse.quote(img_name))
else:
cdn_url = urllib.parse.urljoin(CDN_BASE, urllib.parse.quote(img_name))
# 使用原始字符串替换
protected_line = re.sub(
r'!\[([^\]]*?)\]\(\s*' + escaped_img_path + r'\s*\)',
r'',
protected_line
)
changes_made = True
new_lines.append(protected_line)
# 保存更新
if changes_made:
new_content = '\n'.join(new_lines)
with open(file_path, "w", encoding="utf-8") as f:
f.write(new_content)
print(f" 已更新图片链接")
updated_files += 1
else:
print(" 未修改内容")
return image_mapping, updated_files
def upload_images(image_mapping):
"""上传所有图片到图床(保留原始文件名)"""
print("\n开始使用PicList上传图片...")
success_count = 0
failed_files = []
# 获取当前年份(如果启用年份目录)
current_year = time.strftime("%Y") if USE_YEAR_FOLDER else ""
# 直接上传原始图片文件
for i, (img_path, img_info) in enumerate(image_mapping.items()):
# 解析图片信息
if USE_YEAR_FOLDER:
img_name, img_year = img_info
remote_path = img_year # 使用年份作为远程目录
else:
img_name = img_info
remote_path = "" # 不使用额外目录
print(f"\n上传图片 {i+1}/{len(image_mapping)}: {img_name}")
print(f"图片路径: {img_path}")
if USE_YEAR_FOLDER:
print(f"远程目录: {remote_path}/")
# 检查文件是否存在
if not os.path.exists(img_path):
print(f" ⚠️ 文件不存在,跳过上传")
failed_files.append((img_path, img_name))
continue
# 构建PicList命令
cmd = [PICLIST_PATH, "upload", img_path]
# 如果启用年份目录,添加远程路径参数
if USE_YEAR_FOLDER and remote_path:
cmd.extend(["-p", remote_path])
print(f"执行命令: {' '.join(cmd)}")
try:
# 执行上传
result = subprocess.run(
cmd,
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
encoding='utf-8',
timeout=120 # 2分钟超时
)
if result.returncode == 0:
print("上传成功!")
success_count += 1
else:
print(f"上传失败! 错误代码: {result.returncode}")
print("错误输出:", result.stderr.strip())
failed_files.append((img_path, img_name))
except subprocess.TimeoutExpired:
print("上传超时! 跳过此文件")
failed_files.append((img_path, img_name))
except Exception as e:
print(f"上传错误: {str(e)}")
failed_files.append((img_path, img_name))
# 处理失败的文件
if failed_files:
print("\n⚠️ 以下文件上传失败:")
for img_path, img_name in failed_files:
print(f" - {img_name} ({img_path})")
print("请手动处理这些文件")
print(f"\n上传完成: {success_count}/{len(image_mapping)} 成功")
return success_count
def main():
print("="*50)
print("Markdown图片迁移到云端工具")
print(f"博客根目录: {BLOG_ROOT}")
print(f"文章目录: {POSTS_DIR}")
print(f"图片目录: {IMAGES_DIR}")
print(f"图床URL: {CDN_BASE}")
print(f"PicList路径: {PICLIST_PATH}")
print(f"按年分文件夹: {'是' if USE_YEAR_FOLDER else '否'}")
print("="*50)
print("注意: 图片将保留原始文件名上传,如需重命名请在PicList中启用高级重命名功能")
print("="*50)
start_time = time.time()
# 步骤1: 将相对路径转换为绝对路径
convert_relative_paths_to_absolute()
# 步骤2: 处理文章并收集图片
image_mapping, updated_files = process_posts()
if not image_mapping:
print("\n没有发现需要处理的图片,退出程序")
return
# 步骤3: 上传图片
success_count = upload_images(image_mapping)
# 显示摘要
duration = time.time() - start_time
print("\n" + "="*50)
print("迁移摘要:")
print(f"更新文章: {updated_files}")
print(f"处理图片: {len(image_mapping)}")
print(f"成功上传: {success_count}/{len(image_mapping)}")
print(f"耗时: {duration:.2f} 秒")
print("="*50)
if success_count == len(image_mapping):
print(f"✅ 所有图片成功上传到图床!")
if USE_YEAR_FOLDER:
current_year = time.strftime("%Y")
print(f"图床链接示例: {CDN_BASE}{current_year}/图片名称.png")
else:
print(f"图床链接示例: {CDN_BASE}图片名称.png")
else:
print(f"⚠️ 部分图片上传失败 ({len(image_mapping)-success_count}个),请检查日志")
print("\n操作完成! 请验证文章中的图片链接")
if __name__ == "__main__":
try:
main()
except Exception as e:
print(f"\n❌ 发生未预期错误: {str(e)}")
import traceback
traceback.print_exc()
|