前言

不想在使用腾讯云的对象存储,每月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]

图形界面模式

  1. 直接运行python image_downloader.py启动GUI
  2. 通过拖拽文件或浏览按钮选择输入文件
  3. 指定输出目录(默认为downloaded_images)
  4. 设置基准URL(可选)
  5. 点击开始下载

注意事项

  • 确保运行环境具有网络连接权限
  • 输出目录需要写权限
  • 建议使用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()