简介:这是一份面向金融数据分析初学者与量化研究爱好者的Python实战项目源码,围绕上市公司网络舆情与股票价格之间的相关性展开。项目通过采集新闻文本数据,对上市公司舆情进行量化评分,再将评分与对应股票价格序列做相关性计算,并以图形化方式呈现分析结果,帮助读者理解从数据获取到结论输出的完整链路。压缩包共10个文件,约136KB,包含5个py脚本、2个txt说明、2个xlsx数据表与1份doc使用文档,分别承担爬虫采集、评分计算、相关性分析与结果展示等职责,另附建表语句与操作说明便于快速复现。目前已有6300人学习下载。读者可据此掌握文本舆情评分思路、股票数据对齐方法、相关性检验流程及可视化呈现技巧,并参考排错提示应对网站结构变动,适合作为课程设计、毕业项目或量化入门练手素材。
1. 从一份“python上市公司网络舆情与股票相关性分析项目源码”说起:它到底能解决什么问题
很多人第一次看到“python上市公司网络舆情与股票相关性分析项目源码”这个标题,第一反应是:这不就是个爬虫加个相关系数吗?但真正做过一轮的人会告诉你,坑不在爬虫,也不在corr(),而在“舆情怎么量化成时间序列”和“股票收益怎么对齐到同一根时间轴上”。这两件事没做对,后面算出来的相关性全是玄学。
这个项目要解决的核心问题很具体:给定一批上市公司,抓取它们在公开渠道上的舆情文本(新闻标题、公告摘要、讨论帖),把文本转成每日情绪分,再和对应股票的日收益率做相关性分析,最后输出可视化结果和统计结论。它适合三类人:一是做 python数据分析与可视化课程设计的学生,需要一份能跑通、有文档、有注解的完整源码;二是想入门 python量化交易策略代码的开发者,拿它当“文本因子”的最小实验台;三是做金融数据研究的从业者,想快速验证某个舆情指标到底有没有信号。
标题里写了“含文档、详细注解、运行截图”,说明这份源码的定位是“可复现的教学级项目”,不是生产级系统。所以本文不会假设你拿到了一份完美代码,而是按“如果我自己从零搭这个项目”的路径,把选型、实现、参数和踩坑讲清楚。你照着做,能跑出一份属于自己的版本;你拿到别人的源码,也能看懂每一行在干什么、哪里可能翻车。
2. 舆情数据抓取与清洗:从原始文本到可分析语料
2.1 为什么选 requests + BeautifulSoup 而不是一上来就上 Scrapy
做 python爬虫 抓舆情,第一个决策是框架选型。Scrapy 适合大规模、多站点、需要去重和调度的场景,但这个项目的目标是“几十到几百家上市公司、每天一批文本”,数据量在万级以内,用 Scrapy 属于杀鸡用牛刀,调试成本还高。我一般会选requests+BeautifulSoup的组合,理由有三:第一,代码短,新手能逐行看懂;第二,方便在 Jupyter 里边抓边看结果;第三,出问题容易定位,不像 Scrapy 的中间件链路那么长。
常见做法是:先确定数据源。公开舆情数据一般来自新闻门户的搜索页、交易所公告页、以及一些开放的数据接口。注意,这里不涉及任何需要特殊手段才能访问的渠道,只用公开可访问的页面。抓取前先手动翻几页,确认 URL 规律和分页参数,比如page=1、page=2这种。
下面是一个最小可运行的抓取函数,带重试和延时:
import requests from bs4 import BeautifulSoup import time import random 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_page(url, retry=3): """抓取单页,失败重试,返回 HTML 文本""" for i in range(retry): try: resp = requests.get(url, headers=HEADERS, timeout=10) if resp.status_code == 200: resp.encoding = resp.apparent_encoding # 防止中文乱码 return resp.text except requests.RequestException as e: print(f"第{i+1}次失败: {e}") time.sleep(random.uniform(1, 3)) # 随机延时,降低被封风险 return None def parse_news(html): """从列表页提取标题、时间、链接""" soup = BeautifulSoup(html, "html.parser") items = [] for node in soup.select(".news-item"): # 选择器按实际页面调整 title = node.select_one(".title").get_text(strip=True) date = node.select_one(".date").get_text(strip=True) link = node.select_one("a")["href"] items.append({"title": title, "date": date, "link": link}) return items逻辑说明:fetch_page做了三件事——设置请求头伪装浏览器、超时保护、失败重试。resp.apparent_encoding是关键,很多中文页面不声明编码,直接用resp.text会得到乱码。parse_news里的选择器.news-item、.title、.date必须按你实际抓的页面改,没有通用写法。
参数说明:timeout=10是单次请求上限,舆情页面一般不大,10 秒够用;retry=3是重试次数,超过三次基本说明对方在限流或页面结构变了;random.uniform(1, 3)是随机延时,固定延时容易被识别出机器行为。
2.2 文本清洗:去重、去噪、时间归一化
抓下来的原始文本不能直接进模型。我见过太多人跳过这一步,结果同一篇新闻被转载十次,情绪分被重复计算,相关性直接失真。清洗要做四件事:
第一,去重。按标题做精确去重,再用编辑距离或 SimHash 做近似去重。简单做法是用difflib.SequenceMatcher算相似度,超过 0.85 视为重复。
第二,去噪。去掉 HTML 残留标签、多余空白、广告性文字(如“点击查看”)。用正则处理:
import re def clean_text(text): text = re.sub(r"<[^>]+>", "", text) # 去标签 text = re.sub(r"\s+", " ", text) # 合并空白 text = re.sub(r"(点击查看|阅读全文|广告)", "", text) # 去广告词 return text.strip()第三,时间归一化。不同来源的时间格式不一样,有的是“2024-01-05”,有的是“01月05日”,有的是“3小时前”。统一转成YYYY-MM-DD,因为后面要和交易日对齐。用pandas.to_datetime配合errors="coerce",转不了的标记为缺失。
第四,按公司聚合。每家公司每天可能有多条舆情,需要聚合成一条日度记录。常见做法是取当天所有文本的情绪分均值,或者按热度加权。这个选择会影响最终相关性,后面第 4 章会展开。
提示:抓取频率控制在每天一次、每次不超过几百个请求,既够用又不会给对方服务器造成压力。这是长期能跑下去的前提。
3. 情绪量化与股票数据对齐:把文本变成能算相关的数字
3.1 情绪打分:词典法还是模型法,先看你的数据量
舆情文本要变成数字,才有法和股票收益率算相关。两条路:词典法(如 SnowNLP、知网情感词典)和模型法(如 BERT 微调)。选哪条,取决于你有多少标注数据。
词典法的优点是零训练、可解释、跑得快。缺点是领域适配差——金融文本里“空头”“回调”“破发”这些词,通用词典经常判错。模型法准确率高,但需要标注数据,而且推理慢。
我的建议:如果这是课程设计或第一次做,先用词典法跑通全流程,拿到基线结果;如果要做 python量化交易策略代码 级别的实验,再考虑用金融领域预训练模型微调。下面是一个基于 SnowNLP 的批量打分示例:
from snownlp import SnowNLP import pandas as pd def sentiment_score(text): """返回 0~1 的情绪分,越接近 1 越正面""" if not text or len(text) < 5: return None try: return SnowNLP(text).sentiments except Exception: return None df = pd.read_csv("news_clean.csv") df["sentiment"] = df["title"].apply(sentiment_score) df = df.dropna(subset=["sentiment"]) daily = df.groupby(["stock_code", "date"])["sentiment"].mean().reset_index()逻辑说明:SnowNLP(text).sentiments返回 0 到 1 的分数,0.5 附近表示中性。groupby按股票代码和日期聚合,取均值作为当日情绪分。dropna去掉打分失败的记录。
参数说明:len(text) < 5是过滤太短的文本,这类文本情绪信号极弱;groupby的聚合函数可以换成加权平均,权重用阅读量或评论数,但前提是你抓到了这些字段。
3.2 股票收益率计算与时间轴对齐
股票数据用tushare或akshare获取日线。核心是算日收益率,然后和舆情日期对齐。注意:舆情可能在周末或节假日产生,但股市不开盘,这些日期的舆情要归到下一个交易日。
import akshare as ak import pandas as pd def get_stock_returns(code, start, end): df = ak.stock_zh_a_hist(symbol=code, period="daily", start_date=start, end_date=end) df = df.rename(columns={"日期": "date", "收盘": "close"}) df["date"] = pd.to_datetime(df["date"]) df["return"] = df["close"].pct_change() return df[["date", "return"]].dropna() def align_dates(sentiment_df, return_df): """把舆情日期映射到最近的交易日""" trading_days = return_df["date"].sort_values().tolist() def map_to_trading_day(d): for td in trading_days: if td >= d: return td return None sentiment_df["trade_date"] = sentiment_df["date"].apply(map_to_trading_day) return sentiment_df.dropna(subset=["trade_date"])逻辑说明:pct_change()算日收益率。align_dates把每条舆情映射到当天或之后的第一个交易日,保证两边时间轴一致。
参数说明:period="daily"是日线;start_date、end_date格式为YYYYMMDD。对齐时如果舆情日期晚于最后一个交易日,会被丢弃,这是合理的。
3.3 相关性计算:Pearson、Spearman 还是滞后相关
对齐之后,按股票分组算相关系数。这里有个关键选择:用 Pearson 还是 Spearman?Pearson 测线性相关,对异常值敏感;Spearman 测单调相关,更稳健。金融数据尾部厚,我一般两个都算,对比看。
更重要的是滞后相关。舆情的影响可能不是当天的,而是滞后一到三天。做法是把情绪分往后移 1、2、3 天,分别算相关,看哪个滞后阶数相关性最强。
from scipy.stats import pearsonr, spearmanr def calc_corr(group, max_lag=3): results = [] group = group.sort_values("trade_date") for lag in range(max_lag + 1): s = group["sentiment"].shift(lag) valid = pd.concat([s, group["return"]], axis=1).dropna() if len(valid) < 30: # 样本太少不计算 continue r_p, p_p = pearsonr(valid.iloc[:, 0], valid.iloc[:, 1]) r_s, p_s = spearmanr(valid.iloc[:, 0], valid.iloc[:, 1]) results.append({"lag": lag, "pearson": r_p, "p_pearson": p_p, "spearman": r_s, "p_spearman": p_s}) return pd.DataFrame(results)逻辑说明:shift(lag)把情绪分往后移,模拟“今天的情绪影响未来收益”。len(valid) < 30是样本量保护,少于 30 个点的相关系数没有统计意义。
参数说明:max_lag=3是最大滞后天数,一般不超过 5;p值小于 0.05 才认为显著。注意,这里算的是单只股票的时间序列相关,不是横截面相关,两者含义完全不同。
4. 避坑与排查:那些让相关性结果失真的常见问题
4.1 现象:相关性高得离谱,接近 0.9
原因:最常见的是数据泄漏。比如情绪分里混入了当天收盘后的新闻,而收益率用的是当天收盘价,等于用未来信息预测现在。另一个原因是样本量太小,几十个点很容易算出高相关。
解决:严格按时间切分,情绪分只用当天开盘前能获取的文本;样本量少于 60 个交易日的股票直接剔除。算完相关后画散点图,肉眼确认不是几个异常点拉高的。
4.2 现象:所有股票相关性都不显著
原因:情绪分区分度太低。词典法对金融文本经常把大部分句子判成 0.5 附近,方差极小,自然算不出相关。也可能是舆情覆盖太稀疏,很多交易日没有舆情,对齐后大量缺失。
解决:先看情绪分的分布,如果标准差小于 0.1,说明区分度不够,换模型法或调整词典。再看每只股票的舆情覆盖率,低于 30% 的交易日有舆情的,结果不可信。
4.3 现象:换一批股票,结论完全反转
原因:过拟合。你可能试了很多参数组合(滞后阶数、聚合方式、时间窗口),挑了一个最好看的,但这只是噪声。金融数据信噪比极低,这是血泪经验。
解决:固定一套参数,在样本外验证。比如用前 70% 时间算相关,后 30% 验证。如果样本外不显著,说明样本内的显著是偶然。
4.4 现象:程序跑着跑着报编码错误
原因:中文页面编码不统一,resp.text默认用 ISO-8859-1 解码,遇到 GBK 页面就乱码,后续正则匹配全失败。
解决:统一用resp.apparent_encoding,或者在requests.get里加resp.encoding = "utf-8"。写文件时用encoding="utf-8-sig",避免 Excel 打开乱码。
4.5 现象:tushare 或 akshare 取数失败
原因:接口限流、股票代码格式不对、或者日期范围超限。不同数据源的代码格式不一样,有的要000001,有的要000001.SZ。
解决:先单独跑取数函数,确认返回不为空。加try-except和重试。代码格式按数据源文档来,别凭记忆写。
5. 进阶技巧:用滚动窗口和分组验证让结论更可信
跑通基础流程后,真正决定这个项目值不值得深入做的,是你能不能把“相关性”从一次性计算变成可验证的稳定信号。我一般会加两个东西:滚动窗口相关和行业分组对比。
滚动窗口相关是指:不只用全样本算一个相关系数,而是用 60 个交易日为窗口,逐日滚动,画出相关系数随时间变化的曲线。如果某只股票的舆情-收益相关在某些时段强、某些时段弱,说明这个信号不稳定,不能直接拿来做策略。代码上就是group["sentiment"].rolling(60).corr(group["return"]),注意要先按日期排序。
行业分组对比是指:把股票按行业分组,看舆情相关性在哪些行业更强。通常消费、科技类公司的舆情敏感度高于重资产行业。做法是把行业字段 merge 进来,然后groupby("industry")算平均相关系数。这一步能帮你判断:这个舆情因子到底在哪些场景下有效。
还有一个实用技巧是情绪分的标准化。不同股票、不同时期的舆情量差异很大,直接算相关会被量级影响。我一般对每只股票的情绪分做 z-score 标准化,再算相关。这样比较的是“相对情绪高低”,而不是绝对分数。
def rolling_corr(group, window=60): group = group.sort_values("trade_date").set_index("trade_date") return group["sentiment"].rolling(window).corr(group["return"]).dropna() def zscore_by_stock(df): df["sentiment_z"] = df.groupby("stock_code")["sentiment"].transform( lambda x: (x - x.mean()) / x.std() ) return df逻辑说明:rolling(window).corr()逐窗口算相关,返回时间序列。transform按股票分组做标准化,不改变行数。
参数说明:window=60约等于一个季度,太短噪声大,太长反应慢。z-score要求每只股票至少有 20 个以上数据点,否则标准差不可靠。
最后说个我自己的习惯:每次跑完相关性,先不看系数,先看缺失值比例和情绪分分布。这两个指标正常了,系数才有讨论价值。这个项目不难,难的是对数据保持怀疑。希望帮到你。
本文还有配套的精品资源,点击获取