简介:本资源是一套面向金融数据分析初学者与量化爱好者设计的Python股市情感分析实战代码,聚焦于从网络文本中提取投资者情绪并生成可量化的 sentiment index,辅助投资决策判断。资源共16个文件,包含4个核心Python脚本(用于机器学习建模、情绪指数计算与可视化)、6个CSV数据文件(含原始股评、分词结果、正负向词典及最终情绪指数序列)、2个PNG图表输出样例、2个文本词典及1份README说明文档,压缩包大小为58.46MB。已有1879人下载学习,覆盖高校金融工程课程实践、个人量化策略开发及舆情监测项目参考场景。用户可直接运行三步流程(model_ml.py → compute_sent_idx.py → plot_sent_idx.py),获得完整的情绪建模—计算—可视化闭环,配套数据已预处理,无需额外爬虫或清洗,显著降低入门门槛。
1. 项目概述:从代码到洞察的桥梁
最近几年,身边不少做投资的朋友,从纯看K线、财报,开始转向琢磨另一个维度的信息:市场情绪。大家聊起来,都觉得光看冷冰冰的数据不够,得知道市场上的“人心”在想什么。是恐慌、贪婪,还是观望?这种集体情绪往往先于价格变动,如果能捕捉到,无疑多了一个重要的决策参考。我自己也一直对这个方向感兴趣,所以花了不少时间,用Python捣鼓了一套股市情感分析的源代码。
这套代码的核心目标很明确:自动化地从海量的网络文本中,提取并量化投资者对特定股票或市场的情绪倾向,最终转化为可视化的指标,辅助投资决策。它不是什么“预测神器”,更像是一个高灵敏度的“情绪雷达”,帮你扫描市场噪音背后的真实信号。无论是个人投资者想验证自己的市场感觉,还是量化研究员希望构建多因子模型中的情绪因子,这套代码都能提供一个扎实的起点。
整个项目围绕着几个关键词展开:Python作为实现工具,情感分析作为核心技术,源代码意味着高度可定制和可学习,而投资者情绪则是我们最终要挖掘的宝藏。下面,我就把这套工具的构建思路、核心实现、踩过的坑以及实际应用心得,毫无保留地分享出来。
2. 整体架构与核心思路拆解
在动手写代码之前,得先把逻辑理清楚。一个完整的股市情感分析系统,远不止调用一个情感分析API那么简单。它需要处理从数据获取到情绪呈现的全链路,每个环节的选择都直接影响最终结果的可信度。
2.1 核心流程四步走
我的设计遵循一个清晰的四阶段流程,这也是业界比较通用的做法:
- 数据采集与清洗:情绪藏在哪里?股吧帖子、财经新闻标题、微博大V评论、券商研报摘要,这些都是富矿。这一步的目标是定向、高效、合法地获取这些文本数据,并清洗掉广告、无关符号、重复内容等噪音。
- 文本预处理与特征工程:原始文本机器看不懂。需要分词(对中文尤其关键)、去除停用词(的、了、呢等无意义词)、词干提取或词形还原(英文)。更高级的,可能需要提取命名实体(公司名、人名)或构建词向量。
- 情感判别与量化:这是技术核心。判断一段文本是“看多”还是“看空”,并给出置信度或强度分数。方法从基于词典的规则匹配,到基于机器学习的分类模型,再到如今主流的预训练深度学习模型。
- 情绪聚合与可视化:将成千上万条文本的情感得分,按时间(如每日)、按标的(如某只股票)进行聚合。计算情绪指数(如看多比例、情绪强度均值),并绘制成时序图表,与股价走势进行对比分析。
2.2 技术选型的权衡与决策
为什么用Python?生态成熟是首要原因。pandas、numpy处理数据得心应手;requests、BeautifulSoup、Scrapy搞定数据采集;jieba、snownlp、HanLP处理中文分词;scikit-learn、TensorFlow、PyTorch搭建模型;matplotlib、plotly进行可视化。一条龙服务,几乎都有现成的轮子。
在情感分析模型上,我经历了从简单到复杂的演进:
- 初期-词典法:使用开源的金融情感词典(如知网Hownet、大连理工情感词典),匹配文本中的情感词,根据正向词和负向词的数量差打分。优点是快、可解释性强,缺点是无法理解上下文和反语(比如“这业绩,真是好到没朋友”)。
- 中期-机器学习:将文本转化为TF-IDF特征或词向量,然后用标注好的数据训练一个分类器(如朴素贝叶斯、SVM、逻辑回归)。效果比词典法好,但严重依赖标注数据的质量和数量。
- 当前-深度学习/预训练模型:这是目前的主流和我的选择。直接使用在超大规模语料上预训练好的模型,如
BERT、RoBERTa及其金融领域微调版(如FinBERT)。它们能深刻理解上下文语义,准确率高。我最终选择基于RoBERTa架构,在中文金融新闻和股评数据上进一步微调了一个专属模型,效果提升显著。
注意:模型不是越新越复杂越好。对于实时性要求极高的场景(如监测盘中情绪),复杂的深度学习模型可能因推理速度慢而成为瓶颈。这时,一个精心构建的“词典+简单规则”的轻量级系统,可能更实用。
3. 核心模块源代码深度解析
接下来,我们深入到代码层面。我会拆解几个最核心的模块,并附上关键代码和详细注释。为了清晰和可复现,我假设你已经配置好了基本的Python环境(3.8+),并安装了pandas,requests,jieba,transformers等常用库。
3.1 数据采集模块:定向抓取与合规要点
数据源的质量决定了天花板。我主要聚焦于东方财富股吧和主流财经新闻客户端。这里以抓取股吧帖子列表为例,强调合规与反爬策略。
import requests import pandas as pd import time import random from fake_useragent import UserAgent class StockForumCrawler: def __init__(self, stock_code): self.stock_code = stock_code self.base_url = f"https://guba.eastmoney.com/list,{stock_code}.html" self.ua = UserAgent() self.headers = { 'User-Agent': self.ua.random, 'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8', 'Accept-Language': 'zh-CN,zh;q=0.9', 'Referer': 'https://www.eastmoney.com/' } self.session = requests.Session() def fetch_page(self, page=1): """抓取指定页面的帖子列表""" params = { 'type': '1', # 帖子类型 'page': page } try: # 重要:添加随机延迟,模拟人工操作,尊重网站负载 time.sleep(random.uniform(1, 3)) resp = self.session.get(self.base_url, params=params, headers=self.headers, timeout=10) resp.raise_for_status() # 检查HTTP错误 # 这里应检查响应内容是否包含反爬提示(如验证码) if "验证码" in resp.text: print(f"第{page}页触发反爬,建议更换IP或暂停") return None return resp.text except requests.exceptions.RequestException as e: print(f"抓取第{page}页失败: {e}") return None def parse_page(self, html_content): """解析HTML,提取帖子标题、链接、发布时间、阅读/评论数""" # 使用BeautifulSoup或lxml解析,此处为逻辑示意 posts_data = [] # 假设我们通过CSS选择器找到了帖子列表项 # soup = BeautifulSoup(html_content, 'lxml') # for item in soup.select('.articleh'): # title = item.select_one('.l3 a').text.strip() # link = item.select_one('.l3 a')['href'] # # ... 提取其他字段 # posts_data.append({'title': title, 'link': link, ...}) # 实际代码需要根据目标网站结构具体编写 return posts_data # 使用示例 crawler = StockForumCrawler('sz000001') # 平安银行 page_html = crawler.fetch_page(1) if page_html: posts = crawler.parse_page(page_html) df_posts = pd.DataFrame(posts) print(df_posts.head())实操心得:
- 遵守Robots协议:抓取前务必检查目标网站的
robots.txt,明确哪些路径允许爬取。 - 设置友好间隔:
time.sleep(random.uniform(a, b))是基本道德,避免对服务器造成压力。 - 轮换User-Agent和IP:使用
fake-useragent库,对于大规模抓取,需要考虑代理IP池。 - 处理结构化与反结构化数据:股吧列表页相对规整,但帖子详情页可能结构复杂,且包含图片、广告等干扰,解析规则需健壮。
- 数据存储:及时将抓取到的数据存入数据库(如SQLite、MySQL)或文件(CSV、Parquet),并记录抓取状态,便于断点续爬。
3.2 文本预处理模块:中文处理的特殊性
中文情感分析,预处理是关键,核心是分词。我对比了jieba、pkuseg、HanLP等工具,最终选择jieba,因其在速度和效果上取得了很好的平衡,并且支持自定义词典。
import jieba import jieba.analyse import re from collections import Counter class TextPreprocessor: def __init__(self, user_dict_path=None, stopwords_path='stopwords.txt'): """ 初始化预处理器 :param user_dict_path: 自定义金融词典路径,格式:'词 词频 词性' :param stopwords_path: 停用词文件路径 """ if user_dict_path: jieba.load_userdict(user_dict_path) # 加载金融、股票专有名词 self.stopwords = self.load_stopwords(stopwords_path) # 编译常用正则表达式,提升效率 self.pattern_url = re.compile(r'https?://\S+') self.pattern_mention = re.compile(r'@\w+') self.pattern_hashtag = re.compile(r'#\S+#') self.pattern_emoji = re.compile(r'[\U00010000-\U0010ffff]', flags=re.UNICODE) self.pattern_punctuation = re.compile(r'[^\w\s\u4e00-\u9fff]') # 保留中文、英文、数字、空格 def load_stopwords(self, path): """加载停用词表""" with open(path, 'r', encoding='utf-8') as f: stopwords = set([line.strip() for line in f]) # 添加一些金融场景下的常见无意义词 stopwords.update(['公告', '请问', '各位', '老师', '今天', '明天']) return stopwords def clean_text(self, text): """深度清洗文本""" if not isinstance(text, str): return '' text = text.lower() # 英文转小写 text = self.pattern_url.sub(' URL ', text) # 替换URL text = self.pattern_mention.sub(' MENTION ', text) # 替换@提及 text = self.pattern_hashtag.sub(' HASHTAG ', text) # 替换话题标签 text = self.pattern_emoji.sub(' EMOJI ', text) # 处理表情符号(可保留或替换) text = self.pattern_punctuation.sub(' ', text) # 移除非中英文数字的标点 text = re.sub(r'\s+', ' ', text).strip() # 合并多余空格 return text def segment(self, clean_text, use_stopwords=True, cut_all=False): """分词,可选择是否去除停用词""" # jieba.cut 返回生成器, cut_all控制全模式/精确模式 words = jieba.cut(clean_text, cut_all=cut_all) if use_stopwords: words = [w for w in words if w not in self.stopwords and len(w) > 1] # 过滤停用词和单字 else: words = [w for w in words if len(w) > 1] return list(words) def extract_keywords(self, text, topK=10, withWeight=False): """基于TF-IDF提取关键词,用于快速把握帖子主题""" # allowPOS 参数可以指定保留的词性,如名词、动词 keywords = jieba.analyse.extract_tags(text, topK=topK, withWeight=withWeight, allowPOS=('n', 'vn', 'v')) return keywords # 使用示例 preprocessor = TextPreprocessor(user_dict_path='finance_dict.txt') raw_text = “平安银行(SZ000001)今天财报公布了,净利润增长15%,超预期!股价要起飞了?//@股海无涯:我觉得还行,但市场情绪好像一般。” cleaned = preprocessor.clean_text(raw_text) print(f"清洗后: {cleaned}") words = preprocessor.segment(cleaned) print(f"分词结果: {words}") keywords = preprocessor.extract_keywords(cleaned) print(f"关键词: {keywords}")注意事项:
- 自定义词典至关重要:必须加载包含股票代码(如
sz000001)、公司简称/全称(平安银行)、金融术语(净利润、市盈率、放量上涨)的词典,否则这些关键实体会被错误切分。 - 停用词表需场景化:通用停用词表(如
的、了)之外,要添加分析场景下的无意义高频词(如公告、请问、老师)。 - 清洗策略影响情绪:谨慎处理否定词和程度副词。例如,“不”字被误删会完全反转语义。在清洗阶段,我通常只移除纯干扰符号,而将否定词和程度副词的保留与处理交给后续的情感分析模型。
- 分词模式选择:
jieba.cut默认的精确模式在大多数情况下足够。全模式(cut_all=True)会产生大量冗余词组,一般不推荐。
3.3 情感分析模块:从规则到模型的演进
这是系统的“大脑”。我将展示从简单的词典匹配到使用预训练Transformer模型的完整代码路径。
方案一:基于金融情感词典的规则匹配(轻量、快速)
class LexiconSentimentAnalyzer: def __init__(self, pos_dict_path='positive.txt', neg_dict_path='negative.txt', degree_dict_path='degree.txt', negation_dict_path='negation.txt'): self.pos_words = self.load_dict(pos_dict_path) self.neg_words = self.load_dict(neg_dict_path) self.degree_dict = self.load_degree_dict(degree_dict_path) # 程度副词权重,如{'非常':1.5, '略微':0.8} self.negation_dict = self.load_dict(negation_dict_path) # 否定词,如{'不', '没', '无'} def load_dict(self, path): with open(path, 'r', encoding='utf-8') as f: return set([line.strip() for line in f if line.strip()]) def load_degree_dict(self, path): degree = {} with open(path, 'r', encoding='utf-8') as f: for line in f: word, weight = line.strip().split() degree[word] = float(weight) return degree def analyze_sentence(self, word_list): """分析一个已分词的句子""" sentiment_score = 0 i = 0 while i < len(word_list): word = word_list[i] local_weight = 1.0 # 检查程度副词 if word in self.degree_dict: local_weight *= self.degree_dict[word] i += 1 if i >= len(word_list): break word = word_list[i] # 检查否定词 negation_flag = 1 if word in self.negation_dict: negation_flag = -1 i += 1 if i >= len(word_list): break word = word_list[i] # 检查情感词 if word in self.pos_words: sentiment_score += 1 * local_weight * negation_flag elif word in self.neg_words: sentiment_score -= 1 * local_weight * negation_flag i += 1 return sentiment_score # 使用示例 lexicon_analyzer = LexiconSentimentAnalyzer() test_words = ['非常', '看好', '平安银行', '的', '未来', '表现'] score = lexicon_analyzer.analyze_sentence(test_words) print(f"词典法情感得分: {score}") # 预期为正分方案二:基于微调预训练模型(精准、强大)
这是当前推荐的做法。我们使用transformers库加载一个在金融文本上微调过的中文BERT模型。
import torch from transformers import AutoTokenizer, AutoModelForSequenceClassification import numpy as np class TransformerSentimentAnalyzer: def __init__(self, model_name_or_path='./finbert-zh', device=None): """ :param model_name_or_path: 本地模型路径或HuggingFace模型ID """ self.device = device if device else ('cuda' if torch.cuda.is_available() else 'cpu') print(f"使用设备: {self.device}") # 加载分词器和模型 self.tokenizer = AutoTokenizer.from_pretrained(model_name_or_path) self.model = AutoModelForSequenceClassification.from_pretrained(model_name_or_path) self.model.to(self.device) self.model.eval() # 设置为评估模式 self.id2label = {0: 'negative', 1: 'neutral', 2: 'positive'} # 根据你的模型标签调整 def predict(self, text, max_length=128): """预测单条文本的情感类别和置信度""" # 编码文本 inputs = self.tokenizer(text, return_tensors="pt", truncation=True, padding=True, max_length=max_length) inputs = {k: v.to(self.device) for k, v in inputs.items()} # 推理 with torch.no_grad(): outputs = self.model(**inputs) logits = outputs.logits probabilities = torch.softmax(logits, dim=-1).cpu().numpy()[0] predicted_class_id = np.argmax(probabilities) predicted_label = self.id2label.get(predicted_class_id, 'unknown') confidence = probabilities[predicted_class_id] return predicted_label, confidence, probabilities def predict_batch(self, texts, batch_size=16, max_length=128): """批量预测,提高效率""" results = [] for i in range(0, len(texts), batch_size): batch_texts = texts[i:i+batch_size] inputs = self.tokenizer(batch_texts, return_tensors="pt", truncation=True, padding=True, max_length=max_length) inputs = {k: v.to(self.device) for k, v in inputs.items()} with torch.no_grad(): outputs = self.model(**inputs) batch_logits = outputs.logits batch_probs = torch.softmax(batch_logits, dim=-1).cpu().numpy() batch_preds = np.argmax(batch_probs, axis=1) for idx, pred_id in enumerate(batch_preds): label = self.id2label.get(pred_id, 'unknown') confidence = batch_probs[idx][pred_id] results.append((label, confidence)) return results # 使用示例 # 假设你已经有一个微调好的模型在 './finbert-zh' 目录下 model_analyzer = TransformerSentimentAnalyzer('./finbert-zh') texts_to_analyze = [ “公司业绩暴雷,股价跌停,看不到希望。”, “平安银行发布年报,营收稳健增长,符合市场预期。”, “大盘震荡,该股今日无重大消息。” ] for text in texts_to_analyze: label, conf, probs = model_analyzer.predict(text) print(f"文本: {text[:30]}...") print(f" 预测: {label}, 置信度: {conf:.4f}") print(f" 各类别概率: {probs}") print("-" * 40)核心要点解析:
- 模型选择:
'./finbert-zh'是一个示意路径。你需要自己准备标注好的金融情感数据(正面、负面、中性),在一个基础的中文BERT(如bert-base-chinese)或RoBERTa模型上进行微调。也可以尝试寻找开源社区已经微调好的金融情感模型。 - 输入长度:BERT类模型有最大长度限制(通常512)。对于长文章,需要截断或分段处理。对于股吧短文本,128或256通常足够。
- 批量推理:
predict_batch函数能显著提升处理大量数据时的速度。 - 置信度利用:输出的
confidence(概率)很有用。可以设置一个阈值(如0.7),低于此阈值的预测结果视为“不确定”,在后续聚合时可以选择剔除或单独处理。
3.4 情绪聚合与可视化模块:从点到面的洞察
单条文本的情感分析结果是“点”,我们需要将其聚合成“面”——时间序列上的情绪指数。
import pandas as pd import matplotlib.pyplot as plt import matplotlib.dates as mdates from datetime import datetime, timedelta class SentimentAggregator: def __init__(self): pass def aggregate_by_time(self, df, time_col='publish_time', sentiment_col='sentiment_label', value_col='sentiment_confidence'): """ 按时间(如天)聚合情绪 :param df: 包含时间、情感标签、置信度的DataFrame :return: 按时间聚合的DataFrame """ # 确保时间列为datetime类型 df[time_col] = pd.to_datetime(df[time_col]) df['date'] = df[time_col].dt.date # 计算每日情绪指标 daily_stats = df.groupby('date').apply( lambda x: pd.Series({ 'total_posts': len(x), 'positive_count': (x[sentiment_col] == 'positive').sum(), 'negative_count': (x[sentiment_col] == 'negative').sum(), 'neutral_count': (x[sentiment_col] == 'neutral').sum(), 'avg_confidence': x[value_col].mean() if value_col in x.columns else None, # 计算情绪指数: (正面数 - 负面数) / 总帖数, 范围[-1, 1] 'sentiment_index': ((x[sentiment_col] == 'positive').sum() - (x[sentiment_col] == 'negative').sum()) / max(1, len(x)) }) ).reset_index() daily_stats['positive_ratio'] = daily_stats['positive_count'] / daily_stats['total_posts'] daily_stats['negative_ratio'] = daily_stats['negative_count'] / daily_stats['total_posts'] return daily_stats def plot_sentiment_trend(self, daily_stats, stock_price_df=None, price_col='close'): """绘制情绪趋势图,可选叠加股价""" fig, ax1 = plt.subplots(figsize=(14, 7)) # 绘制情绪指数(主坐标轴) color = 'tab:blue' ax1.set_xlabel('日期') ax1.set_ylabel('情绪指数', color=color) # 确保日期排序 daily_stats = daily_stats.sort_values('date') ax1.plot(daily_stats['date'], daily_stats['sentiment_index'], color=color, marker='o', linestyle='-', linewidth=2, label='情绪指数') ax1.tick_params(axis='y', labelcolor=color) ax1.axhline(y=0, color='gray', linestyle='--', linewidth=0.8) # 零线 ax1.fill_between(daily_stats['date'], 0, daily_stats['sentiment_index'], where=daily_stats['sentiment_index']>=0, color='lightgreen', alpha=0.3) ax1.fill_between(daily_stats['date'], 0, daily_stats['sentiment_index'], where=daily_stats['sentiment_index']<0, color='lightcoral', alpha=0.3) # 如果有股价数据,绘制在次坐标轴 if stock_price_df is not None: ax2 = ax1.twinx() color = 'tab:red' ax2.set_ylabel('股价', color=color) # 确保股价数据日期与情绪数据对齐(可能需要合并) merged_df = pd.merge(daily_stats[['date']], stock_price_df, left_on='date', right_on='trade_date', how='left') ax2.plot(merged_df['date'], merged_df[price_col], color=color, linestyle='--', linewidth=1.5, label='收盘价') ax2.tick_params(axis='y', labelcolor=color) # 美化图表 ax1.xaxis.set_major_formatter(mdates.DateFormatter('%m-%d')) ax1.xaxis.set_major_locator(mdates.WeekdayLocator(interval=1)) fig.autofmt_xdate() ax1.grid(True, which='major', axis='both', linestyle='--', alpha=0.5) # 合并图例 lines1, labels1 = ax1.get_legend_handles_labels() if stock_price_df is not None: lines2, labels2 = ax2.get_legend_handles_labels() ax1.legend(lines1 + lines2, labels1 + labels2, loc='upper left') else: ax1.legend(loc='upper left') plt.title('投资者情绪指数与股价走势对比') plt.tight_layout() plt.show() # 模拟数据使用示例 # 假设 df_results 是预处理和情感分析后的DataFrame,包含'publish_time', 'sentiment_label', 'sentiment_confidence'列 # df_results = pd.read_csv('sentiment_results.csv') # aggregator = SentimentAggregator() # daily_sentiment = aggregator.aggregate_by_time(df_results) # print(daily_sentiment.head()) # aggregator.plot_sentiment_trend(daily_sentiment, stock_price_df) # 传入股价DataFrame可视化解读:
- 情绪指数:围绕零轴波动。正值表示市场情绪偏积极,负值表示偏消极。观察其趋势和拐点。
- 与股价叠加:将情绪指数与股价曲线对比,可以直观看到情绪是否领先或同步于价格变动。例如,情绪指数连续多日走低而股价横盘,可能预示着下跌风险。
- 帖子数量:
total_posts本身也是一个重要指标。发帖量激增往往伴随重大事件或价格剧烈波动,是市场关注度的体现。
4. 系统集成与自动化部署
单个脚本跑一次是demo,一个能持续运行、自动更新的系统才是生产力工具。我采用模块化设计,并用定时任务将其串联。
4.1 主控流程与调度
我编写了一个主控脚本main_pipeline.py,它协调各个模块的工作流,并可以配置为定时任务(如每天收盘后运行)。
# main_pipeline.py import schedule import time from datetime import datetime from data_crawler import StockForumCrawler from text_processor import TextPreprocessor from sentiment_analyzer import TransformerSentimentAnalyzer from aggregator import SentimentAggregator import pandas as pd import logging logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s') def daily_job(stock_list=['sz000001', 'sh600519'], lookback_days=3): """每日执行的任务""" logging.info(f"开始执行每日情感分析任务,时间: {datetime.now()}") all_results = [] analyzer = TransformerSentimentAnalyzer('./finbert-zh') preprocessor = TextPreprocessor(user_dict_path='finance_dict.txt') for stock_code in stock_list: logging.info(f"处理股票: {stock_code}") # 1. 数据采集 crawler = StockForumCrawler(stock_code) posts_data = [] for page in range(1, 6): # 假设抓取最近5页 html = crawler.fetch_page(page) if html: posts_data.extend(crawler.parse_page(html)) time.sleep(2) # 页间延迟 df_posts = pd.DataFrame(posts_data) if df_posts.empty: logging.warning(f"股票{stock_code}未抓取到数据") continue # 2. 文本预处理 df_posts['cleaned_text'] = df_posts['title'].apply(preprocessor.clean_text) df_posts['segmented'] = df_posts['cleaned_text'].apply(lambda x: preprocessor.segment(x, use_stopwords=True)) # 3. 情感分析 (批量处理提升速度) texts = df_posts['cleaned_text'].tolist() sentiment_results = analyzer.predict_batch(texts, batch_size=32) df_posts['sentiment_label'] = [r[0] for r in sentiment_results] df_posts['sentiment_confidence'] = [r[1] for r in sentiment_results] df_posts['stock_code'] = stock_code all_results.append(df_posts[['stock_code', 'publish_time', 'title', 'sentiment_label', 'sentiment_confidence']]) # 4. 合并所有股票结果并聚合 if all_results: final_df = pd.concat(all_results, ignore_index=True) final_df.to_csv(f'sentiment_results_{datetime.now().strftime("%Y%m%d")}.csv', index=False, encoding='utf-8-sig') logging.info(f"情感分析结果已保存至CSV文件。") # 5. 按股票和时间聚合 aggregator = SentimentAggregator() for stock_code in stock_list: stock_df = final_df[final_df['stock_code'] == stock_code] if not stock_df.empty: daily_stats = aggregator.aggregate_by_time(stock_df) daily_stats.to_csv(f'daily_sentiment_{stock_code}_{datetime.now().strftime("%Y%m%d")}.csv', index=False) # 这里可以调用 plot_sentiment_trend 生成图表,或发送聚合报告到邮箱/钉钉 logging.info(f"股票{stock_code}的日度情绪统计已生成。") else: logging.error("今日未采集到任何有效数据。") logging.info("每日任务执行完毕。") if __name__ == '__main__': # 立即执行一次 daily_job() # 设置每天下午6点定时执行(收盘后) schedule.every().day.at("18:00").do(daily_job) while True: schedule.run_pending() time.sleep(60)4.2 数据存储与历史回溯
为了进行历史分析和模型回测,需要将每日结果存入数据库。我使用SQLite(轻量)或PostgreSQL(生产环境)。
import sqlite3 from contextlib import contextmanager class SentimentDB: def __init__(self, db_path='sentiment_analysis.db'): self.db_path = db_path self._init_db() def _init_db(self): """初始化数据库表""" with self.get_connection() as conn: cursor = conn.cursor() # 原始帖子情感结果表 cursor.execute(''' CREATE TABLE IF NOT EXISTS raw_sentiment ( id INTEGER PRIMARY KEY AUTOINCREMENT, stock_code TEXT NOT NULL, publish_time DATETIME NOT NULL, source TEXT, content TEXT, sentiment_label TEXT, sentiment_confidence REAL, crawl_time DATETIME DEFAULT CURRENT_TIMESTAMP ) ''') # 日度聚合情绪指数表 cursor.execute(''' CREATE TABLE IF NOT EXISTS daily_sentiment_index ( id INTEGER PRIMARY KEY AUTOINCREMENT, stock_code TEXT NOT NULL, date DATE NOT NULL, total_posts INTEGER, positive_count INTEGER, negative_count INTEGER, sentiment_index REAL, UNIQUE(stock_code, date) ) ''') conn.commit() @contextmanager def get_connection(self): conn = sqlite3.connect(self.db_path) try: yield conn finally: conn.close() def insert_raw_sentiment_batch(self, data_list): """批量插入原始情感数据""" with self.get_connection() as conn: cursor = conn.cursor() sql = '''INSERT INTO raw_sentiment (stock_code, publish_time, source, content, sentiment_label, sentiment_confidence) VALUES (?, ?, ?, ?, ?, ?)''' cursor.executemany(sql, data_list) conn.commit() logging.info(f"批量插入 {cursor.rowcount} 条原始情感记录。") def upsert_daily_index(self, stock_code, date, stats_dict): """插入或更新日度情绪指数""" with self.get_connection() as conn: cursor = conn.cursor() sql = '''INSERT OR REPLACE INTO daily_sentiment_index (stock_code, date, total_posts, positive_count, negative_count, sentiment_index) VALUES (?, ?, ?, ?, ?, ?)''' cursor.execute(sql, (stock_code, date, stats_dict['total_posts'], stats_dict['positive_count'], stats_dict['negative_count'], stats_dict['sentiment_index'])) conn.commit()5. 常见问题、调优与避坑指南
在实际运行中,你会遇到各种各样的问题。下面是我踩过的一些坑和对应的解决方案。
5.1 数据源与采集问题
| 问题 | 可能原因 | 解决方案 |
|---|---|---|
| 抓取不到数据/返回空 | 网站反爬(IP被封、请求头被识别) | 1. 使用更真实的User-Agent池和代理IP。2. 增加请求间隔,模拟人类浏览行为。 3. 检查目标页面结构是否已更新,需调整解析规则。 |
| 数据质量差,噪音多 | 帖子中包含大量水军评论、广告、无关内容 | 1. 在预处理阶段加强清洗规则(如过滤包含特定广告词、链接的帖子)。 2. 利用发帖历史、用户等级等信息进行简单过滤。 3. 结合帖子互动数据(点赞/点踩数)进行加权,降低垃圾帖权重。 |
| 数据覆盖不全 | 仅抓取单一来源(如股吧),视角片面 | 多源交叉验证。同时抓取财经新闻标题、雪球/微博大V观点、券商研报摘要。不同来源的情绪可能分化,综合研判更有价值。 |
5.2 情感分析准确率问题
| 问题 | 表现 | 优化方向 |
|---|---|---|
| 模型误判反语/讽刺 | “这操作真是666,亏得我裤衩都没了”被判断为正面。 | 1.领域微调是关键:用大量标注好的金融论坛、股评数据微调预训练模型,让它学习金融领域的特殊表达。 2.引入上下文:分析整个帖子而不仅是单句。前后文可能提供讽刺的线索。 3.规则后处理:针对已知的高频反语模式,编写规则进行纠正(如“真是好到没朋友”后面接负面结果,则整体判负)。 |
| 对中性陈述敏感 | “公司今日无公告”被误判为负面(因包含“无”字)。 | 1.改进词典/模型:在训练数据中增加更多中性样本,并确保标签准确。 2.设置置信度阈值:对于置信度低于阈值(如0.6)的预测,将其归类为“中性”或“不确定”,不参与情绪指数计算。 3.主题过滤:先进行主题分类,只对明确表达观点的文本进行情感分析。 |
| 无法识别新词/网络用语 | “yyds”(永远的神)、“麻了”等网络用语无法识别。 | 1.定期更新词典:将新出现的网络热词加入情感词典,并赋予初始情感极性。 2.使用动态词向量:像BERT这类模型能根据上下文推断新词的语义,比静态词典更灵活。 3.数据驱动:持续收集和标注新语料,定期重新训练或微调模型。 |
5.3 系统性能与工程化问题
- 实时性要求:如果要做盘中实时情绪监控,深度学习模型推理速度可能跟不上。解决方案:
- 模型轻量化:将模型转换为
ONNX格式或用TensorRT加速,或使用更小的模型(如ALBERT、TinyBERT)。 - 异步流水线:采用消息队列(如
RabbitMQ、Kafka),将数据采集、预处理、情感分析、存储/推送解耦,并行处理。 - 服务化部署:将情感分析模型封装为
REST API(使用FastAPI或Flask),方便其他系统调用和水平扩展。
- 模型轻量化:将模型转换为
- 结果漂移与概念漂移:市场语言风格会变,今天的“牛”和去年的“牛”语境可能不同。解决方案:
- 持续学习:定期(如每季度)用新数据对模型进行增量训练或微调。
- 模型监控:跟踪模型在近期标注数据上的准确率,设置下降警报。
- A/B测试:同时运行新旧模型,对比其情绪指数与股价的相关性,选择效果更好的。
5.4 情绪指标的应用与解读陷阱
这是最容易出错的地方。情绪指标是辅助工具,不是圣杯。
- 不要孤立使用:情绪指数必须与基本面、技术面、资金面等其他维度结合分析。情绪极度悲观时,可能是底部信号(别人恐惧我贪婪);情绪极度乐观时,可能是风险警示(别人贪婪我恐惧)。
- 注意领先/滞后关系:情绪与股价的领先滞后关系并非一成不变。有时情绪是先行指标(如财报发布前),有时是同步甚至滞后指标(如受突发新闻影响的恐慌性抛售)。需要结合具体事件分析。
- 区分噪音与信号:短期情绪波动(如单日帖子情绪突变)可能是噪音,需要观察趋势的持续性(如连续3天情绪转向)。可以引入移动平均线来平滑情绪指数。
- 量化回测验证:在将情绪因子纳入实际交易策略前,必须进行严格的历史回测。计算情绪指数与未来N日收益率的相关性、构建多空组合看收益、计算夏普比率等。只有经过历史验证的稳定规律,才有参考价值。
6. 进阶方向与扩展思路
当你把基础系统跑通后,可以考虑以下几个进阶方向,让这个“情绪雷达”更强大:
- 细粒度情感分析:不止于“正面/负面/中性”三分类。可以尝试识别更具体的情绪,如“贪婪”、“恐惧”、“乐观”、“怀疑”、“惊讶”。这需要更精细的标注数据和模型。
- 主体情感关联:分析情绪是针对哪个主体的?是“大盘”、“行业”还是“个股”?例如“科技股全完了,但茅台挺稳”,需要对“科技股”和“茅台”分别进行情感判断。这涉及方面级情感分析或命名实体识别与情感关联。
- 情绪传播图谱分析:在社交媒体中,情绪会像病毒一样传播。可以构建用户互动网络,分析关键意见领袖(KOL)如何影响群体情绪,识别情绪传播的关键节点。
- 多模态情感融合:除了文本,投资者的情绪也体现在语音(财经节目语调)、视频(主播表情)、甚至市场交易数据(如订单流不平衡)中。尝试融合多模态信息,构建更全面的情绪画像。
- 结合另类数据:将网络情绪数据与传统的另类数据(如供应链数据、卫星图像、招聘信息)结合,或许能发现独特的阿尔法来源。
构建一套可用的股市情感分析系统,是一个典型的“数据获取 -> 数据处理 -> 模型应用 -> 结果解读”的数据科学项目。它考验的不仅是编程和算法能力,更是对金融市场、投资者行为和心理的深刻理解。这套源代码提供了一个完整的框架和实现细节,你可以基于它,根据自身的需求和数据源进行修改和优化。记住,模型永远在迭代,市场永远在变化,保持学习的心态,用批判性的思维看待分析结果,才能真正让技术为决策赋能。
本文还有配套的精品资源,点击获取