
影视评分到底怎么来的和观众口味差多远与其在几个平台之间来回切着猜不如自己把这些数据抓下来算一遍。今天这篇就走一遍完整流程用Python爬虫抓取影视剧评分与评价数据清洗入库再做评分统计和观众偏好分析。适合已经会Python基础语法、熟悉requests安装和基本用法的朋友还不太熟也没关系每个环节都会讲清楚为什么这么做踩过的坑也都标出来了。这个项目不是单纯教你写爬虫它是一条完整的数据链路采集、清洗、聚合、分析、可视化。数据源以公开可访问的影视评分平台为主总量控制在一千部左右既能反映统计规律又不会把反爬压力搞得太大。全文代码都用Python 3依赖库只有requests、pandas、SQLite3内置模块和matplotlib尽量少装东西。1. 项目整体设计与数据链路1.1 这个项目到底要解决什么问题评分是观众决策的重要参考这是不争的事实。但评分平台之间差异很大有的偏文艺有的偏大众有的高分冷门、有的烂片爆火。如果只看单一平台的高分榜很容易被带偏。所以这个项目要回答三类问题哪些影视剧评分最高评价人数最多这两个指标是同一批吗高评分到底受什么因素影响年份、题材、地区有没有相关性观众的观影偏好聚在哪个区间高分冷门和低分爆款各占多少比例要回答这些问题光有评分不够还得有剧名、年份、国家地区、题材类型、导演演员、评价人数、评分星级分布。把这些字段组合起来才能做交叉分析。数据流整体是目标页面列表获取 → 详情页抓取 → 字段清洗与结构化 → 存入SQLite → 用pandas聚合统计 → 用matplotlib出图。整个过程在单机就能完成不涉及分布式也不依赖付费接口。1.2 数据字段规划与表结构设计抓之前先把目标结构定好这个习惯在爬虫项目里非常重要。看到什么抓什么后面清洗会痛不欲生。我的字段规划如下字段名类型说明titleTEXT影视剧名称yearINTEGER上映年份regionTEXT国家/地区genreTEXT题材类型逗号分隔ratingREAL综合评分0-10分rating_countINTEGER评价人数热度参考directorsTEXT导演多人用逗号分隔actorsTEXT主演取前三位即可为什么一定要同时保存评分和评价人数举个简单例子A剧9.2分但只有1000人评价B剧8.0分但有20万人评价。单看评分A吊打B单看人数B完胜。只有两个字段放在一起才能判断“这是高分冷门还是大众爆款”。后面分析观众偏好时这两个字段就是最核心的交叉维度。表结构不需要太复杂一张movie表就够。SQLite建表语句在后面存储部分给出。先不用管抓下来的数据先存在Python字典列表里清洗完再统一入库。2. 技术选型与环境准备2.1 为什么用requests而不是Scrapy新手很容易陷入框架选择焦虑。Scrapy功能强大支持并发、中间件、管道但学习曲线陡而且对一个小体量的数据分析项目来说略重。requests配合BeautifulSoup在这个项目体量下最合适原因有三代码直观出错好排查。一个脚本从请求到解析到存储全链路一目了然。控制精细。请求频率、超时、重试都是自己写的逻辑不会像框架那样在多个组件间来回跳。依赖少。requests和beautifulsoup4就够了部署在哪台机器上都能跑。如果未来数据量上到百万级、需要分布式采集再迁移到Scrapy也不迟。见过太多人一上来就上Scrapy光调管道就调了一整天列表页还没抓到一张。工具服务于场景不是场景服务于工具。2.2 环境安装与依赖版本Python版本建议3.9以上3.11、3.12都没问题。如果你还没装好Python先把基础环境准备好装完记得确认pip可用python --version pip --version pip install requests beautifulsoup4 pandas matplotlib网络环境不稳定时pip可以换国内镜像源pip install requests beautifulsoup4 pandas matplotlib -i https://pypi.tuna.tsinghua.edu.cn/simple这几个库安装完后建议在IDE里先跑一个最小测试确认requests能正常拿到页面再开始写后面的代码。这个步骤只需要20秒但能过滤掉八成环境问题。3. 抓取策略与解析细节3.1 列表页抓取拿到剧目标题和详情页链接以影视评分平台的榜单页作为入口。榜单页通常包含剧名、年份、评分、评价人数、主演等信息但字段不全所以策略是先拿列表页数据再对每一部剧请求详情页补齐导演、剧情简介等字段。列表页的HTML结构在不同平台差异很大但规律类似。核心思路是先定位每部剧所在的标签块再在块内做二次提取。BeautifulSoup的select_one在“块内定位”上比全局find好用得多因为范围被限定选择器写起来简单也不容易误匹配。请求列表页时有个容易忽略的细节部分平台的内容是服务端渲染的直接requests就能拿到完整HTML但有些平台前端动态加载requests拿不到真实数据。判断方法很简单把response.text打印出来搜一下剧名搜得到就是服务端渲染搜不到就要另想办法。本项目优先选服务端渲染的页面做数据源复杂度低很多。3.2 详情页抓取与增量更新策略详情页没必要每部剧都抓一次。评分和基础列表信息已经够了详情页主要补导演和题材。如果只是做统计列表页信息通常就够用这一步骤可以跳过。但如果你想把分析做到“类型偏好”这个维度题材字段就得从详情页拿。请求频率控制在2到4秒一个请求这个速度对单机脚本来说足够安全也够用了。全套数据量在几百部级别时整个流程耗时也就半小时左右。抓完一批下次再跑时先查库库里已有的剧目直接跳过这就是最简单的增量更新。增量更新的实现思路爬之前先从数据库读出已有title放到一个set里在抓取循环中判断。这个逻辑非常简单但很多新手会忽略导致每次重跑都要等完整流程。3.3 请求头伪装与频率控制平台的反爬基本从两个维度出发头信息合法性和请求频率。头信息这里只需要一个User-Agent用浏览器里的值就行。注意UA要和操作系统匹配比如Windows Chrome和macOS Safari的值不一样用错了虽然不会必然被拦但显得可疑。请求频率控制是更加关键的合规手段。合法采集和恶意攻击的边界很大程度上就体现在频率上。每请求一次就随机sleep 2到4秒import time import random time.sleep(random.uniform(2, 4))这个节奏不快但对数据量有限的分析项目完全够用。如果目标平台提示操作频繁立刻停止脚本等一段时间再继续而不是暴力硬扛。尊重平台规则是爬虫开发者的基本素养个人学习项目尤其要克制。3.4 解析细节评分和评价人数的清洗列表页拿到的原始字段往往是“8.7分”和“23.4万评价”这种格式。直接存字符串后面没法做统计需要提取数字并做单位换算。这块是清洗的第一个关键点def parse_rating(rating_str): # 输入形如 8.7分 return float(rating_str.replace(分, )) def parse_count(count_str): # 输入形如 23.4万 或 8900 s count_str.replace(人评价, ).replace(,, ) if 万 in s: return int(float(s.replace(万, )) * 10000) return int(s)“万”这个单位处理要特别小心有的平台写“3.2万”有的写“32000”有的“3.2万人评价”还带了单位。一个parse_count函数把所有情况统一收口后面分析才能放心用数值型数据。4. 评分统计的核心环节实现4.1 抓取主流程与数据入库抓取主流程代码如下可以把它当成骨架模板复用from bs4 import BeautifulSoup import requests import sqlite3 import time import random HEADERS {User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36} def init_db(): conn sqlite3.connect(movies.db) conn.execute( CREATE TABLE IF NOT EXISTS movie ( id INTEGER PRIMARY KEY AUTOINCREMENT, title TEXT, year INTEGER, region TEXT, genre TEXT, rating REAL, rating_count INTEGER, directors TEXT, actors TEXT, UNIQUE(title) ) ) return conn def fetch_list_page(page_url): resp requests.get(page_url, headersHEADERS, timeout10) resp.raise_for_status() resp.encoding resp.apparent_encoding return resp.text def parse_list_html(html): soup BeautifulSoup(html, html.parser) items [] # 具体选择器按目标平台结构调整核心是先在块级元素内做二次提取 for block in soup.select(.movie-item): title_node block.select_one(.title) rating_node block.select_one(.rating) count_node block.select_one(.rating_count) info_node block.select_one(.info) if not title_node or not rating_node: continue item { title: title_node.text.strip(), rating: parse_rating(rating_node.text.strip()), rating_count: parse_count(count_node.text.strip()), info: info_node.text.strip() if info_node else } items.append(item) return items conn init_db() for page in range(0, 5): url fhttps://example.com/top/{page * 25} html fetch_list_page(url) items parse_list_html(html) for item in items: conn.execute( INSERT OR IGNORE INTO movie (title, year, region, genre, rating, rating_count, directors, actors) VALUES (?, ?, ?, ?, ?, ?, ?, ?), (item[title], item.get(year), item.get(region), item.get(genre), item[rating], item[rating_count], , ) ) conn.commit() print(f第{page 1}页完成累计 {conn.total_changes} 条变动) time.sleep(random.uniform(2, 4)) conn.close()这段代码的核心思路是“先入库后补全”关键是INSERT OR IGNORE配合UNIQUE约束靠title字段天然去重。第二次跑同一批页面时不会重复插入。详情页字段缺失就留空后面再单独更新。用UNIQUE去重而不是先SELECT再INSERT好处是少了查询逻辑且天然防并发重复。这类细节在实际项目中价值特别大因为一旦跑定时任务重复数据会污染所有统计结果。4.2 为什么用SQLite而不是直接存CSV很多人抓完数据直接to_csv就完事这个做法在小项目里确实可以但数据分析过程中要反复清洗过滤CSV会频繁读写烦不胜烦。SQLite单文件数据库不需要安装服务端Python内置支持适合这种数据分析的中间存储。SQLite的好处体现在几个场景按类型过滤时用SQL语句一行搞定增量更新时靠UNIQUE自动去重数据量大时不需要全部加载进内存。CSV是最终的导出格式不是中间存储的最佳选择。数据入库后再读取统计逻辑放在SQL或者pandas里都行SQL负责过滤pandas负责聚合配合默契。4.3 数据分析评分分布与偏好挖掘抓完数据进入最核心的统计环节。第一件事是看整体评分分布import pandas as pd import sqlite3 conn sqlite3.connect(movies.db) df pd.read_sql_query(SELECT * FROM movie, conn) conn.close() print(df[rating].describe()) print(df[rating_count].describe())运行后会看到均值、中位数、最小值、最大值的差异这个差异本身就是洞察。如果均值是7.2、中位数是7.4说明低分剧被少数极端值拉低了。如果评分分布是双峰说明好片和烂片的评价惯性很强中间档反而少。评分和评价人数的相关性是观众偏好的核心指标corr df[rating].corr(df[rating_count])corr系数大于0.7说明评分越高人越多高口碑获得高关注平台生态比较健康。系数接近0说明评分和热度脱节可能存在“高分冷门、低分爆款”的情况。这个指标特别能反映一个平台的用户构成。再深一步按年份分组看评分变化趋势year_stats df.groupby(year)[rating].agg([mean, count]).dropna()这能揭示出观众评分行为的历史变化是越来越宽容还是越来越苛刻哪几年的剧质量波动大。这些都是后面可视化的内容基础。4.4 观众偏好分析从评分到口味标签观众偏好的本质是评分在不同维度上的倾斜。我的做法是构造一张“类型平均分”透视表genre_stats df.assign(genredf[genre].str.split(,)).explode(genre) genre_rating genre_stats.groupby(genre)[rating].agg([mean, count]).sort_values(mean, ascendingFalse)explode是pandas里非常顺手的一个函数可以把逗号分隔的类型拆成多行再groupby一个类型对应一个平均分和一个样本量。样本量少于10的类型平均分参考价值有限直接过滤掉genre_rating genre_rating[genre_rating[count] 10]这个过程反映了一个重要的分析逻辑样本量不够的结论不可信。题材平均分再高只有五部样本说明不了任何问题。把高分的样本量阈值加上结论才站得住。5. 可视化呈现5.1 评分分布直方图数据最后要变成图表才能让人一眼看懂。先画评分分布直方图import matplotlib.pyplot as plt plt.rcParams[font.sans-serif] [SimHei] plt.rcParams[axes.unicode_minus] False plt.hist(df[rating], bins20, edgecolorblack, alpha0.7) plt.xlabel(评分) plt.ylabel(电影数量) plt.title(影视剧评分分布) plt.show()matplotlib默认字体不支持中文如果不设置中文字体坐标轴上的中文标签会变成方框。Windows用SimHeimacOS用Arial Unicode MS这是最容易踩的坑之一。5.2 年份-评分趋势折线图可视化评分随时间的变化规律直接反映行业质量波动。评分均值按年聚合画折线图再把样本量也画成辅助柱状图一图两用fig, ax1 plt.subplots(figsize(12, 5)) ax2 ax1.twinx() ax1.plot(year_stats.index, year_stats[mean], colorred, markero, label平均分) ax1.set_ylabel(平均评分) ax2.bar(year_stats.index, year_stats[count], alpha0.3, label样本量) ax2.set_ylabel(样本数量) plt.title(历年影视剧评分趋势与样本量) plt.show()twinx是双坐标轴左轴放评分、右轴放样本量。这样能同时看到趋势和可信度某年均分特别高但样本量只有几个那就只是一个偶然现象。5.3 题材平均分横向柱状图题材偏好用横向柱状图展示最直观。类型名可能较长横向布局不会被截断genre_top genre_rating.sort_values(mean, ascendingFalse).head(10) genre_top.plot.barh() plt.xlabel(平均评分) plt.title(评分最高的10种影视题材) plt.show()横向柱状图有个细节pandas的plot.barh会让排名第一的类型显示在底部越往上排名越低。如果希望第一名在顶部就反转数据顺序。这个视觉细节全看个人习惯但统一逻辑能提升图表的专业感。5.4 评分-热度散点图散点图是揭示“高分爆款”和“高分冷门”分布的最佳工具。横轴是评价人数纵轴是评分每个点代表一部剧plt.scatter(df[rating_count], df[rating], alpha0.5) plt.xscale(log) plt.xlabel(评价人数对数刻度) plt.ylabel(评分) plt.title(影视剧评分与热度的关系) plt.show()横轴取log是必需的因为评价人数可能从几百到几十万跨度极大。不取log的话绝大多数点会挤在左下方。log变换后数据点的分布形状就清晰了。右上角是高评分高热度左上角是高评分低热度——高分冷门右下角是低分高热度——争议爆款三个区域各归各的。这个散点图是整个项目里信息密度最高的一张图一眼就能看出平台的观众偏好结构。真要做推荐系统的用户画像这个思路也可以直接迁移过去。6. 常见问题与排查技巧6.1 抓到的页面内容缺失或被拦截最常见的情况是requests拿到的HTML里没有预期数据。先用response.status_code检查状态码如果是200但页面结构对不上大概率是动态渲染页面requests拿到的只是空壳。解决思路有三个换数据源选择服务端渲染的页面。用Selenium或Playwright渲染JS后再解析成本高一些。找目标平台是否有公开的API接口有就直接请求接口最稳定。这里有个排查技巧把response.text保存成本地HTML文件用浏览器打开和正常访问的页面做对比。这个操作比debug时反复猜测高效得多一眼就能看出差在哪。6.2 中文乱码问题requests拿到的页面编码不一定是UTF-8有的站是GBK或者GB2312。直接使用response.text有时会乱码。用apparent_encoding让requests自动探测编码再解码resp.encoding resp.apparent_encoding值得注意的是这个自动探测在部分页面上会失效尤其是meta标签里没有明确charset的页面。备选方案是手动指定编码在目标页面源码里看一眼charset的值然后硬编码。6.3 数据库重复记录问题重复记录经常出现在重跑脚本时。解决的关键不是事后去重而是建表时就加上UNIQUE约束写入时用INSERT OR IGNORE。这个设计一劳永逸后面不管脚本怎么反复执行数据都不会膨胀。6.4 反爬被限制后的应对思路如果请求频率控制得合理一般不太容易被限。真被提示操作频繁了先停止、过段时间再跑这是最稳妥的做法。调整策略方面可以考虑增加随机延时区间、把User-Agent换成更真实的组合。关于代理IP个人学习项目不建议碰成本高、复杂度大而且合规风险不好把控。6.5 常见问题速查表问题表现解决方案中文乱码打印内容出现“锟斤拷”设置resp.encoding resp.apparent_encoding页面无数据状态码200但找不到剧名页面是动态渲染换数据源或改用渲染工具评分单位不统一“23.4万”“234万”“2340”混合写统一parse_count函数按单位换算重复数据重跑脚本后数据翻倍建表时加UNIQUE约束写入用INSERT OR IGNORE图表中文乱码坐标轴出现方框设置plt.rcParams[font.sans-serif]为SimHei频控触发返回403或要求验证降低频率延长随机sleep过段时间再试评价人数跨度大散点图数据挤在一起横轴取log刻度数据分布才清晰数据太少按年份分组后多数为空降低最低样本量阈值或扩大抓取范围7. 拓展思路这个项目还能往哪个方向走这套数据链路跑通之后深度学习过的工程能力可以迁移到很多场景。比如把目标从影视剧换成书籍、游戏、应用商店App整体逻辑不变只是解析规则要重新适配。这也正是爬虫技术最典型的应用方式一次掌握方法迁移到不同领域复用。如果一个影视剧分析项目想做得更完整可以考虑这几个方向基于评分数据做观众画像建模联合评论内容做文本情感分析挖掘不同题材观众群的关注焦点。引入时间维度做轮询采集观察评分随时间的波动比如“开播评分”和“完播评分”的差异。结合多个平台数据做交叉对比找出平台间评分差异大的剧目通常这就是争议片分析价值很高。在实战能力提升后遇到更大规模的采集需求可以了解分布式采集的思路但不要一开始就上先跑通单机再说。数据采集的边界感很重要。个人学习用的小批量抓取、控制频率、尊重平台规则这些都做到了项目的技术含量和合规性就都站得住。这个项目我自己跑过好几轮最大的体会是爬虫最难的部分从来不是请求页面而是把一堆脏数据分工明确地变成可分析的结构化数据并能从中得出有效结论。建议你把抓下来的数据从CSV导出后自己多尝试几个分析角度不要只停留在复现代码。换个维度画图表你可能会发现自己之前没注意到的有意思规律。