十年匠心定制 · 商业建站与技术教学双线并行 咨询热线:400-886-1026 service@lmnt.cn
ARTICLE DETAIL

资讯详情

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

Python网络舆情分析实战:从弹幕采集到情感演化

Python网络舆情分析实战:从弹幕采集到情感演化 简介这份项目源码包以电影《雄狮少年》的社交媒体讨论为对象实现网络舆情数据采集、清洗、分析与可视化适合计算机、数学、电子信息等相关专业学生用于课程设计、期末大作业或毕业设计参考。压缩包共157个文件约40.47MB主要包含Python程序py、舆情数据表格csv、分析图表png/webp、说明与笔记文档md/txt以及部分网页展示件html项目说明与源码目录完整便于直接运行和二次开发。目前已有65人学习/下载。从数据抓取到结果呈现资源形成完整分析链路csv数据文件涵盖多平台评论与热门话题py脚本提供采集与处理逻辑png/webp图像直观展示情感分布和关键词热度pdf/caj等文档辅助理解研究背景。下载后可作为参考资料学习将网络爬虫、自然语言处理与数据可视化结合也可根据课程或毕设需要自行调整功能模块。1. 网络舆情分析的起点《雄狮少年》这样的电影该看哪些信号网络舆情分析首先不是跑一段模型而是确定你能看到的数据边界。对《雄狮少年》这种院线动画真正有价值的样本不是全网讨论而是上映前一周到上映后四周内在豆瓣、B站、微博等公开场景产生的评论。拿到一个名为“网络舆情分析源码项目说明.zip”的包时先读项目说明不要先读算法平台、时间窗、样本量决定了后面所有结论的边界。我习惯把这类项目拆成采集、清洗、分析、交付四层。采集负责落盘清洗负责去噪分析把文本变成情感分值交付把运行参数写清楚。接下来我会按这条链路讲给出能直接改的 Python 片段并指出哪些环节最容易出现漂亮但不可信的结果。2. 采集层把《雄狮少年》的弹幕和短评抓成本地样本任何网络舆情分析项目第一步都不是建模而是确认数据边界。电影舆情并不是“全网声音”而是几个代表性平台在指定时间段里能公开获取的样本。处理《雄狮少年》这类项目时我通常选择豆瓣短评、B站弹幕和微博评论三个来源项目里如果有采集模块几乎也是一个平台一个模块因为字段结构、接口限额和反爬策略完全不同混在一起维护成本很高。2.1 先圈定数据源和时间窗再写第一行爬虫电影舆情往往在上映前一周开始预热上映后四周内出现多次波动。时间窗不锁死采集回来的数据会被预告片阶段的弹幕以及上映很久之后的口碑贴一起搅浑情感分布自然失真。所以我在config.yaml里固定start_date、end_date采集脚本里只保留落在窗内的send_time而不是把所有能抓到的历史数据都导进来。选择弹幕还有个理由弹幕自带video_time可以对齐正片剧情。短评和微博评论只能告诉你用户整体上是否满意弹幕能告诉你画面放到哪一段时弹幕量突增、负面评论集中在哪个情节。两者结合正好覆盖“整片口碑”和“场景反应”两个层次。如果你的数据源只有一个我优先留弹幕因为时间戳字段更细后面做演化分析时比短评好用得多。2.2 B站弹幕采集脚本把 XML 转成结构化 DataFrame下面是一份最小可用的采集器。这里先不加入请求频率控制、重试和断点续传那些工程化手段会干扰你理解数据字段等链路跑通再加不迟。import hashlib import requests import xml.etree.ElementTree as ET import pandas as pd headers { User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 } def fetch_danmaku(cid: int, save_path: str data/raw/danmaku.xml) - str: # cid 是视频分 P 的编号不同视频不一样 url fhttps://comment.bilibili.com/{cid}.xml # 示例接口以现网可用端口为准 resp requests.get(url, headersheaders, timeout10) resp.raise_for_status() with open(save_path, wb) as f: f.write(resp.content) return save_path def parse_danmaku(xml_path: str) - pd.DataFrame: root ET.parse(xml_path).getroot() rows [] for node in root.iter(d): p node.attrib[p].split(,) # p[0] 弹幕出现时间p[4] 发送时间戳p[6] 用户hash rows.append({ comment_id: hashlib.md5(node.attrib[p].encode()).hexdigest(), video_time: float(p[0]), send_time: int(p[4]), user_hash: p[6], content: node.text }) return pd.DataFrame(rows, columns[comment_id, video_time, send_time, user_hash, content]) if __name__ __main__: raw fetch_danmaku(cid1145141919) # 示例 cid运行前换成真实视频的 cid df parse_danmaku(raw) df.to_parquet(data/processed/danmaku.parquet, indexFalse) print(df.shape)这段代码里有两个容易写错的地方。一是不能直接用文本内容做去重同一句话可能被不同用户发送多次我用p字段整体做 MD5把发送时间、用户 hash 和位置都带进去生成的comment_id更接近“一次弹幕行为”的唯一键。二是接口返回内容必须用resp.content而不是resp.text去解析因为 XML 声明里带编码信息ElementTree 读原始字节能自动识别编码避免中文乱码。采集完立刻做一个最小验证df[sys_time] pd.to_datetime(df[send_time], units) print(df[sys_time].min(), df[sys_time].max()) print(df.shape)如果min、max与项目说明里的时间窗对不上说明cid选错了或者采集到的是已经下线很久的旧弹幕池。此时不管情感模型调得多好结论都建立在错误的样本上。这类问题越早发现越便宜所以我通常在采集环节就丢掉不合格的数据而不是等分析完成后再补救。提示平台公开接口可能随版本调整接口路径最好从collect.py里拆出去放进config.yaml避免每换一部电影就改一次代码。2.3 字段表与存储格式为后续分析预留扩展位解析完成后字段要先固定下来。内容、时间戳、用户、出现位置是最低要求其他平台字段可以后续追加。字段类型是否必须用途comment_idstring是去重主键由用户、时间、内容共同生成 MD5video_timefloat是弹幕在视频中的位置用于剧情对齐send_timeint是发送的 Unix 时间戳用于演化分析user_hashstring可选匿名用户标识用于去重不能还原为 UIDcontentstring是弹幕原始文本保留最原始版本保存时我推荐parquet而不是 CSV。弹幕文本里包含换行、特殊符号和引号CSV 写入和再读取时容易错位parquet 是列式存储保留 pandas 的类型信息send_time不会在第二次读入时变成字符串。项目说明里也要标注“原始 XML 不删除”接口变化后还能重新解析不用回源平台再爬一遍。3. 清洗和情感计算网络舆情分析如何把“口碑”变成分值弹幕和短评进入 DataFrame 之后不能直接丢给情感模型。原始文本里混着话题标签、URL、at 用户、表情符号甚至还有一些残缺的引号和不完整词不清理分词会把这些噪声当成独立词情感分值也会被“哈哈哈”“笑死”这类语气词带偏。清洗不是越多越好而是要先想清楚“让模型看到什么”。3.1 先清洗再算情感正则替换的 3 个优先级我的清洗顺序是先删表情符号再删 URL 和 at最后删话题标签。顺序有讲究表情符号落在特殊 Unicode 区域先处理可以避免后续正则把半个表情符号误认为普通字符URL 和 at 是结构噪声对情感判断没有贡献话题标签“#雄狮少年#”会重复出现在每条微博评论里如果不删除分词和 TF-IDF 的结果会被同一个词反复占据。import re import pandas as pd EMOJI_PATTERN re.compile([\U0001F300-\U0001FAFF\u2600-\u27BF\uFE0F]) URL_PATTERN re.compile(rhttps?://\S) AT_PATTERN re.compile(r[\w\u4e00-\u9fa5\-]) TOPIC_PATTERN re.compile(r#.*?#) def clean_text(text: str) - str: text EMOJI_PATTERN.sub(, text) text URL_PATTERN.sub(, text) text AT_PATTERN.sub(, text) text TOPIC_PATTERN.sub(, text) text re.sub(r\s, , text).strip() return text df[clean_text] df[content].astype(str).map(clean_text) df df[df[clean_text].map(len) 2] df df.drop_duplicates([comment_id, clean_text])len 2是过滤单字弹幕比如“好”“顶”“燃”。这类单字词没有明确对象统计到正负面比例里容易放大噪声但如果你想保留微博里的“哈哈哈哈”阈值会再往上调。这里的取舍要在项目说明里写清楚否则对方看到情感标签数不对会先怀疑清洗脚本有 bug。有一点要注意如果模型换成 BERT 或大模型表情符号应该保留。SnowNLP 对 emoji 没有专门向量留着只会干扰输入长度和分词但 BERT 类模型能把表情符号和语气关联起来。所以清洗规则不是通用模板而是跟着模型走。后面 3.2 的阈值表默认针对 SnowNLP换模型后必须重新标定。3.2 SnowNLP 打分和阈值表0.45/0.55 比 0.5 更稳SnowNLP 的sentiments输出在 0 到 1 之间很多人习惯直接拿 0.5 当正负分界线。问题在于电影弹幕里存在大量“还行”“一般”“不是不好看但……”这类犹豫表达默认模型会把它们压在 0.5 附近。我把正负判断设成两个阈值中间留一个中性区情感均值才不会因为模型自身噪声反复横跳。from snownlp import SnowNLP def score(text: str) - float: try: return SnowNLP(text).sentiments except Exception: return 0.5 df[sentiment] df[clean_text].map(score) df[label] pd.cut( df[sentiment], bins[0, 0.45, 0.55, 1.01], labels[negative, neutral, positive], rightFalse ) print(df[label].value_counts())rightFalse代表左闭右开0.45 分划到 negative0.55 分划到 positive。如果漏写这个参数pandas 默认rightTrue0.45 会被分类到中性组中性评论占比明显偏高正负对比被压缩。1.01只是为了避免 float 边界把 1.0 排除实际打分不会超过 1。情感分值范围标签使用建议[0, 0.45)negative找负面集中时段和负面关键词优先抽原文人工复核[0.45, 0.55)neutral不参与正负对比但计入评论量代表讨论热度[0.55, 1.01)positive找正面动因比如“画面”“音乐”“台词”相关词阈值定成 0.45/0.55 之后要检查正负样本量是否失衡。如果 negative 占比不到 5%要么是电影口碑真的高要么是清洗时把大量反讽文本删掉了。判断方法是随机抽 50 条 negative 和 50 条 neutral 看原文对照项目说明里的业务预期再决定是否微调阈值。3.3 聚合指标不要只报告情感均值单条评论的情感值抖动很大“还行”是 0.49“太烂了”是 0.02均值很容易被中间段评论拉向 0.5。所以我通常按天或按小时计算mean、std、pos_ratio、neg_ratio四个字段均值只当参考正负面占比才是判断舆情方向的主指标。df[date] pd.to_datetime(df[send_time], units).dt.date stats df.groupby(date).agg( mean_score(sentiment, mean), std_score(sentiment, std), comment_count(comment_id, count), pos_ratio(label, lambda x: (x positive).mean()), neg_ratio(label, lambda x: (x negative).mean()), ).round(4) print(stats.describe())pos_ratio和neg_ratio用 lambda 逐组计算在这个量级下没问题超过 10 万条数据后效率会下降。优化时可以把 label 映射成 0/1/2再用数值均值去算占比逻辑是一样的。这里按自然日聚合适合看每日趋势如果发现某天波动异常再到小时粒度里找具体爆发点也就是第 4 章要做的事。4. 演化分析与主题挖掘定位《雄狮少年》舆情的拐点前面算出的情感分值是静态截面真正能写进项目报告的是分值在时间轴上的移动。电影舆情不是均匀变化的某个负面话题出现后几小时内弹幕量陡增情感均值突然下行只看得分值不看得分变化率你会错过真正的舆情转折节点。4.1 用小时聚合找到“情绪拐点”不能只看日线日线适合画总结不适合找拐点。比如下午 3 点出现一条负面热搜日线只能显示“今天均分下降”小时粒度则能看到负面讨论在 15:00—16:00 之间快速到达峰值又在晚上回落。我在项目里把send_time按小时取整然后计算相邻小时的变化率。import pandas as pd df[hour] pd.to_datetime(df[send_time], units).dt.floor(h) hourly df.groupby(hour).agg( count(comment_id, count), mean(sentiment, mean), neg_ratio(label, lambda x: (x negative).mean()) ) hourly[prev_count] hourly[count].shift(1) hourly[growth] (hourly[count] - hourly[prev_count]) / hourly[prev_count] burst hourly[hourly[growth] 0.5].copy() burst burst[burst[prev_count] 50] print(burst.sort_values(growth, ascendingFalse).head(10))growth 0.5表示评论量比上一小时提升 50% 以上这个阈值不是固定值数据量越大可以调得越保守。prev_count 50很关键凌晨 2 点每小时只有 4 条弹幕下一小时变成 8 条增长率是 100%但这是随机波动不是舆情爆发。定位到爆发小时之后再切一个时间窗口把对应时段里被标记为 negative 的弹幕抽出来focus_hour burst.index[0] mask (df[hour] focus_hour) (df[label] negative) sample df.loc[mask, [video_time, clean_text]].head(30) print(sample.to_string())这段样本是给人工复核用的。纯文本情感模型很难识别反讽比如“画面很燃剧情像一杯白开水”这类评论模型可能给出中性甚至正面分数但实际表达的是失望。人工抽看 30 条远比直接相信neg_ratio列更接近真实舆论。4.2 正负面分组提关键词TF-IDF 比词频更好用找到爆发时间后下一个问题通常是“为什么爆发”。我一般把样本按 negative 和 positive 两组分别做 TF-IDF而不是对所有文本统计词频。词频只能看到“电影”“雄狮”这种背景词TF-IDF 能把每组文本中相对独特的关键词顶上来更容易看出负面和正面各自的动因。import jieba from sklearn.feature_extraction.text import TfidfVectorizer df[tokens] df[clean_text].map(lambda x: .join(jieba.cut(x))) for label in [negative, positive]: sub df[df[label] label] if len(sub) 20: continue vec TfidfVectorizer( max_features10, token_patternr(?u)\b\w\b ).fit(sub[tokens]) print(label, vec.get_feature_names_out())这里最容易掉的坑是token_pattern。jieba 产出的文本是用空格分隔的词序列\b\w\b能按空格切分同时排除标点符号。如果你不写这个参数sklearn 默认正则会把整句“雄狮少年 好燃”当成一个 token关键词基本没有参考价值。参数推荐值作用floorh按小时对齐消除秒级抖动growth 阈值0.5评论量较上小时提升 50%随数据量调整prev_count 下限50过滤低基数时段防止凌晨空档误触爆发max_features10每个情感分组只保留 10 个词便于人工审阅token_pattern\b\w\b配合 jieba 空格分词按词切分关键词输出之后我会把 negative 组里的词和爆点小时重叠起来看。比如爆发时间是上映首日晚 20:00negative 关键词里有“改编”“失望”“节奏”那基本能锁定争议集中在剧情改编上如果关键词是“配音”“画面”“角色”方向又不同。主题挖掘的作用不是替你下结论而是让下结论的位置更具体。5. 交付层让“源码项目说明.zip”能被重新验证一个包含源码和项目说明的 zip价值不在预先算好的图表而在接收方能否用同样数据把结论重新跑出来。我在整理交付物时会刻意把“可复现”放在“好看”前面。项目说明里如果缺了数据边界和运行顺序别人只能看到结果看不到结果是怎么来的。5.1 目录结构与配置项常见做法是放一个 README.md、一个 requirements.txt、一个 config.yaml再加采集、分析、报告几个入口脚本。原始数据不做进 zip只放脱敏样本完整数据留在本地压缩包体积和传输时间都会可控很多。xiongshi_opinion/ ├── README.md ├── requirements.txt ├── config.yaml ├── collect.py ├── analyze.py └── data/ ├── raw/ │ └── danmaku.xml └── processed/ └── danmaku.parquetREADME 里的项目说明至少要有三块数据来源和时间窗、运行顺序、关键结论的可复现性。如果只写“采集了《雄狮少年》弹幕”接收方不会知道cid从哪来、parquet是谁生成的也无法判断结论是否能复现。5.2 用一个最小命令完成端到端验证我会在 README 里留下一段等价的最小命令链pip install -r requirements.txt python collect.py --config config.yaml python analyze.py --config config.yaml --output output/report.jsonconfig.yaml里一般包含这样的结构data: start_date: 2025-01-01 end_date: 2025-02-01 platform: bilibili cid: 10765125 model: sentiment: snownlp pos_threshold: 0.55 neg_threshold: 0.45 output: top_keywords: 10cid放在配置文件里而不是写死在代码中参数化的好处是换一部电影也能复用同一套源码。脚本里用argparse或click读取 config输出路径、阈值、关键词数量都可以通过命令行覆盖debug 时不用反复改配置文件。最后留一个验证技巧跑完analyze.py后把total_comment_count、negative_ratio、burst_time三个数值写进output/report.json。后续改代码或换模型重新运行后先比较这三个值。弹幕总数变化超过 5%说明采集层的时间窗或接口变了负向占比变化超过 10%说明清洗规则或情感阈值被改动只有当这些基础值和上次输出一致时新的分析结果才有可比性。本文还有配套的精品资源点击获取
返回列表