简介:围绕“上证指数吧”评论数据,这套资源提供完整的股票评论情感分析Python项目,适合金融数据分析、自然语言处理入门者及量化投资爱好者,用于从海量股吧评论中捕捉市场情绪并观察其随时间变化。项目既有网络爬虫脚本,也有基于NLP的情感分类与时间序列分析流程,能够帮助理解从数据采集、清洗标注到情感建模与趋势可视化的完整分析路径。压缩包为rar格式,共6个文件,以5个Python脚本为主体,涵盖评论爬虫、数据标注、情感分类、趋势可视化等环节,另含一个按时间整理的文本数据文件,整体仅236KB,结构紧凑。已有1835人学习下载,适合快速上手体验。脚本覆盖数据清洗与标注、模型评估与优化等关键环节,可帮助读者掌握金融舆情分析的基本范式,也可在此基础上接入实时数据,构建自己的市场情绪观察工具。 我一直觉得股市分析最缺的往往不是更复杂的模型,而是对市场参与者情绪的第一手感知。做股票评论的情感分析,尤其是像上证指数吧这种人气极高的股吧,本质上就是在给市场的“心跳”做记录。这个项目把两个方向结合到了一起:一是对海量、非结构化的中文股票评论做情感打分,二是把分数按时间轴排列,找出情绪和指数走势之间的微妙关系。简单说,就是用爬虫抓取股吧帖子或评论,跑一个情感分析模型,然后按日粒度计算情绪均值、情绪分化度等指标,最终得到一条能跟K线对比的情绪曲线。这篇文章我会把从数据获取、清洗、情感打分到时间序列可视化的完整过程都拆开讲,适合想入门金融文本挖掘、或者正在找练手项目的Python学习者参考。
1. 项目到底想解决什么问题:从“乱糟糟的评论”里捞出“市场温度”
1.1 为什么选上证指数吧,而不是个股吧
上证指数吧在东方财富社区里属于流量最大的几个吧之一,日发帖量、回复量都是海量级别。选择它作为研究对象有几个很现实的好处:一是样本量充足,能保证按天聚合之后情感曲线不会因为样本太少而剧烈跳动;二是它讨论的对象是大盘指数,不是某一只个股,受单一公司消息的干扰小,情绪更能反映整体市场的状态;三是指数本身有公开的行情数据,方便后期做情绪和走势的对照分析,这是很多个股吧不具备的便利条件。
这种“公开数据 + 公开行情”的组合,非常适合用来做情感分析和时间序列相关性的实验。它不需要你有Wind账号,也不需要付费API,一条爬虫加一个开源情感模型就能启动研究。
1.2 情感分析和时间序列在这个项目里的关系
很多初学者会把情感分析理解为“给一句话打一个正负面分数”,这没错,但在金融评论场景里,单条评论的分数几乎是没意义的。一条评论被点踩、被淹没在评论区底部,和一条被几百人回复的热帖,影响权重天差地别。所以项目的核心思路不是“判断单条评论好坏”,而是把大量评论在时间维度上做聚合,形成情绪指数。
情绪指数类似一个“市场体温计”:把某一天的评论分数做平均,得到日情绪均值;把所有分数的标准差保留,得到情绪分化度;再用指数走势做对照,看情绪拐点是否领先或滞后于指数拐点。这就是情感分析和时间序列结合的意义所在。
2. 整体方案设计:数据、模型、时间轴三件套
2.1 技术选型:为什么是Python + 东方财富接口 + SnowNLP
我选用的技术栈非常朴素,没有上大模型,因为做时间序列聚合这种任务,重点不在单句精度,而在效率和稳定性。
| 模块 | 工具 | 选择理由 |
|---|---|---|
| 数据采集 | requests + 东方财富股吧帖子接口 | 该接口无需登录,分页参数简单,返回JSON格式,解析成本低 |
| 文本清洗 | re + jieba | 过滤HTML标签、表情、@用户、重复字符,为后续打分降噪 |
| 情感打分 | SnowNLP 或 金融领域微调模型 | SnowNLP开箱即用,适合快速验证;微调模型效果更好但需要标注语料 |
| 时间序列处理 | pandas + numpy | 按日resample、移动平均平滑、滚动波动率计算,pandas天生擅长这种工作 |
| 可视化 | matplotlib + mplfinance | 情绪曲线和K线的叠加展示,直观观察领先滞后关系 |
SnowNLP默认模型是在电商评论上训练的,说实话直接拿到股吧场景会有一定的偏置,比如“涨停”在它眼里可能不是正面词,“暴跌”在它眼里可能也不是特别负面。后面实操部分会专门讲怎么处理这个偏置问题,常用的方式有两个:一是准备一些股吧语料做模型微调,二是在打分之后对极端值做缩尾处理,不让模型偏差影响整体趋势判断。
2.2 数据采集思路:不是爬全文,而是爬趋势
有的朋友一上来就想把所有评论内容全抓下来,我建议分阶段来。第一阶段只需要抓“标题 + 发布时间 + 热度指标(阅读量/评论量)”,因为情感分析对内容长度不敏感,但热度对情绪权重影响很大。热帖的标题往往就代表了一种强烈的市场观点,比如“今天这根大阳线说明什么?”“完了,要破位了”,这类标题本身已经足够表达情绪。
股吧的帖子接口有一个显著特点:它是按最后回复时间而非发帖时间排序的。这对做时间序列的人来说是个坑,如果直接按列表页顺序抓,抓下来的数据是“被顶起来的旧帖”,不是当天的真实发帖分布。解决办法是抓取时带上发帖时间筛选参数,或者抓下来后再按发帖时间过滤,只保留目标时间窗口内的数据。
3. 核心代码与实现细节
3.1 第一步:写一个“温和”的爬虫,别把自己搞进黑名单
这里说的温和,指两个方面:请求频率要控制,别每秒几十次请求去打人家服务器;数据量要克制,不需要百万级样本,每天均匀采集一部分就能稳定估算情绪趋势。
下面是我常用的抓取函数,核心是利用东方财富的帖子列表接口,分页读取帖子标题和发帖时间。
import requests import pandas as pd import time def get_guba_posts(page_no=1, page_size=50): url = "https://gbapi.eastmoney.com/api/Article/GetArticleList" params = { "code": "000001", # 上证指数吧的代码 "type": "1", # 1表示全部帖子 "pageSize": page_size, "pageIndex": page_no, "sortType": "1", # 按发帖时间排序 "callback": "" } headers = { "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36", "Referer": "https://guba.eastmoney.com/" } resp = requests.get(url, params=params, headers=headers, timeout=10) if resp.status_code != 200: return pd.DataFrame() data = resp.json().get("result", {}) posts = data.get("list", []) df = pd.DataFrame(posts) if not df.empty: df = df[["post_title", "post_publish_time", "post_click_count", "post_reply_count"]] df.rename(columns={ "post_title": "title", "post_publish_time": "publish_time", "post_click_count": "click_count", "post_reply_count": "reply_count" }, inplace=True) return df注意这里有两个关键细节:
sortType=1表示按发帖时间排序,这个参数一定要显式传,否则默认按最后回复时间排,会拿到大量旧帖。code=000001是上证指数吧的代码,这是东方财富内部的板块编码,跟行情软件里的000001不是一回事,直接替换成个股代码是拿不到对应吧数据的。
采集端还需要做随机延时处理,我一般用time.sleep(random.uniform(1, 3)),既不给服务器造成压力,也能降低被风控的概率。实测下来,用这个频率跑一天能采集几万条帖子数据,完全够做情绪分析了。
3.2 第二步:清洗文本,把噪声挡在情感模型之前
股吧文本的噪声类型和普通社交平台不太一样,主要问题包括:
- 大量“顶”“支持”“666”等无意义短词
- 股票代码和数字夹杂在中文里(“今天000001真是坑”)
- 股民自创的谐音梗(“韭菜”“站岗”“接盘侠”)
- 重复的标点符号和表情符号(“!!!!”)
清洗策略不需要特别复杂的模型,正则表达式就能解决大部分问题:
import re def clean_text(text): if not isinstance(text, str): return "" # 去掉URL text = re.sub(r"http\S+", "", text) # 去掉@用户 text = re.sub(r"@\w+", "", text) # 去掉HTML标签 text = re.sub(r"<.*?>", "", text) # 去掉表情符号和特殊符号 text = re.sub(r"[^\u4e00-\u9fa5a-zA-Z0-9,。!?、]", "", text) # 去掉重复标点 text = re.sub(r"([,。!?、])\1+", r"\1", text) # 去除纯数字或过短内容 if len(text) < 2: return "" return text.strip()清洗这一步的价值在于,情感模型的输入越干净,模型语义空间的判断越稳定。像“!!!”这种噪声如果不清理,SnowNLP会给整句分数产生难以预期的扰动,日维度聚合时虽然会被平均掉一部分,但碰到极端样本少的日期,一条脏数据就能让当天得分偏离0.1以上。
3.3 第三步:情感打分,以及怎么处理模型偏置
清洗完文本之后,就是情感打分。我之前试过几种方案,包括直接使用SnowNLP、基于金融语料微调的BERT模型,以及基于规则的情感词典。简单对比一下:
| 方案 | 精度 | 速度 | 适用场景 |
|---|---|---|---|
| SnowNLP原版 | 中等 | 快 | 快速验证、小规模数据 |
| SnowNLP + 股吧语料微调 | 较高 | 快 | 本项目推荐 |
| BERT金融微调模型 | 高 | 慢 | 对精度要求较高的研究场景 |
| 基于词典的规则方法 | 低 | 极快 | 大规模数据的粗筛 |
SnowNLP有一个特别方便的特性:支持train()方法自定义训练,喂给它两个语料文件,一个是正面评论,一个是负面评论,就能微调模型权重。我攒了大概一万条人工标注的股吧评论,训练效果在个股吧场景下有明显提升,典型例子是“利好出尽”这种话,原版模型判断为正面,微调后能识别为负面,因为股吧语境下这句话表达的往往是失望情绪。
from snownlp import SnowNLP def sentiment_score(text): clean = clean_text(text) if not clean: return None s = SnowNLP(clean) return s.sentiments # 返回0~1之间的情感分,越接近1越正面 df["sentiment"] = df["title"].apply(sentiment_score) df = df.dropna(subset=["sentiment"])这里有个值得注意的点:sentiments输出是0到1之间的置信度,不是严格意义上的概率,更不是“看多概率”。0.6和0.4之间的差距在单条评论上没有解释意义,但在几百条评论取平均以后,0.01的差异也可能对应着情绪面的微妙转变。所以后续分析一定要基于聚合数据,不要拿单条分数去做判断。
3.4 第四步:按时间维度聚合,生成情绪指数
情感打完之后,真正的“随时间变化”分析才开始。这一步的核心操作就是pandas的resample聚合,把每一秒都在产生的评论映射到“天”这个时间维度上。
df["publish_time"] = pd.to_datetime(df["publish_time"]) df.set_index("publish_time", inplace=True) # 按日聚合情绪指标 daily_sentiment = df["sentiment"].resample("D").agg(["mean", "std", "count"]) daily_sentiment.columns = ["sentiment_mean", "sentiment_std", "comment_count"] # 做7日移动平均平滑,消除周末和节假日带来的样本量波动 daily_sentiment["sentiment_ma7"] = daily_sentiment["sentiment_mean"].rolling(7).mean() # 情绪分化度:标准差越大说明市场分歧越大 daily_sentiment["deviation_ma7"] = daily_sentiment["sentiment_std"].rolling(7).mean()我一般同时保留三个指标:日平均情感分、情感标准差和评论总数。平均分描述市场情绪的中心位置,标准差描述分歧程度,评论总数描述关注热度。这三个维度合成起来看,比单独看均值信息量要大得多。
比如某一天平均分是0.45,看起来偏负面,但如果标准差只有0.1,说明大家一致看空;如果标准差是0.3,说明多空分歧严重,平均分代表的只是中间位置,不能简单翻译成“市场情绪悲观”。这种多指标联动的分析,才是时间序列情感分析真正有意思的地方。
4. 实操过程中的踩坑记录与排查技巧
4.1 高频问题速查表
我在做这个项目的过程中踩过不少坑,有些是数据层面的,有些是模型层面的,整理了一个速查表发给大家参考。
| 问题 | 表现 | 原因与解决方案 |
|---|---|---|
| 抓到的帖子都是旧帖 | 当天数据量远小于预期 | 排序参数是最后回复时间,必须显式设置按发帖时间排序 |
| 情感分数全部集中在0.4~0.6 | 模型输出看不出区分度 | SnowNLP对短文本有天然的中性偏置,需要微调模型,或者在聚合时做极值缩放 |
| 某一天样本量极少 | 日情感均值突变剧烈 | 节日、周末、非交易日样本量天然少,需要用7日移动平均或者过滤低样本日期 |
| 文本被截断 | 部分帖子标题明显不完整 | 接口本身有长度限制,分析时不要过度依赖长文本,短标题情绪表达往往更直接 |
| 爬虫被封IP | 请求返回403 | 请求频率太高,增加随机延迟,切换IP代理或降低数据采集量 |
4.2 一个容易忽略的细节:非交易日数据要过滤
股吧评论虽然全年都在产生,但交易日的评论样本量和非交易日完全不在一个量级。周六周日大家聊的往往是“下周怎么走”“有什么消息”,情绪和盘面脱钩;周一的情绪里则包含了周末两天的积累。这种结构性差异会直接影响时间序列的平稳性。
我建议在聚合之前把非交易日的样本直接过滤掉,或者做交易日重采样。最简单的办法是引入交易日历,只保留交易日数据再画情绪曲线。否则周末的“假冷清”和周一开盘后的“假爆发”会在曲线上形成明显的锯齿,干扰对真实趋势的观察。
如果不是专门研究节假日效应,直接过滤掉非交易日是最省心且最合理的选择。
4.3 情感曲线和指数K线的对照:怎么画才有信息量
情感曲线单独看价值有限,必须和指数走势叠加才能看出“谁领先谁滞后”。我习惯用mplfinance做蜡烛图,把情感均值作为第二子图,用fill_between填充0.5基准线上下的区域,这样一眼就能看出情绪的翻转节奏。
import matplotlib.pyplot as plt import mplfinance as mpf # daily_sentiment和index_data都按交易日对齐 plot_df = pd.concat([index_data, daily_sentiment], axis=1).dropna() # 先画K线,再在下方子图画情绪均值 fig, axes = plt.subplots( 2, 1, figsize=(14, 10), sharex=True, gridspec_kw={"height_ratios": [3, 1]} ) mpf.plot( plot_df, ax=axes[0], type="candle", volume=False, style="yahoo" ) axes[1].plot(plot_df.index, plot_df["sentiment_ma7"], color="orange") axes[1].axhline(0.5, color="gray", linestyle="--", linewidth=1) axes[1].fill_between( plot_df.index, 0.5, plot_df["sentiment_ma7"], where=plot_df["sentiment_ma7"] >= 0.5, color="red", alpha=0.3 ) axes[1].fill_between( plot_df.index, 0.5, plot_df["sentiment_ma7"], where=plot_df["sentiment_ma7"] < 0.5, color="green", alpha=0.3 ) plt.show()我在实际对照中还发现一个现象:短线情绪均值往往和指数走势同步性很高,但情绪标准差指标通常有提前见顶或见底的特征。也就是说,市场还在涨,但评论里的分歧度已经在悄悄放大,这往往是行情进入分歧期的早期信号。这种领先关系不一定稳定,但作为观察维度非常值得跟踪。
5. 进阶思路:情绪指数还能往哪个方向走
到这里,基础版的情感时间序列分析已经跑通了。如果你想让这个项目更有深度,我建议往两个方向延伸。
第一是构建更复杂的情绪状态指标,不是简单看均值,而是把均值、标准差、评论量组合成一个“情绪温度计”。比如用均值判断多空、用标准差判断分歧、用评论量判断关注度,三个维度组合产生八种状态,例如“高关注 + 强分歧 + 偏多”就是一种典型的顶部区域特征,“低关注 + 低分歧 + 偏空”则可能对应缩量阴跌的低迷期。把这些状态编码后做转移概率矩阵,可以研究情绪状态之间的切换规律。
第二是把情绪序列和指数收益率做滞后相关分析,看情绪是不是具备预测能力。这一步用pandas的shift函数就能实现,核心是计算不同滞后天数下的相关系数或互信息。需要注意处理数据的平稳性,一般对日收益率序列可以直接用,但如果看价格序列要先做差分。我试过把情绪均值滞后3到5天后和指数日收益率做相关,在某些时间段能到0.2以上的相关性,已经远超随机水平了,说明市场情绪确实包含了一部分未来价格的信息。
刚开始做这个项目的人最容易掉进的误区是追求模型复杂度和单句准确率,但实际上,做时间序列情感分析,数据质量、时间口径的一致性和特征构造的合理性,远比模型本身的AUC重要得多。先跑通一个稳定、可重复的情感曲线生成流程,后续再慢慢加花样,这条路走起来会顺畅很多。
回到实操层面,这个项目最大的门槛其实不在算法,在数据。今天把代码逻辑和踩坑细节都写清楚了,你完全可以照着这套流程自己拉一份数据跑一遍,把情绪曲线画出来之后再回来对照K线看,那种“原来情绪提前发出了信号”的感觉,会给你继续做下去的信心。
本文还有配套的精品资源,点击获取