news 2026/10/7 11:11:25

用Python爬虫+情感分析,从1.6万条评论还原U23决赛真实舆论

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
用Python爬虫+情感分析,从1.6万条评论还原U23决赛真实舆论

凌晨一点,我关掉直播,屏幕定格在0比4。U23亚洲杯决赛,中国U23国家队输给了日本,拿了亚军。按说亚军已经是这些年难得的好成绩,可打开评论区,什么声音都有:有说虽败犹荣的,有说技不如人的,还有大量纯粹发泄情绪的话。我写了六七年Python,那晚没去网上跟人抬杠,而是打开电脑,决定写个爬虫,把网友的真实情绪量化出来。我给自己定了三个问题:骂声真的占多数吗?理性讨论是不是被淹没了?比赛结束后的三个小时里,情绪到底怎么变化的?

结果比我预想的有意思得多,甚至有个结论直接推翻了我刷手机时的直觉。这篇就把整个项目复盘一下,包括评论数据怎么抓、情绪怎么算、哪些地方容易翻车,以及那个让我反复确认了好几次的意外发现。

1. 比赛踢完的那晚,我为什么非写个爬虫不可

1.1 球迷身份下,信息噪声已经大到我听不见真相

赛后的第一件事,我打开了常用的几个App。热搜上挂着好几个相关词,短视频平台的封面清一色是日本球员庆祝的照片,评论区前排要么是“丢人现眼”,要么是“已经可以了,这群小伙子还有戏”。两种声音都极其高亢,但谁也没说服谁。

我真正不舒服的地方在于:这些内容背后的推荐算法,天然会把情绪最极端的评论推到我眼前。因为它们互动率高,平台就认为它们“优质”。于是,我看到的所谓“舆论”,其实只是几个吵架吵得最凶的人。而大量沉默的、只点了个赞的、认认真真分析比赛的普通人,在信息流里几乎是隐形的。

我意识到,如果想知道“网友到底什么态度”,不能靠刷手机刷出来,得靠数据抽出来。

1.2 情绪这东西,其实是可以被量化的

写代码的人有个毛病:遇到一堆文本,第一反应是能不能结构化。评论区看起来是几千行非结构化的中文句子,但剥开看,无非就是几个维度:说好话还是坏话、情绪强烈到什么程度、什么时候说的、有没有人认同(点赞数)。

这些维度都能用Python量出来。说好话还是坏话,交给情感分析模型;情绪强度,可以用正负向词典打分;什么时候说的,看评论发布时间;有没有人认同,看点赞数。把这些字段拼起来,就够了。

所以那天晚上我给自己定的项目目标很朴素:

  • 抓取若干平台与这场比赛相关的评论正文、发布时间、点赞数。
  • 对每条评论做情感倾向判定,分成正向、中性、负向三类。
  • 按时间切片,看情绪如何变化。
  • 最后结合点赞数,看看“被认同的”到底是哪类声音。

这个目标不复杂,但做完之后,我发现自己对这场比赛的看法都被改变了。

1.3 项目边界先划清楚,不然会掉进无底洞

动手之前,我得先承认一件事:互联网上的评论是抓不完的。微博热搜几百个入口,各大论坛帖子实时更新,短视频评论区又是另一套体系。真要全量采集,我得先上一套分布式爬虫框架,再加代理池,最后还得跟平台的风控斗智斗勇——这明显不是一晚能搞定的事。

所以我把范围缩小到三个我自己平时也在用的平台:微博、B站、再加少量懂球帝的赛后讨论帖。时间窗锁定在比赛结束后的四个小时。样本量控制在五位数以内。这个规模不需要上分布式爬虫,一台电脑、一个requests库、几杯咖啡,够了。

个人项目最大的风险不是抓不到数据,而是目标太大导致进度失控。先把边界划清楚,后面每一步都会轻松很多。

2. 评论去哪抓:微博、B站、懂球帝的取舍

2.1 三个平台的接口难度,相差悬殊

先说结论:不是所有平台的评论都值得爬。有的平台接口干净得像教科书,有的平台光参数签名就能劝退新手。

平台数据入口难度主要难点
微博移动端评论接口中等需要登录Cookie,有频率限制,分页逻辑特殊
B站评论区/弹幕接口中高评论接口有签名参数,弹幕是XML格式
懂球帝App接口较高请求带签名与时间戳校验,逆向成本高

我最终的选择是:主抓微博评论,辅抓B站弹幕和评论,懂球帝只作为人工抽检对照,不写完整爬虫。原因很现实——我不打算在一篇复盘文里通宵逆向别人的App签名。懂球帝的讨论质量确实高,但为了那几百条数据去对抗签名校验,投入产出比太低。

如果你只是想复现一个舆论分析项目,我建议你也按这个思路来:先挑一个数据量最大、接口最友好的平台跑通全流程,其他平台作为补充。一口气贪多,大概率会在某个平台的反爬机制上卡到天亮。

2.2 微博评论:先找到那条“主微博”再说

微博的评论接口并不难找,难的是知道该去评论哪条微博。我第一步是搜索“U23 日本 决赛”之类关键词,找到比赛期间热度最高、由媒体账号发布的几条比赛报道微博,复制它们的微博ID。

有了微博ID之后,评论接口就顺理成章了。我使用的是微博移动端的Web接口,返回的是JSON格式,里面直接带着评论正文、用户昵称、发布时间、点赞数,还有一个关键字段——楼中楼回复数。结构大概是:

{ "data": { "comments": [ { "text": "虽然输了,但踢得有点东西", "created_at": "04-12 23:05", "like_count": 123, "reply_count": 3 } ] } }

这个接口需要带登录后的Cookie,否则会被风控挡下来。好在我手机端常年挂着微博,登录态一拷,短时间抓取没问题。注意,这个接口的分页不是简单的page=1、2、3,它是靠max_id来翻页的。第一次写的时候我用普通页码怼上去,一直重复拿到同一批数据,排查了二十分钟才发现问题。这类细节,平台文档不会写,真得踩一次才能记住。

2.3 B站弹幕:BV号转cid,剩下的就是解XML

B站这边,我选了一个比赛集锦视频,先拿到BV号。然后调用视频信息接口拿到视频的cid,弹幕接口就會返回一坨XML:

<d p="30.2,1,25,12345,0,0,0">差距还是有的</d> <d p="31.8,1,25,12346,0,0,0">未来可期</d>

每条弹幕的正文在标签体里,前面那个p属性里则是时间戳、弹幕类型、用户ID等元数据。用Python的xml.etree.ElementTree解析,循环取标签的text就行,非常丝滑。

B站评论区的接口反向需要处理一个叫WBI签名的东西,涉及请求参数按字典排序再哈希。说实话,第一次自己写很烦,后来我直接用了现成的开源库bilibili-api-python,自己只负责调用和存储。做项目要分清主次,我的目标是拿数据做情绪分析,不是发明B站签名算法。能省的时间,绝不硬肝。

2.4 样本量控制在什么范围,数据才够看

最终我跑了两个小时,拿到有效评论和弹幕约1.6万条。其中微博评论约9000条,B站弹幕约6000条,B站评论约1000条,懂球帝那边我只人工翻阅了热度最高的赛后帖,记录了大概几十条典型言论用作对照。

这个量级做粗粒度的情绪分析绰绰有余。每条评论统一清洗成四个字段:来源平台、评论文本、发布时间、点赞数。存成CSV,后面分析全靠它。

3. 爬虫落地的关键代码和三个容易翻车的细节

3.1 一套能用的通用爬虫骨架

不管是微博还是B站,我都基于requests.Session写了一套通用骨架。Session的好处是能自动维持Cookie,配合自定义请求头,看起来更像一个正常访客。我习惯加上随机延时和重试机制,毕竟评论抓取不是性能测试,没必要怼着服务器猛敲。

import requests import time from random import random session = requests.Session() session.headers.update({ "User-Agent": "Mozilla/5.0 (iPhone; CPU iPhone OS 17_0 like Mac OS X) " "AppleWebKit/605.1.15 (KHTML, like Gecko) Version/17.0 Mobile/15E148 Safari/604.1", "Referer": "https://m.weibo.cn/", "X-Requested-With": "XMLHttpRequest" }) def fetch_json(url, max_retry=3): for i in range(max_retry): try: resp = session.get(url, timeout=10) if resp.status_code == 200: return resp.json() except Exception: pass time.sleep(1 + random() * 2) return None

移动端的UA可以大大降低被风控标记的概率,因为多数反爬策略对PC头更敏感。Referer字段也得认真写,对防爬严格的接口,少了它可能直接返回403。

3.2 从浏览器复制Cookie,别急着写自动登录

评论区接口的登录态绕不开。我的做法很简单:手机浏览器登录微博后,在开发者工具里把Cookie整段复制到本地配置文件里。两张小时内抓紧把该抓的抓完,过期了再去浏览器重新复制一次。

不推荐在这个项目里做自动登录。微博的登录体系有滑块验证、设备指纹、短信验证码,一环扣一环。为了一个一次性分析项目去弄自动登录,至少要多写两百行代码,还得处理各种奇怪的验证码弹出。与其这样,不如手动复制Cookie,把精力留给后面的情感分析和可视化。数据抓取的核心指标是“到手率”,不是“自动化率”。

3.3 XPath里text()的坑,我替各位踩过了

抓B站网页版评论或某些论坛帖子时,我会用XPath提取正文。这里有个经典坑://div[@class='content']/text()只能取当前节点的直接文本,如果评论主体的结构是“外层div里有子span”,/text()取到的就几乎为空。

正确写法是:

comment = selector.xpath("//div[@class='content']//text()[normalize-space()]") comment = "".join(c.strip() for c in comment)

//text()拿到的是所有后代文本节点构成的列表,包括子标签里的文字,再拼接成完整句子。normalize-space()的作用是剔除空白节点,防止列表里混入换行和空格。这个细节不大,但第一次遇到时我一度以为目标页改了结构,绕了很大的弯。

3.4 数据清洗:评论里全是“@”和“#话题#”,不能直接用

原始评论基本没法直接丢给情感分析模型。里面夹杂着@某用户、#某话题#、链接、表情符号,还有大量重复转发。我清洗时做了这样几件事:

  • 去掉所有@用户名片段,只保留正文主体。
  • 去掉#话题#两个井号及其中内容。
  • 去掉URL和纯图片标记。
  • 文本长度小于2的删掉,基本是“哈哈哈”“……”这类无法判断情绪的碎片。
  • 用集合去重,微博评论区经常有复制粘贴刷屏的队形,这类重复内容会严重拉偏情感统计。

清洗后有效数据大概剩1.4万条。别嫌少,清洗本身就是数据质量的一部分。把脏数据留在里面,后面的情绪分析结果会被严重污染。

4. 情绪评分怎么做:SnowNLP、词典规则和人工抽检

4.1 为什么选本地库,而不调大模型API

做情感分析的路子很多:可以调大模型接口,可以申请云厂商的自然语言处理API,也可以用本地开源库。我最后选了本地方案:SnowNLP加词典规则融合。

原因有两个。第一,评论量是一万多条,如果全走大模型接口,算下来是一笔小额但没必要的账单,而且个人项目的数据本来就属于公共评论,没必要为了分析而把数据发到第三方服务。第二,本地方案可解释性更强。SnowNLP给出一个0到1的分数,我可以手动抽查,可以针对足球场景调整词典,整个过程自己完全可控。

大模型在细腻情感理解上更强,但在这个场景里,我需要的是粗粒度的正/中/负三分法,本地库完全够用。

4.2 SnowNLP的基础用法,以及它的“偏科”问题

SnowNLP的用法简单到只需要两行:

from snownlp import SnowNLP score = SnowNLP(text).sentiments

分数越接近1越正向,越接近0越负向。但问题在于,SnowNLP的训练语料偏电商评论,“发货快”“质量好”这类句子它能判断得很准,到了体育评论里就开始犯迷糊。

我踩到的实际案例是:“真服了”这种明显负向的句子,SnowNLP给了0.68分,居然是正向偏积极。“无语”也是,被识别成中性偏正。原因是这类口语在训练语料里出现得太少,模型根本没学会它们的真实情绪。

这就是我不迷信单个模型的原因。任何通用模型落到垂直领域,都需要做校准,否则结果就是精致的废话。

4.3 用足球场景词典做规则修正

为了解决“偏科”,我加了一层词典规则。大致逻辑是:用jieba分词,遍历文本里的词,按出现的正负向词进行加减分。我手工整理了两个词表:

  • 负向词表:解散、滚、垃圾、丢人、下课、废、摆烂、丢球、不配、呵呵、服了。
  • 正向词表:未来可期、虽败犹荣、拼了、尽力、有进步、希望、加油、稳住、敢打。

规则模型很简单,给每个词一个权重,加权求和后映射到0到1区间:

neg_score = sum(neg_word_weights.get(w, 0) for w in words) pos_score = sum(pos_word_weights.get(w, 0) for w in words) lex_score = (0.5 + pos_score - neg_score) / (1 + pos_score + neg_score) lex_score = max(0, min(1, lex_score))

最终得分是SnowNLP和词典规则的融合,我取了0.6的SnowNLP权重加0.4的词典权重:

final_score = 0.6 * snownlp_score + 0.4 * lex_score

大于0.6算正向,小于0.4算负向,中间算中性。融合之后,“真服了”这类句子总算被掰回了负向区间。整个修正过程花了大概一个小时,收益率很高。

4.4 人工抽检200条,确定模型可信度

模型调整完,不能直接拿结果出去说事。我随机抽了200条评论,自己一条一条看,人工打标签,再和模型结果做对比。最终准确率约78%:正负向判得比较准,主要集中在“中性”的界定上——很多评论本来就是吐槽夹杂着鼓励,人工智能分不清楚,人其实也拿不准。

78%的准确率对粗粒度舆论分析够用了。我不会拿它去发论文,但我敢拿它来回答“网友情绪到底偏正还是偏负”这个问题。

5. 可视化结果里的三个发现,以及那个“没想到”

5.1 情绪总盘:负面确实最多,但不是全部

把1.4万条有效数据的情绪分布画成饼图后,第一版结果是这样的:

  • 负向评论:56%
  • 正向评论:21%
  • 中性评论:23%

负面占多数,和我的预期一致。0比4的比分摆在那里,指望舆论一片叫好不现实。但让我注意的是,负面并没有我刷手机时感觉的那么压倒性——信息流里的骂声显得很多,是因为它们点赞高、回复多、被算法反复推送到眼前。真实评论海里,超过四成的声音其实并非纯发泄。

5.2 词云里的高频词:骂得最响的是一小撮

我对清洗后的文本做了词云。去掉了“日本”“中国”“比赛”这类无意义高频词之后,冒出来的关键词很有层次。

最显眼的是“防守”,这个词出现在大量战术分析评论里,比如“防守站位太松”“中场对后卫的保护不够”。第二个梯队是“未来可期”“年轻”“还有机会”,典型的鼓励派用词。第三梯队才是“解散”“丢人”这类纯情绪词。

词云不会骗人。骂声确实存在,但不是评论广场的唯一主题。很多人还是愿意看比赛内容本身,只是这类中性的、理性的表达没有骂声那种传播力。

5.3 情绪时间线:从崩溃到冷静,只需要三个小时

把时间切成半小时一个桶,我画了一张折线图。比赛刚结束时,负向情绪占比冲到70%以上,刷屏的几乎全是发泄。但大约一个半小时后,负向比例开始明显下降,正向和中性讨论追了上来。到赛后第四个小时,负向已经回落到50%上下,关于“未来还有戏”的讨论占比明显增加。

这个曲线很有意思。它说明大部分网友的情绪爆发是短期的,时间一过,人还是会回到相对理性的状态。所以我一直觉得,比赛刚结束那半小时看到的“全网骂声”,并不是全网,只是全网最激动的那一批。

5.4 让我反复确认的意外:按点赞加权后,理性声音反超了

这是整晚最让我意外的结果。把每条评论的点赞数作为权重重新统计情绪占比,事情彻底反转了:

  • 按评论条数统计,负向56%。
  • 按点赞数加权统计,正向+中性合计超过了55%。

也就是说,虽然发“发泄型评论”的人更多,但被网友真正认可、真正点赞顶上去的,其实还是理性分析和鼓励类的内容。骂声只是一阵风,点赞才是人心的投票。

这个结论让我头皮发麻。我刷手机时以为所有人都很愤怒,结果数据的真实回答是:大多数人还是保持着冷静,并且他们更愿意认同那些认真讨论比赛的人。算法没有骗我,但它只给我看了声量最大、最刺激的那部分。舆论场上的“多数”和“音量”从来不是一回事。

5.5 平台差异:微博最直接,B站更爱玩梗

分平台拆开看,情绪结构差异非常大。微博评论负向占比最高,评论区充斥着大量短句发泄,这和微博广场式的传播机制有关,人人可以冲上去吼一句。B站弹幕里的负向占比低很多,但充斥着“就这?”“寄”这类玩梗中性表达,很难被情感模型精确识别,很多被我归到了中性。懂球帝的赛后讨论则是另一番气象,战术分析帖居多,虽然也批评球员,但几乎每一条都在说具体问题,不是情绪攻击。

这提醒我,不同平台的人群气质完全不同,跨平台汇总得出的“网友情绪”,本质是一个混合口味,如果只看单一平台,你对整个舆论的判断就可能跑偏。

6. 复盘:限频、乱码、IP封禁,以及数据背后的分寸

6.1 这次踩过的四个典型坑

爬虫项目总是看着简单,跑起来全在踩坑。我这次遇到的几个问题,列出来给各位做个参考:

  • 微博翻页参数不是page而是max_id,用错会死循环在同一批数据上。
  • 微博跑到大概两千条时,接口开始返回风控错误码。解决方式是加随机睡眠,把请求间隔从1秒拉到3秒,再随机更换移动端UA。
  • B站弹幕XML文件默认不是UTF-8,读取时忘记指定编码,解析出来一堆乱码。记住用resp.apparent_encoding或直接声明encoding='utf-8'。
  • CSV文件用Excel打开乱码。写入CSV时参数必须带上encoding='utf-8-sig',只写utf-8的话Excel用自己的编码解读,中文全乱。

这些坑没有一个是原理层面的,全是实践细节。但只要踩中一个,就能让你在凌晨三点抓耳挠腮。

6.2 结果的可信度边界:先承认样本有偏差

我不太喜欢把个人小项目的结果包装成“全网结论”。在这个项目里,样本只有约1.4万条,来源集中在微博和B站,时间窗只有赛后四小时,所有原声都限定在这场比赛的公共讨论里。这些限制决定了结论只能代表“这几个平台、这个时间段、这批活跃用户”的情绪状态。

但它依然比刷手机可靠得多。因为刷手机看到的不是样本,而是算法筛选后的“高互动切片”。爬虫抓下来的是原始样本,即使有偏差,偏差也来自平台用户结构,而不是推荐机制。只要在写结论时把边界说清楚,这个分析就是有信息量的。

6.3 最后说一点和数据无关的分寸感

项目跑完,我把结果发给朋友看,朋友第一反应是:你写这篇东西,会不会被当成支持哪一方?我觉得不会。我只是想做一个不吵架的旁观者,用数据把舆论场切开来看一眼。

但我也不想把这次分析变成某种“打脸”工具。球员是二十岁出头的年轻人,输了决赛比谁都难受。做数据分析的人更要有分寸感:分析情绪分布,不等于给情绪发泄网开一面;指出理性声音占多数,也不等于替失败开脱。

那天晚上之后,我重新把微博和B站的评论区翻了一遍,心态完全变了。骂声还在,但我开始明白它们只是舆论水面上的泡沫;水面之下,大多数人还是清醒的。

如果你也想做类似的项目,我的建议是:大胆抓,仔细洗,模型别图贵,结论别图大。先把一套流程跑通,再慢慢扩展数据源和分析维度。数据不会替你做决定,但它能让你看到一个更真实的世界——至少比手机屏幕里那个世界真实一点。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/7 11:11:24

大疆热红外R_JPEG解析到温度TIF拼接全流程指南

每次拿到大疆无人机拍回来的热红外数据&#xff0c;我首先会做的事就是打开目录看一眼文件名。如果你的M300 RTK或Mavic 3T拍完之后是一堆DJI_20230701_T.JPG&#xff0c;那基本可以确定我们面对的是同一类问题&#xff1a;这些文件就是所谓的R_JPEG格式热红外图&#xff0c;里…

作者头像 李华
网站建设 2026/10/7 11:11:12

Godot 4双人战斗源码拆解:从状态机到判定盒的本地对战实现

简介&#xff1a;基于VC开发的双人对战游戏完整源代码&#xff0c;面向C初学者与游戏开发入门者&#xff0c;可用于学习Windows平台上经典小游戏的工程组织与实现逻辑。项目已在VC环境下调试通过&#xff0c;包含20幅对战地图&#xff0c;支持本地双人实时战斗&#xff0c;涵盖…

作者头像 李华
网站建设 2026/10/7 11:10:30

5G信令流程从文档到排障:Wireshark过滤与现网分支实战

简介&#xff1a;本资源是一份面向5G网络优化工程师与通信专业学习者的中级认证备考资料&#xff0c;聚焦5G核心信令流程原理与实践要点&#xff0c;系统解析注册流程、身份标识机制&#xff08;SUPI/SUCI/PEI&#xff09;、随机接入过程&#xff08;含竞争/非竞争模式&#xf…

作者头像 李华
网站建设 2026/10/7 11:09:29

机器学习期末复习与课程设计实战:从西瓜书到完整应用流程

每年到这个时候&#xff0c;打开搜索框&#xff0c;“机器学习期末复习”“人工智能大作业”“机器学习课程设计选题”“机器学习西瓜书”“机器学习应用流程”这些热词总扎堆出现。不奇怪&#xff0c;很多人最初把机器学习等同于人工智能机器人&#xff0c;真正面对一门课、一…

作者头像 李华
网站建设 2026/10/7 11:09:24

C++ GoogleTest 常用断言详解:从 EXPECT_EQ 到崩溃检测 EXPECT_DEATH

本文通过一个可直接编译运行的 C++17 工程,集中演示 GoogleTest 中常用的布尔、比较、字符串、浮点、异常、谓词、测试夹具和进程终止断言。工程已经在 Windows、Visual Studio 2022 和 GoogleTest 1.15.2 环境下验证,17 个测试全部通过。 一、为什么要分类学习 GoogleTest 断…

作者头像 李华
网站建设 2026/10/7 11:08:59

switch里能塞表达式吗?全等比较与类型收窄详解

你是不是也曾经在代码里写过这样的逻辑&#xff1a;遇到多状态、多分支的业务场景&#xff0c;顺手就想用一个 switch 来搞定。结果要么是编译报错“表达式必须包含类类型”&#xff0c;要么是线上功能完全不生效&#xff0c;所有分支都静悄悄地走 default &#xff0c;你翻…

作者头像 李华