B站青少年模式数据分析:Python从采集到可视化全流程
别小看“青少年模式”这几个字它既是B站产品功能里的一个开关也是大量家长、教育从业者和未成年人内容创作者反复讨论的焦点。我做的这个项目就是用Python去采集B站上与“青少年模式”相关的视频数据再做一套数据分析与可视化系统把“谁在聊这个话题、聊了什么、热度怎么变化、哪些内容互动更高”这些问题用图表直观地呈现出来。项目整体涉及B站接口采集、数据清洗、多维度分析和Flaskpyecharts的Web可视化下面把完整的设计思路和实操过程都摊开来写希望能给做数据分析实战、毕业设计或者产品调研的同学一个可复现的参考。1. 项目整体设计与思路拆解1.1 核心需求拆解三个问题决定项目形态任何一个数据分析项目动手写代码之前最怕的就是思路不清。我拿到这个题目之后第一件事不是装环境、调接口而是先问了自己三个问题。第一个问题数据从哪里来要采哪些字段B站没有直接开放“青少年模式”的使用统计后台但用户关于这个功能的讨论都集中体现在视频的标题、简介、评论和播放数据里。通过B站的搜索接口按“青少年模式”这个话题关键词去检索视频就能拿到一批高度相关的内容记录包括标题、UP主、播放量、点赞数、发布时间、视频分区等。这些字段足以支撑后续的趋势分析、内容聚类和互动效果分析实用性很强。第二个问题分析哪些维度才能回答真实业务问题如果只是把视频列表展示出来那不叫数据分析叫爬虫。我真正想回答的是这几件事青少年模式这个话题的热度随时间怎么变化、用户更关心它的哪个侧面比如时长限制、内容过滤、家长控制、使用教程、哪些UP主在持续产出这类内容、什么样的内容形态更容易引爆互动。这些维度对应了后面数据表里的时间字段、标题文本、UP主信息和播放互动字段一环扣一环。第三个问题结果如何呈现给非技术读者原始的数据表只有我自己看得懂要把它变成能用的分析工具需要一套可视化界面。我选择了Web化的方式用Flask启动一个本地服务通过pyecharts生成可交互的图表页面让使用的人通过浏览器就能查看分析结果。这三个问题想清楚之后项目的技术路线也就自然浮出水面了接下来就是选型和落地。1.2 技术选型的取舍逻辑技术选型这件事我见过太多人一上来就上重框架其实完全没必要。这个项目的数据量级在几万条以内单机处理绰绰有余所以选型的主基调就是“轻量、快速、够用”。语言层面Python 3.8 是确定的原因很直白数据处理生态太成熟了requests做采集、pandas做清洗和分析、pyecharts做可视化全部都是现成的轮子不需要造任何重复代码。有人会问为什么不用Scrapy我的想法是这个项目采集的任务量并不大搜索接口翻页几十次就能达到样本量要求Scrapy的并发调度优势完全发挥不出来反而会让代码结构复杂化。用requests加循环延时代码直观、可控性强出了问题也好排查。数据库方面我选了SQLite而不是MySQL理由是项目是单机应用数据量不大SQLite一个文件就能搞定不需要安装数据库服务端把DB文件放到项目目录里就能读写对后期迁移和分享都友好。等你真的需要多人并发访问或者数据量破百万再换MySQL也不迟但在这一阶段SQLite是最省心的方案。可视化层我用了pyecharts配合Flask。pyecharts的优点在于它生成的是HTML和JavaScript底层是ECharts图表交互能力强同时Python代码可以直接生成完整网页不用自己写前端。Flask则负责把这些图表页面组织成一个完整的Web应用通过路由来切换不同维度的分析页面。这个组合是我反复比对后的结果比用Matplotlib画的静态图片信息量更大又比完全前后端分离的方案简单得多。1.3 系统架构与信息流转整个系统的结构我按照“采集层、存储层、分析层、展示层”四层来组织每一层各干各的活边界非常清楚。采集层是一个独立的Python脚本它负责构造搜索请求、发送HTTP请求、解析返回的JSON数据并把有用的字段整理成结构化记录。存储层就是SQLite数据库里面建了一张videos表保存所有采集到的视频明细。分析层是我的核心数据处理环节通常用pandas把数据库的表读成DataFrame再进行分组聚合、关键词统计、相关计算输出汇总后的结果表。展示层则由Flask和pyecharts负责Flask提供路由和模板把pyecharts生成的图表嵌入到网页中。数据流动的方向很明确脚本采集原始数据入库pandas从库里取数做分析分析结果交给pyecharts转成图表最后通过Flask渲染到浏览器。这样的分层设计最大的好处是每一层都可以独立替换和调试。比如你以后想换MySQL只需要修改存储层的连接方式采集和分析代码都不用大动。我在项目开发过程中也是先跑通采集入库再单独调试分析代码最后才做可视化整合每一步的问题都能快速定位不会出现“一锅粥”的情况。2. 数据采集层从B站接口到本地数据库2.1 采集字段设计采集字段是整个分析的基础字段选得好不好直接决定了后续分析能做得多深。我在这个项目里一开始就确定了必须包含以下信息视频标题、视频简介、UP主名称、播放量、点赞数、投币数、收藏数、评论数、视频时长秒、发布时间Unix时间戳以及视频分区标签。这里要说一下为什么特别关注互动数据。单纯看播放量只能说明“看到的人多”但不能说明“看完的人是否认可”。点赞、投币、收藏、评论这些指标组合起来才能反映内容对用户的真实触动力。尤其对于“青少年模式”这种本身就带有争议性和实用讨论价值的话题用户愿不愿意点赞、愿不愿意在评论区争论都是衡量内容质量与话题热度的重要信号。另外我把视频的bvidB站视频唯一ID也存了下来它的作用是去重。B站的搜索结果可能存在重复项用bvid做唯一键数据清洗阶段就能很方便地排除重复记录。没有这个字段后面去重还得靠标题和UP主联合判断麻烦很多。2.2 搜索接口请求与解析B站有一个常用的搜索接口路径是https://api.bilibili.com/x/web-interface/search/type通过GET参数指定搜索类型和关键词。对于视频搜索核心参数是search_typevideo和keyword另外用page控制翻页page_size控制每页条数。构造请求的时候有几个关键细节很容易踩坑。第一必须带上合适的请求头尤其是User-Agent和Referer。User-Agent需要伪装成浏览器否则很容易被识别为脚本请求Referer最好指向B站首页因为B站部分接口会做来源校验。第二搜索接口通常需要登录Cookie才能稳定返回结果否则可能触发风控。我在代码里是把浏览器里复制出来的Cookie放进请求头里的实测下来稳定不少。第三请求间隔不能太短我每翻一页都加了一个1到2秒的随机延时一方面是为了降低平台压力另一方面也是实际工程经验——接口一旦返回-412风控错误等待解封的时间成本远高于你省下来的那几秒钟。解析逻辑也不复杂请求返回的是JSON结构视频列表嵌在data.result这个数组里每个元素是一个视频对象里面就有标题、播放量、发布时间这些字段。有一个细节是标题字段经常带HTML高亮标签比如em classkeyword青少年模式/em入库前需要把这些标签清洗掉。我写了一个简单的正则替换函数把.*?直接剔除。播放量这个字段也需要注意有时候接口返回的是整数有时候显示“--”这些都要在清洗阶段统一处理。下面是采集模块的核心代码结构我贴出来作为一个可运行的参考模板import requests import json import time import random import re # 请求头需要按实际情况补充Cookie HEADERS { User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36, Referer: https://www.bilibili.com/, Cookie: 你的登录Cookie } def clean_title(title): 去掉标题中的HTML高亮标签 return re.sub(r.*?, , title) def fetch_page(keyword, page): 获取某一页的视频搜索结果 url https://api.bilibili.com/x/web-interface/search/type params { search_type: video, keyword: keyword, page: page, page_size: 42 } resp requests.get(url, paramsparams, headersHEADERS, timeout10) if resp.status_code ! 200: print(f请求失败: {resp.status_code}) return [] data resp.json() if data.get(code) ! 0: print(f接口异常: {data.get(message)}) return [] return data[data][result] or [] def collect_data(keyword, max_pages): 批量采集搜索结果返回结构化数据列表 records [] for page in range(1, max_pages 1): results fetch_page(keyword, page) for item in results: records.append({ bvid: item.get(bvid), title: clean_title(item.get(title, )), author: item.get(author, ), play: item.get(play, 0), like: item.get(like, 0), coin: item.get(coin, 0), favorite: item.get(favorite, 0), comment: item.get(comment, 0), duration: item.get(duration, 0), pubdate: item.get(pubdate, 0), tag: item.get(tag, ) }) time.sleep(random.uniform(1, 2)) return records if __name__ __main__: # 实际使用时可以把关键词换成多个循环采集 data collect_data(青少年模式, max_pages5) print(f采集到 {len(data)} 条记录)我实际运行的时候大概翻5页能拿到100到150条有效数据。如果你想增加样本量可以把关键词进一步细分比如加上“青少年模式 家长”“青少年模式 功能”“B站青少年模式 设置”等长尾词再把结果合并去重这样分析样本会更充分。2.3 数据清洗与SQLite入库采集到的原始数据不能直接用尤其在播放量、时间字段上必须做类型统一和缺失值处理。我清洗数据的流程是先转成pandas的DataFrame然后按bvid去重再处理播放量里的特殊字符最后把Unix时间戳转成可读的日期格式。播放量这个字段接口有时会返回1.2万这种带单位的字符串有时是整数还有的时候是--。统一处理逻辑是如果是字符串且包含“万”就乘以10000包含“亿”就乘以100000000遇到“--”或者空值直接置为0。这个转换逻辑很短但少了它后面的排序和聚合分析全都会乱掉。发布时间字段是Unix时间戳比如1700000000这种纯数字直接用pd.to_datetime配合units转成北京时间即可。转换之后我会新增一列“月份”格式是YYYY-MM后续做月度趋势分析就直接按这一列分组。清洗完成后写入SQLite建表语句和插入代码都不复杂。我实际把表结构设计成下面的样子字段和采集字段一一对应另外加了一个id自增主键CREATE TABLE IF NOT EXISTS videos ( id INTEGER PRIMARY KEY AUTOINCREMENT, bvid TEXT UNIQUE, title TEXT, author TEXT, play INTEGER, like_count INTEGER, coin INTEGER, favorite INTEGER, comment INTEGER, duration INTEGER, pubdate TEXT, month TEXT, tag TEXT );写入的时候用INSERT OR IGNORE这样可以借助bvid的唯一约束自动跳过重复记录不需要手动判断。我试过连续跑好几次采集脚本数据库记录数不会重复上涨这个约束非常重要。3. 数据分析维度与业务解读3.1 关注度趋势话题热度跟着什么在变数据入坑之后我做的第一个分析是时间趋势。方法很直接按“月份”字段分组统计每个月的视频发布数量和播放量中位数。通过这两条曲线能清晰看到“青少年模式”这个话题从什么时候开始讨论变多、什么时候又出现新的波峰。我跑完数据之后发现一个明显规律每年的寒暑假期间尤其是2月和7月、8月相关视频的发布量会有一个小高峰寒暑假是未成年人集中使用视频平台的时期家长和学校对“青少年模式”的关注度会随之上升。另外当B站官方对这个功能进行版本更新、或者媒体报道未成年人网络保护相关政策的时候视频数量也会出现短期脉冲式上涨。这说明这个分析维度的业务价值很高——你不需要去猜一个功能什么时候被用户关注数据曲线会直接告诉你答案。实现这段聚合分析的代码非常简单核心就是pandas的groupbyimport pandas as pd df pd.read_sql_query(SELECT * FROM videos, conn) trend df.groupby(month).agg( video_count(bvid, count), median_play(play, median) ).reset_index()这里我特意加了播放量中位数而不是平均值是因为个别爆款视频的播放量可能是几百万平均值会被极端值拉偏而中位数更能反映普通视频的真实热度水平。这个细节在分析类项目里很重要我建议做同类分析时都带上这个思考。3.2 内容主题聚类大家都在聊青少年模式的哪些侧面只看发布时间远远不够我还想知道这些视频的标题里到底在聊什么。这里用到了jieba分词和词频统计。流程是这样的把所有视频标题拼成一个长文本用jieba做精准模式分词再过滤掉一类“青少年”“模式”“B站”这些跟话题强相关但没有信息量的词以及其他常见停用词最后用collections.Counter统计高频词。实际跑完排在最前面的词包括“时长”“限制”“关闭”“设置”“内容”“过滤”“家长”“破解”“教程”“经验”等。这些关键词其实已经把用户关注点划分成了几个明显类别功能设置类怎么开启、怎么关闭、怎么设置时长、效果讨论类能不能真正限制、内容过滤到什么程度、有没有漏洞、家长教育类家长如何引导、孩子如何使用、使用教程类详细操作步骤、功能体验评测。如果你想做更深一层可以基于这些关键词做一个简单的规则分类比如标题里含有“设置”“开启”“关闭”就归类到功能设置含有“家长”“孩子”归类到家庭教育。有了类别字段后续做饼图和对比分析就更好讲。分词和统计代码很短但有几个滤词细节值得说。一部分词虽然和主题无关比如“一个”“我们”“如何”可以直接用停用词表过滤另一部分词比如“青少年”“模式”本身是搜索词它们出现频率最高但正是因为是搜索词反而不具备区分内容的分析价值也要在国内手动过滤掉。如果这一步不处理干净词云图出来就是一对无意义的重复词。3.3 UP主生态谁在持续产出这类内容这个分析维度的价值在于看清内容供给生态。统计维度很简单按作者名分组计算每个UP主发布的“青少年模式”相关视频数量再关联这批视频的播放总量和点赞总量。数据跑出来后能看到一个典型的内容生态结构少量头部UP主承担了大量内容产出其中既有科技数码类UP主从功能体验角度做的评测也有教育类UP主从家长视角做的解读数量更多的则是粉丝量在几千到几万的腰部UP主他们发的内容更像个人经验分享播放量虽然不高但胜在视角多元。这类长尾内容其实对用户很有价值只是被搜索排序算法压在了后面。从产品调研的角度看这个分布告诉我们的信息是青少年模式话题的内容供给存在集中度头部内容主导舆论方向长尾声音分散但主题丰富。如果B站官方想推动这个功能被更好理解完全可以引导更多中腰部创作者做多角度的使用教程和体验分享。3.4 互动效果评估什么内容更容易引发共鸣最后一个核心分析是互动率。我定义了一个简单的指标互动率 点赞数 投币数 收藏数 评论数 / 播放数。这个指标比单纯看播放量更能反映内容的用户认可度。我按视频分区和时长区间分别计算平均互动率发现两个有意思的现象。第一校园教育、科技科普分区的视频互动率普遍高于资讯类内容因为用户看完之后更容易产生“有用、值得收藏、想讨论”的行为。第二视频时长在3到8分钟的内容互动率最高太短的内容讲不清功能细节太长则容易流失观众。这个结论对做内容运营的同学有直接的参考价值做选题和脚本的时候可以更靠近这个区间。另外我还做了播放量、点赞数、收藏数三个变量之间的相关分析主要看它们之间的线性相关程度。B站用户有“先收藏后看”的习惯收藏数往往比点赞数更能说明内容的长尾价值。具体到“青少年模式”这类实用教程内容收藏与播放的相关系数明显高于泛娱乐内容这也侧面验证了用户看这类视频是带着明确学习目的的。4. 可视化系统设计与实现4.1 为什么用Flask pyecharts这套组合可视化方案我对比过三条路一条是纯Matplotlib画静态图一条是用Flask 原生ECharts写前端还有一条就是Flask pyecharts。最终选了Flask加pyecharts的组合原因有三个。首要原因是开发效率。pyecharts直接可以用Python生成配置好的图表你不用写一行JavaScript也不会出现前端报错却不知道错在哪里的困境。对做数据分析的人来说把精力集中在“分析什么”而不是“怎么画”上是效率最高的选择。第二个原因是图表交互性。pyecharts生成的图表自带鼠标悬停查看数值、图例筛选、数据缩放这些交互功能比静态PNG图的信息承载量大得多。做趋势图的时候你鼠标拖到某个月份就能看到当月视频数量和播放量中位数两条数据这种体验是静态图给不了的。第三个原因是项目结构清晰。Flask负责路由和模板渲染pyecharts负责生成图表HTML两者之间可以无缝集成。我可以用page.ECharts()把多个图表组合到同一个页面里也可以每个图表单独放在一个页面中通过导航切换非常灵活。4.2 图表设计五个视图对应五类问题可视化不是画一堆图就完事每张图都要对应一个分析问题。我这个系统一共设计了五个核心视图下面逐个说清楚。第一个是月度趋势折线图对应的是“热度随时间怎么变化”这个问题。横轴是月份纵轴是视频发布数量再叠加一条播放量中位数的副纵轴。采用双向Y轴的原因是两个序列的量级差异很大发布数量通常只有几十而播放量中位数可能上万放同一个坐标轴下变化趋势会被掩盖。第二个是视频分区占比饼图对应“内容集中在哪里”的问题。按分区字段分组统计视频数量用饼图展示各分区比例。这里我用了环形图而不是标准饼图纯粹是视觉上的取舍环形图中间可以放总视频条数信息利用率更高。第三个是关键词词云图对应“大家在聊什么”的问题。把前面用jieba统计出来的高频词按词频生成词云字体大小映射出现次数。这个图在报告里最适合用来做“观点概括”一眼就能看出用户讨论的焦点。第四个是UP主产出量横向条形图对应“谁是核心内容生产者”的问题。按视频数量取TOP15的UP主绘制横向条形图好处是UP主名称长也不会被截断而且一眼能看出梯度差异。第五个是互动率对比散点图横轴是视频时长秒纵轴是互动率颜色深浅代表播放量。这张图能直观看出“哪个时长区间的内容更容易获得互动”找出散点比较密集、纵轴数值较高的聚集区域内容运营的选题方向就清晰了。实现这些图表的核心代码以折线图为例pyecharts的写法很直观from pyecharts import options as opts from pyecharts.charts import Line def make_trend_chart(trend_df): c ( Line() .add_xaxis(trend_df[month].tolist()) .add_yaxis( 视频发布数量, trend_df[video_count].tolist(), yaxis_index0, color#fb7299 ) .add_yaxis( 播放量中位数, trend_df[median_play].tolist(), yaxis_index1, color#5470c6 ) .extend_axis( yaxisopts.AxisOpts( name播放量中位数, type_value, positionright ) ) .set_global_opts( title_optsopts.TitleOpts(title青少年模式相关视频月度趋势), tooltip_optsopts.TooltipOpts(triggeraxis) ) ) return cpyecharts每个图表对象都有.render()方法直接输出一个完整的HTML文件。后面再用iframe把这些文件嵌进Flask页面就完事了。4.3 页面整合与动态刷新为了不让图表孤零零地挂在网页里我设计了三个主要页面首页汇总页、趋势分析页、内容分析页。首页放几张核心KPI卡片比如视频总条数、参与UP主数、平均播放量、总互动量下面再嵌入月度趋势图和分区饼图。趋势分析页专门放折线图和播放量中位数趋势。内容分析页则放词云、UP主条形图和互动散点图。Flask的路由设计很短我用一个主路由渲染首页模板另外两个路由分别渲染两个子页面。为了减少页面加载的等待时间我把所有图表生成步骤封装在一个Python函数里数据有更新时只需要运行一次图表生成脚本所有HTML文件会重新生成页面刷新后就是最新数据。动态刷新这里有个坑我得提醒一下浏览器会缓存HTML和JS文件有时候你改了数据重新生成了图表HTML刷新页面却还是旧图。我踩过这个坑之后在模板里给iframe的src加了一个时间戳参数比如iframe src/static/charts/trend.html?t{{ timestamp }}/iframeFlask在渲染模板时传入当前时间的毫秒数浏览器就会强制加载最新版本。这类缓存问题在本地开发时很隐蔽因为开发服务器刷新通常比较及时但如果你把项目打包给别人或者部署到服务器问题就会暴露出来。提前把时间戳方案设计好能省去后期很多排查时间。5. 常见问题与排查技巧实录5.1 接口请求频繁被风控怎么办B站搜索接口对请求频率有隐性的限制最常见的表现是前几页请求正常翻到后面突然返回code-412提示请求被拦截数据完全拉不下来。这基本就是被风控了短时间内再怎么发请求都是徒劳。我的处理经验是分两层来解决。第一层是预防请求头一定要伪装完整User-Agent、Referer、Cookie缺一不可翻页间隔至少1秒以上最好加上随机延时让请求间隔呈现 1到2秒之间的随机波动而不是固定的整数间隔固定的延时同样容易被识别为脚本特征。第二层是被风控后的处理f一发短时间连续重试只会让风控时间更长正确做法是停止脚本5到10分钟等限制解除后再继续。如果你需要大量采集建议把任务拆成多个时间段跑每次只采集一部分而不是一口气跑完。5.2 “万”字播放量怎么统一成数值B站搜索接口返回的播放量字段并不总是纯数字。我遇到过三种情况直接返回整数、返回带单位的中文数字、返回“--”占位符。如果不做统一处理排序和绘图的时候会遇到各种报错或者图表里的数据完全对不上。我写了一个转换函数放在数据清洗的入口处统一执行def convert_play_count(value): 把1.2万、3434、--统一处理为数值 if isinstance(value, (int, float)): return int(value) text str(value).strip() if text in (, --, None): return 0 if 万 in text: return int(float(text.replace(万, )) * 10000) if 亿 in text: return int(float(text.replace(亿, )) * 100000000) try: return int(float(text)) except ValueError: return 0这个函数虽然短但它是整个清洗环节里最容易被忽略却又最关键的代码之一。少了它后面所有关于播放量的统计都会失真而且报错的坑位还难排查因为图表上只会显示空值或者异常值你很难一眼发现是原始数据没转换。5.3 生成图表时中文显示成方块pyecharts生成的图表如果运行环境中缺少对应的中文字体页面上会出现一堆方块乱码。这个问题在Linux服务器部署时尤其常见Windows本地环境反而很少遇到。解决办法很简单在pyecharts的配置里指定中文字体族。具体来说可以在set_global_opts的textstyle_opts里设置font_familyMicrosoft YaHeiWindows或者Noto Sans CJK SCLinux。还有一种更省事的方式是在机器上安装中文字体比如Linux下执行apt install fonts-noto-cjk装完SK重启服务大部分乱码都能解决。5.4 Flask部署后页面数据不刷新这个问题在前面提到过但因为它太典型我专门拎出来再说一遍。Flask开发模式下模板文件的变化往往能被自动加载但pyecharts生成的静态HTML文件不会自动刷新浏览器缓存会把旧页面牢牢记住。我在项目里用了两层处理。第一层是在页面模板中给所有图表链接加上时间戳参数也就是前面说过的?t方案。第二层是图表生成的脚本输出文件名保持不变这样页面里的引用地址不会变动但每次生成都覆盖同名文件配合时间戳强制刷新就能确保每次打开页面看到的都是最新数据。如果在生产环境部署还需要注意Flask默认是单线程开发的服务器只适合开发和测试部署到公网或者多人访问时建议用Waitress或者Gunicorn托管Flask应用避免并发访问时出现阻塞。这些问题排查表我整理成一个速查表方便你直接对照问题现象可能原因快速解决接口返回-412请求频率过高触发风控加随机延时等待5-10分钟后继续播放量无法排序字段含“万”或“--”用convert_play_count统一转数值图表中文乱码运行环境缺中文字体安装Noto Sans CJK SC或指定font_family页面刷新后数据不变浏览器缓存静态HTMLiframe src追加时间戳参数Flask启动后页面卡死开发服务器单线程阻塞生产环境换Waitress/Gunicorn我做了这个项目之后最大的感触是数据分析可视化的价值不在于图表有多炫而在于你能不能从一个看似简单的功能话题里挖掘出值得被看见的规律。青少年模式作为平台保护机制它身上承载的讨论热度、内容生态和用户情绪其实都清晰地记录在这些视频数据里。这套系统后续还可以往更深处扩展比如接入评论内容做情感分析、结合弹幕文本做更细颗粒度的舆情拆解甚至把多平台同一话题的数据拉通做横向对比方向很多。做数据分析就是这样一个项目打通之后后续的每次扩展都是在既有地基上盖新楼层。希望这篇完整的过程拆解能让你在自己的数据项目里少踩几个坑把时间真正花在分析和思考上。