
简介这是一份面向金融数据分析初学者与量化研究爱好者的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 requests2.28.2 pip install pandas1.5.3 pip install numpy1.23.5 pip install matplotlib3.7.1 pip install beautifulsoup44.12.2 pip install lxml4.9.2 pip install openpyxl3.1.2 pip install pymysql1.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 ) ENGINEInnoDB DEFAULT CHARSETutf8mb4; -- 舆情评分表存每天每只股票的舆情得分 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) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4; -- 股票行情表存日线级别的开盘收盘价和涨跌幅 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) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4;逻辑说明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, page1): # 注意这里的 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, headersheaders, timeout10) 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(stripTrue) 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只是示例实际运行时你要用浏览器的开发者工具去看新闻列表的真实标签。参数上timeout10避免网络卡死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, indexFalse) print(f评分完成已保存到 {output_path}) else: df.to_excel(output_path, indexFalse) 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], howinner) 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], alpha0.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--, labelf拟合线 y{z[0]:.2f}x{z[1]:.2f}) ax.set_xlabel(舆情评分) ax.set_ylabel(日涨跌幅%) ax.set_title(舆情评分与股票涨跌幅相关性) ax.legend() ax.grid(True, alpha0.3) plt.tight_layout() plt.savefig(correlation_plot.png, dpi150) plt.show() return corr if __name__ __main__: merge_and_correlate(score.xlsx, share.xlsx)逻辑说明pd.merge用howinner只保留两个表都有的日期自动把周末和节假日过滤掉。corr默认算皮尔逊相关系数适合线性关系。画图部分用np.polyfit拟合一次线能直观看出正负相关。参数上alpha0.6控制散点透明度dpi150保证保存的图片清晰。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, colorb) ax1.tick_params(axisy, labelcolorb) ax2 ax1.twinx() ax2.plot(merged_df[trade_date], merged_df[pct_norm], r-, label涨跌幅标准化) ax2.set_ylabel(涨跌幅 Z-Score, colorr) ax2.tick_params(axisy, labelcolorr) plt.title(舆情评分与涨跌幅时间序列对比) fig.legend(locupper left, bbox_to_anchor(0.1, 0.9)) plt.tight_layout() plt.savefig(time_series.png, dpi150) 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_existsreplace但这样会清空整表适合全量重跑的场景。6. 进阶技巧用滞后相关和滚动窗口验证舆情因子的有效性把基础流程跑通之后你很快会发现一个问题当天的舆情评分和当天的涨跌幅相关性可能只有 0.1 甚至更低这很正常。新闻的影响往往有滞后性或者市场需要时间消化。我一般会做两件事来验证这个因子到底有没有用。第一件事是算滞后相关性。把舆情评分往后移一到五个交易日分别和涨跌幅算相关系数看哪一期的相关性最高。如果滞后一期的相关性明显高于当期说明舆情对股价有预测作用虽然这个预测可能很弱。def lag_correlation(merged_df, max_lag5): results [] for lag in range(0, max_lag 1): # 把舆情评分往后移 lag 天再和当天的涨跌幅对齐 merged_df[fscore_lag{lag}] merged_df[score].shift(lag) temp merged_df.dropna(subset[fscore_lag{lag}, pct_change]) if len(temp) 10: corr temp[fscore_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, window30): merged_df merged_df.sort_values(trade_date).reset_index(dropTrue) # 滚动窗口算相关系数min_periods 保证窗口内至少有 10 个样本 merged_df[rolling_corr] merged_df[score].rolling(windowwindow, min_periods10).corr(merged_df[pct_change]) fig, ax plt.subplots(figsize(12, 5)) ax.plot(merged_df[trade_date], merged_df[rolling_corr], g-, labelf{window}日滚动相关系数) ax.axhline(y0, colorgray, linestyle--, alpha0.5) ax.set_xlabel(日期) ax.set_ylabel(滚动相关系数) ax.set_title(f舆情评分与涨跌幅的{window}日滚动相关性) ax.legend() ax.grid(True, alpha0.3) plt.tight_layout() plt.savefig(rolling_corr.png, dpi150) plt.show() return merged_df逻辑说明rolling(window).corr()在每个窗口内算皮尔逊相关系数min_periods10防止窗口前期样本不足时算出离谱的值。画图时加一条y0的虚线方便看正负切换。参数上window取 30 适合捕捉月度情绪变化取 60 更平滑但滞后更明显。如果滚动相关系数在正负之间反复横跳说明舆情和股价的关系不稳定不能当稳定的 alpha 因子用。提示滞后相关和滚动相关都只是验证手段不能替代样本外测试。真正要判断因子有效性还得做分组回测看高舆情组和低舆情组的未来收益有没有显著差异。这份源码没有包含回测框架但你可以把score.xlsx和share.xlsx导出后用backtrader或者自己写一个简单的分组收益统计。从那以后我每次拿到一份新的舆情数据都会先跑一遍滞后相关和滚动相关再决定要不要把它放进多因子模型里。很多看起来热闹的新闻情绪其实在统计上跟股价没什么稳定关系早点发现比事后亏钱强。希望这份拆解能帮到你少走一些我当年走过的弯路。本文还有配套的精品资源点击获取