
简介这是一份面向金融数据分析初学者与量化研究爱好者的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 属于杀鸡用牛刀调试成本还高。我一般会选requestsBeautifulSoup的组合理由有三第一代码短新手能逐行看懂第二方便在 Jupyter 里边抓边看结果第三出问题容易定位不像 Scrapy 的中间件链路那么长。常见做法是先确定数据源。公开舆情数据一般来自新闻门户的搜索页、交易所公告页、以及一些开放的数据接口。注意这里不涉及任何需要特殊手段才能访问的渠道只用公开可访问的页面。抓取前先手动翻几页确认 URL 规律和分页参数比如page1、page2这种。下面是一个最小可运行的抓取函数带重试和延时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, retry3): 抓取单页失败重试返回 HTML 文本 for i in range(retry): try: resp requests.get(url, headersHEADERS, timeout10) if resp.status_code 200: resp.encoding resp.apparent_encoding # 防止中文乱码 return resp.text except requests.RequestException as e: print(f第{i1}次失败: {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(stripTrue) date node.select_one(.date).get_text(stripTrue) 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必须按你实际抓的页面改没有通用写法。参数说明timeout10是单次请求上限舆情页面一般不大10 秒够用retry3是重试次数超过三次基本说明对方在限流或页面结构变了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配合errorscoerce转不了的标记为缺失。第四按公司聚合。每家公司每天可能有多条舆情需要聚合成一条日度记录。常见做法是取当天所有文本的情绪分均值或者按热度加权。这个选择会影响最终相关性后面第 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(symbolcode, perioddaily, start_datestart, end_dateend) 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把每条舆情映射到当天或之后的第一个交易日保证两边时间轴一致。参数说明perioddaily是日线start_date、end_date格式为YYYYMMDD。对齐时如果舆情日期晚于最后一个交易日会被丢弃这是合理的。3.3 相关性计算Pearson、Spearman 还是滞后相关对齐之后按股票分组算相关系数。这里有个关键选择用 Pearson 还是 SpearmanPearson 测线性相关对异常值敏感Spearman 测单调相关更稳健。金融数据尾部厚我一般两个都算对比看。更重要的是滞后相关。舆情的影响可能不是当天的而是滞后一到三天。做法是把情绪分往后移 1、2、3 天分别算相关看哪个滞后阶数相关性最强。from scipy.stats import pearsonr, spearmanr def calc_corr(group, max_lag3): 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]], axis1).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_lag3是最大滞后天数一般不超过 5p值小于 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。写文件时用encodingutf-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, window60): 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按股票分组做标准化不改变行数。参数说明window60约等于一个季度太短噪声大太长反应慢。z-score要求每只股票至少有 20 个以上数据点否则标准差不可靠。最后说个我自己的习惯每次跑完相关性先不看系数先看缺失值比例和情绪分分布。这两个指标正常了系数才有讨论价值。这个项目不难难的是对数据保持怀疑。希望帮到你。本文还有配套的精品资源点击获取