ARTICLE DETAIL

资讯详情

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

用Python分析B站青少年模式:从爬虫到可视化大屏实战

用Python分析B站青少年模式:从爬虫到可视化大屏实战 用Python做Bilibili数据分析最让人上头的不是看播放量曲线而是从一堆公开数据里挖出用户真实的态度。我最近做了一套基于Bilibili青少年模式使用情况的可视化系统整个项目从数据采集、清洗、情感分析到Web大屏展示花了两周时间觉得可以把过程完整写下来。如果你正在学Python数据分析或者想找一个能写在简历上的可视化项目这篇文章里的架构思路、踩坑记录和核心代码都能直接用。这套系统要回答一个很简单的问题青少年模式在B站到底是被接受的还是被嫌弃的官方不会公开用户使用数据所以我换了个思路把B站上跟“青少年模式”相关的视频、评论、弹幕当作样本用自然语言处理和统计可视化去还原大家的真实态度。整个项目跑下来不仅能拿到情感倾向、高频关键词、发布趋势这些结论也能完整走一遍“采集—存储—分析—可视化—Web展示”的数据分析流程非常适合当成中级Python实战项目来练手。1. 项目拆解青少年模式的“真实使用情况”从哪里来1.1 数据口径我们到底在分析什么严格来说平台内部的青少年模式开关率、使用时长、拦截记录这些数据普通人是拿不到的。能拿到的是用户围绕这个功能产出的公开内容。我在做系统前先给自己定了一个数据口径把B站上所有标题、简介、评论、弹幕里包含“青少年模式”的公开内容当成分析对象通过内容去反推用户的使用场景和态度。这样处理有两个好处。第一合规安全不碰任何接口权限之外的字段也不用去模拟用户行为。第二统计意义可解释当你在B站搜索“青少年模式”会找到大量测评视频、家长吐槽、学生党讨论甚至官方号的说明视频这些内容本身就是用户用脚投票的结果。把视频和评论量连在一起看可以大致还原功能上线后的舆论变化曲线。当然这个口径也有局限。比如大量用户开了青少年模式但从不发评论这部分沉默用户我们看不到。所以我在可视化系统里明确标注了“本分析基于公开UGC数据”结论只能反映舆论倾向不直接等于真实使用率。做数据分析最怕的就是把样本当全量先把这个边界讲清楚后面所有图表才有说服力。1.2 整体架构五个模块组成的数据分析系统这套系统的代码结构我按数据流动方向拆成了五层。采集层负责从B站公开接口抓取视频信息和评论数据存储层用SQLite保存原始数据方便多次分析分析层承担数据清洗、情感打分、关键词提取可视化层基于pyecharts生成HTML图表Web层用Flask把图表整合成一个大屏页面。具体模块分工如下表模块核心职责主要技术数据采集层搜索视频、分页拉取评论/弹幕元信息requests、threading数据存储层原始数据落库、去重、增量更新sqlite3、pandas数据分析层清洗字段、情感分析、关键词抽取pandas、SnowNLP、jieba可视化展示层生成可交互图表文件pyechartsWeb服务层路由、渲染大屏、数据接口Flask分层最直接的好处是出了问题不用全盘重来。比如采集层被反爬限制只需要修改请求模块想换一个正文主题只需要改搜索关键词分析层和展示层完全不用动。我当时把代码分成bili_spider.py、data_clean.py、analysis.py、charts.py、app.py五个文件每个文件只负责一件事调试起来非常省心。1.3 技术选型这套技术栈背后的取舍很多初学者会纠结要不要上Scrapy、Spark或者Django。我的建议是看项目规模。这个项目单次采集数据量在几万条级别用Scrapy属于大炮打蚊子而且调试Scrapy中间件、管道比写几十行requests更浪费时间。Spark更是完全没必要单机pandas处理几万条数据连一秒都用不了非要上分布式反而是给自己挖坑。数据处理选pandas核心是因为它的DataFrame在清洗、分组聚合、时间重采样上实在太顺手。情感分析用SnowNLP虽然它自带的情感模型更偏向电商评论但在网络短文本上表现基本可用胜在轻量。分词和关键词提取用jieba里面自带的analyse.extract_tags可以直接走TF-IDF算法比手动计算词频省事太多。可视化用pyecharts因为生成的图表是独立的HTML文件带交互天然适合嵌入Flask页面不需要额外写前端JS。2. 数据采集如何合规、稳定地拿到B站公开数据2.1 明确采集边界只拿公开接口不碰非公开数据做爬虫最忌讳“来都来了就全爬下来”。这个项目从一开始就给自己画了三条红线第一只访问B站网页端公开接口不需要登录态就能访问的搜索和评论接口第二不下载视频文件、不解析付费内容只拿文本类统计信息第三不对任何用户进行定向追踪评论数据只保留文本和发布时间不存储用户名等个人识别字段。如果你看到某些工具声称能下载B站视频或者提取充电视频那是另一个领域的事情不要混到数据分析项目里。我们的目标是研究“青少年模式使用情况”而不是做资源搬运。守住这个边界项目既能在学习阶段踩真实的坑也不会给自己惹合规上的麻烦。2.2 三个关键接口的请求封装B站的网页版接口一直在调整我在项目里只封装了三个核心接口搜索视频列表、获取视频详情、分页获取评论。搜索接口用于确定样本范围视频详情接口用于补齐播放量、发布时间、UP主信息评论接口用于拿文本内容。下面这段代码是搜索接口的示例import requests import time HEADERS { User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36, Referer: https://www.bilibili.com/ } def search_videos(keyword青少年模式, page1): url https://api.bilibili.com/x/web-interface/search/type params { keyword: keyword, page: page, search_type: video } resp requests.get(url, paramsparams, headersHEADERS, timeout10) resp.raise_for_status() data resp.json() result_items data.get(data, {}).get(result, []) or [] for item in result_items: yield { bvid: item.get(bvid), title: item.get(title), author: item.get(author), play: parse_play_count(item.get(play)), comment: item.get(comment), pub_date: item.get(pubdate) }这里有个特别容易踩的坑搜索接口返回的数据结构可能会因为search_type参数不同而变化而且play字段可能带“万”字后缀。我在parse_play_count函数里做了单位转换否则后面做聚合时会把“1.2万播放”当成“1.2播放”整个折线图就全错了。这类单位转换问题在真实业务数据里非常常见建议每个字段都先用眼过一遍再写清洗逻辑。评论接口是另一个常见堵点。B站评论接口虽然不需要登录但分页参数pagination_str经常变化。早期版本用pn和ps后来部分接口改成游标方式。我建议不要硬编码参数名而是先打印一次返回的JSON确认分页字段后再写循环。代码大概长这样def fetch_comments(oid, max_page20): for page in range(1, max_page 1): url https://api.bilibili.com/x/v2/reply/main params {type: 1, oid: oid, mode: 3, next: page} resp requests.get(url, paramsparams, headersHEADERS, timeout10) data resp.json() replies data.get(data, {}).get(replies) or [] for reply in replies: yield reply[content][message] if len(replies) 20: break time.sleep(random.uniform(1, 2))注意这里我用next字段做翻页而不是迷信旧教程里的pn。接口是活的项目代码必须跟着适应。每次请求后必须sleep这不是怂而是对别人的服务器负责任。即使只是分析公开文本请求频率过高同样会触发风控。2.3 请求频率、重试与异常兜底网络请求永远有失败的可能所以我在采集层加了两个机制。一是随机延时每请求一次就睡1到2秒避免形成固定频率的访问模式。二是指数退避重试遇到超时或者状态码412时先等2秒重试再等4秒最多三次。重试完仍然失败就跳过当前请求记录日志而不是让整个采集脚本崩溃。为了加快几万条评论的采集我用concurrent.futures.ThreadPoolExecutor开了一个最多8线程的池子。线程数千万不能开太高B站对并发是有限制的8线程加随机延时已经能比较平滑地跑完数据。实测下来采集2000条评论大概需要15到20分钟这个速度完全可以接受。把每个视频的oid分发给不同线程时要注意控制线程池的任务队列长度别一次性塞进去两千个任务否则内存很容易被占满。3. 数据处理与核心分析把“使用情况”变成可量化的指标3.1 清洗与标准化给数据“擦干净”原始数据拿回来后还是半成品采集到的标题里可能嵌了HTML标签和高亮标记评论区有大量带昵称前缀的引用文本播放量还存在“万”和纯数字两种格式。我在清洗阶段把所有字段统一成规范类型重点做了三件事去除重复评论、统一时间格式、转换单位。去重不能只看字符串完全一致因为同一条评论可能在多个视频下被复制发布。我用hashlib.md5对评论文本的去除空格小写版本做指纹再在SQLite里以指纹字段建唯一索引第二次采集时直接用INSERT OR IGNORE跳过重复记录。时间字段从B站返回的是Unix时间戳我用pd.to_datetime(..., units)转成北京时间再按天聚合。播放量的转换函数如下def parse_play_count(raw): if raw is None: return 0 if isinstance(raw, str): if raw.endswith(万): return int(float(raw.strip()[:-1]) * 10000) return int(raw.strip()) return int(raw)清洗时最容易忽略的是评论里的“空行”和“回复引用”。B站手机端会把回复别人的内容折叠在reply_to字段里如果直接拿主评论字段做情感分析会丢失一部分对话上下文。我后期把父评论和子评论合并成一条文本再交给情感模型准确率有明显提升。3.2 情感分析用SnowNLP给评论态度打分情感分析是这套系统的亮点也是最容易翻车的地方。我选择SnowNLP核心原因是它开箱即用不需要训练自己的模型。调用方式非常直接from snownlp import SnowNLP def sentiment_score(text): text text.strip() if len(text) 2: return None return SnowNLP(text).sentimentssentiments返回0到1之间的值越接近1表示越正向越接近0表示越负向。我用0.6和0.4作为分界点得分不低于0.6标记为正面不高于0.4标记为负面中间是中性。真实文本往往会给出0.43、0.57这种暧昧分数全靠阈值卡。这里要特别提醒SnowNLP的底层模型对网络新词和阴阳怪气的表达很不敏感比如“真好一定要用”“我谢谢你全家”这种反讽模型很容易判成正向。应对办法是在分析前加一个讽刺词典规则。我手动整理了几十个常见反讽词比如“谢谢你”“真是太好了”“感动哭了”如果评论里同时出现正向词和反讽词就强制把情感倾向调成负面。这个后处理很粗暴但胜在可控。做情感分析不要追求满分准确率工业界能到70%就算能用我们看的是整体比例趋势不是单条评论的最终定性。3.3 关键词提取用jieba加TF-IDF找出用户关心的具体话题情感分数只能告诉你好还是不好但用户具体在聊什么还要靠关键词提取。jieba自带的analyse.extract_tags实现了TF-IDF算法可以直接返回带权重的关键词import jieba.analyse def extract_keywords(comments, top_k30): text .join(comments) tags jieba.analyse.extract_tags(text, topKtop_k, withWeightTrue) return tags跑出来的高频词大多是“孩子”“内容”“时间”“限制”“成年人”“默认关闭”这类词但里面也会有“B站”“视频”这种对整个分析没价值的通用词。解决办法是维护一个停用词表把平台词和泛化技术词过滤掉。我试了两轮后把“视频”“bilibili”“真的”“感觉”加进了停用词词云立刻清晰非常多。TF-IDF对短文本评论列表的效果比单纯词频好因为它在计算时降低了“青少年模式”这种搜索关键词本身的干扰。不过要注意如果一手滑把几千条评论全部拼接成一个超长字符串内存会爆炸。更好的做法是先把评论按视频分组对每个组提取TopN关键词再汇总统计这样还能分析出不同视频作者受众的关注点差异。4. 可视化系统从数据到图表的完整背后逻辑4.1 图表选型先想明白这张图要回答什么问题做可视化系统最大的误区是一上来就堆图表。我在动手前先列了一份“指标—图表”对照表每一张图都必须能回答一个具体问题。回答“相关视频播放量怎么随时间变化”的用折线图回答“正面负面评论谁更多”的用南丁格尔玫瑰图回答“用户最关心什么话题”的用词云回答“哪些UP主最关注这个主题”的用横向柱状图。分析问题适合图表展示重点内容发布与播放量随时间变化折线图趋势、爆发点评论情感分布玫瑰图/饼图占比与单一情感突出程度高频话题词词云关键词量级对比UP主活跃度排行横向柱状图个体差异视频类型分布环形饼图品类集中度我一直觉得图表不是越炫越好。B站有大量可交互的3D图表但如果它并不能让读者更快得出结论那就是噪音。这个系统里选用pyecharts自带的多种图形全部保持白色背景和统一配色最后拼出来的大屏风格协调也比默认皮肤好看。4.2 用pyecharts生成可交互图表pyecharts最方便的地方是图表对象可以直接渲染成独立的HTML文件后续嵌入Flask模板非常顺滑。下面以词云和折线图为例from pyecharts.charts import WordCloud, Line from pyecharts import options as opts wordcloud WordCloud() wordcloud.add(, data_pair, word_size_range[15, 80], shapecircle) wordcloud.render(templates/charts/wordcloud.html) line Line() line.add_xaxis(date_list) line.add_yaxis(播放量, play_counts, is_smoothTrue) line.set_global_opts( title_optsopts.TitleOpts(title青少年模式相关视频播放量趋势), tooltip_optsopts.TooltipOpts(triggeraxis) ) line.render(templates/charts/trend.html)注意生成词云时需要把权重归一化不能直接拿原始TF-IDF值否则最大字号会溢出容器。我之前偷懒直接用原始权重试了一次词云里“孩子”两个字大得快占满整张图换成归一化后马上正常。pyecharts的全局配置项非常多建议先设置标题、提示框、图例三种就够等整个系统跑通再回来调样式避免一开始就陷进样式调整的泥潭。4.3 用Flask把图表串成一个大屏系统图表生成后是散落的HTML文件我需要一个壳把它们组合到一起。Flask是这里最合适的角色from flask import Flask, render_template app Flask(__name__) app.route(/) def dashboard(): return render_template(index.html) if __name__ __main__: app.run(debugTrue)index.html里用iframe同时加载trend.html、wordcloud.html、sentiment.html或者直接用Pyecharts的Page对象把所有图表合并成一个长页面。我实际用的是后者因为iframe太多会拖慢首屏加载。用Page比较简单from pyecharts.charts import Page page Page() page.add(line, wordcloud, bar, pie) page.render(templates/charts/dashboard.html)大屏页面分成四块顶部放标题和统计总览卡片下面左半部分放趋势折线图和情感玫瑰图右半部分放词云和UP主排行底部放视频类型分布。所有图表都保留了tooltip和缩放功能鼠标悬停就能看到具体数值。这样一个纯Python开发的系统视觉上已经完全接近产品级的数据看板。5. 系统实测一次真实跑批的结果复盘5.1 从数据里看到什么我用“青少年模式”作为关键词跑了一次完整数据采集。搜索出来的相关视频数量在几千条量级按播放量排序后取前200个视频继续抓取这些视频的评论区最终拿到有效评论两万多条。清洗完后做情感分析整体分布是正面约55%中性约25%负面约20%。这个结果初看是“用户总体认可青少年模式”但点开负面评论细看吐槽主要集中在“少儿内容过于低龄化”“误伤成年人正常内容”“初始密码太简单”这几个具体点上。播放量趋势图也很有意思一系列相关视频的发布并不是均匀分布的而是集中在几个时间节点上每次都是功能更新或官方说明后出现一波小高峰。这提醒我们舆论热度跟产品动作高度绑定如果只看总量不看时间切片很容易误判用户关注度是恒定的。这类洞察只有把数据分析可视化系统完整跑出来才能发现靠肉眼看几条热门视频是总结不出来的。5.2 实测中的性能瓶颈与优化方法第一次跑批时评论采集速度慢得离谱。单线程逐页请求200个视频有一半评论超过20页全部跑完用了接近两个小时。后来我改成8线程并发每个视频一个爬虫任务任务内部还是顺序翻页总耗时压缩到40分钟以内。线程池这里有个细节所有共享数据要加锁否则多个线程同时往SQLite写数据会报database is locked错误。另一个性能瓶颈在情感分析阶段。SnowNLP本身是逐句处理两万条评论在单进程下跑了十几分钟。我先用TextRank对评论做快速分类过滤掉明显的中性短句只对长度超过8个字符的评论做情感打分时间一下子降到了三四分钟。这说明数据分析系统的优化点通常不在算法复杂度而在减少无效计算量。5.3 部署经验分享系统开发完成后我在一台2核4G的云服务器上用gunicorn部署Flask应用配置了4个worker。SQLite数据库放在服务器本地每周末用cron跑一次增量采集脚本更新图表数据。如果你也打算长期跑这套系统建议在采集脚本里加一个--update参数只抓取最近一周的新评论避免每次全量扫描。部署过程中我踩过一次坑pyecharts生成的图表资源文件会引用CDN上的JS库服务器如果外网连通不好大屏页面会白屏。后来我把所有js/css依赖下载到本地静态目录并修改了render生成的HTML链接才稳定下来。一个小技巧是直接用pyecharts的OnlineHost参数改为本地host这样离线也能展示全部图表。6. 常见问题排查速查表6.1 典型问题表我把开发过程中真正遇到过的Bug整理成一张速查表方便你复制到自己的项目笔记里。问题现象可能原因解决办法搜索接口返回空列表search_type参数不对或接口风控先打印raw JSON确认字段名切换video和all请求返回-412请求频率过高降低线程数、加随机延时、使用重试退避SQLite报database is locked多线程并发写库使用单写多读模式或每次写入后提交并关闭连接播放量出现“万”导致图形异常未做单位转换统一清洗成数字再聚合情感分析把反讽判成正向模型不适应网络语言加讽刺词典后处理规则词云出现大量无意义词停用词不够增加“真的”“感觉”“B站”等泛化词pyecharts图表在服务器上白屏JS资源走外网CDN将JS下载到本地修改引用路径如果你遇到暂时排不掉的问题建议在项目目录里加一个debug_log.txt把每个接口的响应状态码、耗时、异常堆栈都打进去。很多时候不是逻辑错而是接口临时调整有了日志一眼就能定位。6.2 一个印象最深的调试经历最折磨我的一个问题是评论情感分布长期偏正面无论怎么调阈值都不太对。后来随手打印了几条被标记为“正面”的评论发现SnowNLP把“这模式太好笑了吧”识别成正向。原因是“好”和“笑”两个词都被模型当成了积极信号但它实际表达的是调侃。从那次以后我给项目加了一条前置规则如果评论里同时出现“太”“还”“居然”这类程度副词加正向词先按负向处理。这个规则当然也不完美但它让我认识到情感分析真正难的不是调用算法而是理解中文互联网语料里的隐性情绪。7. 这一套系统还能往哪里扩展7.1 换数据源、加预测整个系统的核心价值在于“数据处理加可视化”的骨架换一个关键词立刻能变成另一套舆情系统。比如把“青少年模式”换成某个新功能名称或者某类美食关键词只需要改采集层的搜索词和停用词表。再加上一个时间序列预测模块用statsmodels或Prophet对播放量做回归预测系统就有了预警能力不再只是展示历史数据。7.2 从单机到分布式的演进路径当数据量从几万条涨到几亿条时单机pandas确实会扛不住。到那个阶段可以把采集层换成Flume或者Kafka分析层换成Spark SQL存储层换成ClickHouse但思想还是这一套数据管线分层、清洗标准化、指标可解释、图表服务化。一开始直接用Spark反而容易淹没在分布式环境的细节里所以我更推荐先用做透这个中小体量项目再去横向扩展技术栈。做这个项目最大的感受是数据分析系统的难点从来不是画图写文章而是把“一句模糊的问题”翻译成“一组可采集、可量化、可解释的指标”。青少年模式使用情况本身是一个偏社会学的话题但通过Python和公开数据最终把它变成了一张情感分布图、一条趋势线和一个词云。这个过程里练到的数据清洗能力、接口调试能力、Flask部署能力远比项目结论本身更值钱。如果你也想练手建议从今天热榜上的某个关键词开始架一套同样结构的系统跑完第一版数据后你一定会理解我说的这些坑有多真实。
返回列表