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
| import os import re import requests import shutil from pathlib import Path import time
MARKDOWN_DIR = "E:\BLOG\source\_posts" PICGO_API_URL = "http://127.0.0.1:36677/upload" BACKUP_ORIGINAL_FILES = True BACKUP_SUFFIX = ".backup"
IMAGE_REGEX = re.compile(r"!\[(.*?)\]\((?!https?://|data:)(.*?)\)")
HTML_TAG_REGEX = re.compile(r"<img[^>]+>", re.IGNORECASE)
SRC_ATTR_REGEX = re.compile( r"""\bsrc\s*=\s*(["'])((?!(?:https?://|data:))[^"'>]+?)\1""", re.IGNORECASE )
def upload_image_to_picgo(image_path): """ 使用 PicGo API 上传图片 (发送 JSON 格式的图片路径列表)。 Args: image_path (str): 本地图片的绝对路径。 Returns: str or None: 上传成功返回在线 URL,否则返回 None。 """ try: payload = {"list": [image_path]} headers = {"Content-Type": "application/json"}
response = requests.post(PICGO_API_URL, json=payload, headers=headers, timeout=30) response.raise_for_status()
result = response.json()
if result.get("success") and result.get("result"): if isinstance(result["result"], list) and len(result["result"]) > 0: uploaded_url = result["result"][0] return uploaded_url else: print(f" [错误] PicGo 返回的 result 格式不正确: {result.get('result')}") return None else: error_message = result.get('message', '未知错误') print(f" [错误] PicGo 上传失败: {error_message}") print(f" [错误] PicGo 完整响应: {result}") return None except requests.exceptions.HTTPError as http_err: print(f" [错误] HTTP 错误发生: {http_err}") if http_err.response is not None: print(f" [错误] PicGo 服务器响应状态码: {http_err.response.status_code}") try: print(f" [错误] PicGo 服务器响应内容: {http_err.response.text}") except Exception: pass return None except requests.exceptions.RequestException as e: print(f" [错误] 连接 PicGo API 失败或请求过程中出错: {e}") return None except Exception as e: print(f" [错误] 上传图片 '{image_path}' 时发生未知错误: {e}") return None
def original_full_tag_summary(tag_string, max_len=70): """Helper function to print a summary of a tag if it's too long.""" if len(tag_string) > max_len: return tag_string[:max_len-3] + "..." return tag_string
def process_markdown_file(md_file_path_str): """ 处理单个 Markdown 文件,上传本地图片 (Markdown 和 HTML 格式) 并替换链接。 """ print(f"--- 正在处理文件: {md_file_path_str} ---") md_file_path_obj = Path(md_file_path_str) md_dir = md_file_path_obj.parent
try: with open(md_file_path_obj, 'r', encoding='utf-8') as f: original_content = f.read() except Exception as e: print(f" [错误] 读取文件 '{md_file_path_str}' 失败: {e}") return
content_being_processed = original_content modified_in_this_file = False
print(" Pass 1: Processing Markdown images ``...") new_md_pass_content_parts = [] last_md_end = 0 markdown_images_found_count = 0 markdown_images_replaced_count = 0
for match in IMAGE_REGEX.finditer(content_being_processed): markdown_images_found_count += 1 new_md_pass_content_parts.append(content_being_processed[last_md_end:match.start()]) alt_text = match.group(1) local_image_path_str = match.group(2) original_md_tag = match.group(0)
print(f" MD_IMG: Found: {original_full_tag_summary(original_md_tag)}")
local_image_path = Path(local_image_path_str) if not local_image_path.is_absolute(): absolute_image_path = (md_dir / local_image_path).resolve() else: absolute_image_path = local_image_path.resolve()
if not absolute_image_path.exists(): print(f" [警告] MD_IMG: File not found, skipping: {absolute_image_path}") new_md_pass_content_parts.append(original_md_tag) else: online_url = upload_image_to_picgo(str(absolute_image_path)) if online_url: new_image_md_tag = f"" new_md_pass_content_parts.append(new_image_md_tag) modified_in_this_file = True markdown_images_replaced_count += 1 print(f" MD_IMG: Replaced with: {new_image_md_tag}") else: print(f" MD_IMG: Upload failed for '{local_image_path_str}', skipping replacement.") new_md_pass_content_parts.append(original_md_tag) last_md_end = match.end() new_md_pass_content_parts.append(content_being_processed[last_md_end:]) content_after_md_pass = "".join(new_md_pass_content_parts)
if markdown_images_found_count > 0: print(f" Pass 1 Summary: Found {markdown_images_found_count} MD images, Replaced {markdown_images_replaced_count}.") else: print(f" Pass 1 Summary: No Markdown images `` found.")
print(" Pass 2: Processing HTML images `<img src='path'>`...") content_for_html_pass = content_after_md_pass new_html_pass_content_parts = [] last_html_end = 0 html_tags_processed_count = 0 html_src_replaced_count = 0
for tag_match in HTML_TAG_REGEX.finditer(content_for_html_pass): new_html_pass_content_parts.append(content_for_html_pass[last_html_end:tag_match.start()]) original_full_tag = tag_match.group(0) modified_tag_output = original_full_tag
src_attr_match = SRC_ATTR_REGEX.search(original_full_tag)
if src_attr_match: html_tags_processed_count +=1 quote_char = src_attr_match.group(1) local_image_path_str = src_attr_match.group(2) original_src_attr_part = src_attr_match.group(0)
print(f" HTML_IMG: Found local src='{local_image_path_str}' in tag: {original_full_tag_summary(original_full_tag)}")
local_image_path = Path(local_image_path_str) if not local_image_path.is_absolute(): absolute_image_path = (md_dir / local_image_path).resolve() else: absolute_image_path = local_image_path.resolve()
if not absolute_image_path.exists(): print(f" [警告] HTML_IMG: File not found, skipping: {absolute_image_path}") else: online_url = upload_image_to_picgo(str(absolute_image_path)) if online_url: new_src_attr_part = f'src={quote_char}{online_url}{quote_char}' modified_tag_output = original_full_tag.replace(original_src_attr_part, new_src_attr_part, 1) if original_full_tag != modified_tag_output: modified_in_this_file = True html_src_replaced_count +=1 print(f" HTML_IMG: Replaced src. New tag: {original_full_tag_summary(modified_tag_output)}") else: print(f" HTML_IMG: Upload failed for '{local_image_path_str}', skipping replacement in tag.") new_html_pass_content_parts.append(modified_tag_output) last_html_end = tag_match.end()
new_html_pass_content_parts.append(content_for_html_pass[last_html_end:]) final_content = "".join(new_html_pass_content_parts)
if html_tags_processed_count > 0: print(f" Pass 2 Summary: Processed {html_tags_processed_count} <img> tags for local 'src', Replaced {html_src_replaced_count} 'src' attributes.") else: print(f" Pass 2 Summary: No HTML <img> tags with processable local 'src' attributes found.")
if modified_in_this_file: try: if BACKUP_ORIGINAL_FILES: backup_file_path = md_file_path_obj.with_suffix(md_file_path_obj.suffix + BACKUP_SUFFIX) shutil.copy2(md_file_path_obj, backup_file_path) print(f" 已备份原始文件到: {backup_file_path}")
with open(md_file_path_obj, 'w', encoding='utf-8') as f: f.write(final_content) print(f" 文件已更新: {md_file_path_str}") except Exception as e: print(f" [错误] 写入文件或备份文件失败: {e}") if BACKUP_ORIGINAL_FILES and 'backup_file_path' in locals() and Path(backup_file_path).exists(): try: shutil.copy2(backup_file_path, md_file_path_obj) print(f" [警告] 写入失败,已尝试从备份 {backup_file_path} 恢复。请检查文件。") except Exception as restore_e: print(f" [严重错误] 写入失败且恢复备份也失败: {restore_e}。原始文件可能已损坏,请从手动备份中恢复。") else: print(f" 文件 '{md_file_path_str}' 无需修改。") print("-" * 30)
def main(): """ 主函数,遍历目录并处理 Markdown 文件。 """ markdown_dir_path = Path(MARKDOWN_DIR) if not markdown_dir_path.is_dir(): print(f"[错误] 指定的目录不存在或不是一个目录: {MARKDOWN_DIR}") return
print(f"开始扫描目录: {markdown_dir_path}") file_count = 0 for md_file in markdown_dir_path.rglob("*.md"): if BACKUP_SUFFIX in md_file.name: print(f"跳过备份文件: {md_file}") continue process_markdown_file(str(md_file)) file_count += 1 if file_count == 0: print("在指定目录中未找到 .md 文件。") else: print(f"所有 {file_count} 个 Markdown 文件处理完毕。")
if __name__ == "__main__": print("*********************************************************************") print("* Typora 图片上传脚本 (Markdown & HTML) *") print("*********************************************************************") print("* 重要提示: *") print("* 1. 请确保 PicGo 正在运行并已正确配置图床和Server。 *") print("* 2. 脚本将修改 Markdown 文件中的本地图片链接。 *") print(f"* 3. 配置的笔记目录: {MARKDOWN_DIR} *") print(f"* 4. PicGo API: {PICGO_API_URL} *") if BACKUP_ORIGINAL_FILES: print(f"* 5. 原始文件将备份为 *.md{BACKUP_SUFFIX}。 *") else: print("* 5. 文件备份已禁用。 *") print("* 6. 强烈建议在首次运行或对重要笔记操作前备份您的整个笔记目录。 *") print("*********************************************************************\n")
if MARKDOWN_DIR == "/path/to/your/typora/notes": print("[配置错误] 请务必修改脚本中的 `MARKDOWN_DIR`变量,指向你的 Typora 笔记目录!") exit(1)
confirm = input(f"确认开始处理目录 '{MARKDOWN_DIR}' 下的 Markdown 文件吗? (yes/no): ").lower() if confirm == 'yes': print("脚本将在 3 秒后开始执行... 按 Ctrl+C 取消。") try: time.sleep(3) main() except KeyboardInterrupt: print("\n操作已由用户取消。") except Exception as e: print(f"\n[严重错误] 脚本执行过程中发生意外错误: {e}") else: print("操作已取消。")
|