ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

豆瓣电影Top250数据可视化:Python爬虫到分析全流程实战

豆瓣电影Top250数据可视化:Python爬虫到分析全流程实战 豆瓣电影的数据被人玩出花来已经不是一两天的事了但真正自己动手从爬虫到可视化做一遍和看别人分析结果是完全两回事。去年我因为想研究“高分电影到底有什么共性”顺手写了个基于Python的豆瓣电影数据可视化分析项目从爬取Top250榜单开始到数据清洗、指标分析最后用图表把规律呈现出来。这个项目做完之后我最大的感受是技术上其实没有多高深的东西但把整个链路走通特别是把数据从页面上的零散文本变成一张张能讲故事的图表中间每一步都有不少细节坑。这篇文章就完整复盘一下整个项目的设计与实现过程把核心代码、参数选择逻辑、以及我踩过的坑和解决办法都摊开来讲。适用的人群我觉得有三类刚开始学爬虫想找一个完整练手项目的人对数据可视化感兴趣但不知道从哪下手的朋友以及想通过电影数据这个熟悉的场景理解数据分析全流程的同学。项目用到的技术栈也相对基础——Python、requests、BeautifulSoup、pandas、pyecharts都是入门阶段绕不开的工具。1. 项目概述与整体设计思路1.1 豆瓣电影数据为什么适合做分析选豆瓣电影作为数据源不是因为它的数据多难爬恰恰相反是因为它的数据质量高、结构规整、且带有大量文本属性。Top250榜单页面每页25条电影每条都包含电影名称、导演、主演、年份、地区、类型、评分、评价人数、经典台词等字段。这些字段组合起来就能回答很多有意思的问题比如“哪个年代的电影评分普遍更高”“哪种类型的电影最容易进榜单”“评价人数和评分之间有没有关系”“导演的产出频次和口碑是否存在规律”。这个项目要是换成爬电商评论或者微博热帖数据清洗的复杂度会直接翻好几倍反而不适合做分析。豆瓣数据的字段类型相对干净年份、评分、评价人数都是标准格式主要处理完地区拆分和类型拆分就可以直接进入分析环节。对初学者来说这是很友好的上手数据源。另外这个项目覆盖了数据采集、数据存储、数据处理、数据分析、数据可视化五个环节和真实企业里做数据报表的链路基本一致只是规模缩小了。做完就能理解一套完整的数据项目该怎么分层、怎么组织代码这对后续学更复杂的框架其实帮助很大。1.2 技术栈选型逻辑技术选型上我没有刻意追求复杂工具核心原则是够用、好调试、生态成熟。具体组合如下表环节工具选型理由爬虫requests BeautifulSoup轻量、解析HTML直观适合小规模静态页面Scrapy对250条数据来说太重了数据处理pandas numpy表格处理能力极强筛选、分组、聚合一句话搞定可视化pyecharts matplotlibpyecharts做交互式图表展示效果好matplotlib用于快速验证数据分布数据存储CSV文件数据量小CSV读取方便Excel也能直接打开检查这里多说一句为什么不用Scrapy。很多人一看爬虫就想到框架但框架的学习成本、调试成本远高于直接写脚本。数据量只有250条用requests配合循环完全没有任何性能问题代码反而清晰很多。等到需要爬几万条分布式采集的时候再换框架也来得及。这个选择不是说Scrapy不好而是要匹配项目体量。可视化方面我选了双轨方案pyecharts负责产出最终展示用的HTML页面matplotlib负责在开发过程中快速出图验证数据形态。比如我想看看评分字段有没有异常值用matplotlib画个直方图一眼就能看出来这种即时性的验证还是matplotlib更方便。1.3 项目模块划分与架构图整个项目按数据流方向拆成四个模块采集模块、清洗模块、分析模块、可视化模块。每个模块之间解耦数据通过CSV文件或者DataFrame传递。模块划分遵循的规则很简单——“每一块只干自己最擅长的事”。采集模块不管数据质量只管把页面上的原始文本拿下来存好清洗模块不关心数据从哪来只负责把字段规整化分析模块专注计算指标可视化模块专注图表展示。这样拆分带来的直接好处是遇到问题时不用满代码找人定位特别快。比如爬到的数据数量不对直接看采集模块输出图表上中文乱码只在可视化模块找配置不用爬虫和清洗代码里翻来翻去。2. 数据采集与预处理最容易踩坑的部分2.1 爬取豆瓣Top250的规则分析豆瓣Top250的URL规则非常规律每页25条共10页。第一页是https://movie.douban.com/top250第二页是https://movie.douban.com/top250?start25第三页是https://movie.douban.com/top250?start50以此类推。这个规律意味着只需要用一个循环改变start的步长就能覆盖全部页面连复杂的翻页解析都不用做。第一次做这个项目的朋友最常犯的错误是直接用requests.get请求首页然后发现返回403这是因为没有设置User-Agent和Accept-Language请求头服务器识别出是脚本访问就直接拒绝了。我当时第一次跑脚本也遇到了这个问题因为浏览器访问完全没有障碍但requests请求默认的User-Agent是一串Python标识自然被识别出来了。解决办法就是骗过服务器把自己伪装成浏览器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, Accept-Language: zh-CN,zh;q0.9, Accept: text/html,application/xhtmlxml,application/xml;q0.9,*/*;q0.8 }拿到页面之后用BeautifulSoup解析HTML重点提取两个部分一是每部电影条目所在的div标签二是条目内部的具体字段。每部电影的标题在span.title里评分在span.rating_num里评价人数在div.star里的span中导演主演和年份地区等信息混合在一句话里需要按规则做切片。2.2 反爬应对与请求安全策略爬虫写出来能跑只是第一步能稳定跑完整个项目才是关键。豆瓣对请求频率是有明确限制的直接用瞬时并发去抓很容易被拉黑。我的策略简单粗暴——每次请求之间强制休眠1到3秒配合随机数避免出现规律性访问特征import random import time for page in range(10): # 请求逻辑 time.sleep(random.uniform(1, 3))除了限制频率还需要对请求异常做兜底。当时我遇到的典型情况是爬到第五页突然返回了一个验证页面title变成了“检测到有异常请求”。第一次遇到的时候脚本直接崩了因为解析不到预期的结构。后来我加了异常判断逻辑——如果响应内容和期待的标签结构不匹配就重新请求一次同时打印详细信息方便排查。另外控制爬取总量也很重要。只爬250条数据的压力其实很小但如果有扩充到Top500甚至爬所有评分数据的计划就建议在请求层加代理或者限速队列避免对目标服务器造成负担。做任何爬虫项目都要记住一个原则在不影响目标站点正常运行的前提下采集数据并且只把爬取的数据用于个人学习和分析。2.3 数据清理细节处理原始页面提取下来的数据基本是半格式化文本比如年份和地区混杂在“1994 中国大陆 / 中国香港”这样的格式中导演和演员字段也在同一个字符串里。这一步如果不做后面的分析根本无法进行。我拆分字段的规则是这样的年份用正则表达式匹配四位数字地区、类型按/符号分割导演取“导演:”前缀后的内容取到“主演:”前缀之前提取完成后还需要做类型转换。评分字段初始是字符串转为float才能做均值计算评价人数带有“人评价”的文本需要去掉文本只取数字再转int年份同理。缺失值的处理也在这时候做。250条数据整体质量很高但还是有个别字段缺失的情况比如部分电影的经典台词为空。我的策略是缺失值不上报也不要直接丢弃——首先统计缺失比例如果缺失低于5%就填充为“未知”如果缺失超过30%就直接放弃这个字段的分析因为它不具备统计意义。2.4 数据存储CSV还是SQLite清洗后的数据最终要固化下来。250条数据用SQLite显然有点大材小用而且后续用Excel直接查看、用pandas继续分析都不是很方便所以我选择存CSV。写CSV时有一个小坑——编码问题。直接用to_csv(douban250.csv)生成的默认编码是UTF-8Excel打开会乱码。实际处理时我带上了encodingutf-8-sig参数这样Excel也能正常识别不会出现个“锘”字。如果说数据量将来膨胀到上万条那就建议改用SQLite。它的优势是支持条件查询和事务回滚比CSV更适合频繁读写。但现阶段项目体量的合理性优先于技术上的“完美”CSV就是最优解。3. 数据分析与可视化核心实现3.1 明确分析维度与指标设定数据清洗完不等于分析结束好的分析要从问题出发不是为了画图而画图。我设定了以下几个核心问题豆瓣高分电影集中在哪些年份区间评分最高的30部电影有什么共性地区、类型、导演评价人数与评分之间是否存在正相关高频导演top10有哪些他们的平均评分如何这些问题确定了每个问题对应一个或两个图表分析报告的结构也就出来了。比如第一个问题适合用年代分布柱状图加评分走势折线图的组合图来表达第二个问题适合用评分top10横向柱状图叠加地区类型标签第三个问题适合用散点图观察相关趋势第四个问题适合用横向柱状图排名展示。需要强调的是做数据分析不是把数据塞进图表就完事了关键要能从图表中读出信息。比如我当时发现评价人数和评分有明显的正相关关系这背后对应的其实是豆瓣社区的一个特征口碑好的电影更容易获得广泛关注和讨论所以评价人数自然更多。这个结论可以单独拿出来写一小段洞察比干巴巴地展示一个散点图有价值得多。3.2 可视化选型静态与交互怎么选数据可视化这块如果目标是快速验证数据形态matplotlib是首选如果目标是对外展示项目成果pyecharts是更合适的方案。两者的关系不是替代而是互补。matplotlib适合开发过程中的中间验证。比如我想快速看评分分布是不是正态命令是import matplotlib.pyplot as plt df[rating].plot.hist(bins20) plt.title(Rating Distribution) plt.show()几秒钟就出图快速直观适合在清洗后第一时间检查数据是否合理。但如果拿来交付展示它的默认样式和交互能力就有点不够看了。pyecharts的强项是生成交互式HTML文件支持鼠标悬停显示数值、缩放、拖拽视觉上比静态图好很多。它底层是百度开源的ECharts封装成Python接口之后几乎不需要写JavaScript。对Python开发者很友好。我最后的交付物是一组HTML图表放在浏览器里点击、悬停、切换都不会卡展示效果在技术分享场合是比较加分的。3.3 典型图表的设计与优化细节不同类型的图表适合表达不同的数据关系。我在这个项目里实现了四类图表每一类在设计上都有明确的意图和细节考量。评分Top10横向柱状图表达的是“排名”概念用横向布局让电影名称完整显示避免文字过长被截断。图表标题加上了数据来源说明坐标为评分值排序按照评分降序排列。这里有一个优化的细节如果直接按原始顺序展示Top10图表看起来会比较乱先排序再画图整体会美观很多。年代与评分双轴折线图难点在于两个指标的量纲差异很大。数量可能是几十部评分均值稳定在8.5到9.2之间直接画在一根轴上数量会压扁评分曲线。解决办法是设置双Y轴左轴代表电影数量右轴代表平均评分这样两个指标的走势都能清晰地呈现出来。类型分布图我选的是饼图但饼图有个天然问题——类型占比太分散时不容易观看。豆瓣电影一部可以属于多个类型所以“犯罪”“剧情”“爱情”等类型会有交叉统计饼图适合展示单一归属的数据不适合展示重叠归属这种情况下更合理的选择是横向柱状图展示类型出现频次。当我意识到这一点后就把方案从饼图换成了柱状图效果立刻清楚了。评分分布直方图是验证整体数据形态的重要工具。通过直方图可以观察评分是否集中在某个区间、是否存在侧偏。视觉方面直方图的bins参数设计也值得注意默认的10个分箱粗糙了一些手动调到15个能更清晰地反映分布细节。4. 完整代码实现走读4.1 爬虫模块从零构造采集脚本整个爬虫模块的任务是“从URL到CSV”。我使用requests.get()获取页面内容然后用BeautifulSoup解析。核心代码如下import requests import time import random import pandas as pd 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(url): response requests.get(url, headersheaders, timeout10) if response.status_code 200: return response.text else: print(f请求失败: {response.status_code}) return None def parse_movie_items(html): soup BeautifulSoup(html, html.parser) items soup.select(.grid_view .item) movies [] for item in items: title item.select_one(.title).text rating item.select_one(.rating_num).text comments item.select_one(.star span:nth-of-type(4)).text # 评论人数需要去掉人评价后缀 comments comments.replace(人评价, ) movies.append({ title: title, rating: float(rating), comments: int(comments), }) return movies movies [] for i in range(10): url fhttps://movie.douban.com/top250?start{i * 25} html fetch_page(url) if html: movies.extend(parse_movie_items(html)) time.sleep(random.uniform(1, 3)) df pd.DataFrame(movies) df.to_csv(douban_top250_raw.csv, indexFalse, encodingutf-8-sig)这段代码把豆瓣Top250各个字段都纳入采集范围。注意comments字段需要处理“人评价”后缀否则int()转换时会抛异常。4.2 清洗模块数据规整与字段扩充爬虫模块只拿到了电影的基本字段但导演、年份、地区、类型这些丰富的冷信息还混杂在原始字符串里。清洗模块的任务就是把它们拆出来import re def clean_movie_data(raw_df): df raw_df.copy() # 从 info 字段提取年份 df[year] df[info].str.extract(r(\d{4})) # 分离地区按 / 分割取后段 df[area] df[info].str.split(/).str[1].str.strip() # 导演提取 df[director] df[info].str.extract(r导演:([^/])) # 主演提取 df[actors] df[info].str.extract(r主演:([^/])) # 类型提取不同类型用 / 分隔 df[genres] df[info].str.split(/).str[-1].str.strip() df[rating] df[rating].astype(float) df[comments] df[comments].astype(int) df[year] df[year].astype(int) return df清洗过程中还有一个重要环节是对info列的解析口径保持统一。当时遇到过一个情况某些电影的info字段中导演和主演连在一起没有空格导致正则表达式匹配失败。于是我在提取导演之后对原始字符串做了一步去空格和统一分隔符的预处理确保后续正则匹配的稳定性。4.3 分析模块指标计算与洞察提取完成清洗后分析模块要做的是把数据分组聚合计算各维度指标。这段代码体现了pandas在数据分析中的核心用法# 各年份的电影数量与平均评分 year_stats df.groupby(year).agg( movie_count(title, count), avg_rating(rating, mean) ).reset_index() # 导演作品数量与平均评分 director_stats df.groupby(director).agg( movie_count(title, count), avg_rating(rating, mean) ).reset_index().sort_values(movie_count, ascendingFalse) # 类型出现频次 from collections import Counter genre_counter Counter() for genres in df[genres]: for genre in genres.split(,): genre_counter[genre.strip()] 1 genre_df pd.DataFrame(genre_counter.items(), columns[genre, count]).sort_values(count, ascendingFalse)分析模块计算完指标之后还可以顺手把结论写成注释或者单独的文本文件方便后续做汇报时直接引用。比如我当时的发现包括90年代初到2010年之间是高产高分电影的密集期类型的出现频次分析中“剧情”“犯罪”“爱情”占据前三位说明这些类型更容易出现高口碑作品。4.4 可视化模块用pyecharts产出最终图表可视化部分使用pyecharts生成HTML文件。下面的代码展示了评分Top10柱状图的实现from pyecharts import options as opts from pyecharts.charts import Bar top10 df.nlargest(10, rating)[[title, rating]] bar ( Bar() .add_xaxis(top10[title].tolist()) .add_yaxis(评分, top10[rating].tolist()) .set_global_opts( title_optsopts.TitleOpts(title豆瓣电影Top10评分榜), yaxis_optsopts.AxisOpts(min_8), ) ) bar.render(top10_bar.html)有几个容易被忽略的细节值得单独说明。yaxis_opts中的min_参数很关键原因是评分全部集中在8分以上如果Y轴默认从0开始8.0到9.7之间的差异会在视觉上被压扁柱状图的长度对比会显得很弱。把Y轴起点设为8分之后不同电影之间的差距就立刻变得清晰了。多个图表生成之后还做了合并展示的需求。pyecharts提供了Page类可以把多个图表组合到一个页面中用Tab形式切换浏览。这样最终的交付物就是一个包含多个图标的dashboard.html文件非常方便在浏览器中翻阅和演示。5. 常见问题与排查经验实录5.1 爬虫请求失败与重定向问题这个问题几乎每个做爬虫的人都会遇到。经典的报错场景有两类一类是403 Forbidden说明请求被服务端拒绝大概率是Headers配置问题而不是IP封锁另一类是页面返回一个“检测到异常请求”的验证页面说明访问频率过高。我的排查顺序是先检查请求头是否完整→再检查请求频率是否过快→最后考虑是否要加代理池。正常情况下Top250这样的小规模爬取不需要代理。如果你发现设置了随机等待时间后依然被拦就可以考虑换一个思路看看是不是Headers里的Accept-Language缺失豆瓣对语言偏好是有检测逻辑的。5.2 数据解析结果为空解析结果为空是初学者最常见的问题。原因通常不是页面结构不对而是这个页面压根不是你想要的页面——可能是一个跳转页面或者验证页面。排错方法很简单把返回的HTML打印前300个字符确认是否包含预期的文档结构。有时候看起来是空列表其实只是selector写错了比如豆瓣页面的评分按钮有两种样式span.rating_num和div.star内部的层级关系会因为榜单页面布局差异有细微差别。5.3 matplotlib中文乱码问题matplotlib的默认字体集不包含中文字体所以直接画图会出现方块。解决方法是显式指定支持中文的字体plt.rcParams[font.sans-serif] [SimHei] plt.rcParams[axes.unicode_minus] False第二个参数解决的是负号显示为方块的问题。这个配置在代码开头设置一次整个会话生效。5.4 pandas的SettingWithCopyWarning警告在清洗模块中对DataFrame切片后再赋值容易触发SettingWithCopyWarning。虽然这个警告多数情况下不会影响数值结果但它意味着你的操作可能作用在副本上而不是原数据上后续操作可能失效。解决方法是明确调用.copy()df raw_df.copy()这样就在函数内部把DataFrame的所有权明确下来了不会出现修改不生效这种隐蔽问题。5.5 pyecharts图表不显示pyecharts生成的是HTML文件所以必须在浏览器里打开不是直接在编辑器里渲染。很多人第一次使用时会卡在这里。另外如果使用Jupyter Notebook需要调用bar.load_javascript()或者直接使用render_notebook()才能嵌入显示。5.6 常见问题速查表问题现象可能原因解决办法403 Forbidden缺少请求头伪装设置完整的User-Agent和Accept字段中文乱码编码存储不正确CSV存储用utf-8-sig编码matplotlib中文方块字体不支持中文设置SimHei字体图表空白无数据selector写错打印HTML检查结构评价人数报错文本未彻底清洗先处理“人评价”前缀年份全是NaNinfo字段解析错误检查正则表达式匹配范围6. 后续扩展的三个方向这个项目的基础链路跑通后可以从数据源、分析深度和展示形态三个方向做扩展。数据源方面除了Top250豆瓣还有实时热门榜单、分类排行榜以及评论区的大量短评。这些短评如果利用起来可以引入情感分析把“好评率”作为新的分析维度与评分做对照就能发现很多有意思的差异——有些电影评分不高但短评情感偏正面也有评分很高但短评吐槽点比较集中。这个方向涉及自然语言处理但用Python的snownlp或者jieba分词加简单的规则打分也能实现基础版本。分析深度方面现有的分析集中在单变量和双变量可以考虑做简单的聚类分析。比如用KMeans算法基于评分、评论数、年份三个特征把电影聚成几类看看每一类有什么共性特征。这样就能跳脱“看单部电影”的思维进入“看群体画像”的层面。展示形态方面可以尝试把pyecharts图表嵌入Flask或FastAPI应用做一个动态交互的Web项目。用户可以通过下拉框选择年份区间、类型进行筛选图表随之联动更新。这一步虽然要考虑前后端数据接口但技术难度并没有想象的那么大属于合适的进阶练习。从我个人的体会来说这个项目最大的价值不在于技术本身而在于它逼迫你“走完整个流程”——从确定问题、设计爬虫、清洗数据到分析规律、呈现结论。每一步看上去都很简单但连起来跑通才是真正的挑战。如果你也在学数据分析我强烈建议别只跟着教程敲代码而是像我这样给自己选一个感兴趣的真实数据源把一个完整的小项目从头到尾做一遍。过程中的坑一定会踩但每个坑踩完了你对数据项目的理解就会扎实一大截。
返回列表