欢迎光临
我们一直在努力

批量下载 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
图片/logo https://cdn.aaaa.com/logo.png

上述示例中:

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

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


📦 环境要求

  • Python 3.6+
  • 依赖库:pandas, openpyxl(pandas 读取 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()


输出结果:
输出结果

🔧 配置说明

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

变量名默认值含义
fileNameColumn 0 Excel 中文件名字段的列索引(从 0 开始)
urlColumn 1 Excel 中下载链接字段的列索引
max_workers 30 最大并发线程数
max_retries 3 单个下载失败时的重试次数

🧩 优化思路与技巧

1. 读取 Excel 的坑与对策

  • 问题:xlrd 对 .xlsx 支持不佳,且单元格中的超链接或公式常常导致读取值为 None。
  • 对策:改用 pandas(依赖 openpyxl),并设置 dtype=str 和 keep_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

赞(0)
未经允许不得转载:171主机测评 » 批量下载 Excel 中的文件:一个高效可靠的 Python 脚本
分享到: 更多 (0)

评论 抢沙发

  • 昵称 (必填)
  • 邮箱 (必填)
  • 网址