Python 爬虫案例:爬取豆瓣电影 Top250 数据

发布于:2025-09-02 ⋅ 阅读:(15) ⋅ 点赞:(0)

一、案例背景与目标

豆瓣电影 Top250 是国内权威的电影评分榜单之一,包含电影名称、评分、评价人数、导演、主演、上映年份、国家 / 地区、类型等关键信息。本案例将使用 Python 编写爬虫,实现以下目标:

  1. 自动请求豆瓣电影 Top250 的 10 个分页(每页 25 部电影);
  2. 解析页面 HTML 结构,提取每部电影的 8 项核心信息;
  3. 数据清洗(处理异常字符、统一格式);
  4. 将最终数据保存到 Excel 文件,方便后续分析。

二、技术栈选择

本案例使用轻量级且易上手的技术组合,适合爬虫初学者:

工具 / 库 作用
requests 发送 HTTP 请求,获取网页源代码
BeautifulSoup4 解析 HTML 文档,提取目标数据(非结构化→结构化)
pandas 数据清洗、整理,并将数据写入 Excel
time 控制请求间隔,避免触发网站反爬机制
user-agent 伪装浏览器请求头,绕过基础反爬

三、完整代码实现

# 1. 导入所需库
import requests
from bs4 import BeautifulSoup
import pandas as pd
import time

# 2. 定义核心配置
# 豆瓣电影 Top250 分页 URL 规律:start 参数从 0 开始,每次加 25(0、25、50...225)
BASE_URL = "https://movie.douban.com/top250?start={}&filter="
# 伪装浏览器请求头(避免被识别为爬虫)
HEADERS = {
    "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124.0.0.0 Safari/537.36"
}
# 存储所有电影数据的列表
movie_list = []

# 3. 定义页面解析函数(提取单页电影数据)
def parse_movie_page(html):
    # 初始化 BeautifulSoup 解析器(指定 lxml 解析器,效率更高)
    soup = BeautifulSoup(html, "lxml")
    # 定位所有电影项的父容器(每个 li 对应一部电影)
    movie_items = soup.find_all("li", class_="item")
    
    for item in movie_items:
        # 3.1 提取电影名称(默认取第一个中文名称)
        title_tag = item.find("span", class_="title")
        movie_name = title_tag.get_text(strip=True) if title_tag else "未知名称"
        
        # 3.2 提取评分
        rating_tag = item.find("span", class_="rating_num")
        rating = rating_tag.get_text(strip=True) if rating_tag else "0.0"
        
        # 3.3 提取评价人数(处理格式,如 "123.4万人评价" → "1234000")
        comment_tag = item.find("span", text=lambda x: x and "人评价" in x)
        comment_count = comment_tag.get_text(strip=True).replace("人评价", "").replace("万", "0000") if comment_tag else "0"
        
        # 3.4 提取导演和主演(格式:"导演: 张艺谋 主演: 沈腾, 马丽")
        info_tag = item.find("div", class_="bd").find("p", class_="")
        info_text = info_tag.get_text(strip=True).split("\n") if info_tag else ["", ""]
        director_actor = info_text[0].strip() if len(info_text) > 0 else "未知信息"
        
        # 3.5 提取上映年份、国家/地区、类型(格式:"2023 / 中国大陆 / 喜剧, 剧情")
        detail_text = info_text[1].strip() if len(info_text) > 1 else "未知 / 未知 / 未知"
        detail_list = detail_text.split(" / ")
        release_year = detail_list[0].strip() if len(detail_list) > 0 else "未知年份"
        country = detail_list[1].strip() if len(detail_list) > 1 else "未知国家"
        genre = detail_list[2].strip() if len(detail_list) > 2 else "未知类型"
        
        # 3.6 提取电影简介(处理可能的空值)
        quote_tag = item.find("span", class_="inq")
        intro = quote_tag.get_text(strip=True) if quote_tag else "无简介"
        
        # 3.7 将单部电影数据存入字典
        movie_dict = {
            "电影名称": movie_name,
            "评分": rating,
            "评价人数": comment_count,
            "导演&主演": director_actor,
            "上映年份": release_year,
            "国家/地区": country,
            "类型": genre,
            "简介": intro
        }
        movie_list.append(movie_dict)

# 4. 定义主爬虫函数(循环请求所有分页)
def crawl_douban_top250():
    # 循环 10 个分页(start=0,25,...,225)
    for page in range(10):
        start = page * 25
        url = BASE_URL.format(start)
        print(f"正在爬取第 {page+1} 页,URL:{url}")
        
        try:
            # 4.1 发送 GET 请求(添加超时控制,避免无限等待)
            response = requests.get(url, headers=HEADERS, timeout=10)
            # 4.2 检查请求是否成功(状态码 200 表示正常)
            response.raise_for_status()  # 若状态码非 200,抛出 HTTPError 异常
            
            # 4.3 解析当前页面数据
            parse_movie_page(response.text)
            
            # 4.4 控制请求间隔(1-2 秒),避免给服务器造成压力,降低反爬风险
            time.sleep(1.5)
            
        except requests.exceptions.RequestException as e:
            print(f"爬取第 {page+1} 页失败,错误原因:{str(e)}")
            continue  # 跳过失败页面,继续爬取下一页

# 5. 定义数据保存函数(保存到 Excel)
def save_to_excel():
    if not movie_list:
        print("无数据可保存!")
        return
    
    # 5.1 将列表转换为 DataFrame(pandas 数据结构,便于处理)
    df = pd.DataFrame(movie_list)
    
    # 5.2 数据清洗:处理评价人数的数值格式(如 "123.4000" → 1234000)
    df["评价人数"] = pd.to_numeric(df["评价人数"].str.replace(".", ""), errors="coerce").fillna(0).astype(int)
    
    # 5.3 保存到 Excel(index=False 表示不保存行号)
    excel_path = "豆瓣电影Top250数据.xlsx"
    df.to_excel(excel_path, index=False, engine="openpyxl")
    print(f"数据已成功保存到:{excel_path}")
    print(f"共爬取到 {len(movie_list)} 部电影数据")

# 6. 程序入口(执行爬虫流程)
if __name__ == "__main__":
    print("开始爬取豆瓣电影 Top250 数据...")
    crawl_douban_top250()  # 1. 爬取数据
    save_to_excel()        # 2. 保存数据
    print("爬取任务完成!")

四、代码解析(关键步骤拆解)

1. 环境准备(安装依赖库)

在运行代码前,需要确保所有必要的 Python 库都已正确安装。这些库分别承担不同的功能:

  • requests:用于发送 HTTP 请求,获取网页内容
  • beautifulsoup4:用于解析 HTML 文档,提取所需数据
  • pandas:用于数据处理和分析
  • openpyxl:作为 pandas 的依赖,用于写入 Excel 文件
  • lxml:作为 BeautifulSoup 的解析器,提供高效的 HTML 解析能力

安装命令:

pip install requests beautifulsoup4 pandas openpyxl lxml

提示:如果是在国内网络环境,建议使用国内镜像源加速安装,例如:

pip install -i https://pypi.tuna.tsinghua.edu.cn/simple requests beautifulsoup4 pandas openpyxl lxml

2. 反爬机制规避(核心细节)

网络爬虫需要尊重目标网站的规则,同时也要采取适当措施避免被网站识别并封锁。本代码中采用了多种反爬策略:

2.1 伪装请求头
HEADERS = {
    "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124.0.0.0 Safari/537.36"
}

这是最基础也最重要的反爬措施。网站服务器通过User-Agent字段识别访问者身份,默认情况下,requests库的请求头会显示为 "python-requests/xx.x.x",很容易被识别为爬虫。通过设置一个真实的浏览器User-Agent,可以模拟正常用户的浏览器访问。

你可以通过访问https://httpbin.org/get查看自己浏览器的User-Agent并替换。

2.2 控制请求频率
time.sleep(1.5)

这行代码的作用是在爬取完一页后暂停 1.5 秒再继续。短时间内发送大量请求是爬虫最明显的特征之一,通过添加合理的延迟,可以模拟人类浏览网页的行为,降低被网站识别的概率。

延迟时间可以根据实际情况调整,一般建议在 1-3 秒之间。对于反爬严格的网站,可能需要更长的延迟。

2.3 异常处理机制
try:
    # 发送请求的代码
except requests.exceptions.RequestException as e:
    print(f"爬取第 {page+1} 页失败,错误原因:{str(e)}")
    continue

这段异常处理代码可以捕获所有与请求相关的异常,包括网络连接错误、超时、HTTP 错误状态码等。当某一页爬取失败时,程序不会崩溃,而是会打印错误信息并继续爬取下一页,保证了程序的健壮性。

response.raise_for_status()方法会在 HTTP 请求返回错误状态码(4xx 或 5xx)时抛出异常,让我们能够及时发现并处理请求错误。

3. HTML 解析逻辑(如何定位数据)

解析 HTML 是爬虫的核心步骤,需要仔细分析网页结构,找到目标数据所在的位置。我们可以通过浏览器的开发者工具(F12)来查看网页的 HTML 结构。

3.1 分析网页结构

豆瓣电影 Top250 的页面结构具有一定的规律性:

  • 所有电影条目都包含在<ul class="grid_view">标签中
  • 每个电影条目对应一个<li class="item">标签
  • 每个条目中包含了电影的各种信息:名称、评分、评价人数等
3.2 提取电影列表
movie_items = soup.find_all("li", class_="item")

这行代码使用find_all方法查找所有class为 "item" 的li标签,每个标签对应一部电影的信息。返回的movie_items是一个列表,包含了当前页面所有电影的信息。

3.3 提取单个电影信息
3.3.1 提取电影名称
title_tag = item.find("span", class_="title")
movie_name = title_tag.get_text(strip=True) if title_tag else "未知名称"

电影名称位于class为 "title" 的span标签中。get_text(strip=True)方法用于获取标签内的文本内容,并去除前后的空白字符。通过if title_tag else "未知名称"的判断,可以处理标签不存在的情况,避免程序出错。

3.3.2 提取评分
rating_tag = item.find("span", class_="rating_num")
rating = rating_tag.get_text(strip=True) if rating_tag else "0.0"

评分信息位于class为 "rating_num" 的span标签中。同样使用了条件判断来处理可能的缺失情况。

3.3.3 提取评价人数
comment_tag = item.find("span", text=lambda x: x and "人评价" in x)
comment_count = comment_tag.get_text(strip=True).replace("人评价", "").replace("万", "0000") if comment_tag else "0"

评价人数的提取相对复杂一些,因为它没有特定的class名称。这里使用了一个 lambda 函数作为筛选条件,查找文本中包含 "人评价" 的span标签。

提取到文本后,还需要进行处理:

  • 去除 "人评价" 字符串
  • 将 "万" 转换为 "0000",以便后续转换为数字
3.3.4 提取导演和主演
info_tag = item.find("div", class_="bd").find("p", class_="")
info_text = info_tag.get_text(strip=True).split("\n") if info_tag else ["", ""]
director_actor = info_text[0].strip() if len(info_text) > 0 else "未知信息"

导演和主演信息位于class为 "bd" 的div标签下的第一个p标签中。这里的find("p", class_="")表示查找没有class属性的p标签。

获取文本后,使用split("\n")按换行符分割,取第一部分作为导演和主演信息。

3.3.5 提取上映年份、国家 / 地区、类型
detail_text = info_text[1].strip() if len(info_text) > 1 else "未知 / 未知 / 未知"
detail_list = detail_text.split(" / ")
release_year = detail_list[0].strip() if len(detail_list) > 0 else "未知年份"
country = detail_list[1].strip() if len(detail_list) > 1 else "未知国家"
genre = detail_list[2].strip() if len(detail_list) > 2 else "未知类型"

这部分信息位于上一步中分割得到的info_text的第二部分,格式为 "年份 / 国家 / 地区 / 类型"。我们使用split(" / ")按 "/" 分割,得到一个包含三个元素的列表,分别对应年份、国家 / 地区和类型。

每个字段都添加了条件判断,以处理可能的缺失情况,保证程序的稳定性。

3.3.6 提取电影简介
quote_tag = item.find("span", class_="inq")
intro = quote_tag.get_text(strip=True) if quote_tag else "无简介"

电影简介位于class为 "inq" 的span标签中。同样添加了条件判断,处理没有简介的情况。

3.3.7 存储电影数据
movie_dict = {
    "电影名称": movie_name,
    "评分": rating,
    "评价人数": comment_count,
    "导演&主演": director_actor,
    "上映年份": release_year,
    "国家/地区": country,
    "类型": genre,
    "简介": intro
}
movie_list.append(movie_dict)

将提取到的各项信息存入一个字典,然后将字典添加到movie_list列表中。这样处理后,movie_list将包含当前页面所有电影的信息。

4. 数据清洗与保存

4.1 数据转换
df = pd.DataFrame(movie_list)

使用 pandas 库将列表转换为 DataFrame,这是一种二维表格数据结构,便于进行数据处理和分析。

4.2 数据清洗
df["评价人数"] = pd.to_numeric(df["评价人数"].str.replace(".", ""), errors="coerce").fillna(0).astype(int)

这行代码对 "评价人数" 进行清洗和转换:

  • str.replace(".", ""):去除字符串中的小数点,处理 "123.4 万" 这种格式
  • pd.to_numeric(..., errors="coerce"):将字符串转换为数值类型,无法转换的将设为 NaN
  • fillna(0):将 NaN 值替换为 0
  • astype(int):转换为整数类型

经过这些处理,"评价人数" 字段将成为干净的整数,便于后续的统计分析。

4.3 保存到 Excel
excel_path = "豆瓣电影Top250数据.xlsx"
df.to_excel(excel_path, index=False, engine="openpyxl")

使用 pandas 的to_excel方法将数据保存到 Excel 文件:

  • index=False:表示不保存 DataFrame 的索引列
  • engine="openpyxl":指定使用 openpyxl 库作为引擎,支持.xlsx 格式

保存完成后,会打印保存路径和爬取到的电影数量,方便用户确认结果。

5. 主程序流程

if __name__ == "__main__":
    print("开始爬取豆瓣电影 Top250 数据...")
    crawl_douban_top250()  # 1. 爬取数据
    save_to_excel()        # 2. 保存数据
    print("爬取任务完成!")

这是程序的入口点,使用if __name__ == "__main__":确保只有在直接运行该脚本时才会执行以下代码,而在被导入为模块时不会执行。

五、运行结果与验证

  1. 运行代码:执行程序后,控制台会输出爬取进度(如 “正在爬取第 1 页...”);
  2. 生成文件:程序结束后,当前目录会生成 豆瓣电影Top250数据.xlsx 文件;
  3. 验证数据:打开 Excel 文件,可看到 250 行数据(若部分页面爬取失败,行数可能少于 250),列包含 “电影名称”“评分” 等 8 项信息,数据格式统一、无乱码。

六、拓展与注意事项

1. 功能拓展

  • 多线程爬取:使用 threading 或 concurrent.futures 库实现多线程,提升爬取速度(注意控制线程数,避免给服务器造成过大压力);
  • 数据可视化:用 matplotlib 或 seaborn 绘制评分分布直方图、类型占比饼图等;
  • 增量爬取:记录上次爬取的最后一部电影,下次只爬取更新的数据。

2. 注意事项

  • 遵守网站 robots 协议:豆瓣电影的 robots.txthttps://movie.douban.com/robots.txt)允许爬取 Top250 数据,但需控制频率;
  • 反爬升级应对:若出现 “验证码” 或 “403 禁止访问”,可尝试添加 Cookie(模拟登录状态)、使用代理 IP 池;
  • 法律风险:不得将爬取的数据用于商业用途,遵守《网络安全法》《数据安全法》等法律法规。

通过本案例,可掌握爬虫的核心流程(请求→解析→清洗→保存),理解 HTML 结构分析、反爬规避、数据处理的关键技巧,为后续爬取更复杂网站(如动态加载、需要登录的网站)打下基础。


网站公告

今日签到

点亮在社区的每一天
去签到