前言
不想在使用腾讯云的对象存储,每月10元,像开会员一样,啥都没干,准备使用CF的R2了。
所以,需要把之前文章中的图片下载下来。
故写了一个脚本。
简介
本工具支持从Markdown/HTML/普通文本文件中提取图片链接并批量下载,提供命令行和图形界面两种操作模式。
功能特性
- 支持解析多种文档格式(Markdown/HTML/普通文本)
- 自动处理相对路径转换为绝对URL
- 图形界面支持文件拖拽操作
- 保持原始URL路径的目录结构
- 支持断点续传和错误重试机制
安装要求
pip install requests beautifulsoup4 markdown
使用说明
命令行模式
python image_downloader.py input.md [-o output_dir] [-u base_url]
图形界面模式
- 直接运行
python image_downloader.py
启动GUI - 通过拖拽文件或浏览按钮选择输入文件
- 指定输出目录(默认为downloaded_images)
- 设置基准URL(可选)
- 点击开始下载
注意事项
- 确保运行环境具有网络连接权限
- 输出目录需要写权限
- 建议使用Python 3.8及以上版本
- 图形界面需要tkinter支持(通常Python自带)
代码
import argparse
import os
import sys
import re
import requests
from urllib.parse import urljoin, urlparse
from bs4 import BeautifulSoup
import markdown
try:
# 尝试导入 tkinterdnd2 实现拖拽支持
from tkinterdnd2 import DND_FILES, TkinterDnD
except ImportError:
TkinterDnD = None
DND_FILES = None
def extract_urls_from_html(content, base_url):
"""从HTML内容中提取图片URL"""
soup = BeautifulSoup(content, 'html.parser')
urls = []
for img in soup.find_all('img'):
src = img.get('src')
if src:
urls.append(urljoin(base_url, src))
return urls
def extract_urls_from_markdown(content):
"""从Markdown内容中提取图片URL"""
html = markdown.markdown(content)
soup = BeautifulSoup(html, 'html.parser')
return [img['src'] for img in soup.find_all('img')]
def extract_urls_from_text(content):
"""从通用文本内容中提取图片URL"""
pattern = r'!\[.*?\]\((.*?)\)|<img[^>]+src="([^"]+)"'
matches = re.findall(pattern, content)
return [m[0] or m[1] for m in matches]
def extract_image_urls(file_path, base_url=''):
"""从文档中提取所有图片URL"""
with open(file_path, 'r', encoding='utf-8') as f:
content = f.read()
if file_path.endswith(('.html', '.htm')):
urls = extract_urls_from_html(content, base_url)
elif file_path.endswith('.md'):
urls = extract_urls_from_markdown(content)
else: # 通用文本文件处理
urls = extract_urls_from_text(content)
return list(set(urls)) # 去除重复URL
def download_image(url, output_dir):
"""下载单个图片到指定目录"""
try:
response = requests.get(url, stream=True, timeout=10)
response.raise_for_status()
# 创建目录结构
parsed_url = urlparse(url)
path = parsed_url.path.lstrip('/')
local_path = os.path.join(output_dir, path)
os.makedirs(os.path.dirname(local_path), exist_ok=True)
# 保存图片
with open(local_path, 'wb') as f:
for chunk in response.iter_content(1024):
f.write(chunk)
return True, local_path
except Exception as e:
return False, str(e)
def process_file(input_path, output_dir, base_url):
try:
urls = extract_image_urls(input_path, base_url)
print(f"找到 {len(urls)} 个图片链接")
os.makedirs(output_dir, exist_ok=True)
success = 0
for url in urls:
status, result = download_image(url, output_dir)
if status:
print(f"成功下载:{result}")
success += 1
else:
print(f"下载失败 {url}: {result}")
print(f"\n完成!成功下载 {success}/{len(urls)} 张图片")
return True
except Exception as e:
print(f"发生错误:{str(e)}")
return False
def run_cli():
"""命令行模式入口"""
parser = argparse.ArgumentParser(description='文档图片下载工具')
parser.add_argument('input', help='输入文件路径')
parser.add_argument('-o', '--output', default=os.path.join(os.path.dirname(__file__), 'downloaded_images'),
help='输出目录(默认为当前目录下的downloaded_images)')
parser.add_argument('-u', '--base-url', default='',
help='基准URL(用于处理相对路径)')
args = parser.parse_args()
if not os.path.exists(args.input):
print(f"错误:输入文件 {args.input} 不存在")
return
process_file(args.input, args.output, args.base_url)
def run_gui():
"""图形界面模式入口"""
# 如果支持拖拽,则使用 TkinterDnD.Tk,否则使用 tk.Tk
if TkinterDnD:
root = TkinterDnD.Tk()
else:
import tkinter as tk
root = tk.Tk()
root.title("图片下载工具")
import tkinter as tk
from tkinter import filedialog, messagebox
def select_file():
file_path = filedialog.askopenfilename(
title="选择文档文件",
filetypes=[("文档文件", "*.md;*.html;*.txt")]
)
if file_path:
entry_input.delete(0, tk.END)
entry_input.insert(0, file_path)
def select_output():
dir_path = filedialog.askdirectory(title="选择输出目录")
if dir_path:
entry_output.delete(0, tk.END)
entry_output.insert(0, dir_path)
def start_download():
input_path = entry_input.get()
output_dir = entry_output.get() or 'downloaded_images'
base_url = entry_url.get()
if not input_path:
messagebox.showerror("错误", "请先选择输入文件")
return
if process_file(input_path, output_dir, base_url):
messagebox.showinfo("完成", "图片下载完成!")
else:
messagebox.showerror("错误", "下载过程中发生错误,请查看控制台输出")
def drop(event):
# 处理拖拽事件,将拖拽的第一个文件路径填入输入框中
files = root.tk.splitlist(event.data)
if files:
entry_input.delete(0, tk.END)
entry_input.insert(0, files[0])
# 输入文件选择区域
frame_input = tk.Frame(root, padx=10, pady=10)
frame_input.pack(fill=tk.X)
tk.Label(frame_input, text="输入文件:").grid(row=0, column=0, sticky=tk.W)
entry_input = tk.Entry(frame_input, width=40)
entry_input.grid(row=0, column=1, padx=5)
tk.Button(frame_input, text="浏览...", command=select_file).grid(row=0, column=2)
# 输出目录选择区域
frame_output = tk.Frame(root, padx=10, pady=5)
frame_output.pack(fill=tk.X)
tk.Label(frame_output, text="输出目录:").grid(row=0, column=0, sticky=tk.W)
entry_output = tk.Entry(frame_output, width=40)
entry_output.grid(row=0, column=1, padx=5)
entry_output.insert(0, os.path.join(os.path.dirname(__file__), 'downloaded_images'))
tk.Button(frame_output, text="浏览...", command=select_output).grid(row=0, column=2)
# 基准URL输入区域
frame_url = tk.Frame(root, padx=10, pady=5)
frame_url.pack(fill=tk.X)
tk.Label(frame_url, text="基准URL:").grid(row=0, column=0, sticky=tk.W)
entry_url = tk.Entry(frame_url, width=40)
entry_url.grid(row=0, column=1, padx=5)
# 拖拽区域(放置在窗口下方),点击区域可调出资源管理器
frame_drag = tk.Frame(root, bd=2, relief="groove", padx=10, pady=10)
frame_drag.pack(side=tk.BOTTOM, fill=tk.X, padx=10, pady=5)
label_drag = tk.Label(frame_drag, text="将文件拖拽到此区域,或点击此区域选择文件", fg="gray")
label_drag.pack(expand=True, fill=tk.BOTH)
# 注册拖拽事件到拖拽区域(如果支持拖拽)
if DND_FILES:
frame_drag.drop_target_register(DND_FILES)
frame_drag.dnd_bind('<<Drop>>', drop)
# 为拖拽区域添加点击事件,点击时调用文件选择
label_drag.bind("<Button-1>", lambda e: select_file())
# 操作按钮区域
frame_btn = tk.Frame(root, padx=10, pady=10)
frame_btn.pack(fill=tk.X)
tk.Button(frame_btn, text="开始下载", command=start_download).pack(side=tk.LEFT)
root.mainloop()
def main():
"""主入口,根据命令行参数选择运行模式"""
if len(sys.argv) > 1:
run_cli()
else:
run_gui()
if __name__ == '__main__':
main()