资讯详情

豆瓣电影TOP250数据抓取与可视化分析实战

📅 2026/9/14 22:27:34 | 华诺云谱 👁 阅读
豆瓣电影TOP250数据抓取与可视化分析实战
爬虫抓豆瓣、做数据分析、再画可视化图表——这个组合几乎是每个Python学习者都会碰到的项目。可大多数人抓到数据、画了一两张图就卡住了要么不知道怎么继续深入要么做出来的东西根本看不出分析的价值。这篇文章我把自己的豆瓣电影数据抓取与可视化项目完整拆开讲从需求分析、抓取策略、字段清洗到多图组合看板用一套能直接复用的思路带你走完整个过程适合正在学[数据抓取]、[数据分析]和[可视化]的同学参考也适合想找一个能写进简历的完整Python项目的朋友对照实现。1. 动手前先定需求这个豆瓣分析系统到底要解决什么问题1.1 为什么选豆瓣电影TOP250做数据源很多教程选数据源时只看能不能爬到忽略了数据本身的分析价值。豆瓣电影TOP250这个榜单有个特别好的地方它是经过了大量用户评分后筛选出的高分电影样本量不大不小——250部既能支撑起一些有统计意义的分析又不会大到让新手无从下手。更重要的一点是这些数据天然自带多维度的可分析属性评分、评价人数、导演、主演、年份、国家地区、类型、经典台词。你仔细看一下这几乎就是一个小型的结构化数据集涵盖了数值型字段、分类型字段和时间型字段恰好能把数据分析里最核心的几类操作全串起来。还有一点就是页面结构足够稳定。豆瓣TOP250从诞生到现在DOM结构虽然有小改动但整体非常规整非常适合用来讲解数据抓取。相比那些动态渲染、接口加密的网站豆瓣公开列表页对新手友好太多拿来当教学样本再合适不过。提示任何爬虫操作都要遵守目标网站的robots协议和相关法律法规我将抓取频率控制在较低水平数据仅用于学习分析请勿对线上服务造成压力更不要用于商业用途。1.2 拆解分析需求从画图变成回答问题这是我认为整个项目里最重要的一步。大多数半途而废的项目问题都出在需求模糊上——只想着画几张好看的图但图要回答什么问题完全没想清楚。我在这版项目里给自己定了5个明确的问题后面的每张图都对应一个问题豆瓣高分电影的评分分布呈现什么特征高分和低分之间差距有多大高分电影集中在哪个年代这几十年来评分走势有怎样的变化哪些国家/地区产出的高分电影最多高分电影中哪些类型出现频率最高评价人数和评分之间有没有关系口碑是否等于热度有了这些问题抓取哪些字段、洗哪些数据、画什么图就自然确定了。这比反过来先抓数据再看能画什么图效率高得多。1.3 技术选型为什么是 requests pandas pyecharts选型这件事我一直坚持一个原则够用就好别炫技。抓取层很多人一上来就上Scrapy。但对这个项目来说250条数据分10页requests加BeautifulSoup就能轻松搞定完全不需要引入爬虫框架的调度、管道、中间件这些重量级概念。Scrapy是生产环境的利器却不是教学项目的最优解它会把初学者的注意力从解析页面转移到框架配置上。清洗层选pandas这个没有悬念。它天生就是干这活的分组聚合、缺失值处理、类型转换几行代码搞定。可视化层我选了pyecharts而不是matplotlib。原因有三个一是生成的是HTML页面交互体验好鼠标悬停能看数据二是图表类型丰富柱状图、饼图、折线图、词云都有现成接口三是渲染为HTML文件后可以直接用浏览器打开方便分享不需要像matplotlib那样去调字体、调布局。2. 数据抓取的关键实现从HTML到结构化数据2.1 先看页面结构再写抓取代码我见过太多人拿到项目第一件事就是写爬虫代码结果写一半发现字段提取不到又回头改。正确做法是先打开页面用浏览器开发者工具看清楚数据结构。豆瓣TOP250的列表页URL规律非常直观https://movie.douban.com/top250?start0filter https://movie.douban.com/top250?start25filter https://movie.douban.com/top250?start50filter每页25条start参数跨度为25。页面里每部电影都在ol.grid_view下的li.item节点中关键字段标题在span.title节点注意英文别名也是span.title需要取第一个评分在span.rating_num评价人数在div.star里的最后一个span导演、主演、年份、国家地区、类型都集中在一个没有class的p标签里一句话短评在p.quote里可能不存在把结构弄清楚之后再写代码基本就是一次通过。2.2 分页抓取与字段提取的完整代码这里直接给出我项目里的抓取核心代码注释标得很清楚有基础的同学可以直接跑import time import csv import re import requests from bs4 import BeautifulSoup 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 } def fetch_page(start): url fhttps://movie.douban.com/top250?start{start}filter resp requests.get(url, headersHEADERS, timeout10) resp.raise_for_status() resp.encoding utf-8 return BeautifulSoup(resp.text, html.parser) def parse_movie_items(soup): movies [] items soup.select(ol.grid_view li.item) for item in items: # 标题取第一个 span.title title_tag item.select_one(span.title) title title_tag.text.strip() if title_tag else # 评分 rating_tag item.select_one(span.rating_num) rating rating_tag.text.strip() if rating_tag else try: rating float(rating) except ValueError: rating None # 评价人数div.star 下最后一个 span如 1372303人评价 star_spans item.select(div.star span) people_text star_spans[-1].text if star_spans else people re.sub(r\D, , people_text) # 去掉非数字字符 people int(people) if people else 0 # 导演/主演/年份/地区/类型集中在 bd 下的第一个 p bd item.select_one(div.bd) info_p bd.find(p, class_False) if bd else None info_text info_p.get_text( , stripTrue) if info_p else # 用正则拆出导演简化处理只取第一位 director_match re.search(r导演:\s*([^/]), info_text) director director_match.group(1).strip() if director_match else # 短评 quote_tag item.select_one(p.quote span) quote quote_tag.text.strip() if quote_tag else movies.append({ title: title, rating: rating, people: people, director: director, info: info_text, quote: quote, }) return movies def main(): all_data [] for start in range(0, 250, 25): soup fetch_page(start) all_data.extend(parse_movie_items(soup)) print(f已抓取 {start 25} / 250 条) time.sleep(1.5) # 控制访问节奏避免对服务器造成压力 with open(douban_top250.csv, w, newline, encodingutf-8-sig) as f: writer csv.DictWriter(f, fieldnames[ title, rating, people, director, info, quote ]) writer.writeheader() writer.writerows(all_data) print(f完成共 {len(all_data)} 条) if __name__ __main__: main()这段代码有一个值得注意的设计我没有在抓取阶段就强行把所有字段拆干净而是把包含年份、地区、类型的原始字符串info先原样存下来具体拆分放到清洗阶段做。这样做的原因是原始信息里各种分隔符并不完全统一一次性拆字段容易漏数据先保存再拆分更稳妥。2.3 抓取节奏和请求头怎么做到稳定不封老爬虫最忌讳的就是一股脑地高频请求。豆瓣对异常访问的检测很敏感我遇到过抓几页后服务暂时不可用的情况。几个我实测有效的经验必须设置完整的User-Agent不能挂默认的python-requests每页之间time.sleep(1.5)这个节奏对250条数据整体耗时不到15秒完全够用不并发、不重试过猛出错就先停下来检查抓取的结果一次性落盘不要反复访问页面用比较克制的方式抓公开页面基本不会遇到问题。但还是要提醒一句如果只是做分析抓一次存成本地文件就足够了没有必要反复抓。2.4 落盘为什么用CSV而不是数据库这一步我特意解释一下。很多人做数据分析项目时习惯把数据先放进MySQL或SQLite但对这个项目的体量来说完全没有必要。CSV的优势很直白文本文件任何编辑器能打开pandas一个read_csv就能读进来出了问题肉眼可见。SQLite虽然也很好但对250条数据而言属于杀鸡用牛刀而且初学者调试数据库连接消耗的时间远大于它带来的收益。要注意的是写CSV时我用的是encodingutf-8-sig这个细节很关键。utf-8-sig会在文件开头加上BOM头用Excel打开中文时不会乱码而标准utf-8编码在Windows的Excel里经常显示乱码。这个坑后面还会在中文处理里遇到。3. 数据清洗与特征工程图表好不好看全靠这一步3.1 把混合类型字段变成可分析的数值爬下来的数据不是拿来就能分析的。rating字段虽然是浮点数但偶尔会出现空值或异常字符串people字段虽然我在抓取时做了提取但还是建议在清洗阶段再验证一遍。read_csv读进来后第一件事就是看数据类型和缺失值import pandas as pd df pd.read_csv(douban_top250.csv) print(df.info()) print(df.isna().sum())重点检查几个字段rating必须是float如果有无法转换的值要检查来源people必须是int它在CSV里可能被读成float如果存在空值的话需要决定是填充还是删除info里要能正常提取年份、地区、类型清洗代码大概长这样df[people] pd.to_numeric(df[people], errorscoerce).fillna(0).astype(int) df[rating] pd.to_numeric(df[rating], errorscoerce)errorscoerce的意思是遇到解析不了的值变成NaN先保留位置再统一处理。这样比直接抛异常要好至少能知道脏数据分布在哪些区块。3.2 年份缺失与未上映的处理策略info字段是导演 / 主演 / 年份 / 国家 / 类型的大杂烩典型的值长这样导演: 弗兰克·德拉邦特 主演: 蒂姆·罗宾斯 / 摩根·弗里曼 1994 / 美国 / 犯罪 剧情年份提取我用正则从info里抽四位数字df[year] df[info].str.extract(r(\d{4})).astype(float)这里有个特殊情况某些条目年份可能缺失或者不是纯四位数字。我在实际抓取中遇到的概率不高但清洗时仍然要处理。策略是年份缺失的样本不下滑直接用未知标记或者在按年份分析时过滤掉这些行。250条样本里缺一两条不影响整体趋势结论强行填充反而会引入错误信息。地区字段的拆分我放在后面的可视化部分讲因为那里的处理方式跟具体图表直接相关。3.3 生成星级、年份区间等新特征原始字段足够分析但为了图表更直观我会额外生成几个衍生特征。星级字段按豆瓣评分惯例9分以上算力荐8到9分算优秀7到8分算还行。我用cut函数做区间分箱bins [0, 7, 8, 9, 10] labels [7分以下, 7-8分, 8-9分, 9分以上] df[star_level] pd.cut(df[rating], binsbins, labelslabels, rightFalse)年份区间特征把年份按年代分桶能在折线图趋势之外再补一个各年代数量对比的视角df[era] (df[year] // 10 * 10).astype(Int64).astype(string) 年代 df.loc[df[year].isna(), era] 未知年代这些衍生字段看起来简单但它们的价值在画图时才会完全体现出来——分组聚合的粒度一下子就从每一年变成了每个年代图表更清爽、趋势更明显。3.4 数据质量检查清单清洗完成后我会花30秒做一轮快速校验确认数据没被洗坏len(df)等于250没丢行rating的取值在0到10之间没出现离谱数值year的最小值大于1900最大值小于当前年份people没有负数最大的评价人数在合理范围内这一步虽然简单但能帮你过滤掉后面分析阶段一大半的异常结果。很多跑出来图很奇怪的问题回头一查都是这几个基础校验没过。4. 可视化多图分析一张图回答一个业务问题4.1 评分分布直方图理解豆瓣评分的脾气分析的第一步永远是看分布。评分分布直方图能最快地回答高分电影到底集中在什么区间。我用pyecharts画直方图时处理方式和matplotlib不太一样。pyecharts的Bar本身不直接提供直方图分箱需要先用pandas把数据分箱计数再喂给柱状图import pandas as pd from pyecharts.charts import Bar from pyecharts import options as opts bins [0, 7.0, 7.5, 8.0, 8.5, 9.0, 9.5, 10.0] labels [7以下, 7-7.5, 7.5-8, 8-8.5, 8.5-9, 9-9.5, 9.5以上] df[rating_bin] pd.cut(df[rating], binsbins, labelslabels, rightFalse) rating_counts df[rating_bin].value_counts().sort_index() bar Bar() bar.add_xaxis(rating_counts.index.tolist()) bar.add_yaxis(电影数量, rating_counts.tolist()) bar.set_global_opts( title_optsopts.TitleOpts(title豆瓣TOP250评分分布), xaxis_optsopts.AxisOpts(name评分区间), yaxis_optsopts.AxisOpts(name电影数量), ) bar.render(output/rating_dist.html)跑出来的结果通常会显示TOP250里8.5分以上的电影占了很大比例这很正常——因为这个榜单本身就是高分片精选。这个图的真正价值是帮你建立豆瓣评分尺度的直觉8分是优秀线9分以上就是神片级别。4.2 年份与评分的趋势散点/折线第二个问题高分电影集中在哪个年代这几十年来评分走势怎样。先把数据按年份分组聚合算每年的平均评分和电影数量。由于单年电影数可能很少平均值波动会比较大建议再加一个滚动平均trend df.groupby(year).agg( avg_rating(rating, mean), movie_count(rating, count) ).reset_index() trend[avg_rating_smooth] trend[avg_rating].rolling(5, min_periods1).mean()这个平滑处理非常关键。如果不做折线图会像心电图一样大起大落压根看不出趋势平滑之后才能看到90年代前后是高分片的高产期。画图时间用折线图X轴是年份Y轴是平均分再加上电影数量的柱状图作为对比轴一图讲两个维度。需要说明的是用python等编程工具做这类分析要基于自己合法获取的数据进行大家在实践时也应以学习研究为目的、在合规前提下操作。4.3 国家与地区的条形图地区字段隐藏在info里的年份 / 国家 / 类型段落中例如1994 / 美国 / 犯罪 剧情我的拆分思路是先用正则把/分隔的三个部分取出来取中间那段作为地区。但注意有些电影是合拍片地区会是美国 / 中国大陆所以我在拆分后还会再split一次把多个地区摊开统计def extract_country(info_text): parts info_text.split(/) if len(parts) 2: return parts[-2].strip() return 未知 df[country_raw] df[info].apply(extract_country) df_country df[country_raw].str.split( / ).explode().str.strip() country_counts df_country.value_counts().head(15)explode()是个很实用的pandas操作能把一行里多个值拆成多行单值做多分类统计时就靠它。画条形图时注意标题比较惯例的做法是横向条形图国家在Y轴数量在X轴观感更舒服。pyecharts里设置yaxis_opts做横向即可不再赘述代码逻辑和柱状图完全一样。4.4 电影类型词云与组合看板类型字段的拆分逻辑和地区同理但类型可以画词云视觉效果更直观。pyecharts的WordCloud接收词频列表from pyecharts.charts import WordCloud type_series df[info].apply( lambda x: x.split(/)[-1].strip() if / in x else ) type_freq type_series.str.split().explode().str.strip().value_counts() wc WordCloud() wc.add(, type_freq.items(), word_size_range[20, 80], shapecircle) wc.set_global_opts(title_optsopts.TitleOpts(title豆瓣TOP250电影类型词云)) wc.render(output/type_wordcloud.html)到这里系统已经能产出至少五张图评分分布、年份趋势、国家Top榜、类型词云、评分与评价人数关系。为了阅读和交付方便我用pyecharts的Page把多张图组合成一个HTML大看板一次打开全部分析结果from pyecharts.charts import Page page Page(layoutPage.SimplePageLayout) page.add(bar, trend_chart, country_bar, type_pie, wc) page.render(豆瓣电影可视化看板.html)有了这个组合看板整个系统才真正称得上可视化系统而不是散落的几张图。交付给别人看时一个HTML文件就够了。5. 执行过程中的高频坑位与排查思路5.1 抓下来的文件名出现乱码我第一次用Windows跑这个项目时输出的CSV用记事本打开好好的用Excel打开中文全乱。问题出在编码格式上我最初写文件用的是encodingutf-8而Windows Excel默认用GBK解码。解决办法就是前面提到的encodingutf-8-sig。加了BOM之后Excel能正确识别UTF-8编码乱码直接消失。这个坑其实和爬虫无关属于Windows环境的经典问题但遇到一次就能记住。5.2 评分人数出现1,234,567导致的类型报错pandas读CSV时如果people字段原样存了带千位分隔符的字符串比如1,234,567直接astype(int)会报错。我在抓取阶段已经用re.sub(r\D, , people_text)把非数字字符过滤了所以我的CSV里没有这个问题。但如果你拿的是别人抓好的数据或者想复用其他人的源码一定要在清洗阶段补这一手df[people] df[people].astype(str).str.replace(,, , regexFalse) df[people] pd.to_numeric(df[people], errorscoerce)这种输入数据格式不可控是数据清洗最常见的问题处理思路永远只有一个先看数据长什么样再做转换不要无条件信任字段类型。5.3 一会儿能抓一会儿不能抓我在测试抓取逻辑时为了省时间把sleep时间从1.5秒改成了0.2秒结果没跑几页就触发了访问限制页面返回的不是TOP250列表而是验证页。这个问题的排查链路很典型先看返回状态码200不代表正常要检查页面内容里有没有预期的节点打印响应文本的前500个字符和正常页面对比确认是不是请求频率太高把sleep恢复到合理值过几分钟再跑我后来在代码里加了一个简单的内容校验通过判断页面中是否存在grid_view来决定这次请求是否有效无效就抛异常停止避免在错误响应下继续抓取产生垃圾数据。5.4 pyecharts图表显示空白pyecharts渲染出来的HTML文件在浏览器打开时一片空白这也是新手高频问题。原因通常是两点在Jupyter Notebook里用render_notebook和在脚本里用render结果被搞混pyecharts生成的HTML依赖加载ECharts的JavaScript文件如果页面文件被移动了位置或者用浏览器直接打开时安全策略禁止加载本地JS图表就会白屏我最常用的方案是直接render()成HTML文件后用浏览器打开而且不要在文件渲染后再次移动位置。如果你需要给别人发一个离线可看的报告建议把pyecharts换成离线模式具体做法是下载ECharts的JS文件放到项目本地并在Page初始化时指定js_host参数这里就不展开源码了遇到的同学可以查一下pyecharts的离线配置说明。5.5 HTML报告路径与静态资源问题最后提一个交付层面的坑。如果你把生成的HTML文件发给别人或者拷到别的电脑对方打开时可能图表显示不出来但同一个文件在自己电脑上是好的。这十有八九是JS资源依赖路径的问题——HTML内部引用的是绝对路径或CDN在断网环境或资源没同步的情况下就会失效。我这版项目的处理方式是所有图表统一输出到output目录组合看板放在项目根目录资源路径统一使用相对路径。这样做虽然土但足够可靠换机器、发压缩包都不会出问题。做这个豆瓣电影数据分析可视化项目最有价值的地方不在于你抓了多漂亮的页面、画了多少张图而在于你完整走了一遍从问题出发到数据落地再到图表回答的流程。我后来做其他行业的数据分析项目发现思路完全是一脉相承的先定义清楚问题再决定要什么数据最后才考虑用哪张图表达。这套流程跑通一次后面遇到任何数据源都能迁移过去。源码里的结构和细节如果你照着从头敲一遍收获会比我在这里贴出的任何代码片段都大。
📝

华诺云谱内容团队

资深建站顾问 · 行业研究员

10年+企业数字化服务经验,专注智能建站、SEO优化与品牌营销,持续输出建站技巧、行业洞察与营销干货,已帮助5000+企业实现数字化增长。

你可能需要的服务

订阅华诺云谱资讯周报

每周一封,精选建站技巧、SEO与营销干货,直达邮箱。已有 8,000+ 企业主订阅,助你少走弯路。