> 技术文档 > Python 爬虫案例:爬取豆瓣电影 Top250 数据

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


一、案例背景与目标

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

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

二、技术栈选择

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

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

三、完整代码实现

# 1. 导入所需库import requestsfrom bs4 import BeautifulSoupimport pandas as pdimport 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 的页面结构具有一定的规律性:

  • 所有电影条目都包含在
      标签中
    • 每个电影条目对应一个
    • 标签
    • 每个条目中包含了电影的各种信息:名称、评分、评价人数等
    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.txt(https://movie.douban.com/robots.txt)允许爬取 Top250 数据,但需控制频率;
    • 反爬升级应对:若出现 “验证码” 或 “403 禁止访问”,可尝试添加 Cookie(模拟登录状态)、使用代理 IP 池;
    • 法律风险:不得将爬取的数据用于商业用途,遵守《网络安全法》《数据安全法》等法律法规。

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