
简介基于Python的股市情感分析项目源码包面向毕业设计、课程设计及项目开发场景适合金融NLP、量化分析方向的开发者学习参考。项目从互联网文本中提取投资者情绪利用标注语料训练情感模型再将分析结果构造成情绪指标与股市行情进行关联观察尤其关注国内非有效市场中情绪对价格波动的影响。压缩包共13个文件主要包含4个Python脚本、3个文本说明、2个CSV数据、2张行情/结果分析图、1个压缩数据包及1个Markdown解析文档整体约24.73MB目录分层清楚便于按步骤对照学习和二次开发。目前已有236人浏览学习。使用者可以获得完整可运行的源码、情绪模型训练与指标计算流程、可视化绘图程序及项目解析文档快速掌握“语料标注—情感建模—情绪指标—行情对比”的完整研究链路并在此基础上自行扩展数据源或策略逻辑。1. 股市情感分析到底在做什么一个毕业设计题目的真实边界如果你打开“基于python开发的股市情感分析”这份题目第一反应多半是“用爬虫抓新闻再用一个模型判断利多利空最后画一张K线图”——这个理解不算错但只摸到了三分之一。这类题目能在毕业设计和课程设计里高频出现是因为它天然串起了 Python 爬虫、文本清洗、情感分析模型、pandas 数据处理、matplotlib 可视化和文档撰写几乎覆盖一个完整的数据项目全流程。真正决定这份项目能不能拿高分的地方反而不在模型而在两件事第一新闻或评论的“情感分数”怎么和“行情日期”严格对齐第二整个流程能否在没有人工干预的情况下重复跑通。评审老师看重的不是训练集准确率有多高而是你的数据模型有没有说服力、分析图能不能讲故事、文档结构是否让一个陌生人三天内能复现。适合动手做这个题目的是已经会用 Python 写脚本、但还没独立完成过“采集—建模—出图”闭环的在校生如果你已经跑过量化交易策略代码做这个题目的重点就会自动从“实现”转移到“验证情绪因子的有效性”上。2. 数据模型设计先把文本、情感、行情三类数据塞进同一张时间表2.1 三张核心表新闻表、行情表、日度情感聚合表股市情感分析看似只处理文本做起来才发现真正难的是数据模型。我一般会把整个项目的数据层拆成三张互相关联的表news_article 存原始新闻price_daily 存行情market_sentiment_daily 存每日聚合后的情感指标。这样拆分的好处是第一原始数据和派生数据分开出问题时你能回溯是哪一步算错了第二行情表可以被复用比如你后面想加涨跌幅统计或波动率特征不需要动新闻表结构。下面是一份可以直接建表的 MySQL DDL字段设计上把“文本内容”和“发布时间”单独设字段并给 publish_time 建索引因为后续所有情感聚合都要按时间分组查询。CREATE TABLE news_article ( id INT PRIMARY KEY AUTO_INCREMENT, stock_code VARCHAR(10) COMMENT 股票代码如 600519, source VARCHAR(50) COMMENT 新闻来源如 eastmoney, title VARCHAR(200) NOT NULL, content MEDIUMTEXT COMMENT 正文可能很长, publish_time DATETIME NOT NULL, url VARCHAR(500) UNIQUE, sentiment_raw FLOAT COMMENT 模型原始输出0-1之间, sentiment_label VARCHAR(8) COMMENT positive/neutral/negative, created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, KEY idx_publish_time (publish_time), KEY idx_stock_code_time (stock_code, publish_time) ); CREATE TABLE price_daily ( trade_date DATE NOT NULL, stock_code VARCHAR(10) NOT NULL, open_price DECIMAL(10,2), close_price DECIMAL(10,2), high_price DECIMAL(10,2), low_price DECIMAL(10,2), volume BIGINT, PRIMARY KEY (trade_date, stock_code) );这里的核心取舍是 sentiment_raw 和 sentiment_label 为什么不合并成一个字段。我的经验是模型输出的连续分数很有价值0.49 和 0.51 虽然都会被归为中性但累加聚合时能提供更细的区分度而 label 是给人看的便于在文档里统计“该日几条利好、几条利空”。如果你把原始分数丢弃后面做可视化时就会发现所有情感曲线都像一条直线因为正负样本互相抵消了。2.2 情感分数口径0.5 偏移法还是正负占比法情感分析模型给的是 0 到 1 之间的分数0 表示非常负面1 表示非常正面0.5 是中点。直接拿这个数画折线图会得到一个噪声很大的序列所以一定要先定好聚合口径。最常用的是两种均值偏移法和占比法。均值偏移法把每天所有新闻的 sentiment_raw 求平均再减去 0.5得到当日的“净情绪强度”占比法统计每天 positive 条数与 negative 条数的差值占当天总条数的比例。我推荐你两个都用因为它们在数据模型里承载的信息不同。均值偏移法容易受单条极端新闻影响比如某天一条“重大利空”把均值从 0.52 拉到 0.41占比法更稳健但碰上当天只有两三条新闻时会剧烈震荡。一个可落地的做法是把两种值都存进 market_sentiment_daily 表每天一行后续做分析图时想用哪个都行。你可以在 sentiment_score 字段里存均值偏移值在 positive_ratio 里存占比两列并列。3. 用 Python 从抓取到成图一台普通笔记本就能跑通的最小流水线3.1 数据采集requests 抓新闻列表注意频率和编码做课程设计级别的情感分析不需要上分布式爬虫用 requests 配合 BeautifulSoup 按列表页抓标题就够了。为了演示方便这里以公开可访问的新浪财经列表页为例你在实际项目里也完全可以用东方财富的接口或财经 RSS。抓取时最重要的是三件事请求头伪装成普通浏览器、限速不要暴力抓、解析前先处理编码。import requests from bs4 import BeautifulSoup import time def fetch_news_list(stock_code, pages5): headers { User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 } news_items [] for page in range(1, pages 1): url fhttps://finance.sina.com.cn/roll/index.d.html?cid56592page{page} resp requests.get(url, headersheaders, timeout10) resp.encoding utf-8 soup BeautifulSoup(resp.text, html.parser) for link in soup.select(.list_09 li a): title link.get_text(stripTrue) href link.get(href) news_items.append({title: title, url: href}) time.sleep(2) # 限速避免对目标站点造成压力 return news_items这段代码的逻辑很直白按页码构造列表页 URL每次请求后强制 sleep 两秒用 BeautifulSoup 的 CSS 选择器提取标题和链接。timeout10 是必须设的否则某个链接挂起会让整个脚本卡死编码设成 utf-8 也是必须的否则中文字符会乱成一团。实际项目里你还需要一个去重机制。新闻列表页经常重复出现相同标题建议把 title 的哈希值存入表并建唯一索引或者用 url 去重。如果抓取规模超过几千条建议改用增量抓取——每次记录最新一条新闻的时间下次只抓这个时间之后的内容不要每次全量跑。3.2 情感分析用 SnowNLP 跑通基线再考虑金融领域微调文本情感分析是这类项目的核心但课程设计阶段不建议一上来就上 BERT。SnowNLP 训练语料是电商评论直接用在金融文本上准确率会打折但它的好处是开箱即用、零训练成本能让你先把整个流水线跑通。下面的代码演示了怎么给抓回来的新闻批量打情感分。from snownlp import SnowNLP def analyze_sentiment(news_items, threshold_pos0.6, threshold_neg0.4): labeled [] for item in news_items: text item[title] # 先用标题速度快正文需要另外清洗 score SnowNLP(text).sentiments if score threshold_pos: label positive elif score threshold_neg: label negative else: label neutral labeled.append({**item, sentiment_raw: score, sentiment_label: label}) return labeled我把阈值设成大于等于 0.6 判正、小于等于 0.4 判负中间全部归为中性。这样分的好处是减少噪声——SnowNLP 对很多金融词汇的判断并不稳定0.45 和 0.55 之间的差别基本没有实际意义。如果你发现结果里中性新闻占比过高可以先把阈值收紧到 0.55 和 0.45 观察效果不要一上来就放宽阈值。这里有一个值得写进项目文档的坑SnowNLP 的分数不是概率只是一个 0 到 1 之间的倾向值模型底层是朴素贝叶斯。如果文档里把 sentiment_raw 解释成“看涨概率”答辩时很容易被问住。老实写法是“该模型对文本情感倾向的归一化评分越高代表越正面”。3.3 行情分析图双 y 轴叠加情感指数和收盘价可视化是整个项目的门面课程设计能否让人眼前一亮往往就看这张图。我习惯把情感指数和收盘价画在同一张图里用左右两个 y 轴分别表示这样能看到“情绪领先行情”还是“行情领先情绪”。注意 matplotlib 默认的坐标轴刻度对不上时需要手动对齐零点和单位。import pandas as pd import matplotlib.pyplot as plt def plot_sentiment_vs_price(sentiment_df, price_df): merged pd.merge(sentiment_df, price_df, ontrade_date, howinner) merged merged.sort_values(trade_date).reset_index(dropTrue) merged[sentiment_ma5] merged[sentiment_score].rolling(5, min_periods1).mean() fig, ax1 plt.subplots(figsize(12, 6)) ax2 ax1.twinx() ax1.plot(merged[trade_date], merged[close_price], color#333333, lw1.2, label收盘价) ax2.plot(merged[trade_date], merged[sentiment_ma5], color#d62728, lw1.4, label情感均值(5日平滑)) ax1.set_ylabel(收盘价) ax2.set_ylabel(情感指数(0-1)) fig.autofmt_xdate() ax1.legend(locupper left) ax2.legend(locupper right) plt.title(股市行情与新闻情感对比图) plt.savefig(sentiment_price.png, dpi150, bbox_inchestight)关键参数是 rolling 窗口的 size。窗口设成 5是因为 5 个交易日刚好是一周能滤掉单日新闻数量波动带来的毛刺如果你抓的是小时级数据可以改成 24 小时窗口。merged 的结果必须先按日期排序再画图否则 pandas 会用原始顺序绘线出现一条乱糟糟的回形针曲线。fig.autofmt_xdate() 能把日期标签旋转 30 度避免重叠这个是画时间序列图最容易漏的一步。4. 项目解析课程设计和毕业设计评审最关注的四个模块4.1 模块划分采集、存储、分析、可视化、文档五位一体一份能拿到高分的项目不只是“能跑出图”而要像一个可交付的小型系统。我经历过不少答辩现场老师拿到源码先看目录结构如果你的 Python 文件全堆在根目录第一印象就减分。下面的目录结构是我常用的组织方式也适合直接写进项目解析文档里。stock_sentiment/ ├── README.md # 项目简介、环境依赖、快速启动 ├── requirements.txt # pip 依赖清单 ├── docs/ │ ├── 需求分析.md │ ├── 数据库设计.md │ ├── 接口说明.md │ └── 答辩要点.md ├── src/ │ ├── data_fetcher.py # 爬虫与接口采集 │ ├── text_cleaner.py # 清洗去重、分词、停用词 │ ├── sentiment_analyzer.py # 情感分析模型封装 │ ├── price_loader.py # 行情数据读取与对齐 │ └── visualizer.py # 图表绘制 ├── data/ │ ├── raw/ # 原始 json │ ├── processed/ # 清洗后的 csv │ └── database.sql # 建库建表脚本 └── output/ └── sentiment_price.png # 最终分析图这里的关键设计是 data_fetcher 和 sentiment_analyzer 必须解耦。采集、清洗、分析是三个阶段如果全部写进一个文件调试时你会疯掉——分不清报错来自请求异常还是模型输入异常。另一个细节是 database.sql 和 data/raw 一起提交这样答辩老师能在本地重新建库、导入原始数据、重跑全流程项目才叫“可复现”。如果你担心重跑时间太长可以在 README 里提供一份结果样例对照但源码里必须保留可复现路径。4.2 数据流设计从 URL 到图的四层管道数据模型做完后要给文档配一张数据流图这是“项目解析”部分最有说服力的内容。我不建议用复杂画图工具在 Markdown 里用文字把数据流讲清楚就够。整个管道的核心有四层第一层是采集层输出是带 publish_time、title、url 的原始记录列表。第二层是清洗层去掉重复新闻、过滤掉长度过短或包含乱码的文本再对文本做 jieba 分词和停用词过滤这个步骤的输出是干净语料。第三层是分析层把语料逐条送入情感模型得到 sentiment_raw 和 sentiment_label再按交易日聚合到 market_sentiment_daily 表。第四层是可视化层读 sentiment 表和 price 表对齐日期后绘制双轴图。每一层你都要在文档里问自己一个关键问题这一层输出哪些字段、输入依赖上一层什么字段、如果数据量翻五倍哪层会先崩通常的回答是情感分析层最慢因为 SnowNLP 是逐条计算所以实际项目里我一般会在分析这一层加 tqdm 进度条并统计每秒处理条数用于估算全量数据所需时间。4.3 文档怎么写从需求分析到测试用例的四份必备文档文档在这个项目标题里占了很重的位置很多开发者只把文档当成凑字数但答辩老师判断项目质量的直接途径就是看文档。我把课程设计类项目的文档固定在四份需求分析、数据库设计、接口说明、测试用例。需求分析不要写“本系统实现了股市情感分析功能”这种废话而是写用户故事——谁会在什么场景下使用这个系统他需要什么输入、期望什么输出。建议直接写“用户输入股票代码和日期范围系统返回该区间每日情感指数和对应行情图”。数据库设计文档重点写表结构变更过程比如为什么新闻表要单独建、为什么不用 MongoDB 而用 MySQL这些设计理由能体现你的思考量。接口说明文档另有一个小技巧不一定要写 HTTP API你完全可以把 Python 函数签名当作接口来写示例如下。def analyze_sentiment(news_items: list[dict], threshold_pos: float 0.6) - list[dict]: 输入新闻列表输出带情感标签的列表。 news_items: [{title, url, publish_time}] return: [{title, url, publish_time, sentiment_raw, sentiment_label}] 测试用例文档是最容易被忽略的。至少要写三条一是正常情况输入 20 条已知正负倾向的新闻断言输出的 positive 和 negative 数量在预期范围内二是空列表输入断言函数返回空列表而不是抛异常三是时间对齐测试输入跨周末的新闻和行情断言新闻被正确对齐到下一个工作日。这三条测试案例写清楚评委会认为你考虑到了边界条件。5. 避坑记录中文金融文本情感分析最容易翻车的五个点5.1 新闻正文全是乱码或空白现象requests 抓回的 content 字段要么乱码要么只有标题没有正文入库后情感分析只能对空字符串计算产出 0.5 中性分。原因部分财经页面用的是 gb2312 编码硬设 utf-8 会得到一串乱码还有页面内容是 JavaScript 动态加载的直接抓 HTML 拿不到正文。这是我在一次次翻车里最熟悉的一种几乎每个抓取项目都会碰到。解决抓取响应后先判断 resp.apparent_encoding 再手动覆盖动态加载页面优先找接口而不是解析 HTML比如搜索页面源码里的 json 数据字段。正文为空时把情感分数置空并跳过不进数据库宁缺毋滥。5.2 SnowNLP 把“减持”判断成正面现象“公司股东减持股价承压”这句话SnowNLP 给出的分数可能是 0.62。上周五我复测学生项目时就亲眼见过这种判断。原因SnowNLP 是基于电商评论训练的朴素贝叶斯模型对“减持”“质押”“商誉减值”这些金融术语没有语感。解决加一个金融否定词表做后处理。检测到“减持”“亏损”“处罚”“违规”等词时强制把分数压到 0.2 以下。这是成本最低的方案比重新训练模型快得多。5.3 行情和新闻日期对不上错位一天现象昨天大跌今天出现一堆看涨新闻情感曲线和价格曲线严重“错位”画出的图没有任何参考价值。原因新闻发布时间是自然日行情数据是交易日非交易日发布的新闻会被错误地归到当天的行情上导致对齐错位。解决用交易日历做映射非交易日新闻全部归属到下一个交易日。具体做法是建一张 calendar 表存交易日然后 left join 匹配而不是直接字符串日期相等。这个问题在项目解析文档里写清楚也是数据模型合理性的佐证点。5.4 情感分数聚合后全部趋近 0.5现象日度情感指数画出来后是条水平线几乎没有波动完全看不出市场情绪变化。原因同一天有多个新闻源正负面互相抵消且 0.6 阈值过滤了大量微弱信号日均分自然被拉到中线附近。这不是代码 bug而是聚合策略问题。解决按 tushare 或 akshare 的新闻接口做来源抽样限定前 50 条再聚合或者改用正负占比法先统计当天 positive 和 negative 数量再计算差值比例。两条路都试一下哪条波动大就用哪条。5.5 图表日期轴挤成一坨黑色团块现象plott 出来的图x 轴标签重叠成一团墨迹导出 PNG 后完全无法放进论文。原因数据量多时 matplotlib 默认每隔一个数据点标一个日期几百个日期挤在一起当然糊了。解决用 fig.autofmt_xdate() 旋转标签再设置 MaxNLocator 限制坐标轴刻度数量比如 plt.gca().xaxis.set_major_locator(MaxNLocator(nbins8))保证图上有但不超过 8 个日期节点。这是放进论文前的最后一关也是给人第一印象的分水岭。6. 进阶验证用滞后相关性检验情感分数到底能不能预测行情画完对比图项目已经能交付但如果你想让它从“课程设计”升级到“可以写进简历的项目”一定要补一个验证环节计算情感分数和未来 N 天涨跌幅的相关系数。这一步会回答评审老师最无法反驳的问题——“你这个情感分析有什么用”。import pandas as pd import numpy as np def lead_lag_correlation(sentiment_df, price_df, max_lag_days5): merged pd.merge(sentiment_df, price_df, ontrade_date, howinner) merged merged.sort_values(trade_date).reset_index(dropTrue) merged[future_return_1d] merged[close_price].shift(-1) / merged[close_price] - 1 merged[future_return_3d] merged[close_price].shift(-3) / merged[close_price] - 1 results {} for lag in range(0, max_lag_days 1): score_shifted merged[sentiment_score].shift(lag) results[fsentiment_t-{lag}_vs_return_1d] np.corrcoef( score_shifted.dropna(), merged.loc[score_shifted.dropna().index, future_return_1d] )[0, 1] return results这里的核心逻辑是把当天的情感分数向前平移 1 到 5 个交易日再计算平移后的分数与未来 1 天收益率的相关性。如果 lag1 时的相关系数明显高于 lag0说明情感确实领先行情一天如果所有滞后期的相关系数都接近 0说明情感分数对预测没有增量信息。我跑过的多数结果是相关性绝对值在 0.05 到 0.15 之间这在金融数据里已经不算弱了足以支撑“情感因子是有效弱信号”的结论。我做完这个项目后养成了一个习惯任何情感分析的结果必须先做滞后相关性验证再下结论绝不因为图看起来像就先入为主。这个习惯后来让我在写更多数据分析类代码时少走了很多弯路。希望帮到你动手跑一遍比看十篇解析都管用。本文还有配套的精品资源点击获取