Python爬虫实战:从豆瓣电影TOP250入门数据采集与分析

📅 2026/7/11 4:38:43
Python爬虫实战:从豆瓣电影TOP250入门数据采集与分析
1. 项目概述从零到一用Python爬取并分析豆瓣电影TOP250如果你刚开始学Python想找一个既有成就感又能串联起多个核心知识点的实战项目豆瓣电影TOP250的爬虫与分析绝对是个经典选择。我当年入门爬虫第一个完整项目就是它。这个项目麻雀虽小五脏俱全从发送网络请求、解析HTML结构到数据清洗、存储再到最后用图表进行可视化分析一套流程走下来你对Python在数据处理上的能力会有一个非常直观的认识。更重要的是它面对的是一个真实、结构清晰的网站你能学到如何与真实的网页结构打交道而不是对着一个教学用的“假”页面。简单来说这个项目就是写一个Python程序自动访问豆瓣电影TOP250的榜单页面https://movie.douban.com/top250把每一部电影的排名、片名、评分、评价人数、导演、简介等信息“抓”下来保存到Excel或CSV文件里。然后我们再用Pandas和Matplotlib这些库对这些数据做点简单的分析比如看看评分分布如何评价人数和评分有什么关系生成几张图表。整个过程你会用到requests、lxml或BeautifulSoup、pandas、matplotlib这几个Python生态里极其重要的库。对于新手这是绝佳的练手机会对于有经验的开发者重温这个项目也能帮你梳理爬虫的最佳实践和数据分析的基本流程。2. 核心思路与工具选型为什么是它们在动手写代码之前我们先得把整个项目的骨架搭起来想清楚每一步要用什么工具以及为什么选它。盲目堆砌代码只会事倍功半。2.1 整体流程设计一个完整的爬虫数据分析项目通常遵循“获取 - 解析 - 存储 - 分析 - 展示”这个流程。针对豆瓣TOP250我们可以这样细化目标分析手动打开豆瓣TOP250页面观察其URL规律和页面结构。你会发现榜单是分页的每页25部电影共10页。URL很有规律https://movie.douban.com/top250?start0filterstart参数从0开始每次增加25。页面结构是标准的HTML我们需要的信息片名、评分等都包裹在特定的HTML标签里。数据获取编写程序循环构造这10个页面的URL然后向豆瓣服务器发送HTTP请求获取每个页面的HTML源代码。数据解析从获取到的HTML源代码中精准地定位并提取出我们需要的电影信息字段。数据存储将提取出来的数据通常是一个列表里面包含250个字典每个字典代表一部电影保存到本地文件方便后续使用。CSV和Excel是最通用、最易读的格式。数据分析与可视化读取存储好的数据进行统计计算如评分分布、平均分等并绘制图表将数据以更直观的形式呈现出来。2.2 关键工具库解析为什么选择下面这些库这是基于它们的特性、社区成熟度和学习曲线综合考量的。requests这是Python中用于发送HTTP请求的“瑞士军刀”其API设计极其人性化几行代码就能完成复杂的网络交互。相比于Python内置的urllibrequests的代码更简洁功能更强大如自动处理连接池、会话、SSL验证等。在这个项目里我们用它来获取网页HTML内容。lxml/BeautifulSoup4 (bs4)这是两个主流的HTML/XML解析库。我们的选择是lxml原因在于它的解析速度极快对于豆瓣这种结构规整的静态页面用XPath可以非常精准、高效地定位元素。BeautifulSoup的语法更接近自然语言对新手更友好但在处理大量数据时速度不如lxml。考虑到这是一个学习项目掌握XPath这项技能对未来处理更复杂的页面也大有裨益。pandas数据分析领域的“王牌”。它提供的DataFrame数据结构你可以理解为增强版的Excel表格是处理表格型数据的利器。我们爬取到的数据天然就是表格形式用pandas来承接、清洗、保存和进行初步统计分析再合适不过。它一行代码就能完成数据读写、分组、聚合等复杂操作。matplotlibPython绘图库的“老祖宗”功能强大可定制性极高。虽然现在有seaborn、plotly等更美观的库但matplotlib是基础理解了它的核心概念Figure, Axes, 绘图函数再学其他库会非常轻松。我们用它将分析结果可视化。注意使用爬虫必须遵守法律法规和网站的robots.txt协议。豆瓣的robots.txt对/top250路径没有明确禁止但我们在编写爬虫时一定要遵守规则控制请求频率避免对服务器造成压力。一个负责任的爬虫应该设置请求间隔例如每请求一个页面后休眠1-2秒并使用合理的User-Agent来标识自己。3. 环境准备与核心代码实现理论说完了我们进入实战环节。我会手把手带你搭建环境并逐行解释核心代码。3.1 搭建Python开发环境如果你还没有Python环境建议直接安装Anaconda。它是一个集成了Python、pandas、matplotlib等数百个数据科学库的发行版并且自带包管理工具conda能极大避免库版本冲突的问题。安装Anaconda访问Anaconda官网下载对应你操作系统Windows/macOS/Linux的安装包按照指引安装即可。创建虚拟环境可选但推荐打开终端Windows叫Anaconda Prompt或CMDmacOS/Linux叫Terminal输入以下命令创建一个名为douban_spider的独立环境。conda create -n douban_spider python3.9激活环境conda activate douban_spider安装必要库在激活的虚拟环境中使用pip安装我们需要的库。pip install requests lxml pandas matplotlib openpyxl这里多装了一个openpyxl因为pandas在读写新版Excel文件.xlsx时需要它作为引擎。环境准备好后我推荐使用VS Code或PyCharm作为代码编辑器。VS Code轻量且插件丰富PyCharm是专业的Python IDE功能更全面。选一个你顺手的就行。3.2 爬虫核心代码逐行解析我们来创建一个名为douban_top250.py的Python文件开始编写代码。第一步导入库和设置全局变量import os import time import requests from lxml import etree import pandas as pd from matplotlib import pyplot as plt # 设置中文字体防止图表中文乱码 plt.rcParams[font.sans-serif] [SimHei, Microsoft YaHei] # 指定默认字体 plt.rcParams[axes.unicode_minus] False # 解决负号‘-’显示为方块的问题 # 定义文件保存路径 EXCEL_FILE douban_top250.xlsx CSV_FILE douban_top250.csv # 定义请求头模拟浏览器访问 HEADERS { User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36 }time用于在请求间添加延时遵守爬虫礼仪。plt.rcParams这是matplotlib的运行时配置设置中文字体是可视化时必做的一步否则图表里的中文会变成方框。HEADERS其中的User-Agent是关键。服务器通过这个字段来判断访问者是谁。如果不设置或使用Python默认的很可能被网站拒绝访问。这里我们伪装成一个Chrome浏览器。第二步编写页面请求与解析函数这是爬虫最核心的部分。我们先写一个辅助函数来安全地获取列表的第一个元素避免因为某些电影信息缺失比如没有简介而导致程序崩溃。def safe_get_first(lst, default): 安全地获取列表的第一个元素如果列表为空或出错返回默认值 try: return lst[0].strip() if lst else default except (IndexError, TypeError): return default接下来是爬取单页数据的函数。我们需要先分析豆瓣TOP250页面的HTML结构。用浏览器打开页面按F12打开开发者工具使用“检查元素”功能找到电影信息的HTML代码。你会发现每部电影的信息都包裹在一个li class“item”.../li标签内。我们需要用XPath定位到所有这些li标签然后对每一个li标签再分别提取内部的详细信息。def scrape_single_page(url, headers, page_num): 爬取单个页面的电影数据 :param url: 页面URL :param headers: 请求头 :param page_num: 页码用于日志输出 :return: 包含当前页面所有电影信息的列表每个元素是一个字典 print(f‘正在爬取第 {page_num} 页...’) movie_list [] # 1. 发送请求 try: resp requests.get(url, headersheaders, timeout10) # 设置10秒超时 resp.raise_for_status() # 如果状态码不是200抛出异常 resp.encoding utf-8 # 明确编码为utf-8 except requests.exceptions.RequestException as e: print(f‘请求第 {page_num} 页失败: {e}’) return movie_list # 2. 解析HTML html etree.HTML(resp.text) # 3. 定位到所有电影项。这个XPath路径需要根据实际页面调整。 # 使用开发者工具复制XPath是一个好方法但要注意其通用性。 movie_items html.xpath(//ol[classgrid_view]/li) if not movie_items: print(f‘第 {page_num} 页未找到电影数据请检查XPath或页面结构是否已变更。’) return movie_list # 4. 遍历每个电影项提取信息 for item in movie_items: # 使用相对路径以 ./ 开头在当前item节点下查找 title safe_get_first(item.xpath(.//span[classtitle][1]/text())) # 电影详情页链接 detail_url safe_get_first(item.xpath(.//div[classhd]/a/href)) # 导演和主演信息在一个文本里需要处理 info safe_get_first(item.xpath(.//div[classbd]/p[1]/text())).strip() # 评分 rating safe_get_first(item.xpath(.//span[classrating_num]/text())) # 评价人数需要去掉“人评价”这几个字 rating_people_raw safe_get_first(item.xpath(.//div[classstar]/span[last()]/text())) rating_people rating_people_raw.replace(‘人评价’, ‘’) if rating_people_raw else ‘0’ # 引言/简介 quote safe_get_first(item.xpath(.//span[classinq]/text())) movie_info { ‘标题’: title, ‘详情页链接’: detail_url, ‘信息’: info, # 这里包含了导演、主演、年份、国家等信息后续可以拆分 ‘评分’: rating, ‘评价人数’: rating_people, ‘引言’: quote } movie_list.append(movie_info) print(f‘ 已抓取: {title} - {rating}分’) # 5. 礼貌性延时避免请求过快 time.sleep(1.5) return movie_listresp.raise_for_status()这是一个好习惯它能帮我们及时发现404、500等错误请求。XPath是核心//ol[class“grid_view”]/li表示查找整个文档中class属性为grid_view的ol元素下的所有li子元素。.开头的XPath表示从当前节点item开始查找这是遍历列表时的标准写法。信息提取我们根据豆瓣页面的实际HTML结构编写了对应的XPath。例如.//span[class“title”][1]/text()获取第一个class“title”的span标签内的文本通常是中文名。[last()]用于获取评价人数因为它总是star类下最后一个span标签。time.sleep(1.5)非常重要每次请求后暂停1.5秒这是一个对目标网站友好的做法能显著降低被封IP的风险。第三步整合爬取流程与数据存储现在我们需要一个函数来组织整个爬取过程并调用pandas保存数据。def scrape_all_pages(): 爬取所有10页数据 all_movies [] base_url ‘https://movie.douban.com/top250’ for i in range(10): # 0到9共10页 start i * 25 url f‘{base_url}?start{start}filter’ page_movies scrape_single_page(url, HEADERS, i1) all_movies.extend(page_movies) print(f‘所有页面爬取完成共获取 {len(all_movies)} 条电影数据。’) return all_movies def save_to_file(movie_data): 将数据保存为Excel和CSV文件 if not movie_data: print(‘没有数据可保存。’) return # 转换为pandas DataFrame df pd.DataFrame(movie_data) # 保存为Excel try: df.to_excel(EXCEL_FILE, indexFalse, engine‘openpyxl’) print(f‘数据已保存至Excel文件: {EXCEL_FILE}’) except Exception as e: print(f‘保存Excel文件失败: {e}’) # 保存为CSV (UTF-8编码避免中文乱码) try: df.to_csv(CSV_FILE, indexFalse, encoding‘utf-8-sig’) # ‘utf-8-sig’能让Excel正确识别编码 print(f‘数据已保存至CSV文件: {CSV_FILE}’) except Exception as e: print(f‘保存CSV文件失败: {e}’) return df第四步数据清洗与初步分析爬取到的原始数据往往比较“脏”比如“信息”字段混杂了导演、主演、年份、国家。我们需要进行清洗将其拆分成独立的列并转换数据类型。def clean_and_analyze_data(df): 数据清洗和初步分析 if df.empty: print(‘DataFrame为空无法进行分析。’) return df # 1. 拆分“信息”列 # 示例格式 “导演: 弗兰克·德拉邦特 Frank Darabont 主演: 蒂姆·罗宾斯 Tim Robbins /... 1994 / 美国 / 犯罪 剧情” def split_info(info_str): # 这是一个简单的基于冒号和换行的拆分实际情况可能更复杂 parts info_str.split(‘\n’) director ‘’ actors ‘’ year_country_genre ‘’ for part in parts: part part.strip() if part.startswith(‘导演:’): director part.replace(‘导演:’, ‘’).strip() elif part.startswith(‘主演:’): actors part.replace(‘主演:’, ‘’).strip() else: # 可能是年份/国家/类型信息 year_country_genre part # 进一步拆分年份/国家/类型 year country genre ‘’ if year_country_genre: # 假设格式是 “1994 / 美国 / 犯罪 剧情” ycg_parts year_country_genre.split(‘ / ’) if len(ycg_parts) 1: year ycg_parts[0].strip() if len(ycg_parts) 2: country ycg_parts[1].strip() if len(ycg_parts) 3: genre ycg_parts[2].strip() return pd.Series([director, actors, year, country, genre]) # 应用拆分函数 df[[‘导演’, ‘主演’, ‘年份’, ‘国家/地区’, ‘类型’]] df[‘信息’].apply(split_info) # 删除原始的、杂糅的“信息”列 df.drop(columns[‘信息’], inplaceTrue) # 2. 转换数据类型 df[‘评分’] pd.to_numeric(df[‘评分’], errors‘coerce’) # 转换为浮点数错误转为NaN df[‘评价人数’] pd.to_numeric(df[‘评价人数’], errors‘coerce’) # 转换为整数 df[‘年份’] pd.to_numeric(df[‘年份’], errors‘coerce’) # 年份转为数值 # 3. 计算一些基本统计量 print(‘\n 数据基本统计 ’) print(f‘电影总数: {len(df)}’) print(f‘平均评分: {df[“评分”].mean():.2f}’) print(f‘评分中位数: {df[“评分”].median():.2f}’) print(f‘总评价人数: {df[“评价人数”].sum():,}’) print(f‘最高评分电影: {df.loc[df[“评分”].idxmax(), “标题”]} ({df[“评分”].max()}分)’) print(f‘评价人数最多电影: {df.loc[df[“评价人数”].idxmax(), “标题”]} ({df[“评价人数”].max():,}人)’) # 按年份统计电影数量 year_counts df[‘年份’].value_counts().sort_index() print(‘\n 电影数量年份分布 (前10) ’) print(year_counts.head(10)) return df第五步数据可视化数据清洗好后我们就可以用matplotlib画图了。这里我们画两个最直观的图评分分布直方图和评价人数与评分的散点图。def visualize_data(df): 数据可视化 if df.empty: return # 设置图表风格可选 plt.style.use(‘seaborn-v0_8-darkgrid’) # 图1评分分布直方图 plt.figure(figsize(12, 5)) plt.subplot(1, 2, 1) # 1行2列第1个位置 # 由于评分是9.0, 9.1, 9.2... 这样的离散值用直方图并设置合适的bins plt.hist(df[‘评分’].dropna(), bins15, edgecolor‘black’, alpha0.7, color‘skyblue’) plt.xlabel(‘评分’) plt.ylabel(‘电影数量’) plt.title(‘豆瓣TOP250电影评分分布’) plt.grid(True, linestyle‘--’, alpha0.5) # 图2评价人数 vs 评分 散点图带趋势线 plt.subplot(1, 2, 2) plt.scatter(df[‘评分’], df[‘评价人数’], alpha0.6, c‘coral’, edgecolors‘w’, linewidth0.5) # 计算并绘制趋势线使用numpy的polyfit import numpy as np # 移除NaN值 valid_data df[[‘评分’, ‘评价人数’]].dropna() if len(valid_data) 1: x valid_data[‘评分’] y valid_data[‘评价人数’] z np.polyfit(x, y, 1) # 1次多项式拟合 p np.poly1d(z) plt.plot(x, p(x), “r--”, alpha0.8, labelf‘趋势线: y{z[0]:.0f}x{z[1]:.0f}’) plt.legend() plt.xlabel(‘评分’) plt.ylabel(‘评价人数’) plt.title(‘评价人数与评分关系’) plt.grid(True, linestyle‘--’, alpha0.5) plt.tight_layout() # 自动调整子图间距 plt.savefig(‘douban_top250_analysis.png’, dpi300, bbox_inches‘tight’) # 保存高清图片 plt.show() # 图3电影类型词云如果‘类型’字段拆分得好 # 这里需要安装wordcloud库: pip install wordcloud try: from wordcloud import WordCloud # 将所有类型字符串合并 all_genres ‘ ‘.join(df[‘类型’].dropna().astype(str)) # 将空格分隔的多个类型拆开 genre_list all_genres.replace(‘ ‘, ‘ ’).split() genre_text ‘ ‘.join(genre_list) if genre_text: wordcloud WordCloud(width800, height400, background_color‘white’, font_path‘C:/Windows/Fonts/simhei.ttf’, # Windows系统黑体路径 collocationsFalse).generate(genre_text) plt.figure(figsize(10, 5)) plt.imshow(wordcloud, interpolation‘bilinear’) plt.axis(‘off’) plt.title(‘豆瓣TOP250电影类型词云’) plt.tight_layout() plt.savefig(‘douban_top250_genre_wordcloud.png’, dpi300, bbox_inches‘tight’) plt.show() except ImportError: print(‘未安装wordcloud库跳过词云图生成。请运行 pip install wordcloud 安装。’) except Exception as e: print(f‘生成词云图时出错: {e}’)第六步主函数最后我们把所有功能串联起来。def main(): print(‘开始豆瓣电影TOP250数据抓取与分析项目...’) # 检查文件是否已存在避免重复爬取 if os.path.exists(CSV_FILE): print(f‘检测到已存在数据文件 {CSV_FILE}将直接加载进行分析。’) df pd.read_csv(CSV_FILE, encoding‘utf-8-sig’) else: print(‘未找到本地数据文件开始爬取数据...’) movies scrape_all_pages() df save_to_file(movies) if df is not None and not df.empty: df_cleaned clean_and_analyze_data(df) visualize_data(df_cleaned) print(‘\n项目执行完毕’) print(f‘数据文件: {CSV_FILE}, {EXCEL_FILE}’) print(f‘图表文件: douban_top250_analysis.png, douban_top250_genre_wordcloud.png’) else: print(‘未能获取或加载有效数据项目终止。’) if __name__ ‘__main__’: main()4. 实战中的常见问题与高级技巧代码跑起来可能不会一帆风顺下面是我在多次爬取豆瓣和其他网站时总结的一些“坑”和应对技巧。4.1 爬虫被屏蔽或返回异常数据这是新手最常遇到的问题。症状可能是返回空数据、返回403状态码或者返回一个要求登录或验证的页面。问题根源网站的反爬虫机制检测到了你的请求不像正常浏览器。解决方案完善请求头除了User-Agent有时还需要添加Referer来源页、Accept-Language接受语言等字段。用浏览器开发者工具的Network面板查看一次正常请求的Headers模仿着写。使用会话requests.Session()可以保持一个会话自动处理cookies让多次请求看起来更像同一个用户。session requests.Session() session.headers.update(HEADERS) resp session.get(url, timeout10)添加随机延时固定的time.sleep(2)模式也可能被识别。可以使用random.uniform(1, 3)来生成1到3秒之间的随机延时。使用代理IP如果单个IP请求过于频繁被暂时封禁可以考虑使用代理IP池。但这对于豆瓣TOP250这种小规模、低频的爬取通常不是必须的且涉及额外成本和复杂度。终极方案模拟浏览器对于JavaScript动态渲染的页面豆瓣TOP250是静态页面不需要上述方法可能无效。这时需要使用Selenium或Playwright这类自动化测试工具来驱动真实浏览器获取渲染后的页面源码。4.2 数据解析失败XPath失效今天能跑的代码明天可能就报错了因为网站改版了。问题根源网页的HTML结构发生了变化你写的XPath路径找不到对应的元素了。解决方案使用更健壮的XPath避免使用包含绝对位置如div[3]/div[5]/span或易变class的XPath。尽量使用具有唯一性的id、name属性或者相对稳定的结构特征。多重定位策略可以准备几套备用的XPath如果第一套失效尝试第二套。正则表达式辅助对于某些混杂在文本中的信息XPath提取后可能还需要用正则表达式re模块进行二次清洗。定期检查与更新将核心的XPath字符串作为配置项放在代码开头一旦失效只需修改配置无需改动核心逻辑。4.3 数据清洗的复杂性我们之前写的split_info函数是一个简化版。实际中“信息”字段的格式可能五花八门比如有些电影没有“主演”信息有些国家的表示方式不同。问题根源网页数据的非结构化或半结构化特性。解决方案编写更健壮的解析函数使用更多的try...except块对可能缺失的部分提供默认值。分步清洗先提取出大致结构再用字符串方法.split(),.find(),.strip()和正则表达式进行精细处理。人工抽样检查爬取完成后一定要随机抽查一部分数据特别是第一条、最后一条和中间几条确保解析逻辑覆盖了各种边缘情况。4.4 编码与存储问题保存的CSV文件用Excel打开是乱码或者中文字符显示异常。问题根源编码不一致。Windows系统下的Excel默认可能不是UTF-8编码。解决方案使用df.to_csv(‘file.csv’, indexFalse, encoding‘utf-8-sig’)。utf-8-sig会在文件开头添加一个BOM字节顺序标记帮助Excel正确识别UTF-8编码。或者保存为Excel文件.xlsx可以完全避免编码问题因为Excel原生支持。4.5 可视化图表的美化与解读生成的图表太丑或者不知道怎么看。技巧使用样式plt.style.use(‘seaborn-v0_8-darkgrid’)可以让图表瞬间变得专业好看。matplotlib内置多种样式如‘ggplot’,‘fivethirtyeight’。调整细节figsize调整图幅大小dpi调整输出分辨率bbox_inches‘tight’在保存时自动裁剪白边。添加注释在关键数据点如最高分电影上用plt.annotate()添加文字说明让图表更易读。解读图表以我们的散点图为例如果趋势线是略微向上的说明在TOP250这个高分区间内评分和评价人数存在微弱的正相关评分越高可能评价人数也越多。但这不构成因果关系只是一个观察现象。5. 项目扩展与进阶思考完成基础版本后你可以尝试以下扩展让这个项目更具挑战性和实用性异步爬取使用aiohttp和asyncio库进行异步请求可以同时发起多个页面请求将爬取10页数据的时间从十几秒缩短到两三秒。这是提升爬虫效率的必备技能。深入详情页我们只爬取了列表页的基本信息。你可以进一步爬取每个电影的详情页detail_url获取更丰富的数据如片长、IMDb链接、更多剧照链接、短评/长评、获奖情况等。这涉及到更复杂的页面解析和可能的分页处理。构建数据管道将数据存储到数据库中如SQLite, MySQL, MongoDB而不是简单的文件。然后可以定期运行爬虫更新数据构建一个小的电影资料库。更复杂的分析导演分析统计哪些导演上榜电影最多他们的平均评分如何。国家/地区分析看看TOP250中各国电影的数量和评分分布。时间序列分析电影年份与评分、评价人数的关系是否存在“经典老片评分更高”的现象文本分析对电影的“引言”字段进行情感分析或关键词提取。制作交互式仪表盘使用Plotly Dash或Streamlit库将你的分析和图表整合成一个Web应用可以动态筛选年份、国家、类型查看不同维度的数据。这个项目虽然基础但它像一把钥匙打开了Python数据获取与处理的大门。爬虫的核心思维——分析请求、解析结构、处理异常、尊重规则——在任何数据采集场景下都是通用的。而pandas和matplotlib的组合则是你进行任何数据分析工作的起点。希望你在动手实现的过程中不仅能收获代码运行的成就感更能理解数据从源头到洞见的完整旅程。如果在操作中遇到任何问题回顾代码、查阅文档、善用搜索引擎和开发者工具你总能找到答案。