在日常工作中,我们常常需要从 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 中的“文件名称”不需要包含扩展名,只需提供基础名称(可包含路径)。
示例:
| 某公司合同 | 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


