在数据采集工作中,下载文件(如图片、文档、压缩包)是极为常见的需求。无论是抓取电商平台商品图、采集公开数据集,还是备份网站资源,一个稳定高效的文件下载模块都是爬虫系统的重要组成部分。
本文将结合Python标准库urllib与第三方库requests,从基础方法讲起,逐步深入到工程化实践,帮助您构建一个具备断点续传、异常重试、进度监控等特性的健壮下载模块。
二、Python下载文件的三种基础方法
方法一:urllib.request.urlretrieve --- 最简洁的方式
python
import urllib.request
url = 'https://example.com/image.jpg'
urllib.request.urlretrieve(url, 'local_image.jpg')
优点 :一行代码完成下载,内部自动处理数据块写入。
缺点:缺乏细粒度控制,无法获取下载进度或处理复杂头部。
方法二:urllib.request.urlopen + 分块读取 --- 更灵活
python
import urllib.request
url = 'https://example.com/file.zip'
response = urllib.request.urlopen(url)
with open('file.zip', 'wb') as f:
while True:
chunk = response.read(8192) # 分块读取,避免内存爆炸
if not chunk:
break
f.write(chunk)
优点 :可控制缓冲区大小,支持流式下载大文件。
缺点:代码略显繁琐,需手动处理循环。
方法三:requests 库 --- 工业级首选
python
import requests
url = 'https://example.com/data.csv'
response = requests.get(url, stream=True)
with open('data.csv', 'wb') as f:
for chunk in response.iter_content(chunk_size=8192):
if chunk:
f.write(chunk)
优点 :API简洁优雅,内置丰富的异常处理和会话管理。
缺点 :需要额外安装(pip install requests)。
三、实战:构建一个生产级文件下载模块
下面我们设计一个完整的下载器类,集成以下特性:
- 自动创建目录
- 文件存在性检查(避免重复下载)
- 伪装User-Agent(绕过简单反爬)
- 超时与重试机制
- 下载进度显示
- 异常分类处理与日志记录
完整代码实现
python
import os
import time
import logging
from urllib import request, error
from http.client import IncompleteRead
import requests
# 配置日志
logging.basicConfig(
level=logging.INFO,
format='%(asctime)s - %(levelname)s - %(message)s'
)
logger = logging.getLogger(__name__)
class FileDownloader:
"""通用文件下载器"""
def __init__(self, save_root='./downloads', max_retries=3, timeout=15):
self.save_root = save_root
self.max_retries = max_retries
self.timeout = timeout
self.user_agent = 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36'
def download_with_urllib(self, url, sub_dir='', filename=None, use_chunk=True):
"""
使用urllib下载文件
:param url: 文件URL
:param sub_dir: 子目录(相对于save_root)
:param filename: 保存的文件名(默认从URL提取)
:param use_chunk: 是否分块读取(大文件建议True)
:return: 本地文件路径,失败返回None
"""
save_dir = os.path.join(self.save_root, sub_dir)
os.makedirs(save_dir, exist_ok=True) # 自动创建目录
if filename is None:
filename = url.split('/')[-1] or 'download_file'
local_path = os.path.join(save_dir, filename)
# 文件已存在则跳过
if os.path.exists(local_path):
logger.info(f"文件已存在,跳过: {local_path}")
return local_path
# 重试机制
for attempt in range(1, self.max_retries + 1):
try:
req = request.Request(url, headers={'User-Agent': self.user_agent})
with request.urlopen(req, timeout=self.timeout) as response:
# 获取文件大小(用于进度显示)
total_size = int(response.headers.get('Content-Length', 0))
if use_chunk:
with open(local_path, 'wb') as f:
downloaded = 0
while True:
chunk = response.read(8192)
if not chunk:
break
f.write(chunk)
downloaded += len(chunk)
if total_size > 0:
percent = (downloaded / total_size) * 100
print(f"\r进度: {percent:.1f}% ({downloaded}/{total_size} bytes)", end='')
print() # 换行
else:
data = response.read()
with open(local_path, 'wb') as f:
f.write(data)
logger.info(f"下载成功: {local_path}")
return local_path
except (error.URLError, TimeoutError, IncompleteRead) as e:
logger.warning(f"第{attempt}次下载失败: {e}")
if attempt < self.max_retries:
time.sleep(2 ** attempt) # 指数退避等待
else:
logger.error(f"下载失败,已达最大重试次数: {url}")
return None
except Exception as e:
logger.error(f"未知错误: {e}")
return None
def download_with_requests(self, url, sub_dir='', filename=None):
"""
使用requests下载文件(推荐方法)
"""
save_dir = os.path.join(self.save_root, sub_dir)
os.makedirs(save_dir, exist_ok=True)
if filename is None:
filename = url.split('/')[-1] or 'download_file'
local_path = os.path.join(save_dir, filename)
if os.path.exists(local_path):
logger.info(f"文件已存在,跳过: {local_path}")
return local_path
session = requests.Session()
session.headers.update({'User-Agent': self.user_agent})
for attempt in range(1, self.max_retries + 1):
try:
response = session.get(url, timeout=self.timeout, stream=True)
response.raise_for_status() # 检查HTTP状态码
total_size = int(response.headers.get('content-length', 0))
downloaded = 0
with open(local_path, 'wb') as f:
for chunk in response.iter_content(chunk_size=8192):
if chunk:
f.write(chunk)
downloaded += len(chunk)
if total_size > 0:
percent = (downloaded / total_size) * 100
print(f"\r进度: {percent:.1f}%", end='')
print()
logger.info(f"下载成功: {local_path}")
return local_path
except requests.exceptions.RequestException as e:
logger.warning(f"第{attempt}次下载失败: {e}")
if attempt < self.max_retries:
time.sleep(2 ** attempt)
else:
logger.error(f"下载失败: {url}")
return None
# ---------- 使用示例 ----------
if __name__ == '__main__':
downloader = FileDownloader(save_root='./my_images')
# 示例1:下载单张图片
url = 'https://www.example.com/photo.jpg'
downloader.download_with_requests(url, sub_dir='2024/album')
# 示例2:批量下载(配合您的业务逻辑)
# 假设从数据库获取图片链接列表
pic_list = [
{'id': '001', 'link': 'https://...', 'label': 'landscape'},
{'id': '002', 'link': 'https://...', 'label': 'portrait'},
]
for pic in pic_list:
local_path = downloader.download_with_requests(
url=pic['link'],
sub_dir=f"category/{pic['label']}",
filename=f"{pic['id']}.jpg"
)
if local_path:
# 更新数据库状态
# update_download_status(pic['id'], 'success')
pass
四、常见问题与避坑指南
1. 下载大文件时内存溢出
错误做法 :data = response.read() 一次性读取全部内容。
正确做法 :使用分块读取 response.read(8192) 或 requests 的 stream=True 模式。
2. SSL证书验证失败
python
# 临时解决方案(不推荐生产环境)
response = requests.get(url, verify=False)
# 或指定证书路径
response = requests.get(url, verify='/path/to/cert.pem')
3. 服务器返回非200状态码
务必检查状态码:
python
if response.status_code != 200:
raise Exception(f"HTTP错误: {response.status_code}")
# requests库可使用 response.raise_for_status() 自动检查
4. 文件名非法字符处理
python
import re
safe_filename = re.sub(r'[\\/*?:"<>|]', '_', original_filename)
5. 断点续传(大文件场景)
python
# 使用Range头部实现续传(需要服务器支持)
headers = {'Range': f'bytes={existing_size}-'}
response = requests.get(url, headers=headers, stream=True)
with open(local_path, 'ab') as f: # 追加模式
for chunk in response.iter_content(8192):
f.write(chunk)
五、性能优化建议
- 异步下载 :对于大量文件,使用
aiohttp+asyncio实现并发下载,可大幅提升效率。 - 连接池复用 :使用
requests.Session()复用TCP连接,减少握手开销。 - 限速控制 :避免对目标服务器造成过大压力,可添加
time.sleep()控制请求间隔。
六、结语
文件下载虽看似简单,但生产环境中需考虑的因素颇多:网络波动、服务器限制、磁盘I/O、异常恢复......本文提供的渐进式方案能构建稳定可靠的下载模块。