尧图精选

用Python爬取影视评分与短评,分析观众偏好与口碑热度

🕒 发布时间:2026/9/10 10:28:10 📁 来源:尧图网络
1. 项目概述与整体设计1.1 这个项目到底能做什么先说结论花一个周末用Python写一个爬虫把某影视社区的热门剧集评分、短评、类型标签这些公开数据抓下来再做一轮统计和文本挖掘你就能得到一份类似“观众口味调查报告”的产出知道大家到底偏爱什么样的剧口碑和热度之间的差距在哪里。我最初想做这件事是因为每次选剧都靠朋友推荐推荐来推荐去就那么几部。后来我意识到与其问人不如问数据——评分网站上有海量的公开数据把评分、评分人数、短评内容、剧集类型和年份拿下来用pandas一统计观众偏好其实非常清楚哪类题材的评分稳定偏高哪些高热度剧其实是“流量大于口碑”观众在短评里反复吐槽的到底是什么词。这个项目适合三类人刚学完Python基础、想拿一个完整项目练手的朋友。它能串起requests请求、HTML解析、pandas数据处理、matplotlib画图、jieba分词这些常用技能。做内容运营、视频选品、市场调研的朋友。这套流程改一改就能迁移到其它行业数据抓取和分析上。纯粹好奇“评分到底怎么来的”“观众口味能不能量化”的人。全程只需要一台能联网的电脑代码量控制在300行以内不需要分布式爬虫更不需要上Scrapy这种重型框架。1.2 技术方案选型为什么是requests而不是Scrapy很多人一听到爬虫就想到Scrapy觉得这是“正规军”。但在这个项目里我坚持用requests BeautifulSoup pandas原因有三个。第一目标明确、规模可控。我只需要抓取几百部剧的评分数据和短评单机、串行请求、加一点限速半小时内就能跑完。Scrapy的优势在于大规模、分布式、管道化调度为了这么小的数据量去搭一套Scrapy工程光写spider、item、pipeline的配置就要折腾半天属于杀鸡用牛刀。第二调试直观。requests的请求就是一对一的请求出问题直接在脚本里打日志哪里状态码不对、哪里解析出来是空列表一目了然。Scrapy有框架自身的异步调度新手一旦遇到反爬问题排查链路会复杂很多。第三依赖少环境干净。requests、beautifulsoup4、pandas、matplotlib、jieba这几个库加起来就是全部依赖。哪怕你用的是Python 3.8这种老版本也能装齐。如果你的目标是从头到尾理解爬虫的底层流程而不是追求生产级爬虫系统那么这个技术组合是上手成本最低的方案。提示requests库的底层是urllib它帮你把请求头管理、重定向、cookie处理、连接池这些细节封装好了。理解requests其实就是在理解HTTP协议本身。1.3 目标数据字段设计先想清楚要什么再动手写代码写代码之前我先在一张纸上列了字段清单。这一步很多人会跳过但恰恰是最重要的——没有字段清单你解析页面的时候就会漫无目的抓到什么算什么最后数据表长得一团糟。我最终确定的字段如下字段名说明示例值title剧集名称漫长的季节year首播年份2023rating综合评分9.4rating_count评分人数78.6万genre类型标签剧情 / 悬疑region制片地区中国大陆short_comment热门短评“这剧的后劲太大了”这个字段设计的逻辑是评分和评分人数用来判断“口碑”和“热度”类型和地区用来做偏好分组年份用来观察趋势短评则用来做文本挖掘。五个维度合在一起足以回答“观众喜欢什么”和“为什么喜欢”这两个核心问题。实际抓取的时候我会把原始数据先存成CSV文件。这样即使后面分析写错了也不需要重新跑爬虫直接读CSV就行。2. 数据采集页面分析、请求伪装与解析落地2.1 环境依赖与安装先看一眼你本机的Python版本。我建议用Python 3.9以上版本因为pandas和matplotlib在3.9以上版本兼容性更好。python --version然后安装项目依赖。这里我推荐用国内镜像源安装速度快很多。pip install requests beautifulsoup4 lxml pandas matplotlib jieba wordcloud如果你在安装wordcloud的时候遇到报错也不用死磕。wordcloud在Windows上偶尔会缺Visual C编译环境后面我会讲到替代方案。先装上前面几个项目主体功能就能跑通了。2.2 页面结构与数据接口定位我抓取的目标站点是某影视社区的分类浏览页。打开页面后不要急着写代码先在浏览器里按F12进入开发者工具切到“网络”Network面板然后刷新页面。你会看到页面加载时发了几十个请求。重点关注两种以api开头的XHR请求通常返回的是JSON格式数据首屏HTML文档本身。我实际操作下来发现这个影视社区的前端是前后端分离的首屏内容由JavaScript动态渲染直接抓HTML只能拿到一个空壳框架。这时候有两个选择一是用Selenium或Playwright模拟浏览器渲染二是直接从XHR接口拿JSON。我做的是第二种。原因很简单XHR接口返回的是结构化JSON字段现成不需要正则和XPath去HTML里抠数据。而且接口的请求负载很小对目标服务器的压力也小增加了爬虫的友好程度。打开开发者工具点开一个/api/top_search_television之类的接口在“预览”Preview标签页就能看到完整的JSON结构。结构大致是{ data: { list: [ { title: 漫长的季节, year: 2023, rating: 9.4, rating_count: 78.6万, genre: 剧情 / 悬疑, region: 中国大陆 } ], paging: { next: 2, total_pages: 500 } } }用接口而不是解析HTML还有一个隐蔽的好处接口字段是后端直接返回的格式统一不容易像HTML那样因为一两个标签嵌套差异导致解析失败。2.3 请求头伪装与限速别让你的爬虫看起来像轰炸机拿到了接口地址下一步就是构造请求。直接裸用requests.get大概率会碰壁因为目标服务器有简单的反爬策略会校验请求头中的User-Agent。空UA的请求会被判定为非浏览器访问。我第一次跑的时候就被拒了返回403。解决办法是伪装请求头import requests from fake_useragent import UserAgent ua UserAgent() def get_headers(): return { User-Agent: ua.random, Accept: application/json, text/plain, */*, Referer: https://example.tv/, Accept-Language: zh-CN,zh;q0.9 }fake_useragent库可以随机生成各种浏览器的UA字符串。这比固定写死一个UA要可靠得多——固定UA跑几百个请求之后统计特征太明显了。真正的关键在限速。我见过很多人写爬虫循环里不写time.sleep一口气把几百个请求打出去结果三分钟就被封IP。正确的做法是每次请求之间加一个随机延时import time import random def random_sleep(): time.sleep(random.uniform(0.8, 2.5))为什么是随机而不是固定延时因为正常人的浏览行为不可能是时钟般精确的1秒一次。固定的间隔反而更容易被反爬系统的频率检测算法识别出来。0.8到2.5秒这个区间是比较稳妥的既保证速度又不显得异常。如果你要抓的数据量特别大还可以在循环里加一个计数每抓50页就主动休息30秒。这算是对目标服务器的一种礼貌。注意无论什么情况都不要把并发开到几十上百去抓别人的站点。一方面是道德和法律风险另一方面数据量其实没那么大没必要。真正靠谱的做法是“加延时、减并发、留退路”。2.4 数据解析与异常兜底接口返回的是JSON解析就变得很简单了。直接遍历JSON结构把需要的字段提取出来def parse_data(json_data): items [] for item in json_data.get(data, {}).get(list, []): items.append({ title: item.get(title), year: item.get(year), rating: item.get(rating), rating_count: item.get(rating_count), genre: item.get(genre), region: item.get(region) }) return items这里有几个隐藏的问题需要注意第一rating_count可能是字符串“78.6万”也有可能是整数。如果你后续要做数值分析必须把这个字段统一转成浮点数。“万”这个单位要乘以10000。def normalize_count(text): if isinstance(text, (int, float)): return float(text) if 万 in text: return float(text.replace(万, )) * 10000 return float(text)第二genre字段可能同时包含“剧情 / 悬疑 / 犯罪”三个值。直接把整个字符串存进CSV没问题但后期统计类型分布时需要拆分。我习惯用逗号统一替换掉“ / ”这样读入pandas后可以方便地按分隔符拆分。第三接口偶尔会返回空列表或者某个字段缺失。如果直接访问item[title]遇到缺失就会抛KeyError。所以强烈建议所有字段都用.get()方式访问这样就算字段缺失也只是得到None不影响整体流程。主循环部分大概是这个逻辑all_data [] for page in range(1, 100): api_url fhttps://example.tv/api/list?page{page} try: resp requests.get(api_url, headersget_headers(), timeout10) if resp.status_code ! 200: print(fPage {page} failed: {resp.status_code}) continue data resp.json() items parse_data(data) if not items: break all_data.extend(items) print(fPage {page} done, total: {len(all_data)}) except Exception as e: print(fPage {page} error: {e}) random_sleep() import pandas as pd df pd.DataFrame(all_data) df.to_csv(tv_data.csv, indexFalse, encodingutf-8-sig)注意CSV保存时我用了utf-8-sig而不是utf-8。这是Windows平台的坑如果你用Excel打开CSV文件UTF-8编码的文件会乱码但utf-8-sig带BOM头Excel能正常识别。如果你只在Python里读用utf-8就够了。3. 评分统计与观众偏好分析3.1 评分分布与整体口碑画像数据到手后第一步永远是看全貌。我不会直接开始画花哨的图而是先做描述性统计df pd.read_csv(tv_data.csv) print(df[rating].describe()) print(df[year].value_counts().sort_index().tail(10))describe()会输出评分的均值、标准差、最小值、最大值和四分位数。这一步能快速发现数据里有没有脏数据——比如评分大于10或者小于1的记录。以我实测的数据为例抓了大约1200部剧集评分均值在8.1左右中位数8.2说明这个榜单本身就以高口碑作品为主。标准差只有0.6说明评分整体偏集中8分以下就已经算“口碑一般”了。接下来画评分分布直方图import matplotlib.pyplot as plt plt.rcParams[font.sans-serif] [SimHei] # 让中文标签正常显示 plt.rcParams[axes.unicode_minus] False df[rating].hist(bins20, edgecolorwhite) plt.xlabel(评分) plt.ylabel(剧集数量) plt.title(剧集评分分布) plt.show()这里有一个高频坑matplotlib默认字体不支持中文如果你不设置sans-serif图上的标题和轴标签会变成方框。Windows系统用SimHei黑体macOS用Arial Unicode MSLinux可以用WenQuanYi Zen Hei。从直方图能明显看到8分到9分之间是高峰9分以上断崖式减少。这说明9分是真正的口碑门槛能上9分的剧在题材、表演、剧本上至少有一个维度做到了极致。3.2 年份、类型与口碑的交叉分析只统计单一维度不够。观众偏好分析的核心在于交叉分析也就是“哪个年份的剧质量在上升”“哪类题材更容易出高分”。按年份统计平均评分和剧集数量year_group df.groupby(year)[rating].agg([mean, count]) print(year_group.tail(10))从我的数据来看近五年每年上榜剧集中在当年和前两年占比明显超过老剧。这里有个值得注意的结论榜单更偏向近期作品并不完全代表历史口碑佳作因为评分人数会随时间衰减老剧的评分人数不如新剧多。再按类型拆分统计需要先把“剧情 / 悬疑”这类复合类型拆分。我的做法是先把每部剧的类型按分隔符拆开然后用explode函数展开成“一部剧对应一行类型”的长表genre_rating df.copy() genre_rating[genre] genre_rating[genre].str.replace( / , ,) genre_rating genre_rating.drop(genre, axis1).join( genre_rating[genre].str.split(,, expandTrue).stack().reset_index(level1, dropTrue).rename(genre) ) genre_group genre_rating.groupby(genre)[rating].agg([mean, count]) genre_group genre_group[genre_group[count] 10].sort_values(mean, ascendingFalse) print(genre_group)实测下来“剧情”和“悬疑”是高分剧的主力类型平均分都在8.6以上。而“喜剧”和“爱情”类型的平均分相对偏低。这并不能说明喜剧拍得不好而是观众的评分习惯不同喜欢剧情片的观众更倾向于因为“深度”打高分喜欢喜剧的观众则比较挑剔“笑点密度”。这里给零基础读者解释一下explode的思路你把“剧情 / 悬疑”看成一个大盒子里的两个小盒子explode就是把它们拿出来分别放到两行里方便分组统计。这是pandas里处理“一对多”关系最常用的操作。3.3 观众偏好词云短评文本挖掘评分只能告诉你“好不好”但观众为什么这么打分藏在短评里。我抓取每个剧集的短评时额外保存了前几十条热门短评。短评是观众最直接的情绪表达把这些文本汇总后用jieba分词再统计词频就能知道高频关键词是“剧情紧凑”“演技在线”“结局烂尾”还是“改编失败”“节奏太慢”。分词核心代码如下import jieba from collections import Counter def load_stopwords(pathstopwords.txt): with open(path, r, encodingutf-8) as f: return set(line.strip() for line in f) stopwords load_stopwords() all_comments .join(df_comment[short_comment].tolist()) words jieba.lcut(all_comments) filtered_words [ w.strip() for w in words if len(w.strip()) 1 and w.strip() not in stopwords ] word_count Counter(filtered_words) print(word_count.most_common(30))停用词表里要放哪些词至少要放“我”“你”“他”“这个”“那个”“真的”“感觉”“一部”“没有”“还是”这类无实际信息量的高频词。如果不做这一步词频统计会被代词和语气词淹没根本看不出观众关注什么。我建议每个项目的停用词表都根据统计结果动态迭代。第一次跑出来的高频词里只要发现是无效词汇就加进停用词表再跑一次。迭代两三轮之后词云才会变得有信息量。分词完成后用wordcloud生成词云from wordcloud import WordCloud wc WordCloud( font_pathC:/Windows/Fonts/simhei.ttf, width800, height600, background_colorwhite, max_words200 ).generate( .join(filtered_words)) plt.imshow(wc, interpolationbilinear) plt.axis(off) plt.show()注意wordcloud默认字体不支持中文必须指定font_path指向一个中文字体文件。Windows一般在C:/Windows/Fonts/simhei.ttfmacOS在/System/Library/Fonts/PingFang.ttc。如果wordcloud在你的环境里装不上也可以用pyecharts的词云图组件替代效果类似。3.4 从数据看出的三条“观众偏好”结论经过评分和短评交叉验证后数据呈现出了三条明显规律第一条口碑与热度之间并不完全正相关。评分最高的前10部剧里有一半的评分人数不超过两万属于“小众神作”。反过来评分人数超过五十万的“爆款”里评分大都在8分上下很难达到9分以上。这说明大众口味追求的是“容易入口的好看”而9分以上需要某种程度的“观影门槛”。第二条观众高度敏感于剧情节奏和结局质量。短评高词频词里“节奏”“结局”“烂尾”“拖沓”出现的频率出奇得高。这从侧面反映了当前观众的耐心阈值很低前两集不够抓人后面再精彩也留不住人。第三条类型偏好呈现明显的“口碑聚集效应”。“悬疑”“科幻”“历史”三个类型的评分方差小相对稳定“爱情”“喜剧”虽然爆款不少但差评也更集中。分析这类数据对内容策划很有参考价值它直接告诉你哪个品类更安全、哪个品类上限高但风险也大。4. 常见问题与排查技巧实录4.1 403 ForbiddenUA、请求头与频率三板斧这是我遇到最多的报错。403的本质是服务器认为“你不是正常人”。排查顺序依次是检查UA是否被识别为空或爬虫特征检查请求头是否漏了Referer或Accept字段检查是否请求频率过高被临时封禁。在只换了UA还不行的情况下我建议你把Referer字段也加上。很多站的防盗链逻辑很简单——你直接访问API接口时没有本站页面跳转来源它就不给数据。如果加了UA和Referer还是403大概率是频率问题。这时候不用急着换代理IP先停半小时把random_sleep区间调大再重启任务。绝大多数情况下小规模爬虫并不需要代理频率降下来就能解决。4.2 解析出来的字段对不上JSON结构变化导致List越界接口返回的JSON结构不是一成不变的。我第一次跑通之后隔两天再跑突然发现某些剧的rating_count字段变成了null导致后续normalize_count直接抛异常。这类问题最好的防御方式就是我在2.4节提的所有字段用.get()方式访问并且在解析函数里加一层类型判断。如果返回的是None填一个默认值或者挖掉这条记录。宁可少一条数据不能让整个任务崩溃。4.3 中文乱码编码问题的三个真实场景中文乱码有三种常见情况。第一种是CSV文件在Excel里打开乱码解决办法是用utf-8-sig编码保存。第二种是requests请求时页面返回的是GBK编码但requests默认用UTF-8解码导致字符串乱码。解决办法是resp.encoding resp.apparent_encoding第三种是matplotlib绘图中文变方框解决办法是在全局设置中文字体。这些都是新手最容易踩的坑每个都能折腾一小时以上提前了解能省不少时间。4.4 常见问题速查表我把自己遇到过的典型问题和解决办法整理成了一张表方便你快速对照现象可能原因解决办法请求状态码403请求头被识别为爬虫加随机UA、补Referer、降低频率页面返回空JSON接口需要登录cookie在headers中加入Cookie字段数据里有大量None字段缺失或接口结构变更统一用.get()获取字段并兜底短评报错或为空短评接口单独分页或需额外请求单独写一个抓取短评的函数按剧集ID拉取CSV用Excel打开乱码编码用了utf-8而非utf-8-sig保存时改为encodingutf-8-sig图片中文变方框matplotlib字体不含中文字符设置plt.rcParams[font.sans-serif]跑了几十页后突然全部超时触发了服务器的IP临时限流停止15-30分钟恢复后增大延时5.1 大型改造前先想清楚分布式要不要上很多朋友跑完这个项目后第一反应是“我能不能把它做成分布式爬虫抓下载的都抓下来”。我的回答通常是看你有没有真正的数据需求。如果只是做分析几万条数据足够产生结论了。分布式爬虫引入的消息队列、任务调度、多节点协调每一样都是额外的大脑负担。对于个人项目单机加限速是最优解。真正做分布式的前提是数据量到了百万级别而且你能容忍部署和运维的成本。5.2 项目扩展方向这个项目本身就是一个完整的模板可以扩展的方向非常清晰加入短评的时间维度看剧集上线后口碑随时间的变化趋势分析“高开低走”还是“低开高走”。加入导演和演员字段做导演风格与观众偏好的关联分析。引入更细粒度的“评分”分布数据比如各星级的占比比平均数更能反映口碑结构。用大模型对短评做情感分类或主题聚类得到比词云更深层的结论。短评文本量不大调用接口的费用很低但分析出来的主题分布会专业很多。我自己后续最想做的方向是情感分析加评论区对话的语义网络。影视剧评论区其实是一个巨大的民间舆情场观众在里面聊剧情、聊角色、聊人生如果能把讨论的焦点提取出来会比单纯的评分更接近作品真正触动了什么。5.3 最后说几句大实话爬虫这个领域工具学起来永远是快的难的是“分寸感”。你要理解请求频率该控制在什么范围内理解目标站点的数据哪些是公共的、哪些是受限的理解你的行为会不会给对方服务器造成负担。我在这类项目上踩过最多的坑从来不是Python语法而是低估了数据的“脏”。写爬虫容易清洗数据才是真正吃时间的地方。你可能会花一个小时抓完数据然后花三个小时处理日期格式、空值和单位换算。做这个项目最大的收获不是学会了怎么写爬虫而是建立了一种“用数据思考内容”的习惯。当别人还在纠结“最近哪部剧好看”的时候你可以直接拉出评分曲线、关键词分布和类型均值用数据说话。这种能力放在任何领域都值钱。
上一篇/下一篇内容由系统自动关联 返回资讯列表 →