引言:为什么需要批量下载豆瓣电影数据?

在当今数据驱动的时代,电影数据分析已成为影视行业研究、市场分析和学术研究的重要组成部分。豆瓣电影作为中国最具影响力的电影评分平台之一,拥有海量的电影信息、用户评分和影评数据。无论是进行电影市场趋势分析、用户偏好研究,还是构建推荐系统,获取这些数据都具有极高的价值。

然而,手动逐个复制粘贴电影信息不仅效率低下,而且容易出错。批量下载技术可以帮助我们自动化地获取大量电影数据,显著提高工作效率。本文将详细介绍如何使用Python编程语言,通过多种方法高效、合规地获取豆瓣电影数据,并解析常见问题及解决方案。

一、准备工作:环境配置与必要库的安装

在开始编写爬虫程序之前,我们需要配置Python开发环境并安装必要的第三方库。这些库将帮助我们发送HTTP请求、解析HTML内容、处理数据以及避免被反爬虫机制拦截。

1.1 安装必要的Python库

打开终端或命令提示符,运行以下命令安装所需库:

pip install requests beautifulsoup4 lxml pandas openpyxl fake-useragent selenium webdriver-manager

各库的作用如下:

  • requests:发送HTTP请求,获取网页内容
  • beautifulsoup4lxml:解析HTML文档,提取所需数据
  • pandas:数据处理和分析,方便将数据导出为Excel或CSV格式
  • openpyxl:支持Excel文件的读写操作
  • fake-useragent:生成随机User-Agent,模拟浏览器访问,降低被封禁风险
  • seleniumwebdriver-manager:用于处理动态加载内容或需要JavaScript渲染的页面

1.2 验证安装

安装完成后,可以通过以下代码验证是否成功安装:

import requests
from bs4 import BeautifulSoup
import pandas as pd
from fake_useragent import UserAgent

print("所有库已成功安装!")

二、基础爬虫:使用Requests和BeautifulSoup获取静态页面

对于大多数静态网页,我们可以使用requests库发送HTTP请求,然后用BeautifulSoup解析HTML内容。豆瓣电影的简介页面通常是静态的,因此这种方法适用于大部分场景。

2.1 获取单个电影的简介信息

以下是一个获取单个电影简介的示例代码:

import requests
from bs4 import BeautifulSoup
from fake_useragent import UserAgent

def get_movie_info(movie_id):
    """
    获取单个电影的简介信息
    :param movie_id: 电影ID,例如 '26794435' 对应《肖申克的救赎》
    :return: 包含电影信息的字典
    """
    # 构造URL
    url = f"https://movie.douban.com/subject/{movie_id}/"
    
    # 生成随机User-Agent
    ua = UserAgent()
    headers = {
        'User-Agent': ua.random,
        'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8',
        'Accept-Language': 'zh-CN,zh;q=0.8,zh-TW;q=0.7,zh-HK;q=0.5,en-US;q=0.3,en;q=0.2',
        'Accept-Encoding': 'gzip, deflate, br',
        'Connection': 'keep-alive',
        'Upgrade-Insecure-Requests': '1',
    }
    
    try:
        # 发送请求
        response = requests.get(url, headers=headers, timeout=10)
        response.raise_for_status()  # 如果状态码不是200,抛出异常
        
        # 解析HTML
        soup = BeautifulSoup(response.text, 'lxml')
        
        # 提取电影标题
        title = soup.find('span', property='v:itemreviewed').text.strip() if soup.find('span', property='v:itemreviewed') else 'N/A'
        
        # 提取导演
        director = soup.find('a', rel='v:directedBy').text.strip() if soup.find('a', rel='v:directedBy') else 'N/A'
        
        # 提取编剧
        screenwriter = []
        screenwriter_tags = soup.find_all('a', class_='attrs')
        for tag in screenwriter_tags:
            if '编剧' in tag.text:
                screenwriter = [a.text for a in tag.find_all('a')]
                break
        
        # 提取主演
        actors = [a.text for a in soup.find_all('a', rel='v:starring')][:5]  # 只取前5个主演
        
        # 提取类型
        genres = [g.text for g in soup.find_all('span', property='v:genre')]
        
        # 提取制片国家/地区
        country_tag = soup.find(text=lambda text: text and '制片国家/地区' in text)
        country = country_tag.next_sibling.strip() if country_tag else 'N/A'
        
        # 提取语言
        language_tag = soup.find(text=lambda text: text and '语言' in text)
        language = language_tag.next_sibling.strip() if language_tag else 'N/A'
        
        # 提取上映日期
        release_date = soup.find('span', property='v:initialReleaseDate').text.strip() if soup.find('span', property='v:initialReleaseDate') else 'N/A'
        
        # 提取片长
        runtime = soup.find('span', property='v:runtime').text.strip() if soup.find('span', property='v:runtime') else 'N/A'
        
        # 提取评分
        rating = soup.find('strong', property='v:average').text.strip() if soup.find('strong', property='v:average') else 'N/A'
        
        # 提取评分人数
        rating_count = soup.find('span', property='v:votes').text.strip() if soup.find('span', property='v:votes') else 'N/A'
        
        # 提取简介
        summary_tag = soup.find('span', property='v:summary')
        summary = summary_tag.text.strip() if summary_tag else 'N/A'
        
        # 返回数据
        return {
            '电影ID': movie_id,
            '标题': title,
            '导演': director,
            '编剧': ', '.join(screenwriter) if screenwriter else 'N/A',
            '主演': ', '.join(actors) if actors else 'N/A',
            '类型': ', '.join(genres) if genres else 'N/A',
            '制片国家/地区': country,
            '语言': language,
            '上映日期': release_date,
            '片长': runtime,
            '评分': rating,
            '评分人数': rating_count,
            '简介': summary
        }
        
    except requests.exceptions.RequestException as e:
        print(f"请求失败: {e}")
        return None
    except Exception as e:
        print(f"解析失败: {e}")
        return None

# 测试获取单个电影信息
if __name__ == '__main__':
    movie_data = get_movie_info('26794435')  # 《肖申克的救赎》
    if movie_data:
        for key, value in movie_data.items():
            print(f"{key}: {value}")

2.2 代码解析与注意事项

  1. User-Agent伪装:使用fake_useragent库生成随机的浏览器User-Agent,避免被豆瓣识别为爬虫。
  2. 异常处理:使用try-except结构捕获网络请求和解析过程中的异常,确保程序稳定性。
  3. 超时设置:设置timeout=10,避免因网络延迟导致程序长时间卡住。
  4. HTML解析:使用lxml解析器,比默认的html.parser更快且容错性更好。
  5. 数据提取:通过findfind_all方法定位特定HTML元素,注意检查元素是否存在,避免AttributeError

2.3 批量获取多个电影信息

有了单个电影的获取函数,我们可以扩展为批量获取:

def batch_get_movies(movie_ids, delay=2):
    """
    批量获取多个电影信息
    :param movie_ids: 电影ID列表
    :param delay: 请求间隔时间(秒)
    :return: 包含所有电影信息的列表
    """
    import time
    movie_data_list = []
    
    for i, movie_id in enumerate(movie_ids):
        print(f"正在获取第 {i+1}/{len(movie_ids)} 个电影: {movie_id}")
        movie_data = get_movie_info(movie_id)
        if movie_data:
            movie_data_list.append(movie_data)
        time.sleep(delay)  # 避免请求过于频繁
    
    return movie_data_list

# 示例:批量获取电影信息
if __name__ == '__main__':
    # 电影ID列表(示例)
    movie_ids = [
        '26794435',  # 肖申克的救赎
        '1292052',   # 霸王别姬
        '1291546',   # 阿甘正传
        '1295124',   # 这个杀手不太冷
        '1292720',   # 辛德勒的名单
    ]
    
    movies = batch_get_movies(movie_ids, delay=2)
    
    # 转换为DataFrame并保存
    if movies:
        df = pd.DataFrame(movies)
        df.to_excel('douban_movies.xlsx', index=False)
        print(f"成功保存 {len(movies)} 条电影数据到 douban_movies.xlsx")

三、进阶技巧:处理动态加载内容与反爬虫机制

豆瓣电影的部分内容(如影评、短评)是动态加载的,需要使用Selenium模拟浏览器操作。此外,豆瓣有严格的反爬虫机制,需要采取更多措施来避免被封禁IP或限制访问。

3.1 使用Selenium获取动态内容

Selenium可以模拟真实浏览器行为,适用于需要JavaScript渲染的页面。以下示例展示如何获取电影的短评数据:

from selenium import webdriver
from selenium.webdriver.common.by import By
from selenium.webdriver.chrome.service import Service
from selenium.webdriver.chrome.options import Options
from webdriver_manager.chrome import ChromeDriverManager
import time
import random

def get_movie_short_reviews(movie_id, max_pages=3):
    """
    使用Selenium获取电影短评数据
    :param movie_id: 电影ID
    :param max_pages: 最大获取页数
    :return: 短评数据列表
    """
    # 配置Chrome选项
    chrome_options = Options()
    chrome_options.add_argument('--headless')  # 无头模式,不显示浏览器窗口
    chrome_options.add_argument('--disable-gpu')
    chrome_options.add_argument('--no-sandbox')
    chrome_options.add_argument('--disable-dev-shm-usage')
    chrome_options.add_argument('--user-agent=Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36')
    
    # 初始化WebDriver
    service = Service(ChromeDriverManager().install())
    driver = webdriver.Chrome(service=service, options=chrome_options)
    
    reviews = []
    
    try:
        for page in range(max_pages):
            url = f"https://movie.douban.com/subject/{movie_id}/comments?start={page*20}&limit=20&sort=new_score&status=P"
            print(f"正在获取第 {page+1} 页短评: {url}")
            
            driver.get(url)
            time.sleep(random.uniform(2, 4))  # 随机等待
            
            # 等待内容加载
            try:
                # 等待评论容器出现
                from selenium.webdriver.support.ui import WebDriverWait
                from selenium.webdriver.support import expected_conditions as EC
                WebDriverWait(driver, 10).until(
                    EC.presence_of_element_located((By.CSS_SELECTOR, ".comment-item"))
                )
            except:
                print(f"第 {page+1} 页加载超时,跳过")
                continue
            
            # 提取评论
            comment_items = driver.find_elements(By.CSS_SELECTOR, ".comment-item")
            
            for item in comment_items:
                try:
                    # 评论者
                    user = item.find_element(By.CSS_SELECTOR, ".comment-info a").text
                    
                    # 评分
                    rating_elem = item.find_element(By.CSS_SELECTOR, ".rating")
                    rating = rating_elem.get_attribute("title") if rating_elem else "N/A"
                    
                    # 评论时间
                    comment_time = item.find_element(By.CSS_SELECTOR, ".comment-time").text
                    
                    # 评论内容
                    content = item.find_element(By.CSS_SELECTOR, ".short").text
                    
                    reviews.append({
                        '用户': user,
                        '评分': rating,
                        '时间': comment_time,
                        '内容': content
                    })
                except Exception as e:
                    print(f"提取评论失败: {e}")
                    continue
            
            # 随机延迟,模拟人类行为
            time.sleep(random.uniform(1, 3))
            
            # 如果没有更多评论,提前退出
            if not comment_items:
                break
    
    finally:
        driver.quit()
    
    return reviews

# 示例:获取《肖申克的救赎》短评
if __name__ == '__main__':
    short_reviews = get_movie_short_reviews('26794435', max_pages=2)
    if short_reviews:
        df = pd.DataFrame(short_reviews)
        df.to_excel('douban_short_reviews.xlsx', index=False)
        print(f"成功保存 {len(short_reviews)} 条短评数据")

3.2 反爬虫应对策略

  1. IP代理池:使用代理IP轮换,避免单一IP被封禁。可以使用免费代理(如free-proxy-list.net)或付费代理服务(如阿布云、快代理)。
import random

def get_proxies():
    """
    获取代理IP列表(示例)
    """
    # 这里可以替换为从代理API获取
    proxies = [
        {'http': 'http://123.45.67.89:8080', 'https': 'https://123.45.67.89:8080'},
        {'http': 'http://98.76.54.32:3128', 'https': 'https://98.76.54.32:3128'},
    ]
    return proxies

def use_proxy_request(url, headers):
    """
    使用代理发送请求
    """
    proxies = get_proxies()
    proxy = random.choice(proxies)
    
    try:
        response = requests.get(url, headers=headers, proxies=proxy, timeout=10)
        return response
    except:
        # 如果代理失败,尝试其他代理或直接请求
        return requests.get(url, headers=headers, timeout=10)
  1. 请求频率控制:严格控制请求间隔,建议设置2-5秒的随机延迟。
  2. Session保持:使用requests.Session()保持会话,模拟真实用户行为。
def use_session_request(url):
    """
    使用Session保持会话
    """
    session = requests.Session()
    ua = UserAgent()
    headers = {
        'User-Agent': ua.random,
        'Referer': 'https://movie.douban.com/',
    }
    session.headers.update(headers)
    
    try:
        response = session.get(url, timeout=10)
        return response
    except Exception as e:
        print(f"请求失败: {e}")
        return None
  1. 识别验证码:如果遇到验证码,可以使用OCR库(如pytesseract)识别,或使用打码平台(如超级鹰、云打码)人工识别。

四、数据存储与处理

获取数据后,需要将其存储为结构化格式,便于后续分析。常见格式包括Excel、CSV和JSON。

4.1 保存为Excel文件

使用pandas可以轻松将数据保存为Excel:

import pandas as pd

def save_to_excel(data, filename):
    """
    将数据保存为Excel文件
    :param data: 数据列表(字典列表)
    :param filename: 文件名
    """
    if not data:
        print("没有数据可保存")
        return
    
    df = pd.DataFrame(data)
    df.to_excel(filename, index=False, engine='openpyxl')
    print(f"数据已保存到 {filename}")

# 示例
movies = [
    {'标题': '肖申克的救赎', '评分': 9.7},
    {'标题': '霸王别姬', '评分': 9.6},
]
save_to_excel(movies, 'movies.xlsx')

4.2 保存为CSV文件

CSV格式更通用,适合大数据量:

def save_to_csv(data, filename):
    """
    将数据保存为CSV文件
    """
    if not data:
        print("没有数据可保存")
        return
    
    df = pd.DataFrame(data)
    df.to_csv(filename, index=False, encoding='utf-8-sig')  # utf-8-sig 支持中文
    print(f"数据已保存到 {filename}")

4.3 保存为JSON文件

JSON格式适合存储嵌套结构的数据:

import json

def save_to_json(data, filename):
    """
    将数据保存为JSON文件
    """
    if not data:
        print("没有数据可保存")
        return
    
    with open(filename, 'w', encoding='utf-8') as f:
        json.dump(data, f, ensure_ascii=False, indent=4)
    print(f"数据已保存到 {filename}")

五、常见问题解析

5.1 问题1:请求被拒绝(403 Forbidden)

原因:豆瓣检测到爬虫行为,封禁了IP或User-Agent。

解决方案

  • 更换User-Agent,使用fake-useragent库生成随机浏览器标识。
  • 使用代理IP轮换。
  • 降低请求频率,增加随机延迟。
  • 使用Session保持会话。

5.2 问题2:动态内容无法获取

原因:页面内容通过JavaScript动态加载,requests无法获取。

解决方案

  • 使用Selenium模拟浏览器操作。
  • 分析AJAX请求,直接调用数据接口(如果存在)。
  • 使用requests-html库(支持JavaScript渲染)。

5.3 问题3:数据提取失败

原因:HTML结构变化或元素定位方式不正确。

解决方案

  • 使用浏览器开发者工具检查最新HTML结构。
  • 使用更灵活的定位方式,如CSS选择器或XPath。
  • 添加异常处理,跳过无法解析的元素。

5.4 问题4:IP被封禁

原因:短时间内请求过于频繁。

解决方案

  • 立即停止请求,等待一段时间后重试。
  • 使用代理IP池轮换。
  • 联系豆瓣官方申请API权限(如果需要大量数据)。

5.5 问题5:中文乱码

原因:文件保存时编码格式不正确。

解决方案

  • CSV文件使用utf-8-sig编码。
  • Excel文件使用openpyxl引擎。
  • JSON文件确保使用ensure_ascii=False

六、最佳实践与法律合规

6.1 遵守robots.txt协议

在爬取任何网站前,应检查其robots.txt文件,了解哪些内容允许爬取。豆瓣电影的robots.txt地址:https://movie.douban.com/robots.txt

6.2 控制请求频率

  • 每个请求之间至少间隔2-5秒。
  • 避免在高峰时段(如晚上8-10点)大量爬取。
  • 设置每日爬取上限,如不超过1000条数据。

6.3 数据使用范围

  • 仅用于个人学习或研究目的。
  • 不得用于商业用途或二次分发。
  • 尊重版权,不侵犯用户隐私。

6.4 使用官方API(如果可用)

豆瓣开放平台提供有限的API接口,虽然功能有限,但合法合规。建议优先考虑使用官方API。

七、总结

本文详细介绍了使用Python批量下载豆瓣电影简介和影评数据的完整流程,包括环境配置、基础爬虫编写、动态内容处理、反爬虫策略、数据存储以及常见问题解决方案。通过这些技巧,你可以高效地获取所需的电影数据,同时避免常见的陷阱和问题。

记住,爬虫技术是一把双刃剑,合理使用可以带来巨大价值,但滥用可能导致法律风险。请始终遵守网站的使用条款,尊重数据版权,做一名负责任的数据使用者。

通过本文的示例代码和详细解析,相信你已经掌握了批量获取豆瓣电影数据的核心技能。在实际应用中,可以根据具体需求调整代码,扩展功能,构建属于自己的电影数据分析系统。# 豆瓣电影简介批量下载技巧与常见问题解析助你高效获取影评数据

引言:为什么需要批量下载豆瓣电影数据?

在当今数据驱动的时代,电影数据分析已成为影视行业研究、市场分析和学术研究的重要组成部分。豆瓣电影作为中国最具影响力的电影评分平台之一,拥有海量的电影信息、用户评分和影评数据。无论是进行电影市场趋势分析、用户偏好研究,还是构建推荐系统,获取这些数据都具有极高的价值。

然而,手动逐个复制粘贴电影信息不仅效率低下,而且容易出错。批量下载技术可以帮助我们自动化地获取大量电影数据,显著提高工作效率。本文将详细介绍如何使用Python编程语言,通过多种方法高效、合规地获取豆瓣电影数据,并解析常见问题及解决方案。

一、准备工作:环境配置与必要库的安装

在开始编写爬虫程序之前,我们需要配置Python开发环境并安装必要的第三方库。这些库将帮助我们发送HTTP请求、解析HTML内容、处理数据以及避免被反爬虫机制拦截。

1.1 安装必要的Python库

打开终端或命令提示符,运行以下命令安装所需库:

pip install requests beautifulsoup4 lxml pandas openpyxl fake-useragent selenium webdriver-manager

各库的作用如下:

  • requests:发送HTTP请求,获取网页内容
  • beautifulsoup4lxml:解析HTML文档,提取所需数据
  • pandas:数据处理和分析,方便将数据导出为Excel或CSV格式
  • openpyxl:支持Excel文件的读写操作
  • fake-useragent:生成随机User-Agent,模拟浏览器访问,降低被封禁风险
  • seleniumwebdriver-manager:用于处理动态加载内容或需要JavaScript渲染的页面

1.2 验证安装

安装完成后,可以通过以下代码验证是否成功安装:

import requests
from bs4 import BeautifulSoup
import pandas as pd
from fake_useragent import UserAgent

print("所有库已成功安装!")

二、基础爬虫:使用Requests和BeautifulSoup获取静态页面

对于大多数静态网页,我们可以使用requests库发送HTTP请求,然后用BeautifulSoup解析HTML内容。豆瓣电影的简介页面通常是静态的,因此这种方法适用于大部分场景。

2.1 获取单个电影的简介信息

以下是一个获取单个电影简介的示例代码:

import requests
from bs4 import BeautifulSoup
from fake_useragent import UserAgent

def get_movie_info(movie_id):
    """
    获取单个电影的简介信息
    :param movie_id: 电影ID,例如 '26794435' 对应《肖申克的救赎》
    :return: 包含电影信息的字典
    """
    # 构造URL
    url = f"https://movie.douban.com/subject/{movie_id}/"
    
    # 生成随机User-Agent
    ua = UserAgent()
    headers = {
        'User-Agent': ua.random,
        'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8',
        'Accept-Language': 'zh-CN,zh;q=0.8,zh-TW;q=0.7,zh-HK;q=0.5,en-US;q=0.3,en;q=0.2',
        'Accept-Encoding': 'gzip, deflate, br',
        'Connection': 'keep-alive',
        'Upgrade-Insecure-Requests': '1',
    }
    
    try:
        # 发送请求
        response = requests.get(url, headers=headers, timeout=10)
        response.raise_for_status()  # 如果状态码不是200,抛出异常
        
        # 解析HTML
        soup = BeautifulSoup(response.text, 'lxml')
        
        # 提取电影标题
        title = soup.find('span', property='v:itemreviewed').text.strip() if soup.find('span', property='v:itemreviewed') else 'N/A'
        
        # 提取导演
        director = soup.find('a', rel='v:directedBy').text.strip() if soup.find('a', rel='v:directedBy') else 'N/A'
        
        # 提取编剧
        screenwriter = []
        screenwriter_tags = soup.find_all('a', class_='attrs')
        for tag in screenwriter_tags:
            if '编剧' in tag.text:
                screenwriter = [a.text for a in tag.find_all('a')]
                break
        
        # 提取主演
        actors = [a.text for a in soup.find_all('a', rel='v:starring')][:5]  # 只取前5个主演
        
        # 提取类型
        genres = [g.text for g in soup.find_all('span', property='v:genre')]
        
        # 提取制片国家/地区
        country_tag = soup.find(text=lambda text: text and '制片国家/地区' in text)
        country = country_tag.next_sibling.strip() if country_tag else 'N/A'
        
        # 提取语言
        language_tag = soup.find(text=lambda text: text and '语言' in text)
        language = language_tag.next_sibling.strip() if language_tag else 'N/A'
        
        # 提取上映日期
        release_date = soup.find('span', property='v:initialReleaseDate').text.strip() if soup.find('span', property='v:initialReleaseDate') else 'N/A'
        
        # 提取片长
        runtime = soup.find('span', property='v:runtime').text.strip() if soup.find('span', property='v:runtime') else 'N/A'
        
        # 提取评分
        rating = soup.find('strong', property='v:average').text.strip() if soup.find('strong', property='v:average') else 'N/A'
        
        # 提取评分人数
        rating_count = soup.find('span', property='v:votes').text.strip() if soup.find('span', property='v:votes') else 'N/A'
        
        # 提取简介
        summary_tag = soup.find('span', property='v:summary')
        summary = summary_tag.text.strip() if summary_tag else 'N/A'
        
        # 返回数据
        return {
            '电影ID': movie_id,
            '标题': title,
            '导演': director,
            '编剧': ', '.join(screenwriter) if screenwriter else 'N/A',
            '主演': ', '.join(actors) if actors else 'N/A',
            '类型': ', '.join(genres) if genres else 'N/A',
            '制片国家/地区': country,
            '语言': language,
            '上映日期': release_date,
            '片长': runtime,
            '评分': rating,
            '评分人数': rating_count,
            '简介': summary
        }
        
    except requests.exceptions.RequestException as e:
        print(f"请求失败: {e}")
        return None
    except Exception as e:
        print(f"解析失败: {e}")
        return None

# 测试获取单个电影信息
if __name__ == '__main__':
    movie_data = get_movie_info('26794435')  # 《肖申克的救赎》
    if movie_data:
        for key, value in movie_data.items():
            print(f"{key}: {value}")

2.2 代码解析与注意事项

  1. User-Agent伪装:使用fake-useragent库生成随机的浏览器User-Agent,避免被豆瓣识别为爬虫。
  2. 异常处理:使用try-except结构捕获网络请求和解析过程中的异常,确保程序稳定性。
  3. 超时设置:设置timeout=10,避免因网络延迟导致程序长时间卡住。
  4. HTML解析:使用lxml解析器,比默认的html.parser更快且容错性更好。
  5. 数据提取:通过findfind_all方法定位特定HTML元素,注意检查元素是否存在,避免AttributeError

2.3 批量获取多个电影信息

有了单个电影的获取函数,我们可以扩展为批量获取:

def batch_get_movies(movie_ids, delay=2):
    """
    批量获取多个电影信息
    :param movie_ids: 电影ID列表
    :param delay: 请求间隔时间(秒)
    :return: 包含所有电影信息的列表
    """
    import time
    movie_data_list = []
    
    for i, movie_id in enumerate(movie_ids):
        print(f"正在获取第 {i+1}/{len(movie_ids)} 个电影: {movie_id}")
        movie_data = get_movie_info(movie_id)
        if movie_data:
            movie_data_list.append(movie_data)
        time.sleep(delay)  # 避免请求过于频繁
    
    return movie_data_list

# 示例:批量获取电影信息
if __name__ == '__main__':
    # 电影ID列表(示例)
    movie_ids = [
        '26794435',  # 肖申克的救赎
        '1292052',   # 霸王别姬
        '1291546',   # 阿甘正传
        '1295124',   # 这个杀手不太冷
        '1292720',   # 辛德勒的名单
    ]
    
    movies = batch_get_movies(movie_ids, delay=2)
    
    # 转换为DataFrame并保存
    if movies:
        df = pd.DataFrame(movies)
        df.to_excel('douban_movies.xlsx', index=False)
        print(f"成功保存 {len(movies)} 条电影数据到 douban_movies.xlsx")

三、进阶技巧:处理动态加载内容与反爬虫机制

豆瓣电影的部分内容(如影评、短评)是动态加载的,需要使用Selenium模拟浏览器操作。此外,豆瓣有严格的反爬虫机制,需要采取更多措施来避免被封禁IP或限制访问。

3.1 使用Selenium获取动态内容

Selenium可以模拟真实浏览器行为,适用于需要JavaScript渲染的页面。以下示例展示如何获取电影的短评数据:

from selenium import webdriver
from selenium.webdriver.common.by import By
from selenium.webdriver.chrome.service import Service
from selenium.webdriver.chrome.options import Options
from webdriver_manager.chrome import ChromeDriverManager
import time
import random

def get_movie_short_reviews(movie_id, max_pages=3):
    """
    使用Selenium获取电影短评数据
    :param movie_id: 电影ID
    :param max_pages: 最大获取页数
    :return: 短评数据列表
    """
    # 配置Chrome选项
    chrome_options = Options()
    chrome_options.add_argument('--headless')  # 无头模式,不显示浏览器窗口
    chrome_options.add_argument('--disable-gpu')
    chrome_options.add_argument('--no-sandbox')
    chrome_options.add_argument('--disable-dev-shm-usage')
    chrome_options.add_argument('--user-agent=Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36')
    
    # 初始化WebDriver
    service = Service(ChromeDriverManager().install())
    driver = webdriver.Chrome(service=service, options=chrome_options)
    
    reviews = []
    
    try:
        for page in range(max_pages):
            url = f"https://movie.douban.com/subject/{movie_id}/comments?start={page*20}&limit=20&sort=new_score&status=P"
            print(f"正在获取第 {page+1} 页短评: {url}")
            
            driver.get(url)
            time.sleep(random.uniform(2, 4))  # 随机等待
            
            # 等待内容加载
            try:
                # 等待评论容器出现
                from selenium.webdriver.support.ui import WebDriverWait
                from selenium.webdriver.support import expected_conditions as EC
                WebDriverWait(driver, 10).until(
                    EC.presence_of_element_located((By.CSS_SELECTOR, ".comment-item"))
                )
            except:
                print(f"第 {page+1} 页加载超时,跳过")
                continue
            
            # 提取评论
            comment_items = driver.find_elements(By.CSS_SELECTOR, ".comment-item")
            
            for item in comment_items:
                try:
                    # 评论者
                    user = item.find_element(By.CSS_SELECTOR, ".comment-info a").text
                    
                    # 评分
                    rating_elem = item.find_element(By.CSS_SELECTOR, ".rating")
                    rating = rating_elem.get_attribute("title") if rating_elem else "N/A"
                    
                    # 评论时间
                    comment_time = item.find_element(By.CSS_SELECTOR, ".comment-time").text
                    
                    # 评论内容
                    content = item.find_element(By.CSS_SELECTOR, ".short").text
                    
                    reviews.append({
                        '用户': user,
                        '评分': rating,
                        '时间': comment_time,
                        '内容': content
                    })
                except Exception as e:
                    print(f"提取评论失败: {e}")
                    continue
            
            # 随机延迟,模拟人类行为
            time.sleep(random.uniform(1, 3))
            
            # 如果没有更多评论,提前退出
            if not comment_items:
                break
    
    finally:
        driver.quit()
    
    return reviews

# 示例:获取《肖申克的救赎》短评
if __name__ == '__main__':
    short_reviews = get_movie_short_reviews('26794435', max_pages=2)
    if short_reviews:
        df = pd.DataFrame(short_reviews)
        df.to_excel('douban_short_reviews.xlsx', index=False)
        print(f"成功保存 {len(short_reviews)} 条短评数据")

3.2 反爬虫应对策略

  1. IP代理池:使用代理IP轮换,避免单一IP被封禁。可以使用免费代理(如free-proxy-list.net)或付费代理服务(如阿布云、快代理)。
import random

def get_proxies():
    """
    获取代理IP列表(示例)
    """
    # 这里可以替换为从代理API获取
    proxies = [
        {'http': 'http://123.45.67.89:8080', 'https': 'https://123.45.67.89:8080'},
        {'http': 'http://98.76.54.32:3128', 'https': 'https://98.76.54.32:3128'},
    ]
    return proxies

def use_proxy_request(url, headers):
    """
    使用代理发送请求
    """
    proxies = get_proxies()
    proxy = random.choice(proxies)
    
    try:
        response = requests.get(url, headers=headers, proxies=proxy, timeout=10)
        return response
    except:
        # 如果代理失败,尝试其他代理或直接请求
        return requests.get(url, headers=headers, timeout=10)
  1. 请求频率控制:严格控制请求间隔,建议设置2-5秒的随机延迟。
  2. Session保持:使用requests.Session()保持会话,模拟真实用户行为。
def use_session_request(url):
    """
    使用Session保持会话
    """
    session = requests.Session()
    ua = UserAgent()
    headers = {
        'User-Agent': ua.random,
        'Referer': 'https://movie.douban.com/',
    }
    session.headers.update(headers)
    
    try:
        response = session.get(url, timeout=10)
        return response
    except Exception as e:
        print(f"请求失败: {e}")
        return None
  1. 识别验证码:如果遇到验证码,可以使用OCR库(如pytesseract)识别,或使用打码平台(如超级鹰、云打码)人工识别。

四、数据存储与处理

获取数据后,需要将其存储为结构化格式,便于后续分析。常见格式包括Excel、CSV和JSON。

4.1 保存为Excel文件

使用pandas可以轻松将数据保存为Excel:

import pandas as pd

def save_to_excel(data, filename):
    """
    将数据保存为Excel文件
    :param data: 数据列表(字典列表)
    :param filename: 文件名
    """
    if not data:
        print("没有数据可保存")
        return
    
    df = pd.DataFrame(data)
    df.to_excel(filename, index=False, engine='openpyxl')
    print(f"数据已保存到 {filename}")

# 示例
movies = [
    {'标题': '肖申克的救赎', '评分': 9.7},
    {'标题': '霸王别姬', '评分': 9.6},
]
save_to_excel(movies, 'movies.xlsx')

4.2 保存为CSV文件

CSV格式更通用,适合大数据量:

def save_to_csv(data, filename):
    """
    将数据保存为CSV文件
    """
    if not data:
        print("没有数据可保存")
        return
    
    df = pd.DataFrame(data)
    df.to_csv(filename, index=False, encoding='utf-8-sig')  # utf-8-sig 支持中文
    print(f"数据已保存到 {filename}")

4.3 保存为JSON文件

JSON格式适合存储嵌套结构的数据:

import json

def save_to_json(data, filename):
    """
    将数据保存为JSON文件
    """
    if not data:
        print("没有数据可保存")
        return
    
    with open(filename, 'w', encoding='utf-8') as f:
        json.dump(data, f, ensure_ascii=False, indent=4)
    print(f"数据已保存到 {filename}")

五、常见问题解析

5.1 问题1:请求被拒绝(403 Forbidden)

原因:豆瓣检测到爬虫行为,封禁了IP或User-Agent。

解决方案

  • 更换User-Agent,使用fake-useragent库生成随机浏览器标识。
  • 使用代理IP轮换。
  • 降低请求频率,增加随机延迟。
  • 使用Session保持会话。

5.2 问题2:动态内容无法获取

原因:页面内容通过JavaScript动态加载,requests无法获取。

解决方案

  • 使用Selenium模拟浏览器操作。
  • 分析AJAX请求,直接调用数据接口(如果存在)。
  • 使用requests-html库(支持JavaScript渲染)。

5.3 问题3:数据提取失败

原因:HTML结构变化或元素定位方式不正确。

解决方案

  • 使用浏览器开发者工具检查最新HTML结构。
  • 使用更灵活的定位方式,如CSS选择器或XPath。
  • 添加异常处理,跳过无法解析的元素。

5.4 问题4:IP被封禁

原因:短时间内请求过于频繁。

解决方案

  • 立即停止请求,等待一段时间后重试。
  • 使用代理IP池轮换。
  • 联系豆瓣官方申请API权限(如果需要大量数据)。

5.5 问题5:中文乱码

原因:文件保存时编码格式不正确。

解决方案

  • CSV文件使用utf-8-sig编码。
  • Excel文件使用openpyxl引擎。
  • JSON文件确保使用ensure_ascii=False

六、最佳实践与法律合规

6.1 遵守robots.txt协议

在爬取任何网站前,应检查其robots.txt文件,了解哪些内容允许爬取。豆瓣电影的robots.txt地址:https://movie.douban.com/robots.txt

6.2 控制请求频率

  • 每个请求之间至少间隔2-5秒。
  • 避免在高峰时段(如晚上8-10点)大量爬取。
  • 设置每日爬取上限,如不超过1000条数据。

6.3 数据使用范围

  • 仅用于个人学习或研究目的。
  • 不得用于商业用途或二次分发。
  • 尊重版权,不侵犯用户隐私。

6.4 使用官方API(如果可用)

豆瓣开放平台提供有限的API接口,虽然功能有限,但合法合规。建议优先考虑使用官方API。

七、总结

本文详细介绍了使用Python批量下载豆瓣电影简介和影评数据的完整流程,包括环境配置、基础爬虫编写、动态内容处理、反爬虫策略、数据存储以及常见问题解决方案。通过这些技巧,你可以高效地获取所需的电影数据,同时避免常见的陷阱和问题。

记住,爬虫技术是一把双刃剑,合理使用可以带来巨大价值,但滥用可能导致法律风险。请始终遵守网站的使用条款,尊重数据版权,做一名负责任的数据使用者。

通过本文的示例代码和详细解析,相信你已经掌握了批量获取豆瓣电影数据的核心技能。在实际应用中,可以根据具体需求调整代码,扩展功能,构建属于自己的电影数据分析系统。