Python爬虫实战:豆瓣电视剧数据采集与可视化分析

发布时间:2026/8/31 5:50:21
Python爬虫实战:豆瓣电视剧数据采集与可视化分析 简介本资源是一套面向Python初学者与数据分析爱好者的豆瓣电视剧数据采集与可视化分析实战项目解决影视类数据获取难、分析流程不完整等实际问题适用于课程设计、毕业设计或自学进阶场景。压缩包共31个文件含17个核心Python脚本覆盖爬虫调度、HTML解析、数据清洗、统计建模与图表绘制、6张PNG可视化图表如各国电视剧平均分、分类数量分布、7分以上作品时间散点图等、2个结构化CSV数据文件电视剧基础信息与用户评论、2张JPG示意图及配套配置与说明文档整体仅346KB轻量易部署。已有391人学习下载提供从requests/BeautifulSoup网络请求、pandas数据处理到matplotlib/seaborn图表生成的完整链路代码目录按spider/data_visualization等模块清晰划分并内置反爬策略模拟与数据格式标准化逻辑可直接运行复现分析结果。 想追剧的时候打开豆瓣榜单、评分、短评摆了一大堆可信息越丰富越难做决定——评分高的人气未必高人气高的质量未必稳想看口碑和热度交叉验证的结论几乎得手动翻几十页。后来我实在忍不了这种原始操作索性用Python写了一个豆瓣电视剧爬虫把电视剧的基础信息、评分、评分人数、类型、地区这些字段抓下来落库再用pandas做统计分析最后用matplotlib出图。这个项目跑通之后我发现它特别适合拿来练手爬虫数据清洗统计分析的完整链路所以整理了一篇详细的实战记录从反爬策略到源码结构都摊开讲。这个项目不追求抓几万条数据核心目标是把一条链路走通豆瓣电视剧列表页的批量抓取、详情页的信息补全、SQLite存储、pandas聚合分析、可视化图表输出。适合已经有Python基础、想系统接触爬虫与数据分析的读者也适合需要一份能看懂能改的课程设计源码的人。我把项目拆成七个部分来讲每一部分都有对应的代码思路和实际踩坑记录。1. 项目立项为什么偏偏选豆瓣电视剧来练手1.1 需求来源数据化选剧的朴素愿望这个项目的缘起其实特别简单。我平时补剧主要靠豆瓣但豆瓣的页面结构是分类浏览站内搜索想回答近五年哪类国产剧评分最稳某年口碑剧和热度剧的重合度有多高这种问题靠人眼翻页几乎不可能完成。既然数据已经公开挂在网页上了那就让爬虫替我做这个重复劳动。电视剧数据和电影数据不一样它有几个更适合练手的特征字段更丰富集数、首播年份、制片地区、类型、主演类别层级更清晰而且列表页到详情页的跳转结构很适合演示调度器的写法。用电视剧这个品类做爬虫既不会像电影那样数据量过大导致频繁触发反爬又比冷门品类更有统计意义。1.2 为什么是电视剧而不是电影如果有人想复现同类项目我会建议首选用电视剧而不是电影来练手原因有三。第一电视剧的单页信息密度足够高。一个列表页就有几百条剧集的可视化概览抓取效率高。第二电视剧的字段结构比电影更适合做统计分析——集数首播年份单集片长这些维度可以直接参与数值分析而电影的核心字段相对集中。第三电视剧的更新频率相对可控增量爬虫的演示效果更好每次跑一遍只抓新增部分数据管理更贴近真实生产环境。1.3 项目边界不追求大数据量追求完整链路在动手之前我给这个项目立了几个边界数据量控制在千条级别、抓取频率保持在每秒一两条、不追求突破反爬极限、统计分析用最常见的描述性统计方法。这很重要。很多爬虫项目死在想抓太多上——并发调高、抓取范围铺太大结果触发封禁项目烂尾。我的做法是先把完整链路跑通一次再决定要不要扩展。2. 豆瓣反爬机制拆解你以为的难点未必是难点2.1 豆瓣究竟在防什么很多人第一次写豆瓣爬虫最担心的就是被封。其实豆瓣的防护逻辑并不神秘它的核心目标不是完全不让爬虫抓而是识别出非人类行为的请求特征。换句话说你的请求如果表现得像一个正常用户豆瓣根本不会理你。豆瓣的常规反爬手段包括User-Agent合法性校验、请求频率统计、单IP单位时间内的请求数监控以及极端情况下的验证码和IP封禁。初学者最容易犯的错是用默认的requests库UA比如python-requests/2.28.1这种特征在服务端日志里非常扎眼。我见过不少人拿到403就以为是IP被封实际上一换UA就恢复了。2.2 User-Agent与请求频率最基础的生存线反爬的首要解决方案不是IP代理而是伪装成正常浏览器。我把请求头单独拎出来管理每次请求带上完整的浏览器指纹包括UA、Accept、Accept-Language、Referer其中Referer一定要带豆瓣对直接访问列表页的请求会留下明显痕迹。第二个关键点是请求频率。正常用户浏览豆瓣肯定不是每秒钟点一次列表链接。我的做法是在请求之间加入随机延时延时间隔取1到3秒的随机值避免形成固定节奏。有些教程喜欢固定延时两秒这种固定间隔反而容易被识别因为真实用户的操作间隔不可能精准地永远等于两秒。2.3 验证码与封禁遇到之后怎么办即便做了伪装高频请求仍然可能触发验证码或者403。遇到这种情况我的处理原则很简单不硬扛先退。具体来说如果连续出现几个403优先停止当前抓取任务进入等待状态间隔拉长到5到10分钟后再恢复。验证码页面的特征是返回的HTML里包含验证码相关的关键词或URL标识识别到这个特征就直接放弃当前请求不要尝试去破解验证码——个人学习项目不值得冒这个风险。IP封禁是最后一道防护一旦被限制通常的表现是所有请求都返回403。我的对策是降低每日抓取总量把任务拆成多个会话执行而不是一次跑完。2.4 合规红线robots.txt与君子协议个人学习爬虫也必须注意合规问题。豆瓣的robots.txt对爬虫有明确限制作为个人项目应当自觉控制抓取频率和抓取范围不把抓取数据用于商业用途。在源码注释和文档里我也明确标注了这个项目的学习用途。这一点建议所有复现此项目的读者都保留——爬虫的底线是别给目标服务器制造压力别拿数据做越界的事。3. 爬虫核心模块实现从请求到解析的完整链路3.1 字段设计先想好要什么再去抓什么写爬虫最容易忽略的是字段设计。很多人一上来就写requests.get抓到HTML再说要什么字段结果解析的时候才知道有些字段在列表页有、详情页才有再去补写逻辑来回折腾。我的做法是先设计好表结构再写爬虫。项目里的核心表是电视剧基本信息表字段如下字段名类型说明idINTEGER自增主键titleTEXT剧集名称ratingREAL豆瓣评分可能为空rating_peopleINTEGER评分人数yearINTEGER首播年份regionTEXT制片国家/地区genreTEXT类型多个类型用逗号分隔directorTEXT导演actorsTEXT主演episode_countINTEGER集数aired_dateTEXT首播日期urlTEXT UNIQUE详情页URL用于去重确定字段之后列表页只需要抓title、rating、rating_people、url其余字段交给详情页解析。这样职责清晰列表页负责批量获取入口信息详情页负责补全细节。3.2 请求模块重试机制与随机延时的配合请求模块是爬虫的地基。我封装了一个带重试机制的请求函数核心逻辑是正常返回200就直接返回文本遇到403就切换UA增大休眠时间遇到网络异常或超时就重试最多重试三次。import requests import time import random from requests.exceptions import RequestException BASE_HEADERS { User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124.0.0.0 Safari/537.36, Accept: text/html,application/xhtmlxml,application/xml;q0.9,image/webp,*/*;q0.8, Accept-Language: zh-CN,zh;q0.9,en;q0.8, Referer: https://movie.douban.com/ } def fetch_html(url, retries3): headers dict(BASE_HEADERS) for attempt in range(retries): try: resp requests.get(url, headersheaders, timeout10) if resp.status_code 200: return resp.text if resp.status_code 403: print(f[!] 触发访问限制等待恢复: {url}) time.sleep(random.uniform(8, 15)) continue print(f[!] HTTP {resp.status_code}: {url}) except RequestException as e: print(f[!] 请求异常: {e}) time.sleep(random.uniform(1, 3)) return None这段代码的关键在于退避策略。每一次失败后的等待时间都比上一次更长给服务端的频率检测一个恢复窗口。实际运行中这种带随机退避的请求方式比固定延时稳健得多。3.3 解析模块选择器与容错处理解析模块我用的BeautifulSoup加lxml解析器。豆瓣页面的HTML结构比较规范class命名基本稳定但选择器编写的时候要留一手——页面结构会发生微调所以每一处解析都做了兜底处理。解析列表页的核心逻辑是定位剧集条目节点再从每个条目里提取标题、评分和评分人数。代码骨架如下from bs4 import BeautifulSoup def parse_list_page(html): soup BeautifulSoup(html, lxml) items soup.select(.list-item) if not items: # 页面结构变化时降级到另一种选择器 items soup.select(.item) results [] for item in items: title_node item.select_one(.title a) rating_node item.select_one(.rating .rating_num) people_node item.select_one(.rating .people_count) url_node item.select_one(a.nbg) if not title_node or not url_node: continue results.append({ title: title_node.get_text(stripTrue), rating: float(rating_node.get_text(stripTrue)) if rating_node else None, rating_people: parse_people_count(people_node.get_text(stripTrue)) if people_node else 0, url: url_node[href] }) return results这里有个很容易踩的坑评分人数在页面上显示的是12345人评价直接存字符串没法参与数值计算必须提取数字。parse_people_count这个函数就是干这个的用正则提取第一个数字序列并转成整数。如果转换失败就返回0保证字段类型统一。详情页的解析逻辑类似难点在于有的剧没有评分比如尚未开播有的剧没有集数信息比如单集综艺这些空值在解析阶段就统一填成None不强行造数据。3.4 调度策略列表页到详情页的衔接总体调度逻辑是访问多个分类的列表页先拿到一批剧集的URL再通过详情页补全数据。调度器的关键在于去重。豆瓣列表页之间有重叠重复抓同一个详情页不仅浪费请求次数还可能因为多余请求引发限流。所以在调度器里维护一个cached_urls集合已经抓过的URL直接跳过。具体去重逻辑我放在存储层判断数据库层用url UNIQUE约束做兜底插入重复数据时用INSERT OR IGNORE跳过。4. 数据落库与预处理清洗环节比想象中更花时间4.1 数据库选型SQLite对个人项目足够对这个项目的数据量级——千条级别的电视剧数据——SQLite是性价比最高的选择。它不需要单独部署数据库服务Python标准库直接支持所有数据存储在一个文件里迁移备份都方便。用MySQL反而增加了环境依赖对想复现源码的读者不友好。建表语句和插入逻辑我在项目里封装成了一个database.py模块对外暴露两个核心函数init_db()负责建表save_tv_item()负责插入或更新单条记录。插入时依赖INSERT OR IGNORE配合url的唯一约束天然实现去重。4.2 字段级清洗字符串解析与空值处理拿到原始数据以后真正花时间的是清洗。我遇到的最大坑是episode_count字段——豆瓣页面上有的写的是24集、有的是全24集、有的干脆没有集数信息。针对这种情况我用正则先提取纯数字提取不到就置为None不让脏数据混进统计。import re def parse_episode_count(raw): if not raw: return None digits re.search(r\d, raw) return int(digits.group()) if digits else Noneregion字段的处理逻辑也类似。豆瓣上有的剧标了多个制片地区比如中国大陆 / 中国香港存储时我保留了原始字符串分词任务交给统计阶段处理。genre字段同样保留多值字符串统计时再拆分。4.3 去重与增量更新个人爬虫项目最容易忽略的是增量更新。如果爬虫每次运行都全量重跑数据量小的时候没问题但总有一天会遇到需要重新抓取的情况。我的做法是为url字段建立唯一索引插入之前先检查该URL是否已经存在。这样爬虫每次运行只抓取新增剧集不需要清空旧数据。对于评分变化我采用覆盖更新的策略如果同一个URL已经存在再次爬取时更新评分和评分人数。这个策略保证了统计分析的数据是相对新鲜的。5. 统计分析模块从评分到口碑的五个分析维度5.1 年度评分走势电视剧质量在变好吗数据落库之后分析模块就派上用场了。最直接的分析是按首播年份聚合评分均值看看电视剧质量有没有随时间变化。import pandas as pd import sqlite3 conn sqlite3.connect(douban_tv.sqlite) df pd.read_sql_query(SELECT * FROM tv_douban, conn) df[year] pd.to_numeric(df[year], errorscoerce) year_stats df.dropna(subset[year, rating]).groupby(year)[rating].agg([mean, count, median]) print(year_stats.tail(10))这个分析的结果可能会出乎意料——有些年份评分均值高不是因为那年剧更好而是因为评分人数少、打分基数小。所以分析的时候不能只看均值要结合count列看样本量。样本量少于10的年份均值的参考价值很有限我在出图时会对这类数据进行过滤。5.2 类型分布哪种类型口碑最稳类型的统计需要先把逗号分隔的多值字段拆开再逐类型聚合。pandas里可以用explode方法处理type_df df.assign(genredf[genre].str.split( / )).explode(genre) type_stats type_df.dropna(subset[genre, rating]).groupby(genre)[rating].agg([mean, count]).sort_values(count, ascendingFalse)这里要注意str.split的分隔符是/还是/取决于清洗阶段的原始数据格式。豆瓣页面展示类型时用的是空格分隔实际解析时我统一转成了/分隔避免统计时出现剧情和剧情 这种近似重复的类别。5.3 地区对比不同产地的口碑差异地区维度可以单独做一张统计表按制片地区分组计算评分均值和评分人数均值。这个维度在可视化时适合做横向条形图一眼就能看出哪个地区的剧集口碑更高。分析的时候我会把多地区联合制片的剧单独归为一个联合制片类别避免重复计数。5.4 评分人数与评分的相关性沉默的大多数这个分析是项目里最有意思的部分。高分剧是不是一定热门评分人数是不是评分的可靠代理变量用相关系数可以验证import numpy as np sub df.dropna(subset[rating, rating_people]) corr np.corrcoef(sub[rating], np.log1p(sub[rating_people]))[0, 1]我加了log变换处理评分人数的长尾分布。几部冷门小剧场剧评分很高但评分人数只有几十而一些现象级剧评分可能只有7分左右评分人数却有几万。这种差异恰恰说明口碑和热度是两码事做榜单的时候两者都要参考。5.5 高分剧筛选用数据对抗榜单失真最后一个分析维度是筛选高分剧。简单按评分排序会有偏差——评分人数极少的剧可能以9.5分排在最前面参考价值有限。我的做法是加一个人气过滤阈值只保留评分人数超过阈值的剧再进行排序。这个阈值可以自己定比如1000人。在此基础上还可以计算一个加权评分公式可以简单用加权分 rating * sqrt(min(rating_people, 5000)) / sqrt(5000)这种经验公式让高人气剧集排名稍微靠前。这种加权方式不是标准的贝叶斯平均但作为个人分析足够直观。6. 可视化的呈现让统计结果自己会说话6.1 图表选型什么结论配什么图统计分析的结论需要用图表表达这部分的选型逻辑是趋势用折线图、分布用直方图、排名用横向条形图、相关性用散点图。我给项目配了四张核心图表年度评分均值折线图反映口碑走势评分分布直方图反映整体数据形态评分与评分人数散点图反映口碑与热度的关系高分剧Top10横向条形图直观展示推荐结果6.2 中文乱码与样式调整用matplotlib画图的老问题——中文乱码我在项目里直接指定了中文字体。Windows环境下用的黑体macOS环境下用的是Arial Unicode MS写的时候做了兼容处理。import matplotlib.pyplot as plt plt.rcParams[font.sans-serif] [SimHei, Arial Unicode MS] plt.rcParams[axes.unicode_minus] False散点图呈现评分与评分人数关系时因为评分人数的量级差异太大直接用原始数据画图会挤成一团。我把它转成rating_people的对数后再画图形分布会合理很多。6.3 导出HTML报告最后我把四张图表汇总到一个HTML报告里加上关键统计数字的文字说明形成一份可以分享的结果文件。生成方式很简单用matplotlib先把图表保存成PNG图片然后用字符串模板拼一个HTML页面把图片嵌进去。比直接用jupyter notebook更轻量也更适合没有Python环境的读者直接打开浏览。7. 项目源码结构复盘哪些代码值得沉淀成模板7.1 整体目录设计与模块职责整个项目的源码组织方式是这样的douban_tv_pipeline/ ├── config.py # UA、延时区间、目标URL等配置 ├── spider/ │ ├── crawler.py # 请求模块fetch_html │ ├── parser.py # 解析模块列表页/详情页解析 │ └── scheduler.py # 调度模块控制抓取流程 ├── storage/ │ └── database.py # SQLite建表与存储 ├── analysis/ │ ├── stats.py # 统计分析逻辑 │ └── charts.py # 图表生成 ├── data/ │ └── douban_tv.sqlite ├── output/ │ └── reports/ ├── main.py # 总入口爬取→清洗→分析→出图 └── requirements.txt # requests, beautifulsoup4, lxml, pandas, matplotlib这个结构最大的优点是模块边界清晰换数据源时可以复用大部分代码。比如把豆瓣换成某个公开图书榜单只需要改config.py里的URL、parser.py里的选择器然后微调database.py里的字段爬虫和分析代码基本不用动。7.2 复用与扩展换一个数据源需要改哪里如果读者想把这个项目复用到其他网站我建议按这个顺序改代码先改字段设计再看列表页和详情页的HTML结构重写parser.py的选择器逻辑最后调整stats.py的分析维度。其他模块包括请求重试、去重存储、图表生成都可以原样保留。唯一要重写的是scheduler.py里的URL构造逻辑。豆瓣列表页的翻页URL规则是?start0limit20这种偏移量翻页换个网站可能变成?page2这种页码翻页这部分必须改。7.3 常见问题排查表写项目过程中遇到过不少问题我整理成一张排查表方便复现项目的读者对照现象可能原因解决方案请求全部返回403UA被识别或请求频率过高更新UA放慢频率等待解除限制解析结果为空页面结构变化或选择器失效重新审查HTML结构更新选择器评分数据为0页面未正确渲染或字段被反爬混淆确认请求头完整补充解析兜底逻辑中文字体乱码matplotlib字体配置缺失更新plt.rcParams中的字体设置数据重复去重逻辑失效检查url唯一约束和INSERT OR IGNORE逻辑结尾一点实操体会这个项目从头到尾跑通我最深的体会是爬虫项目里抓数据只占三成工作量剩下七成都在清洗和统计。很多人写爬虫只写到能抓回页面就停了觉得后面的分析没什么技术含量但实际上数据分析才是让爬虫产生价值的环节——同样是抓一千条数据有人只能拿到一个数据文件有人却能得出近年来某地区剧集口碑总体走稳但热度与口碑无明显相关这样可验证的结论差距就在统计分析这里。如果你想用这个项目练手我的建议是先照源码把流程跑通然后试着改两个地方一是换一个分类维度抓数据比如换成综艺、动漫或电影二是增加一个分析维度比如抓取电视剧的短评内容做简单的文本情感分析。这些扩展都不需要动架构却能让你真正理解每个模块的边界。爬虫和数据分析本来就是一项技能的两面这个项目最大的价值就是帮你把这两面焊在一起。本文还有配套的精品资源点击获取