tianyu234头像
关注
批量下载 Excel 中的文件:一个高效可靠的 Python 脚本封面图

批量下载 Excel 中的文件:一个高效可靠的 Python 脚本

在日常工作中,我们常常需要从 Excel 表格中提取下载链接,批量下载大量文件(例如电子回单、合同、报表等)。手动下载显然不可行,而许多脚本要么读取 Excel 时遇到障碍(如超链接识别问题),要么在并发下载时缺乏良好的错误处理与进度反馈。为此,我编写了一个功能齐全的 Python 脚本,解决了这些痛点,现分享给大家。


🎯 核心功能

  • 动态指定 Excel 文件:无需修改代码,通过命令行参数或交互输入即可传入 Excel 路径。
  • 自动生成下载目录:下载目录与 Excel 文件同名(去掉扩展名),管理方便。
  • 智能读取:使用 pandas(底层 openpyxl)读取 .xlsx,支持超链接单元格和纯文本,解决常见的读取为空问题。
  • 多线程并发下载:可配置并发数,充分利用网络带宽,大幅提升下载速度。
  • 自动重试机制:对失败的任务自动重试(默认 3 次),提高整体成功率。
  • 实时进度反馈:每个任务完成时即时输出结果(成功/失败),并伴有进度点(.),让您时刻掌握进度。
  • 完整错误记录:所有失败的行号均被记录,方便事后补下。
  • 空文件统计:下载后文件大小为 0 也被视为有效,但单独统计数量,便于甄别异常。

📋 Excel 文件格式要求

脚本要求 Excel 文件(.xlsx 格式)的第一行为字段名(标题行),从第二行开始为数据。其中必须包含两列:

列内容说明
文件名称下载后保存的文件名(可以包含子目录路径,例如 合同/2025/001,脚本会自动创建对应子目录)
文件 url文件的下载链接(支持 http://https://

文件名与扩展名规则

  • 脚本会自动从 URL 中提取文件扩展名(如 .pdf.jpg),并追加到文件名后。
  • 如果 URL 中没有扩展名(例如 https://example.com/download?id=123),则默认使用 .pdf 作为后缀。
  • 因此,Excel 中的“文件名称”不需要包含扩展名,只需提供基础名称(可包含路径)。

示例

文件名称文件 url
某公司合同https://doc.aaaa.com/aaabbb/ccc.pdf
2025/报表/销售数据https://doc.aaaa.com/report/123?format=pdf
图片/logohttps://cdn.aaaa.com/logo.png

上述示例中:

  • 第一行下载后保存为 下载目录/某公司合同.pdf
  • 第二行 URL 无扩展名,保存为 下载目录/2025/报表/销售数据.pdf
  • 第三行保存为 下载目录/图片/logo.png

⚠️ 注意:列顺序默认第 1 列为“文件名称”,第 2 列为“文件 url”(从 0 开始计数)。如果需要调整,可修改脚本开头的 fileNameColumnurlColumn 变量。


📦 环境要求

  • Python 3.6+
  • 依赖库:pandas, openpyxlpandas 读取 Excel 时需要)

安装命令:

pip3 install pandas openpyxl

🚀 使用方法

1. 保存脚本

将以下完整脚本保存为 download.py(或任意名称)。

2. 运行

python3 download.py

或直接传入 Excel 路径:

python3 download.py /path/to/your/文件.xlsx
或
python3 download.py C:\Users\Name\Documents\文件.xlsx

3. 输入路径(若未传入)

程序会提示您输入 Excel 文件的完整路径:

请输入 Excel 文件的完整路径: /Users/data/郴州合同260804.xlsx

4. 等待完成

脚本会自动创建下载目录(例如 /Users/data/郴州合同260804),并发下载所有文件,并在终端实时显示每个任务的完成情况。


📜 完整脚本

# coding=utf-8
import urllib.request
import os
import time
import sys
from concurrent.futures import ThreadPoolExecutor, as_completed
import pandas as pd

# ------------------- 配置(根据需求调整) -------------------
fileNameColumn = 0      # Excel 中文件名所在列(0-based)
urlColumn = 1           # Excel 中下载链接所在列(0-based)
max_workers = 50        # 并发线程数,建议 10~50
max_retries = 3         # 失败重试次数
# ----------------------------------------------------------

def get_excel_path():
    """获取 Excel 文件路径(命令行参数或交互输入)"""
    if len(sys.argv) > 1:
        path = sys.argv[1]
    else:
        path = input("请输入 Excel 文件的完整路径: ").strip()
    path = path.strip(' "\'')
    if not path or not os.path.isfile(path):
        print(f"❌ 文件不存在或路径无效: {path}")
        sys.exit(1)
    return path

def do_download(row_num, url, save_path):
    """下载单个文件,支持重试,返回 (row_num, success, file_path, size)"""
    for attempt in range(1, max_retries + 1):
        try:
            safe_url = urllib.parse.quote(url, safe='/:?=&')
            urllib.request.urlretrieve(safe_url, save_path)
            if os.path.exists(save_path):
                size = os.path.getsize(save_path)
                return (row_num, True, save_path, size)
            else:
                raise Exception("下载后文件不存在")
        except Exception as e:
            if attempt < max_retries:
                print(f"⚠️ 第 {row_num} 行第 {attempt} 次重试失败,稍后重试...")
                time.sleep(1)
            else:
                print(f"❌ 第 {row_num} 行下载失败(重试 {max_retries} 次后):{e}  URL={url}")
                return (row_num, False, save_path, 0)
    return (row_num, False, save_path, 0)

def count_files(directory):
    """统计目录下文件总数及空文件数"""
    total = empty = 0
    for root, _, files in os.walk(directory):
        for f in files:
            full = os.path.join(root, f)
            if os.path.isfile(full):
                total += 1
                if os.path.getsize(full) == 0:
                    empty += 1
    return total, empty

def main():
    start_time = time.time()

    # 1. 获取 Excel 路径并创建下载目录
    excel_path = get_excel_path()
    download_dir = os.path.splitext(excel_path)[0]   # 去掉扩展名
    os.makedirs(download_dir, exist_ok=True)
    print(f"📁 Excel 文件: {excel_path}")
    print(f"📁 下载目录: {download_dir}")

    # 2. 读取 Excel(pandas 读取,强制字符串类型)
    df = pd.read_excel(excel_path, header=0, dtype=str, keep_default_na=False)
    data = df.iloc[:, [fileNameColumn, urlColumn]].values
    total_rows = len(data)
    print(f"📊 待处理数据行数(不含标题): {total_rows}")

    # 3. 统计变量
    skip_invalid = 0
    skip_exist = 0
    fail_rows = []
    success_count = 0
    empty_count = 0

    # 4. 提交所有下载任务
    executor = ThreadPoolExecutor(max_workers=max_workers)
    futures = []

    for excel_row, row in enumerate(data, start=2):   # 行号从2开始(对应Excel实际行)
        raw_name = str(row[0]).strip() if row[0] else ""
        url = str(row[1]).strip() if row[1] else ""

        if not url:
            print(f"⏭️ 第 {excel_row} 行 URL 为空,跳过。")
            skip_invalid += 1
            continue

        # 提取扩展名:如果 URL 中有 '.',取最后一段,否则默认 'pdf'
        ext = url.split('.')[-1] if '.' in url else 'pdf'
        # 如果扩展名包含查询参数(如 ?...),则去掉查询参数
        if '?' in ext:
            ext = ext.split('?')[0]
        if '/' in ext:
            ext = ext.split('/')[0]   # 防止意外

        if '/' in raw_name:
            save_path = os.path.join(download_dir, raw_name + '.' + ext)
        else:
            save_path = os.path.join(download_dir, raw_name + '.' + ext)

        # 创建子目录
        target_dir = os.path.dirname(save_path)
        if target_dir and not os.path.exists(target_dir):
            os.makedirs(target_dir, exist_ok=True)

        # 文件已存在则跳过
        if os.path.exists(save_path):
            print(f"⏭️ 文件已存在:{save_path}")
            skip_exist += 1
            continue

        # 提交异步任务
        future = executor.submit(do_download, excel_row, url, save_path)
        futures.append(future)

    # 关闭线程池(不再接受新任务)
    executor.shutdown(wait=False)

    print("\n开始下载,实时输出每个任务的完成情况('.' 表示进度)...")

    # 5. 使用 as_completed 实时获取结果
    completed = 0
    total_tasks = len(futures)
    for future in as_completed(futures):
        completed += 1
        print('.', end='', flush=True)
        if completed % 20 == 0:
            print()   # 每20个点换行

        row_num, success, fname, size = future.result()
        if success:
            success_count += 1
            if size == 0:
                empty_count += 1
            print(f"✅ 异步完成 {row_num}  ->  {fname}  (大小: {size} 字节)")
        else:
            fail_rows.append(row_num)

    print("\n所有任务处理完毕!")

    # 6. 最终统计
    total_files, total_empty = count_files(download_dir)
    elapsed = time.time() - start_time

    print("\n" + "=" * 60)
    print("📊 最终统计:")
    print(f"   Excel 数据行数(不含标题): {total_rows}")
    print(f"   因 URL 为空跳过: {skip_invalid}")
    print(f"   因文件已存在跳过: {skip_exist}")
    print(f"   下载成功: {success_count} (其中空文件: {empty_count})")
    print(f"   下载失败行数: {len(fail_rows)}")
    if fail_rows:
        if len(fail_rows) > 10:
            print(f"   失败行号(前10个): {fail_rows[:10]} ... 共 {len(fail_rows)} 个")
        else:
            print(f"   失败行号: {fail_rows}")
    print(f"   实际文件系统总文件数(含空): {total_files}")
    print(f"   其中空文件数(系统统计): {total_empty}")
    print(f"   总耗时: {elapsed:.2f} 秒")
    print("=" * 60)

if __name__ == "__main__":
    main()

输出结果:
输出结果

🔧 配置说明

您可以根据需要调整脚本开头的几个变量:

变量名默认值含义
fileNameColumn0Excel 中文件名字段的列索引(从 0 开始)
urlColumn1Excel 中下载链接字段的列索引
max_workers30最大并发线程数
max_retries3单个下载失败时的重试次数

🧩 优化思路与技巧

1. 读取 Excel 的坑与对策

  • 问题:xlrd.xlsx 支持不佳,且单元格中的超链接或公式常常导致读取值为 None
  • 对策:改用 pandas(依赖 openpyxl),并设置 dtype=strkeep_default_na=False,确保所有数据以字符串形式读入,避免 NaN 干扰。同时,pandas 能正确识别超链接的显示文本。

2. 全部异步,避免主线程阻塞

  • 所有下载任务均提交给 ThreadPoolExecutor,让线程池自行管理排队与并发,无需在主循环中进行同步下载,简化了代码并提高了效率。

3. 实时反馈

  • 使用 as_completed 迭代已完成的 Future,每完成一个任务立即打印结果,同时输出一个点(.)作为进度指示。这种方式兼顾了实时性和视觉友好。

4. 动态路径设计

  • 脚本接受命令行参数或交互输入 Excel 路径,下载目录自动推断为去掉扩展名的路径,省去每次修改代码的麻烦,便于多项目复用。

5. 完善的统计与错误记录

  • 区分跳过原因(URL无效、文件已存在),统计成功/失败/空文件数量,并记录所有失败行号。失败行号仅显示前10个(若过多),避免刷屏。

❓ 常见问题

Q1: 有些行明明有 URL 却显示“URL为空”?

  • 请检查 Excel 中该单元格是否为超链接或公式,pandas 可能读取到显示文本而非链接地址。您可以先将该列复制后“粘贴为数值”,或使用 HYPERLINK 公式的真实显示文本。本脚本已尝试读取超链接显示文本,但若仍失败,建议将 URL 列转换为纯文本格式。

Q2: 下载速度很慢怎么办?

  • 可以适当提高 max_workers(例如 50),但请注意目标服务器的并发限制。如果出现大量失败,请降低并发数。

Q3: 如何只下载部分行?

  • 您可以临时在脚本中修改 data 切片,例如只处理前 100 行:data = df.iloc[:100, [0,1]].values

Q4: 文件下载后为空,脚本会报错吗?

  • 不会。空文件被视作下载成功,但会单独统计。您可以根据统计中的“空文件”数量进一步排查。

📝 结语

本脚本在解决实际业务问题中不断打磨,既保证了下载的高效性,也兼顾了容错与易用性。如果您有更多需求(如添加进度条、支持断点续传、代理设置等),欢迎在此基础上继续扩展。希望这个工具能为您节省宝贵时间,让批量下载变得轻松愉快!

Happy Coding! 🚀

发布日期:2026-08-04

转载自 CSDN-专业IT技术社区

原文链接:https://blog.csdn.net/duantianyu2009/article/details/163482033

文章来源转载

评论

赞0

评论列表

微信小程序
QQ小程序

关于作者

点赞数:0
关注数:0
粉丝:0
文章:0
关注标签:0
加入于:--