简介:一套面向金融数据分析与舆情挖掘场景的 Python 项目源码,用于采集上市公司新闻文本、计算舆情评分,并与股价数据做相关性分析,最后用图形化展示结论。项目共 10 个文件,压缩包仅 136KB:5 个 .py 脚本分别完成数据采集、舆情评分、股票数据获取、相关性计算等核心逻辑;2 个 .xlsx 存放舆情得分与股价数据;2 个 .txt 包含建表语句和爬取异常时的排错提示;1 个 .doc 为使用说明文档。代码带有详细注解,便于二次开发,并附运行截图方便对照验证;文档中还提示了网站结构变动后如何自行修改解析部分,降低上手门槛。已有 6300 人学习下载,适合具备一定 Python 基础、想通过完整案例掌握舆情分析与股票相关性分析流程的读者,可复用爬虫模块与评分逻辑,快速跑通“文本采集—舆情评分—股价对照—相关性计算—可视化展示”全流程。
1. python上市公司网络舆情与股票相关性分析:先把流程走通再谈预测
"python上市公司网络舆情与股票相关性分析"这个项目名听起来像是金融工程的活儿,实际上拆开看就是一条很朴素的流水线:用爬虫把上市公司的新闻抓下来,按情感词打分,再拿舆情分和股票日线数据做相关性计算,最后用图形把结果画出来。我刚拿到这份源码的时候,第一反应是"五六个脚本能做出什么",跑完一遍倒是意外地完整——从数据采集、入库、评分到分析输出,中间一个环节都没缺。适合论文缺实证数据、想用真实新闻和行情做相关性验证的人,也适合刚入门量化、想看看舆情数据怎么和股票数据对齐的人。这一篇就把脚本职责、运行顺序和数据处理的几个关键坑写清楚。
2. 舆情评分是怎么算出来的:新闻采集、情感打分与聚合口径
2.1 数据源选型:为什么新闻文本比股吧评论更适合做评分
做舆情评分,第一步是选语料来源。源码里提供了 baidu.py 和 sina.py 两个采集脚本,分别对应百度新闻搜索和新浪财经新闻频道,而不是雪球、股吧这类社区文本。这个选择在实际处理过语料之后会觉得很合理:股吧文本口语化严重,谐音字、"梭哈""站岗"这类黑话满天飞,情感词典很难覆盖,而且一条帖子几十个字里塞满了感叹号和表情符号,清洗成本远高于新闻标题。
新闻文本则规矩得多,标题和摘要基本都是陈述句,负面信息里"亏损""违规""处罚""跌停"这类词命中率很高。另一个原因是新闻有发布时间和来源URL,方便按日期聚合到"股票+自然日"的维度,后面才能和行情数据对齐。社区帖子的发布时间和回复热度虽然有,但文本噪声会把评分结果搅乱,你会发现同一只股票在股吧里永远处于"极度负面"状态,因为骂的人多,但股价不一定跌。
2.2 baidu.py 和 sina.py:请求构造、解析规则与"网站改版"埋下的雷
两个采集脚本的结构基本一致:构造搜索请求、解析HTML、抽取标题与发布时间,入库或落盘。核心逻辑示意如下。
import requests from bs4 import BeautifulSoup HEADERS = { "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) " "AppleWebKit/537.36 (KHTML, like Gecko) " "Chrome/120.0 Safari/537.36" } def fetch_baidu_news(keyword: str, page: int = 1) -> list: url = "https://www.baidu.com/s" params = {"wd": keyword, "pn": (page - 1) * 10} resp = requests.get(url, params=params, headers=HEADERS, timeout=10) resp.encoding = "utf-8" soup = BeautifulSoup(resp.text, "html.parser") items = [] for result in soup.select(".result"): title_node = result.select_one("h3 a") if not title_node: continue items.append({ "title": title_node.get_text(strip=True), "url": title_node.get("href", ""), }) return items这段代码里三个参数值得注意。timeout 设成 10 秒,避免单个请求卡死整个采集任务;resp.encoding 手动指定 utf-8,因为百度搜索页面在部分环境下会被 requests 错误识别成 gbk 导致乱码;select(".result") 是当前搜索结果里新闻条目的容器选择器,页面改版时首先失效的就是这里。
源码包里那个 .txt 文件特别注明"若其他步骤都正确的情况下爬取不了数据,可能是网站有变动,可自行修改解析部分",说的就是这段。我建议拿到源码后先跑一个最小测试:打印 items 的长度和第一条 title,确认解析器没失效再全量跑。否则会出现程序不报错、脚本正常结束、但结果表里全是空数据的诡异现象。
2.3 score.py 的评分逻辑:情感词表、强度加权与归一化
新闻抓下来之后,score.py 负责把文本变成分。源码用的方法是构建式情感词典——在脚本里硬编码两组词,一组正面、一组负面,然后对每条新闻的标题和摘要做分词匹配。核心逻辑示意如下。
import jieba POS_WORDS = {"增长", "盈利", "突破", "中标", "回购", "增持"} NEG_WORDS = {"亏损", "违规", "处罚", "跌停", "减持", "诉讼"} def score_news(text: str) -> float: words = jieba.lcut(text) pos = sum(1 for w in words if w in POS_WORDS) neg = sum(1 for w in words if w in NEG_WORDS) total = pos + neg if total == 0: return 50.0 # 中性:没有命中任何情感词 return round(50 + (pos - neg) / total * 50, 2)这里的设计逻辑是映射到 0 到 100 的区间:50 分代表中性,高于 50 偏正面,低于 50 偏负面。分母用 pos + neg 而不是总词数,是为了消除标题长短的影响——一条 20 个字的标题和一条 50 个字的摘要放在同一个尺度上比较。如果你想让负面消息的惩罚更狠,可以把公式改成 50 + (pos - neg) / total * 60,极端情况下分数波动更大,但建议先跑一遍原始公式,再根据结果调整强度。
评分之后还要做聚合操作,因为最终要拿"每天一个舆情分"和当日收益率对齐。常见做法是按股票代码和新闻日期分组,统计新闻条数、正负面词次数、平均分或加权分。加权逻辑可以用新闻条数做权重:新闻越多说明市场关注度越高,舆情信号越强,单纯平均分会把"一条小新闻"和"新闻刷屏"两种情况混为一谈。
2.4 score.xlsx 的产出口径:一条舆情记录怎么按天聚合
score.py 跑完会输出 score.xlsx,这就是后面相关性分析的数据基础。文件字段大致是这样:
| 字段名 | 含义 | 举例 |
|---|---|---|
| stock_code | 股票代码 | 600519 |
| news_date | 新闻发布日期 | 2024-11-20 |
| news_count | 当天新闻条数 | 7 |
| pos_count | 正面词出现总次数 | 3 |
| neg_count | 负面词出现总次数 | 1 |
| sentiment_score | 当日舆情评分 | 62.5 |
常见的问题是聚合时要不要区分"新闻发布时间"到底是当天凌晨还是 15:00 收盘后。这里的口径处理会直接影响相关性结论,我放到第 5 章的避坑部分专门讲。源码里用的是自然日聚合,如果你要更严格一点,可以把 15:00 之后的新闻归到下一个交易日。
3. 股票数据与相关性计算:把舆情评分和行情对齐到同一根时间轴上
3.1 stock.py 的行情采集:复权方式决定收益率算得对不对
舆情分有了,接下来是对照组——股票行情数据。源码里 stock.py 负责把目标公司的日线数据拉下来,落盘成 share.xlsx。这里第一个决策点是复权。常见做法是用后复权数据,因为后复权的价格序列是连续的,计算历史区间的收益率不会因为中间发生过分红除权而出现跳空;前复权价格受最近一次除权影响,早期价格会被压得失真。
我一般会在 stock.py 里加一个参数控制复权方式,默认后复权,同时把成交量、收盘价、涨跌幅一起拿全。如果你用的数据源不支持复权因子,退而求其次的办法是只用涨跌幅(pct_chg)字段,这个字段本身已经包含了除权除息修正,算日收益率时不需要再做任何变换。注意源码只跑了单只股票,如果你想换成多只做横向对比,需要在外层循环里逐个请求,不要试图一次拿完整个板块再拆分,接口层通常会限制单次返回条数。
3.2 先转收益率再算相关性:价格序列是典型的非平稳数据
拿到价格之后不能直接用收盘价和舆情分做 Pearson 相关系数。原因是价格序列是非平稳的——长期趋势、市场整体涨跌都会让两个序列同时走高或走低,算出来的相关系数虚高得离谱。最经典的例子是随便找两只毫无业务关联的股票,因为大盘同步上涨,价格序列的相关性可能高达 0.8 以上,但你拿这个数字去解释"舆情驱动股价"完全站不住脚。
正确做法是先把价格转成收益率序列,再用收益率和相关变量计算相关性。这一步在 analysis.py 里对应如下核心逻辑。
import pandas as pd from scipy.stats import pearsonr df = pd.merge(score_df, stock_df, left_on="news_date", right_on="trade_date") df = df.sort_values("news_date").reset_index(drop=True) # 用 pct_change 计算当日相对前收盘的百分比变动 df["daily_return"] = df["close"].pct_change() # 去掉首行 NaN 和停牌导致的空值 df = df.dropna(subset=["daily_return", "sentiment_score"]) r, p_value = pearsonr(df["sentiment_score"], df["daily_return"]) print(f"Pearson r = {r:.4f}, p-value = {p_value:.4f}")pct_change() 是 pandas 里计算百分比变化的标准方法,默认分母是上一个非空值。这里有一个隐藏细节:如果某天停牌,close 会缺失,pct_change 会自动跳过吗?不会,它是按行位置对齐的,前一天有值、后一天跳过缺失值继续算,收益率会自动跨过停牌日,这其实是正确的处理方式——停牌期间没有交易,本来就不应该产生额外的一天收益率。dropna 的作用是清洗首行和停牌日样本,避免 NaN 直接参与 pearsonr 计算。
3.3 analysis.py 的输出:相关系数表、散点图与 p 值怎么读
analysis.py 最后会输出两样东西:控制台打印的相关系数表,以及一张舆情分对当日收益率的散点图。散点图用 matplotlib 的 scatter 就能画,横轴是 sentiment_score,纵轴是 daily_return,再把线性回归拟合线叠上去。这样做的意义是让你直观看到样本点的分布形态,而不是只盯一个数字。
读 p-value 是新手最容易翻车的地方。p 值小于 0.05 只能说明"相关性在统计上显著不等于 0",不代表相关性有实际预测价值。r = 0.1 配上 p = 0.001,在几千个交易日样本里很常见,但这个系数对应的解释力只有 1%。运行时如果看到这种结果,别急着下"舆情显著影响股价"的结论,先看看散点图是不是一团云。源码默认输出的是当日舆情分与当日收益率的相关系数,实际操作中这个值通常很低,真正有意思的是滞后几天的相关性,见最后一章。
4. 环境配置与完整运行:五个脚本按什么顺序跑才不算白跑
4.1 依赖安装:一个 requirements.txt 说清楚装什么
整个项目涉及的依赖不算多,但版本之间有小坑。建议直接用 pip 安装如下依赖,大版本保持一致即可,不要求完全锁死补丁号。
requests==2.31.0 beautifulsoup4==4.12.3 pandas==2.1.4 numpy==1.26.3 scipy==1.11.4 matplotlib==3.8.2 jieba==0.42.1 openpyxl==3.1.2 pymysql==1.1.0openpyxl很容易漏装,它是 pandas 读写 xlsx 文件的底层引擎,不装的话 score.xlsx 和 share.xlsx 落地时会报ImportError: Missing optional dependency 'openpyxl'。pymysql只在你要把新闻数据写进 MySQL 时才需要,如果只跑通文件落盘,可以先不装。Python 版本建议 3.8 到 3.10,3.11 以上跑 jieba 偶尔会出现依赖编译警告,但不影响结果。
4.2 执行顺序与文件流转:谁产出、谁消费
源码包里的脚本是按数据流水线设计的,执行顺序不能乱。我按实际运行顺序整理成了下面的流程。
# 1. 建库建表(可选,但建议先建) mysql -u root -p < 建表语句.txt # 2. 采集舆情新闻数据 python baidu.py python sina.py # 3. 舆情评分,产出 score.xlsx python score.py # 4. 抓取股票行情,产出 share.xlsx python stock.py # 5. 合并计算相关性,输出结果与图表 python analysis.py脚本之间的依赖关系是:baidu.py 和 sina.py 负责往数据库或本地文件写入原始新闻,score.py 消费这些原始新闻产出舆情评分表;stock.py 独立运行,只依赖股票代码和行情接口;analysis.py 是最后一个环节,同时读 score.xlsx 和 share.xlsx 做合并计算。这里最隐蔽的坑是时序:如果 score.py 跑完,你再改 stock.py 里的股票代码重新抓行情,分析时合并用的股票代码必须和评分时一致,否则 pandas 的 merge 会得到一堆空值,相关性计算结果自然不对。
4.3 建表语句与字段约定:两张表承载全部中间数据
源码里的建表语句.txt 提供了 MySQL 的表结构。即使你不想用数据库、直接落盘 xlsx,也建议看一眼建表语句,因为它把字段类型定义得清清楚楚,反过来能帮你理解脚本内部对数据格式的假设。示意如下。
CREATE TABLE stock_news ( id INT AUTO_INCREMENT PRIMARY KEY, stock_code VARCHAR(10) NOT NULL, news_title VARCHAR(500) NOT NULL, news_url VARCHAR(500), publish_time DATETIME, source VARCHAR(20) DEFAULT 'baidu', created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, KEY idx_stock_date (stock_code, publish_time) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; CREATE TABLE stock_daily ( id INT AUTO_INCREMENT PRIMARY KEY, stock_code VARCHAR(10) NOT NULL, trade_date DATE NOT NULL, open_price DECIMAL(10,3), close_price DECIMAL(10,3), volume BIGINT, UNIQUE KEY uk_stock_date (stock_code, trade_date) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;stock_news表的联合索引idx_stock_date踩过一个坑:按股票代码和时间过滤新闻是评分阶段最频繁的操作,没这个索引时,数据量一旦过万,score.py 每处理一只股票都要全表扫描,慢得让人以为死锁了。stock_daily表的唯一键uk_stock_date则是防重复的兜底——行情接口重复调用时,靠它做 insert ignore 或 on duplicate key update 能避免同一交易日的记录被插两遍。
5. 避坑指南:新闻源改版、日期错位与空数据是三大翻车点
5.1 程序不报错,但 score.xlsx 里全是空数据
现象:baidu.py 和 sina.py 都正常执行完,控制台没有任何异常,但生成的 score.xlsx 只有表头,一行数据都没有。 原因:网站页面结构改版,解析代码里写的选择器(比如select(".result"))已经匹配不到任何页面元素,items一直是空列表,而脚本里没有对空结果做告警。 解决:在所有采集函数末尾加一行调试输出print(f"共抓取 {len(items)} 条新闻"),如果条数为 0 就立即停止后续步骤。然后用浏览器开发者工具重新查看搜索结果页的真实 DOM 结构,更新选择器或正则表达式。源码里的 .txt 文件提示的"自行修改解析部分"说的就是这一个环节。
5.2 停牌日导致合并后出现大量 NaN,相关性结果直接失真
现象:merge 之后样本量只有预期的八成,相关系数变得极不稳定。 原因:股票停牌时没有行情记录,但新闻照常产生,舆情评分表里多出来的日期在行情表里找不到对应行,pd.merge默认的 inner 模式会把这些日期全部丢掉。 解决:先看合并前后的行数差异,如果差异超过 5%,说明停牌日比例不低。要么接受删除停牌样本,但要在分析里注明;要么把缺失的行情数据用前一个交易日的收盘价填充,但这样会人为制造 0 收益率样本,我建议后者只用于长周期分析,日频相关性计算里慎用。
5.3 收盘后的重大新闻被算成当天舆情,相关性被严重稀释
现象:用当日舆情分和当日收益率算出的 r 值几乎为 0,不管怎么调整情感词表都提不上去。 原因:A 股 15:00 收盘之后发布的新闻,被按自然日聚合到了当天,但它的价格影响要等下一个交易日才能体现。比如晚上八点公告"重大亏损",当天收盘价早就定格了,用当天收益率去对应这条利空,等于把信号和结果错开了半天到一天。 解决:把新闻时间按交易时段重新归档,15:00 之后的时间戳顺延到下一个交易日。源码是自然日聚合,我拿到后第一件事就是加了这个偏移逻辑,相关性往往立刻有明显提升。
5.4 情感词典漏掉"辟谣"和"澄清",负面新闻打出高评分
现象:公司发布"关于不实报道的澄清公告",标题里含"违规"(出现在负面词表里)和"不实"(不在任何词表里),score.py 给这条新闻打了低分,但实际上这是利好。 原因:基于词典的情感分析处理不了否定结构,"不违规"被拆成"不"和"违规",负面词命中一次。双重否定和新闻行业的反转属性(辟谣、澄清、立案调查后撤诉)都会让词典模型失灵。 解决:在评分前加一层否定词扫描,如果负面词前一个词是"不""未""否认"就没收这次命中。这个修复虽然简单,但对新闻文本特别有效,因为新闻标题里的"不"字密度远高于日常对话。
5.5 多股票跑批时数据串行,相关系数高到不敢信
现象:一次跑 5 只股票,其中一只的舆情分和另一只的收益率算出了 0.9 的相关系数。 原因:score.xlsx 或 share.xlsx 在生成时没有严格按股票代码过滤,后续分析脚本 merge 时只用日期做关联条件,导致 A 股票的舆情分匹配上了 B 股票的收益率。 解决:给每一步落盘文件都保留stock_code字段,分析脚本里 merge 的 on 条件必须是["stock_code", "date"]两个字段同时匹配。我见过太多次只按日期合并翻车的案例,这个字段是跑批场景的底线保障。
6. 把相关性算得更可信:滞后分析、符号稳定性与情感词典扩展
相关性分析最容易犯的错,是只算一条"当日舆情分 vs 当日收益率",然后下结论说"舆情和股价无关"。我在跑这个项目时试过把舆情分分别对应当天、滞后一天、滞后两天、滞后三天的收益率,结果差异非常大——滞后一天的相关系数往往是当日相关系数的两倍以上。原因是信息传导需要时间,新闻在盘中发酵、盘后传播,真正影响开仓决策通常发生在次日。
滞后相关的实现很简单,核心代码就几行。
import pandas as pd from scipy.stats import pearsonr df = pd.merge(score_df, stock_df, left_on="news_date", right_on="trade_date") df = df.sort_values("news_date").reset_index(drop=True) df["daily_return"] = df["close"].pct_change() for lag in range(0, 4): # shift(-lag):当前舆情分 对应 未来 lag 天的收益率 df[f"fwd_return_{lag}"] = df["daily_return"].shift(-lag) tmp = df.dropna(subset=["sentiment_score", f"fwd_return_{lag}"]) r, p = pearsonr(tmp["sentiment_score"], tmp[f"fwd_return_{lag}"]) print(f"lag={lag} 天 | r={r:.4f} | p={p:.4f}")shift(-lag)的含义是把收益率列整体上移 lag 行,让第 T 天的舆情分和 T+lag 天的收益率对齐。能跑出 p 值小于 0.05 且 r 值方向稳定(连续多天同号)的 lag,才是值得关注的那个。方向稳定性是个很好用的经验法则:如果滞后 1 天 r 为正、滞后 2 天 r 又变成负,大概率只是噪声。
情感词典的扩展也值得做。源码里那张词表是硬编码在 score.py 里的,换一只股票或一个行业,你会发现行业特有词(比如医药的"临床失败"、芯片的"流片成功")一个都匹配不上。常见做法是把词表改成外部文本文件,每行一个词,按正负分目录存放,再给每个词配一个权重值——"涨停"权重 2,"盈利"权重 1,"亏损"权重 2,"诉讼"权重 1.5。这样评分就不会被一两个高频弱情感词主导。
从那以后我每次跑完相关系数,都会强制自己走一遍滞后表和符号稳定性检查,再决定要不要在报告里写"存在显著相关性"。这个习惯帮我挡掉过至少三次不靠谱的实证结论,希望也能帮到你。
本文还有配套的精品资源,点击获取