Python爬虫实战:高效稳定的文件下载模块设计与实现

在数据采集工作中,下载文件(如图片、文档、压缩包)是极为常见的需求。无论是抓取电商平台商品图、采集公开数据集,还是备份网站资源,一个稳定高效的文件下载模块都是爬虫系统的重要组成部分。

本文将结合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)requestsstream=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)

五、性能优化建议

  1. 异步下载 :对于大量文件,使用 aiohttp + asyncio 实现并发下载,可大幅提升效率。
  2. 连接池复用 :使用 requests.Session() 复用TCP连接,减少握手开销。
  3. 限速控制 :避免对目标服务器造成过大压力,可添加 time.sleep() 控制请求间隔。

六、结语

文件下载虽看似简单,但生产环境中需考虑的因素颇多:网络波动、服务器限制、磁盘I/O、异常恢复......本文提供的渐进式方案能构建稳定可靠的下载模块。

相关推荐
江屿风1 小时前
【科普】【差集&交集概念落地云相册】流食般投喂
开发语言·c++·笔记·算法·云相册
代龙涛1 小时前
WordPress header.php 与 footer.php 模板结构详解
开发语言·php·wordpress
黄贵根7 小时前
JavaScript实现教培行业意向登记系统
开发语言·javascript·ecmascript
小趴蔡ha9 小时前
02 Anaconda、Python 与机器学习开发环境入门
python·机器学习·anaconda
2401_868534789 小时前
RTOS之RT-Thread & Linux:线程/进程管理与调度核心差异分析
c++·python
zzzll111110 小时前
Claude Code离线安装方案揭秘
开发语言·php
数字化转型分享点滴10 小时前
富士康、蓝思科技为何选择四化信息?MES制造执行系统的实践
python·科技
wling030110 小时前
vasp虚频计算-python脚本
开发语言·windows·python·vasp计算
魔镜前的帅比10 小时前
(开源项目)x-claw(总)
python·ai·rust·开源