简介:这是一份面向金融数据分析初学者与量化研究爱好者的Python实战项目源码,围绕上市公司网络舆情与股票价格之间的相关性展开。项目通过采集新闻文本数据,对上市公司舆情进行量化评分,再与股票价格序列做相关性计算,并以图形化方式呈现分析结果,帮助读者理解从数据获取到结论输出的完整链路。压缩包共10个文件,约136KB,包含5个py脚本分别负责数据采集、评分与分析,2个xlsx数据表、2个txt说明与建表语句,以及1份doc使用文档,结构紧凑、便于按模块阅读。目前已有6300人学习下载。源码附带详细注解与运行截图,读者可据此掌握舆情指标构建、相关性检验及可视化展示的具体实现,并参考文档中的排错提示应对网站变动导致的采集失败问题,适合作为课程设计或量化入门练手素材。
1. 从一份能跑通的舆情相关性源码说起:它到底解决了什么问题
很多做量化或者数据分析的朋友,第一次接触“网络舆情”和“股票相关性”这两个词放在一起时,脑子里冒出来的往往是各种高大上的商业舆情终端,一年授权费动辄几万块。但实际落到个人研究或者课程设计上,我们需要的往往不是那种黑匣子,而是一份能看得见摸得着、能自己改解析规则、能自己调评分权重的源码。这份 python 上市公司网络舆情与股票相关性分析项目源码,就是冲着这个需求来的。它把新闻文本采集、舆情评分、股票价格对齐、相关性计算和可视化展示串成了一条完整的链路,适合金融专业学生做实证分析、Python 爬虫与数据分析初学者练手,也适合需要快速搭建舆情因子雏形的量化从业者。
这份资源里包含的东西很实在:sina.py负责新闻抓取,score.py负责舆情打分,stock.py处理股票数据,analysis.py做相关性计算和画图,外加建表语句.txt、使用说明.doc和运行截图。它不是那种只给几个函数让你自己猜的“半成品”,而是把数据从哪来、怎么存、怎么算、怎么画都摆出来了。你拿到手之后,最需要关注的不是它用了多复杂的模型,而是它怎么把非结构化的新闻文本变成能参与计算的数值,以及这个数值和股价之间的相关性到底该怎么看才不会得出荒谬结论。接下来的内容,我会按实际复现的顺序,把这份源码拆开讲清楚。
2. 环境搭建与数据表结构:先把地基打对
2.1 Python 环境与依赖库的版本选择
这份源码没有附带requirements.txt,这是很多课程设计类资源的通病。根据代码里用到的库和常见写法,我一般会按下面这个清单来配环境。注意,不要盲目装最新版,尤其是pandas和matplotlib,版本差异会导致画图函数报错。
# 建议使用 Python 3.8 到 3.10 之间的版本,太新的版本某些库兼容性反而折腾 pip install requests==2.28.2 pip install pandas==1.5.3 pip install numpy==1.23.5 pip install matplotlib==3.7.1 pip install beautifulsoup4==4.12.2 pip install lxml==4.9.2 pip install openpyxl==3.1.2 pip install pymysql==1.0.3逻辑说明:requests和beautifulsoup4是sina.py抓取新闻的基础,lxml作为解析器比默认的html.parser快很多。pandas和numpy贯穿整个数据处理流程,openpyxl用来读写score.xlsx和share.xlsx。pymysql是否用得上取决于你是否把数据存进 MySQL,如果只用 Excel 流转,这个库可以先不装。参数上,requests的版本不要低于 2.25,否则某些 HTTPS 站点的证书验证会出问题。
提示:如果你用的是 Anaconda,很多库已经自带,但要注意 conda 源里的
pandas版本可能偏旧,建议用 pip 在虚拟环境里重装一遍。
2.2 建表语句与数据流转逻辑
资源里的建表语句.txt是理解整个项目数据结构的钥匙。虽然源码也支持直接读写 Excel,但如果你要做多只股票、多个时间段的批量分析,数据库表的设计就很重要。常见的做法是建三张表:新闻原始表、舆情评分表、股票行情表。
-- 新闻原始表:存爬下来的标题、内容、发布时间、来源 CREATE TABLE news_raw ( id INT AUTO_INCREMENT PRIMARY KEY, stock_code VARCHAR(10) NOT NULL COMMENT '股票代码', title VARCHAR(500), content TEXT, publish_time DATETIME, source VARCHAR(50), crawl_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; -- 舆情评分表:存每天每只股票的舆情得分 CREATE TABLE sentiment_score ( id INT AUTO_INCREMENT PRIMARY KEY, stock_code VARCHAR(10) NOT NULL, trade_date DATE NOT NULL, score FLOAT COMMENT '舆情综合得分', news_count INT COMMENT '当日新闻条数', UNIQUE KEY uk_code_date (stock_code, trade_date) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; -- 股票行情表:存日线级别的开盘收盘价和涨跌幅 CREATE TABLE stock_price ( id INT AUTO_INCREMENT PRIMARY KEY, stock_code VARCHAR(10) NOT NULL, trade_date DATE NOT NULL, open_price FLOAT, close_price FLOAT, pct_change FLOAT COMMENT '涨跌幅百分比', volume BIGINT, UNIQUE KEY uk_code_date (stock_code, trade_date) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;逻辑说明:news_raw表用stock_code关联股票,publish_time是后续按天聚合的关键字段。sentiment_score表里trade_date和stock_code做了唯一索引,防止重复插入同一天同一只股票的评分。stock_price表的pct_change字段直接存涨跌幅,省得在分析脚本里反复算。参数上,VARCHAR(500)对新闻标题足够,TEXT类型存正文,utf8mb4字符集能兼容特殊符号和表情。
如果你不想折腾数据库,源码里的score.xlsx和share.xlsx就是替代方案。share.xlsx通常存的是股票基础信息或者行情数据,score.xlsx存的是舆情评分结果。我一般会先把 Excel 的表头对齐,确保列名和代码里pd.read_excel后引用的字段一致,否则会直接报KeyError。
3. 新闻采集与舆情评分:从文本到数值的关键一步
3.1 sina.py 的抓取逻辑与解析代码修改点
sina.py是整个项目的数据入口。它的大致逻辑是构造新浪财经某个新闻列表页的 URL,用requests.get拿到 HTML,然后用BeautifulSoup提取标题、链接和时间。资源正文里有一句很关键的提醒:“若其他步骤都正确的情况下,爬取不了数据,可能是网站有变动,可自行修改解析部分的代码”。这句话是血泪经验,因为新闻网站的 DOM 结构说变就变。
import requests from bs4 import BeautifulSoup import pandas as pd import time import random def fetch_sina_news(stock_code, page=1): # 注意:这里的 URL 模板需要根据当前新浪财经的实际页面结构调整 base_url = "https://vip.stock.finance.sina.com.cn/corp/go.php/vCB_AllNewsStock/symbol/{}.phtml" url = base_url.format(stock_code) headers = { "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36", "Referer": "https://finance.sina.com.cn/" } try: resp = requests.get(url, headers=headers, timeout=10) resp.encoding = "gbk" # 新浪财经部分页面是 gbk 编码,写错会乱码 if resp.status_code != 200: print(f"请求失败,状态码:{resp.status_code}") return [] soup = BeautifulSoup(resp.text, "lxml") # 下面的选择器是示例,实际要按 F12 看当前页面的标签结构 news_list = soup.select(".datelist ul li a") results = [] for item in news_list: title = item.get_text(strip=True) link = item.get("href") if title and link: results.append({"title": title, "link": link, "stock_code": stock_code}) return results except Exception as e: print(f"抓取异常:{e}") return [] # 调用示例 if __name__ == "__main__": data = fetch_sina_news("600000") df = pd.DataFrame(data) print(df.head())逻辑说明:函数接收股票代码,拼出新闻列表页 URL。headers里的User-Agent和Referer是必须的,否则容易被识别为脚本请求。resp.encoding = "gbk"这一行很关键,新浪财经不少页面用的是 GBK 编码,不设置的话中文全是乱码。soup.select里的 CSS 选择器.datelist ul li a只是示例,实际运行时你要用浏览器的开发者工具去看新闻列表的真实标签。参数上,timeout=10避免网络卡死,random和time.sleep在批量抓取时用来控制频率,但示例里没展开,你自己加的时候建议每抓一页停 2 到 5 秒。
注意:如果返回的
news_list为空,先检查resp.text里有没有“暂无数据”或者验证码页面,再检查选择器是否匹配。不要一上来就怀疑代码逻辑,八成是页面结构变了。
3.2 score.py 的舆情评分算法与参数调整
score.py是这份源码里最值得细看的部分。它要把新闻标题或者正文变成分数。常见的做法是:先准备一个情感词典,正面词加一分,负面词减一分,再除以新闻总条数做归一化。源码里可能用的是更简单的关键词匹配,但思路大同小异。
import jieba import pandas as pd # 示例情感词典,实际项目里应该用更完整的词典文件 positive_words = ["增长", "盈利", "中标", "回购", "利好", "突破", "合作"] negative_words = ["亏损", "下滑", "违规", "处罚", "减持", "风险", "诉讼"] def calc_sentiment(text): if not isinstance(text, str): return 0 words = jieba.lcut(text) pos = sum(1 for w in words if w in positive_words) neg = sum(1 for w in words if w in negative_words) total = pos + neg if total == 0: return 0 # 得分范围控制在 -1 到 1 之间 return (pos - neg) / total def score_news_file(input_path, output_path): df = pd.read_excel(input_path) # 假设标题列叫 title,如果没有标题列就改成 content df["sentiment"] = df["title"].apply(calc_sentiment) # 按股票代码和日期聚合,这里假设已经有 trade_date 列 if "trade_date" in df.columns: daily = df.groupby(["stock_code", "trade_date"])["sentiment"].mean().reset_index() daily.columns = ["stock_code", "trade_date", "score"] daily.to_excel(output_path, index=False) print(f"评分完成,已保存到 {output_path}") else: df.to_excel(output_path, index=False) print("未找到 trade_date 列,已输出逐条评分结果") if __name__ == "__main__": score_news_file("news_raw.xlsx", "score.xlsx")逻辑说明:calc_sentiment用jieba分词后统计正负词数量,用(pos - neg) / total把分数压到 -1 到 1 之间。这样做的好处是不同长度的新闻可比。score_news_file读取原始新闻 Excel,给每条新闻打分,然后按股票代码和日期求均值,得到日度舆情评分。参数上,情感词典的质量直接决定评分效果,源码自带的词典通常很简陋,我一般会换成知网 HowNet 情感词典或者大连理工情感词典。另外,jieba分词对金融专有名词识别不好,可以加载自定义词典,把“回购”“减持”这类词加进去。
提示:如果你发现评分结果全是 0,先检查
title列是不是空的,再检查情感词典里的词有没有被jieba正确切出来。很多时候是分词把“利好”切成了“利”和“好”,导致匹配失败。
4. 相关性计算与可视化:别被相关系数骗了
4.1 stock.py 与 analysis.py 的数据对齐
stock.py负责获取股票价格数据,analysis.py负责把舆情评分和股价涨跌幅对齐后算相关性。这里最大的坑是时间对齐:新闻在周末和节假日也有,但股市不开盘。如果你直接把所有日期的舆情评分和股价合并,会出现大量空值或者错位。
import pandas as pd import numpy as np import matplotlib.pyplot as plt # 设置中文字体,否则图表里的中文会变成方块 plt.rcParams["font.sans-serif"] = ["SimHei"] plt.rcParams["axes.unicode_minus"] = False def merge_and_correlate(score_path, price_path): score_df = pd.read_excel(score_path) price_df = pd.read_excel(price_path) # 统一日期格式 score_df["trade_date"] = pd.to_datetime(score_df["trade_date"]) price_df["trade_date"] = pd.to_datetime(price_df["trade_date"]) # 只保留交易日的数据,用 inner join 自动过滤非交易日 merged = pd.merge(score_df, price_df, on=["stock_code", "trade_date"], how="inner") if merged.empty: print("合并后为空,检查股票代码和日期格式是否一致") return None # 计算皮尔逊相关系数 corr = merged["score"].corr(merged["pct_change"]) print(f"舆情评分与涨跌幅的相关系数:{corr:.4f}") # 画散点图和趋势线 fig, ax = plt.subplots(figsize=(10, 6)) ax.scatter(merged["score"], merged["pct_change"], alpha=0.6, label="交易日样本") # 拟合一条一次线 z = np.polyfit(merged["score"], merged["pct_change"], 1) p = np.poly1d(z) x_sorted = np.sort(merged["score"]) ax.plot(x_sorted, p(x_sorted), "r--", label=f"拟合线 y={z[0]:.2f}x+{z[1]:.2f}") ax.set_xlabel("舆情评分") ax.set_ylabel("日涨跌幅(%)") ax.set_title("舆情评分与股票涨跌幅相关性") ax.legend() ax.grid(True, alpha=0.3) plt.tight_layout() plt.savefig("correlation_plot.png", dpi=150) plt.show() return corr if __name__ == "__main__": merge_and_correlate("score.xlsx", "share.xlsx")逻辑说明:pd.merge用how="inner"只保留两个表都有的日期,自动把周末和节假日过滤掉。corr默认算皮尔逊相关系数,适合线性关系。画图部分用np.polyfit拟合一次线,能直观看出正负相关。参数上,alpha=0.6控制散点透明度,dpi=150保证保存的图片清晰。plt.rcParams那两行是必须的,否则中文标题和负号显示不出来。
注意:相关系数高不代表因果关系。舆情评分和股价可能同时受第三个因素影响,比如大盘走势。我一般会再算一个滞后相关性,看看前一天的舆情能不能预测今天的涨跌,而不是只看当天。
4.2 可视化图表的选择与解读边界
源码里用散点图加拟合线是合理的,但如果你想展示时间序列上的同步性,折线图更直观。常见做法是把舆情评分和涨跌幅分别做标准化处理,然后画双轴折线图。
def plot_time_series(merged_df): # 标准化处理,让两个不同量纲的序列能画在一张图上 merged_df["score_norm"] = (merged_df["score"] - merged_df["score"].mean()) / merged_df["score"].std() merged_df["pct_norm"] = (merged_df["pct_change"] - merged_df["pct_change"].mean()) / merged_df["pct_change"].std() fig, ax1 = plt.subplots(figsize=(12, 5)) ax1.plot(merged_df["trade_date"], merged_df["score_norm"], "b-", label="舆情评分(标准化)") ax1.set_ylabel("舆情评分 Z-Score", color="b") ax1.tick_params(axis="y", labelcolor="b") ax2 = ax1.twinx() ax2.plot(merged_df["trade_date"], merged_df["pct_norm"], "r-", label="涨跌幅(标准化)") ax2.set_ylabel("涨跌幅 Z-Score", color="r") ax2.tick_params(axis="y", labelcolor="r") plt.title("舆情评分与涨跌幅时间序列对比") fig.legend(loc="upper left", bbox_to_anchor=(0.1, 0.9)) plt.tight_layout() plt.savefig("time_series.png", dpi=150) plt.show()逻辑说明:标准化用 Z-Score,让两条线的波动幅度可比。双轴图能看出同涨同跌的区间,也能看出背离的区间。参数上,bbox_to_anchor调整图例位置,避免挡住曲线。解读时要注意,如果两条线大部分时间各走各的,只在某几天重合,那相关性系数再高也没有实际意义。
5. 避坑与常见问题排查:这些坑我替你踩过了
5.1 爬虫返回空数据或乱码
现象:运行sina.py后news_list为空,或者打印出来的标题是乱码。原因:新闻网站改版导致 CSS 选择器失效,或者页面编码不是 UTF-8 而是 GBK。解决:先用浏览器打开目标页面,右键查看源代码,搜索新闻标题的关键词,定位真实的标签结构。编码问题就加resp.encoding = "gbk"或者用resp.apparent_encoding自动检测。如果返回的是验证码页面,需要降低抓取频率,加time.sleep。
5.2 舆情评分全是零或波动极小
现象:score.xlsx里大部分评分是 0,或者所有股票的评分几乎一样。原因:情感词典覆盖不够,或者分词把关键词切碎了。解决:换用更全的情感词典,并在jieba里加载自定义词典,把“业绩预增”“股东减持”这类短语加进去。另外,检查calc_sentiment里是不是只用了标题,正文里的信息量更大,可以标题和正文加权算分。
5.3 合并后数据为空或日期错位
现象:analysis.py跑完提示“合并后为空”,或者相关系数算出来是nan。原因:舆情评分表和股价表的日期格式不一致,一个是字符串一个是datetime,或者股票代码一个是600000一个是sh600000。解决:在pd.merge之前统一用pd.to_datetime转日期,股票代码用str.zfill(6)补齐六位,或者统一加市场前缀。先打印两个表的head()和dtypes,肉眼确认列名和类型。
5.4 图表中文显示为方块
现象:matplotlib画出来的图,标题和轴标签里的中文全是小方块。原因:默认字体不支持中文。解决:在画图前加plt.rcParams["font.sans-serif"] = ["SimHei"],如果系统没有 SimHei,换成Microsoft YaHei或者WenQuanYi Micro Hei。axes.unicode_minus设为False解决负号显示问题。
5.5 数据库插入报唯一键冲突
现象:重复运行score.py往sentiment_score表插数据时,报Duplicate entry错误。原因:同一天同一只股票的数据被插了两次。解决:把INSERT改成INSERT ... ON DUPLICATE KEY UPDATE,或者在插入前先DELETE当天数据。更稳妥的做法是在代码里用pandas的to_sql配合if_exists="replace",但这样会清空整表,适合全量重跑的场景。
6. 进阶技巧:用滞后相关和滚动窗口验证舆情因子的有效性
把基础流程跑通之后,你很快会发现一个问题:当天的舆情评分和当天的涨跌幅相关性可能只有 0.1 甚至更低,这很正常。新闻的影响往往有滞后性,或者市场需要时间消化。我一般会做两件事来验证这个因子到底有没有用。
第一件事是算滞后相关性。把舆情评分往后移一到五个交易日,分别和涨跌幅算相关系数,看哪一期的相关性最高。如果滞后一期的相关性明显高于当期,说明舆情对股价有预测作用,虽然这个预测可能很弱。
def lag_correlation(merged_df, max_lag=5): results = [] for lag in range(0, max_lag + 1): # 把舆情评分往后移 lag 天,再和当天的涨跌幅对齐 merged_df[f"score_lag{lag}"] = merged_df["score"].shift(lag) temp = merged_df.dropna(subset=[f"score_lag{lag}", "pct_change"]) if len(temp) > 10: corr = temp[f"score_lag{lag}"].corr(temp["pct_change"]) results.append({"lag": lag, "corr": corr, "samples": len(temp)}) result_df = pd.DataFrame(results) print(result_df) return result_df逻辑说明:shift(lag)把评分序列整体后移,dropna去掉因为移位产生的空值。len(temp) > 10是防止样本太少导致相关系数不可靠。参数上,max_lag一般设 5 到 10,对应一周到两周的交易窗口。如果所有滞后期的相关系数都在 0.1 以下,那这个舆情因子基本没有独立预测能力,需要重新考虑评分算法或者加入更多特征。
第二件事是滚动窗口相关性。市场对舆情的敏感度会变化,牛市里利好消息可能被放大,熊市里利好反而被当成出货机会。用 30 天或者 60 天的滚动窗口算相关系数,能看出这种时变特征。
def rolling_correlation(merged_df, window=30): merged_df = merged_df.sort_values("trade_date").reset_index(drop=True) # 滚动窗口算相关系数,min_periods 保证窗口内至少有 10 个样本 merged_df["rolling_corr"] = merged_df["score"].rolling(window=window, min_periods=10).corr(merged_df["pct_change"]) fig, ax = plt.subplots(figsize=(12, 5)) ax.plot(merged_df["trade_date"], merged_df["rolling_corr"], "g-", label=f"{window}日滚动相关系数") ax.axhline(y=0, color="gray", linestyle="--", alpha=0.5) ax.set_xlabel("日期") ax.set_ylabel("滚动相关系数") ax.set_title(f"舆情评分与涨跌幅的{window}日滚动相关性") ax.legend() ax.grid(True, alpha=0.3) plt.tight_layout() plt.savefig("rolling_corr.png", dpi=150) plt.show() return merged_df逻辑说明:rolling(window).corr()在每个窗口内算皮尔逊相关系数,min_periods=10防止窗口前期样本不足时算出离谱的值。画图时加一条y=0的虚线,方便看正负切换。参数上,window取 30 适合捕捉月度情绪变化,取 60 更平滑但滞后更明显。如果滚动相关系数在正负之间反复横跳,说明舆情和股价的关系不稳定,不能当稳定的 alpha 因子用。
提示:滞后相关和滚动相关都只是验证手段,不能替代样本外测试。真正要判断因子有效性,还得做分组回测,看高舆情组和低舆情组的未来收益有没有显著差异。这份源码没有包含回测框架,但你可以把
score.xlsx和share.xlsx导出后,用backtrader或者自己写一个简单的分组收益统计。
从那以后我每次拿到一份新的舆情数据,都会先跑一遍滞后相关和滚动相关,再决定要不要把它放进多因子模型里。很多看起来热闹的新闻情绪,其实在统计上跟股价没什么稳定关系,早点发现比事后亏钱强。希望这份拆解能帮到你,少走一些我当年走过的弯路。
本文还有配套的精品资源,点击获取