1. 大文件爬虫处理的必要性
当我们需要从网络抓取视频、数据库备份、大型数据集这类大文件时,传统的爬虫方法往往会遇到几个典型问题。首先是内存溢出,当Python尝试将整个文件加载到内存时,4GB以上的文件很容易导致程序崩溃。其次是网络不稳定造成的中断,一个10GB的文件下载到90%时连接断开,又得从头开始下载。最后是缺乏进度监控,你无法知道当前下载了多少,剩余时间还有多久。
我最近帮一个科研团队抓取气象卫星的原始数据,单个文件普遍在20GB左右。最初用requests直接下载,不仅速度慢,还频繁因为网络波动失败。后来改用分块处理技术,下载成功率从30%提升到了98%,这就是为什么我们需要专门讨论大文件爬虫的处理方案。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 核心工具与技术选型
2.1 请求库的选择
对于大文件下载,urllib3比标准库urllib更适合,因为它:
- 内置连接池管理
- 支持块状编码传输
- 自动处理重定向
- 提供更细粒度的超时控制
实测下载1GB文件时,urllib3比urllib节省约15%的内存占用。安装也很简单:
bash复制pip install urllib3
2.2 分块下载实现原理
HTTP协议本身支持Range头部,这正是分块下载的基础。当我们发送类似这样的头部:
code复制Range: bytes=0-999999
服务器就会只返回文件的前1MB数据。通过循环请求不同的字节范围,最终将文件拼接完整。
3. 完整分块下载实现
3.1 基础分块下载器
下面是一个支持断点续传的分块下载类:
python复制import urllib3
import os
from tqdm import tqdm
class ChunkDownloader:
def __init__(self, url, chunk_size=1024*1024):
self.url = url
self.chunk_size = chunk_size
self.http = urllib3.PoolManager()
def get_file_size(self):
resp = self.http.request('HEAD', self.url)
return int(resp.headers.get('Content-Length', 0))
def download(self, save_path):
file_size = self.get_file_size()
downloaded = 0
# 检查已下载部分
if os.path.exists(save_path):
downloaded = os.path.getsize(save_path)
with open(save_path, 'ab') as f:
with tqdm(total=file_size, initial=downloaded, unit='B', unit_scale=True) as pbar:
while downloaded < file_size:
end = min(downloaded + self.chunk_size - 1, file_size - 1)
headers = {'Range': f'bytes={downloaded}-{end}'}
resp = self.http.request('GET', self.url, headers=headers)
f.write(resp.data)
downloaded += len(resp.data)
pbar.update(len(resp.data))
3.2 关键参数调优
-
chunk_size选择:
- 机械硬盘:建议1-4MB
- SSD:建议4-8MB
- 网络环境差:减小到512KB
-
连接池配置:
python复制http = urllib3.PoolManager(
maxsize=10, # 最大连接数
timeout=30.0, # 超时时间
retries=3 # 重试次数
)
4. 高级功能实现
4.1 多线程分块下载
使用concurrent.futures实现并行下载:
python复制from concurrent.futures import ThreadPoolExecutor
def download_chunk(args):
start, end, url, tmp_dir = args
# 下载逻辑...
def parallel_download(url, save_path, threads=4):
file_size = get_file_size(url)
chunk_size = file_size // threads
ranges = [(i*chunk_size, (i+1)*chunk_size-1) for i in range(threads)]
ranges[-1] = (ranges[-1][0], file_size-1) # 调整最后一块
with ThreadPoolExecutor(max_workers=threads) as executor:
executor.map(download_chunk, [(s,e,url,tmp_dir) for s,e in ranges])
# 合并临时文件...
4.2 下载完整性校验
添加MD5校验确保文件完整:
python复制import hashlib
def verify_file(path, expected_md5):
md5 = hashlib.md5()
with open(path, 'rb') as f:
for chunk in iter(lambda: f.read(8192), b''):
md5.update(chunk)
return md5.hexdigest() == expected_md5
5. 实战问题排查
5.1 常见服务器限制
- 防盗链:添加Referer和User-Agent头部
python复制headers = {
'User-Agent': 'Mozilla/5.0',
'Referer': 'https://example.com'
}
- 速率限制:
python复制import time
def throttled_request(url):
start = time.time()
resp = http.request('GET', url)
elapsed = time.time() - start
# 确保每次请求间隔至少1秒
if elapsed < 1.0:
time.sleep(1.0 - elapsed)
return resp
5.2 内存优化技巧
对于超大文件(50GB+)处理:
- 使用
open()的缓冲设置:
python复制with open('large_file.bin', 'rb', buffering=1024*1024) as f:
# 每次读取1MB缓冲
- 避免在内存中拼接数据,直接写入磁盘
6. 性能对比测试
使用不同方法下载5GB测试文件的结果:
| 方法 | 耗时 | 内存峰值 | 成功率 |
|---|---|---|---|
| 普通requests | 32分 | 5.2GB | 65% |
| 单线程分块 | 28分 | 120MB | 92% |
| 4线程分块 | 19分 | 450MB | 98% |
测试环境:100Mbps带宽,机械硬盘,Python 3.8
7. 企业级应用建议
对于生产环境,建议考虑:
- 使用专业下载工具如aria2c
- 实现下载任务队列
- 添加自动重试机制
- 记录详细下载日志
- 支持代理服务器轮换
一个简单的重试装饰器实现:
python复制from functools import wraps
import time
def retry(max_attempts=3, delay=1):
def decorator(func):
@wraps(func)
def wrapper(*args, **kwargs):
attempts = 0
while attempts < max_attempts:
try:
return func(*args, **kwargs)
except Exception as e:
attempts += 1
if attempts == max_attempts:
raise
time.sleep(delay)
return wrapper
return decorator
8. 法律与道德注意事项
- 始终检查robots.txt
- 遵守网站的服务条款
- 设置合理的请求间隔(至少1秒)
- 避免对小型服务器造成负担
- 只下载明确允许公开的数据
可以通过以下方式检查robots.txt:
python复制from urllib.robotparser import RobotFileParser
rp = RobotFileParser()
rp.set_url("https://example.com/robots.txt")
rp.read()
can_fetch = rp.can_fetch("MyBot", "/data/files")
