Python requests + BeautifulSoup 爬取豆瓣电影图片

0 阅读9分钟

在互联网数据采集领域,爬虫技术一直是开发者们关注的重点。豆瓣电影作为国内最权威的电影资料库之一,储存了海量的电影海报、剧照等图片资源。本文将详细介绍如何使用 Python 的 requests 库和 BeautifulSoup 工具,快速搭建一个高效稳定的豆瓣电影图片爬虫,并配合亿牛云代理服务突破 IP 限制,实现稳定持续的数据采集。

一、环境准备与依赖安装

在开始编写爬虫之前,我们需要准备好 Python 运行环境以及必要的第三方库。建议使用 Python 3.7 及以上版本,以确保最佳兼容性和性能。

各个库的作用说明如下:

  • requests:Python 最流行的 HTTP 请求库,用于向豆瓣服务器发送网络请求
  • beautifulsoup4:强大的 HTML/XML 解析库,能够快速定位和提取页面中的目标元素
  • lxml:高效的 XML 和 HTML 解析器,作为 BeautifulSoup 的底层解析引擎

二、爬虫基本原理与请求头设置

豆瓣电影页面采用动态加载技术,常规的直接抓取方式可能无法获取完整的电影图片资源。我们需要先分析豆瓣的页面结构,了解图片资源的加载方式。

2.1 请求头伪装

为了避免被豆瓣服务器识别为爬虫程序并限制访问,我们需要在请求时设置合理的 User-Agent 和其他请求头信息。模拟真实浏览器的请求头是最基本的反反爬策略。

def get_headers():
    """生成随机请求头,模拟真实浏览器访问"""
    user_agents = [
        'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36',
        'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/119.0.0.0 Safari/537.36',
        'Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:121.0) Gecko/20100101 Firefox/121.0',
    ]
    return {
        'User-Agent': random.choice(user_agents),
        'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8',
        'Accept-Language': 'zh-CN,zh;q=0.9,en;q=0.8',
        'Accept-Encoding': 'gzip, deflate, br',
        'Connection': 'keep-alive',
        'Referer': 'https://movie.douban.com/',
    }

2.2 构建请求会话

使用 requests.Session() 可以维护一个持久的会话,保持 Cookie 和连接状态,提高请求效率。同时设置合理的超时时间,防止请求无限等待:

session = requests.Session()
session.headers.update(get_headers())

def fetch_page(url, timeout=10):
    """获取页面内容"""
    try:
        response = session.get(url, timeout=timeout)
        response.raise_for_status()
        response.encoding = 'utf-8'
        return response.text
    except requests.RequestException as e:
        print(f"请求失败: {url}, 错误: {e}")
        return None

2.3 亿牛云代理配置

在实际生产环境中,单纯的请求头伪装往往不足以应对严格的反爬机制。豆瓣等主流平台会基于 IP 频率进行流量管控,单一 IP 持续请求很快就会被限流甚至封禁。此时,使用高匿名代理 IP 是最有效的解决方案。

亿牛云是国内知名的代理服务提供商,提供覆盖全国的高匿名代理 IP 池,能够有效隐藏真实请求来源。我们可以轻松地将亿牛云的代理服务集成到爬虫架构中:

# 亿牛云代理配置示例
PROXY_HOST = "http://ip.16yun.cn"  # 代理服务器地址
PROXY_PORT = 31111  # 代理端口
PROXY_USER = "your_username"  # 亿牛云用户名
PROXY_PASS = "your_password"  # 亿牛云密码

def get_proxy():
    """获取亿牛云代理配置"""
    return {
        "http": f"http://{PROXY_USER}:{PROXY_PASS}@{PROXY_HOST}:{PROXY_PORT}",
        "https": f"https://{PROXY_USER}:{PROXY_PASS}@{PROXY_HOST}:{PROXY_PORT}",
    }

def fetch_with_proxy(url, timeout=15):
    """使用亿牛云代理获取页面内容"""
    try:
        response = session.get(url, timeout=timeout, proxies=get_proxy())
        response.raise_for_status()
        response.encoding = 'utf-8'
        return response.text
    except requests.RequestException as e:
        print(f"代理请求失败: {url}, 错误: {e}")
        return None

三、解析豆瓣电影页面结构

豆瓣电影有多个页面可以获取电影图片资源。热门电影列表页面和电影详情页面是我们主要的数据来源。我们以豆瓣电影 Top250 页面为例,分析页面结构并提取图片链接。

3.1 分析页面元素

打开豆瓣电影 Top250 页面,查看页面源码可以发现,每部电影的信息都包裹在一个特定的 div 容器中。电影的封面图片通常位于 img 标签的 src 属性或懒加载属性 data-origin 中。

def parse_movie_covers(html_content):
    """解析页面中的电影封面图片链接"""
    if not html_content:
        return []
    
    soup = BeautifulSoup(html_content, 'lxml')
    movie_items = soup.select('div.item')
    
    cover_urls = []
    for item in movie_items:
        # 尝试获取高清封面图
        img_tag = item.select_one('img[width]')
        if img_tag:
            # 优先获取原始大图
            cover_url = img_tag.get('src') or img_tag.get('data-origin', '')
            if cover_url and 'cover' in cover_url:
                # 将小图替换为大图
                cover_url = cover_url.replace('s_ratio_poster', 'r_ratio_poster')
                cover_urls.append(cover_url)
    
    return cover_urls

3.2 提取多页电影数据

豆瓣 Top250 共有 10 页,每页 25 部电影。我们可以通过循环遍历所有页面,获取尽可能多的电影封面:

def get_all_movie_covers(base_url='https://movie.douban.com/top250'):
    """获取豆瓣 Top250 所有电影的封面链接"""
    all_covers = []
    
    for page in range(10):
        if page == 0:
            url = base_url
        else:
            url = f'{base_url}?start={page * 25}'
        
        print(f'正在抓取第 {page + 1} 页...')
        html = fetch_with_proxy(url)  # 使用代理请求
        
        if html:
            covers = parse_movie_covers(html)
            all_covers.extend(covers)
        
        # 随机延时,避免请求过于频繁
        time.sleep(random.uniform(2, 4))
    
    return all_covers

四、下载保存图片资源

获取到图片 URL 后,我们需要编写函数将图片下载到本地磁盘。为了保证下载的稳定性,需要处理各种异常情况,并提供进度反馈。

4.1 单张图片下载函数

def download_image(url, save_path, timeout=30):
    """下载单张图片到指定路径"""
    try:
        response = session.get(url, timeout=timeout, stream=True, proxies=get_proxy())
        response.raise_for_status()
        
        # 确保目录存在
        os.makedirs(os.path.dirname(save_path), exist_ok=True)
        
        with open(save_path, 'wb') as f:
            for chunk in response.iter_content(chunk_size=8192):
                if chunk:
                    f.write(chunk)
        
        return True
    except Exception as e:
        print(f'下载失败: {url}, 错误: {e}')
        return False

4.2 批量下载与进度管理

def download_batch(urls, save_dir='./douban_covers', delay_range=(2, 3)):
    """批量下载图片"""
    os.makedirs(save_dir, exist_ok=True)
    
    success_count = 0
    fail_count = 0
    
    for index, url in enumerate(urls, 1):
        filename = f'cover_{index:03d}.jpg'
        save_path = os.path.join(save_dir, filename)
        
        print(f'[{index}/{len(urls)}] 正在下载: {filename}')
        
        if download_image(url, save_path):
            success_count += 1
        else:
            fail_count += 1
        
        # 下载间隔,使用代理后可适当缩短
        time.sleep(random.uniform(*delay_range))
    
    print(f'\n下载完成!成功: {success_count}, 失败: {fail_count}')
    return success_count, fail_count

五、完整爬虫程序整合

将上述各个模块整合成一个完整的爬虫程序,增加配置管理、错误处理、日志记录和亿牛云代理轮换等功能:

#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
豆瓣电影图片爬虫
功能:抓取豆瓣电影 Top250 的封面图片
依赖:requests + BeautifulSoup + 亿牛云代理
"""

import requests
from bs4 import BeautifulSoup
import os
import time
import random
import logging
from datetime import datetime

# 配置日志
logging.basicConfig(
    level=logging.INFO,
    format='%(asctime)s - %(levelname)s - %(message)s',
    handlers=[
        logging.FileHandler('crawler.log', encoding='utf-8'),
        logging.StreamHandler()
    ]
)
logger = logging.getLogger(__name__)

# 亿牛云代理配置
PROXY_HOST = "http://ip.16yun.cn"
PROXY_PORT = 31111
PROXY_USER = "your_username"
PROXY_PASS = "your_password"

class DoubanCoverCrawler:
    def __init__(self):
        self.session = requests.Session()
        self.session.headers.update(self._get_headers())
        self.proxy_pool = self._build_proxy_pool()
    
    def _get_headers(self):
        user_agents = [
            'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36',
            'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36',
            'Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:121.0) Gecko/20100101',
        ]
        return {
            'User-Agent': random.choice(user_agents),
            'Accept': 'text/html,application/xhtml+xml',
            'Accept-Language': 'zh-CN,zh;q=0.9',
            'Referer': 'https://movie.douban.com/',
        }
    
    def _build_proxy_pool(self):
        """构建亿牛云代理池"""
        return {
            "http": f"http://{PROXY_USER}:{PROXY_PASS}@{PROXY_HOST}:{PROXY_PORT}",
            "https": f"https://{PROXY_USER}:{PROXY_PASS}@{PROXY_HOST}:{PROXY_PORT}",
        }
    
    def fetch(self, url):
        """使用亿牛云代理获取页面"""
        try:
            resp = self.session.get(url, timeout=15, proxies=self.proxy_pool)
            resp.raise_for_status()
            resp.encoding = 'utf-8'
            return resp.text
        except Exception as e:
            logger.error(f'请求失败: {url}, {e}')
            return None
    
    def parse_covers(self, html):
        covers = []
        soup = BeautifulSoup(html, 'lxml')
        for item in soup.select('div.item'):
            img = item.select_one('img[width]')
            if img:
                url = img.get('data-origin') or img.get('src', '')
                if 'cover' in url:
                    url = url.replace('s_ratio_poster', 'r_ratio_poster')
                    covers.append(url)
        return covers
    
    def download(self, url, path):
        """使用亿牛云代理下载图片"""
        try:
            resp = self.session.get(url, timeout=30, stream=True, proxies=self.proxy_pool)
            resp.raise_for_status()
            os.makedirs(os.path.dirname(path), exist_ok=True)
            with open(path, 'wb') as f:
                for chunk in resp.iter_content(8192):
                    f.write(chunk)
            return True
        except Exception as e:
            logger.error(f'下载失败: {url}, {e}')
            return False
    
    def run(self, pages=10, save_dir='./covers'):
        """运行爬虫主流程"""
        all_covers = []
        base_url = 'https://movie.douban.com/top250'
        
        for page in range(pages):
            url = f'{base_url}?start={page * 25}' if page else base_url
            logger.info(f'正在抓取第 {page + 1} 页')
            
            html = self.fetch(url)
            if html:
                covers = self.parse_covers(html)
                all_covers.extend(covers)
                logger.info(f'本页获取 {len(covers)} 张封面')
            
            time.sleep(random.uniform(2, 4))
        
        logger.info(f'共获取 {len(all_covers)} 个封面链接')
        
        # 下载图片
        success = fail = 0
        for i, url in enumerate(all_covers, 1):
            path = os.path.join(save_dir, f'cover_{i:03d}.jpg')
            if self.download(url, path):
                success += 1
            else:
                fail += 1
            if i % 10 == 0:
                logger.info(f'下载进度: {i}/{len(all_covers)}')
            time.sleep(random.uniform(1.5, 3))
        
        logger.info(f'完成!成功: {success}, 失败: {fail}')

if __name__ == '__main__':
    crawler = DoubanCoverCrawler()
    crawler.run(pages=10, save_dir='./douban_covers')

六、运行效果与注意事项

运行上述程序后,我们可以看到控制台输出的抓取进度。图片会按照下载顺序保存在指定的目录中,文件名格式为 cover_001.jpgcover_002.jpg 等。

6.1 常见问题与解决方案

在实际运行过程中,可能会遇到以下问题:

IP 被封禁:豆瓣有严格的反爬机制,高频请求会导致 IP 被临时封禁。配合代理服务使用后,系统会自动切换不同的代理 IP 进行请求,彻底规避单一 IP 被封的风险。亿牛云提供的高匿名代理能够完全隐藏真实 IP 地址,让目标服务器无法追踪到真实请求来源。

图片 URL 失效:部分图片可能使用了防盗链技术,直接通过 URL 无法下载。这时需要分析目标网站的防盗链策略,可能需要添加 Referer 头或使用 cookies。配合代理使用时,Referer 头信息可以进一步增强请求的真实性。

解析规则失效:豆瓣可能会调整页面结构,导致原有的解析规则无法匹配。发现问题时需要重新分析页面源码,更新解析逻辑。

总结

本文详细介绍了使用 Python requests 和 BeautifulSoup 爬取豆瓣电影图片的完整方案,涵盖了请求伪装、亿牛云代理集成、页面解析、数据存储、错误处理等核心技术点。通过亿牛云代理服务的加持,爬虫能够稳定高效地完成大规模数据采集任务,有效应对目标网站的反爬机制。通过本文的学习,读者可以掌握网页爬虫的基本编写方法,并将其应用到其他网站的图片资源抓取中。