ARTICLE DETAIL

资讯详情

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

豆瓣电视剧数据抓取与分析:Python爬虫方案全拆解

豆瓣电视剧数据抓取与分析:Python爬虫方案全拆解 简介网络爬虫是数据采集的重要技术通过模拟浏览器请求与解析HTML可将公开网页中的结构化信息转化为可分析的数据集。其核心原理包括请求封装、页面解析与数据清洗借助requests和BeautifulSoup等轻量级工具即可实现。爬虫技术广泛应用于市场调研、内容研究和行业趋势观察尤其适合对垂直领域的数据进行批量采集与统计建模。以豆瓣电视剧评分数据为例结合评分、类型、年份等多维字段可以构建从数据抓取到可视化分析的全流程方案。本方案重点拆解了一个基于Python的豆瓣电视剧爬虫项目涵盖请求伪装、解析清洗、存储设计及评分分布等统计分析技巧为数据采集与数据分析实战提供可复用的参考。 豆瓣电视剧数据怎么抓怎么分析这套Python爬虫方案全拆给你看做数据分析和内容研究的朋友大概率都动过豆瓣的念头。尤其是电视剧这个品类豆瓣的评分、评价人数、类型标签、播出年份这些数据几乎是被公认的“行业参考系”。我前阵子正好做了一个基于Python的豆瓣电视剧爬虫与数据统计分析项目整套源码从请求、解析、存储到统计可视化都走了一遍。今天不卖关子直接把设计思路、核心代码、踩坑记录全部分享出来供想入门爬虫、或者正在做数据采集分析的同学参考。先说清楚这个项目能干什么抓取豆瓣电视剧评分榜的数据包括剧名、评分、评分人数、类型、主演、播出年份、集数等信息然后基于这些数据做统计分析比如评分分布、高分剧的特征规律、不同年份的剧集质量变化趋势等。源码里包含了完整的爬虫模块和统计可视化模块拿到手之后改一改请求参数、换一下存储路径就能直接跑出属于你自己的电视剧数据报告。适合Python基础扎实、想进阶爬虫和数据分析的开发者也适合做内容研究、市场调研的人拿来做数据素材。1. 项目整体设计与思路拆解1.1 为什么选豆瓣电视剧而不是电影很多爬虫入门项目都会选豆瓣电影因为结构简单、页面规整但实际做下来你会发现电视剧方向反而更有分析价值。电影的上映周期短票房、口碑的时效性很强数据波动大而电视剧的播出周期长评分沉淀相对稳定更有利于观察“口碑发酵”的过程。另外电视剧的标签体系更丰富类型、地区、语言、集数、首播年份这些字段组合起来能分析的角度非常多。我选择豆瓣还有一个重要原因它的数据结构在同类网站里算是对爬虫友好的。虽然它有反爬机制但只要控制好请求频率、伪装好请求头基本不会遇到太难的问题。对比过其他平台有的需要登录才能看榜单有的页面结构频繁变动还有的干脆就是JS渲染的动态页面抓起来费时费力。豆瓣的电视剧榜是服务端渲染的静态HTML用request加BeautifulSoup就能搞定对初学者和中级开发者来说都是比较合适的实战练手对象。1.2 技术选型requests加BeautifulSoup够用这个项目我用的核心库是requests、BeautifulSoup4、pandas、matplotlib。为什么没有用Scrapy因为数据量级摆在那里。电视剧榜单加起来也就几百条数据用Scrapy引入分布式爬虫、中间件、管道那一套完整框架属于杀鸡用牛刀。requests属于轻量级方案代码直白、调试方便出了问题一眼就能看到是网络层还是解析层。数据解析这块我选了BeautifulSoup而不是正则表达式或者XPath。BeautifulSoup对HTML结构的容错性很好豆瓣的页面虽然规整但也偶尔有些标签嵌套不标准的情况用BeautifulSoup可以避免很多正则匹配的崩溃瞬间。如果你对性能有极致要求可以考虑lxml解析器实测在BeautifulSoup里切换成lxml之后解析速度能提升一个量级。存储方面我最终选择了CSV加SQLite双轨方案。CSV的好处是直接用Excel或者pandas就能看数据分析起来方便SQLite的好处是可以重复查询、增量更新后续做定时任务抓取时不用每次都全量重写。源码里两个存储模块都写好了按需调用即可。2. 核心模块源码拆解2.1 请求模块Headers伪装与Session维持爬虫的第一步永远是请求。豆瓣对请求头比较敏感默认的Python-requests标识会被直接拒绝。我封装了一个请求模块重点处理了两件事Header伪装和Session维持。import requests import random def get_headers(): user_agents [ Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/121.0.0.0 Safari/537.36, Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36, Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Firefox/122.0, ] return { User-Agent: random.choice(user_agents), Accept: text/html,application/xhtmlxml,application/xml;q0.9,image/avif,image/webp,*/*;q0.8, Accept-Language: zh-CN,zh;q0.9,en;q0.8, Connection: keep-alive, Referer: https://movie.douban.com/, }为什么要随机切换User-Agent因为如果每次请求都是同一个浏览器标识短时间内高频访问很容易被识别为脚本行为。Random.choice从列表里随机挑一个能让请求看起来更像真实用户。Session的使用也很关键。requests.Session会保持TCP连接复用同时自动管理Cookies这样后续请求不需要每次都重新建立连接效率更高也更能模拟真实浏览器的会话行为。def create_session(): session requests.Session() session.headers.update(get_headers()) return session但这套Session不是万能的如果触发了豆瓣的安全风控比如请求频率太高对方会返回一个验证页面这时候光加Session是没用的需要配合5.3节讲到的限速策略。2.2 页面解析模块提取电视剧名称评分与评分人数拿到HTML之后就要开始解析。豆瓣的电视剧榜单页面结构比较固定每一条剧集信息都在一个grid_view结构的li标签里。我用BeautifulSoup定位到具体的class然后通过CSS选择器逐字段提取。from bs4 import BeautifulSoup def parse_drama_page(html): soup BeautifulSoup(html, lxml) items [] for li in soup.select(.grid_view li): try: title li.select_one(.title).get_text(stripTrue) rating_num li.select_one(.rating_num).get_text(stripTrue) pl_text li.select_one(.star .pl).get_text(stripTrue) # 评分人数格式(854623人评价) rating_count int(pl_text.replace((, ).replace(人评价), ).replace(,, )) # 其他信息从p标签中提取 info_text li.select_one(.bd p).get_text(stripTrue, separator ) # 类型、主演、地区等信息都在这段文本里按分隔符拆分 items.append({ title: title, rating: float(rating_num), rating_count: rating_count, info: info_text, }) except Exception as e: print(f解析单条数据失败: {e}) continue return items这段代码里有两个细节值得展开。第一个是.get_text(stripTrue, separator )的用法。如果只写stripTrue多个行内元素拼接时会变成一长串没有任何分隔的文本比如“主演: 胡歌 / 刘涛 / 王凯类型: 剧情 / 爱情”看起来是通顺的但实际解析时很难区分字段边界。加上separator 之后每个子标签的文本之间会插入一个空格更容易按规则切分。第二个是评分人数的清洗逻辑。豆瓣页面显示的分母数字是带千位分隔符的比如“854,623人评价”直接用int()转换会直接报错。我的做法是先移除括号字符串再replace掉所有的逗号最后才能安全转型。这个坑看起来小但非常典型几乎所有真实页面数据都需要做清洗加工不能指望原始数据直接可用。2.3 数据存储模块CSV与SQLite双轨落地采集完的数据不能老窝在内存里必须持久化。我同时实现了CSV和SQLite两种存储方案分别应对分析场景和增量积累场景。import csv import sqlite3 def save_to_csv(data_list, filenamedouban_tv.csv): if not data_list: return fieldnames list(data_list[0].keys()) with open(filename, w, newline, encodingutf-8-sig) as f: writer csv.DictWriter(f, fieldnamesfieldnames) writer.writeheader() writer.writerows(data_list) def save_to_sqlite(data_list, db_pathdouban.db): conn sqlite3.connect(db_path) cursor conn.cursor() cursor.execute( CREATE TABLE IF NOT EXISTS tv_drama ( id INTEGER PRIMARY KEY AUTOINCREMENT, title TEXT UNIQUE, rating REAL, rating_count INTEGER, info TEXT, create_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP ) ) for item in data_list: try: cursor.execute( INSERT OR IGNORE INTO tv_drama (title, rating, rating_count, info) VALUES (?, ?, ?, ?), (item[title], item[rating], item[rating_count], item[info]) ) except Exception as e: print(f写入数据库失败: {e}) conn.commit() conn.close()CSV这里我特意用了encodingutf-8-sig而不是默认的utf-8。原因很实际utf-8编码的CSV文件用Excel直接双击打开时中文会乱码而utf-8-sig会在文件头部自动添加BOM标记Excel能正确识别编码并显示中文。这个细节看起来微不足道但能省掉很多跟同事协作时的沟通成本。SQLite这里用了INSERT OR IGNORE搭配title UNIQUE约束来去重。这样即使重复跑爬虫脚本同一部剧也不会生成多行重复数据为后续做增量爬取打好了基础。UNIQUE约束的作用就是数据库级别的数据清洗比代码里先查一遍再插入要高效得多。3. 数据统计分析实战3.1 评分分布与评分人数关系数据抓下来之后真正的重头戏才开始。我用pandas把数据加载进来先看看整体的数据概貌。import pandas as pd df pd.read_csv(douban_tv.csv) print(df.describe())评分分布是最直观的分析维度。我先把rating列做成直方图看看高分剧和低分剧的分布形态。实际跑出来之后就发现豆瓣电视剧的评分分布是明显的右偏形态没有出现正常意义上的正态。大量剧集集中在6到7分区间8分以上属于优秀水平数量稀少9分以上更是凤毛麟角。这符合大众对豆瓣评分的直觉认知但用数据可视化呈现之后冲击感是完全不一样的。评分人数其实是一个更有意思的指标。高评分不一定等于高热度有些评分9分以上的冷门剧集评分人数可能只有几千反而是一些评分只有7分左右的热门剧评分人数能破十万。把这两个指标放在一起做散点图能明显看到评分数量的聚集轴。用相关系数算一下豆瓣电视剧的评分跟评价人数之间并没有强正相关这个结论和很多人“评分高所以看的人多”的直觉相悖。实际原因可能有两种一种是质量高但宣发不足的剧很难被大众看到另一种是数据存在幸存者偏差——烂剧看的人少、评分被沉默的群体稀释了。3.2 基于年份的剧集质量趋势观察把首播年份字段提取出来再按年份计算平均分就能画出一条豆瓣电视剧评分随年份变化的趋势线。实际操作中我发现单纯按年份聚合会有一个问题早年间的剧集样本量少只有那些口碑超好的老剧才会被收录到榜单里所以早期平均分天然虚高而近些年的剧样本丰富各种品类的剧都被收录了平均分被大量及格线徘徊的作品拉低了。这个现象在做时间序列分析时一定要留意不能直接得出“电视剧质量越来越差”的结论更稳妥的说法是“豆瓣榜单收录的作品数量变多评分方差变大”。如果要做得更严谨一点可以引入年份剧数量这个辅助指标画双轴图。这样就能看出早年剧少分高后来剧多分杂最近几年头部作品的质量天花板其实并没有下降。用数据挖掘的术语来说这属于辛普森悖论的轻度版本不做分组聚合的单变量趋势分析很容易得出偏颇结论。3.3 高分剧集的共同特征画像把评分大于等于8.5的剧集过滤出来做一个特征画像分析。我用字符串拆分的方法把类型、地区、主演三个维度拆开然后用collections的Counter做频次统计。from collections import Counter def extract_features(df, column, sep / ): all_features [] for value in df[column].dropna(): all_features.extend(str(value).split(sep)) return Counter(all_features) high_score_df df[df[rating] 8.5] genre_counter extract_features(high_score_df, genre) top_genres genre_counter.most_common(10)8.5分以上的剧集类型分布显示剧情、历史、家庭这三类出现频次最高而古装、言情类虽然数量众多进入高分区的比例相对较低。这说明豆瓣高分剧有一个共同特征剧情逻辑扎实、有现实厚度。这也从侧面印证了“口碑靠内容”的行业铁律——选题、剧本、人物塑造这些硬实力才是评分基础流量和话题度只能影响评价人数很难影响评分本身。4. 常见问题与排查技巧实录4.1 请求返回418或403的应对方案跑豆瓣爬虫最常见的问题就是请求被拒具体表现是HTTP状态码418或403。418状态码很有意思这是IETF在1998年愚人节定义的状态码本意是“我是一个茶壶不能给你泡咖啡”但实际使用中很多反爬系统用它来标识“请求不合法”。我的经验是用三重保障来应对第一是Header伪装必须在请求头和协议头两个维度都到位只加User-Agent是不够的Accept、Accept-Language、Referer这些字段也要一起搞。第二是请求频率必须控制住。豆瓣的反爬阈值大体上在每秒1到2次超过这个频率持续几十秒就会被封IP一段时间。我在代码里用time.sleep(random.uniform(1, 3))做随机延迟一来降低请求压力二来让请求间隔看起来更像真实用户。第三是异常捕捉和重试机制。网络环境不可能100%稳定遇到4xx或5xx状态码时先等几秒然后重试。超过两次重试就跳过这条数据记录到日志中不阻塞整体流程。4.2 编码问题与数据清洗豆瓣页面的编码是UTF-8但偶尔页面里会有一些特殊字符比如全角空格、换行符、多余的制表符。直接用strip()处理不了全角空格需要用正则替换掉空白字符。import re def clean_text(text): if not text: return text text.replace(\xa0, ).replace(\u3000, ) text re.sub(r\s, , text) return text.strip()\xa0是不换行空格在HTML里代表nbsp;Python中如果直接用is_space检查是False的非常容易漏网。\u3000是全角空格常见于中文排版。这两个字符不清理干净做字符串匹配和数据分析的时候会出现很多诡异的问题。数据清洗还有一个容易被忽略的点类型字段的多标签拆分。豆瓣电视剧的类型字段通常是“剧情 / 爱情 / 历史”这种以斜杠分隔的格式如果不拆分就会把整串文本当成一个独立类别进行统计导致数据分析完全失真。正确做法是先按分隔符拆开再逐个清洗和计数。4.3 抓取数据量与维度不足时的应对豆瓣频道里往往会有“全部”“按类型浏览”“按年份浏览”等多个入口有些入口的数据量很大另一些入口数据少。如果只爬一个页面数据维度会非常窄做出来的统计结论代表性不足。我的源码里扩展了参数化的爬取逻辑通过构造不同URL分别抓取“按评分排序”和“按热度排序”两个维度的数据。这样在评分之外还能得到电视剧的排名变化、热度表现等信息。拼合起来之后统计样本的覆盖广度和说服力都提高了。这个过程遇到页面结构不一致的概率很高。不同入口页面的HTML结构虽然大体类似但细节上可能使用了不同的class名或标签层级如果只用一套选择器去解析很容易漏数据或报错。稳妥的做法是先看一下目标页面的结构差异再分别写对应的解析逻辑解析完毕后统一成相同的schema再合并存储。4.4 爬虫项目常见问题速查表现象可能原因解决方案请求返回403请求头不完整或IP被限制补全Headers降低请求频率等待一段时间再试请求返回418被识别为脚本行为随机UA增加延时尝试换代理可选返回页面显示验证码访问频率过高触发风控减少并发数增加随机延时尝试增加cookie解析结果为空页面结构变了或selector选择错误在浏览器中重新检查HTML结构更新选择器中文写进CSV乱码编码格式不对使用utf-8-sig编码写入评分人数转换报错原始文本包含分隔符或括号清洗后再转换使用正则提取数字数据库重复数据没有唯一约束加UNIQUE字段使用INSERT OR IGNORE排行榜页数据稀缺只爬了单一维度入口增加“按类型”“按年份”等可选参数多次抓取5. 源码的扩展方向与从业心得5.1 从批量爬虫升级为增量式爬虫目前的源码设计是一次性批量抓取目标页面的数据。如果想把它做成长期运行的定时任务比如每周跟踪一次排行榜变化就需要升级成增量式爬虫。思路是每次抓取前先从数据库里查询已存在的电视剧标题集合抓取新数据时只看那些标题不在集合里的作品或者总评分人数有变化的作品。刚才SQLite模块里设计的title UNIQUE字段就是为这个功能预留的。接口上还可以增加cache和last_modified字段用于跟踪数据更新时间这样每次入库前就能方便地判断这一条数据是否需要更新。增量型爬虫对榜单变化类项目特别有意义。评分会浮动排名会变动如果每次都是全量重抓不仅慢而且数据对比会失去基础。增量思路让每一次运行都只关注新变化长期积累下来就能形成完整的榜单演化史。5.2 多线程与异步优化的边界这个项目的数据量不算大单线程足够。但如果把目标网域扩展到“按类型浏览”的全部页面几百页甚至上千页的请求量级单线程就会显得捉襟见肘。初学者到这里容易犯一个错误一提到性能优化就上多线程。但实际上豆瓣这类有风控的网站性能瓶颈往往不在代码逻辑而在对方的接口限制。盲目开多线程只会导致并发请求过载触发更严格的风控策略结果就是什么都抓不到。我的建议是先单线程跑通全流程确认数据质量无误再考虑用concurrent.futures.ThreadPoolExecutor控制并发数比如同时3到5个线程并配合一个统一的限速器来保证每个线程请求间隔不低于1秒。对于更大的数据量可以尝试aiohttp做异步Request但复杂度会明显上升收益未必成正比。5.3 爬虫的自我约束与合规思考最后想认真聊聊爬虫的边界问题。豆瓣的用户协议明确禁止未经授权的爬取所以在实际开发中要注意几个原则一是控制频率频率越低越安全同时也越能体现对目标网站的尊重。二是只用于个人学习与研究不用于商业牟利不批量导出他人数据进行二次分发。三是设置数据保存期限不长期囤积不必要的用户数据。四是保护隐私只收集用于分析的最小必要信息。我每次跑这个项目都会明确告诉自己这是学习数据采集与分析技术的练习样本不是把豆瓣数据搬回家的理由。在这个前提下爬虫技术本身是值得深入学习的基础技能它和任何工具一样关键在于怎么用。5.4 编程之外的收获数据思维做这个项目最大的体会其实不在代码本身而在于数据思维的建立。同样一堆评分数据放在列表里就是几行数字做成了分布图和趋势线就能看到电视剧行业的格局变化再结合类型、年代、评分人数能分析出很多有意思的结论。之前我总觉得写代码和做分析是两件事。但在这个项目里爬虫只是前菜真正硬核的部分是怎么把原始数据转化成有效信息的思路。比如在筛选高分剧集时是选8.5分以上还是8.0分以上这个阈值怎么定会直接影响后面的分析结果。尝试用8.0作为阈值的话样本量大增但高分区特征会被大量8分左右的剧集稀释用9.0的话样本太少不够统计。最后定在8.5是一个平衡点既要保证样本量又要能体现出高分剧的特征差异。这类决策在数据分析里非常常见没有标准答案只有基于场景的权衡。能做出一套清晰的决策依据比代码写得漂亮更重要。本文还有配套的精品资源点击获取
返回列表