简介:这份资源面向对量化分析与文本挖掘感兴趣的学习者,围绕股票评论与股价涨跌之间的关联展开,整合了Python爬虫采集、百度AI情感分析与MATLAB数据处理三条技术路线,适合具备一定Python与数据分析基础、希望完成一个完整小项目实战的读者参考。压缩包共10个文件,约1.71MB,包含2个py脚本分别负责评论抓取与情感分析,1个m脚本用于时间序列处理,另有csv与xlsx数据文件承载评论及情感结果,以及4张png图表呈现股价走势与情感分布。已有6466人学习下载,说明该案例在同类项目中具有一定参考热度。资源的价值在于把数据采集、情感标注、统计建模到可视化呈现串成一条可复用的分析链路,读者可据此理解评论情绪如何量化、如何与股价序列对齐比较,并借鉴其数据处理与图表呈现思路,用于课程作业、个人研究或投资情绪分析的入门实践。
1. 从一条“评论能不能预测涨跌”的疑问说起
做量化方向的同学大多有过这个念头:股吧、论坛里每天几千条帖子,情绪明显偏热的时候,第二天股价是不是更容易冲高?这个想法听起来像玄学,但拆开看其实是一条完整的数据链路——采集评论、做语义打分、对齐交易日、统计相关性。这套方案要解决的就是把这条链路跑通并且可复现:用 Python 爬虫抓取股票评论,调用百度 AI 的语义接口把文本转成情绪分,再用 MATLAB 做时间序列对齐和涨跌相关性分析。它适合两类人:一是要做毕设或课程设计、需要一套从采集到分析完整闭环的学生;二是想快速验证“舆情因子”有没有增量信息的从业者。整套东西不追求高频交易级别的精度,追求的是链路清晰、每一步都能解释、结论能站得住。
2. 需求拆解与技术选型:为什么是 Python 采集 + 百度 AI 打分 + MATLAB 分析
先把“评论和涨跌有没有关系”翻译成工程语言。它其实是三个子问题:第一,评论从哪来、怎么稳定拿到;第二,一段中文评论的情绪怎么变成可计算的数值;第三,评论是按自然日产生的,股价是按交易日走的,两者怎么对齐才能做统计。三个子问题对应三段技术栈,选型不是拍脑袋,而是各自解决一个明确约束。
2.1 三段链路的职责边界
Python 负责采集和清洗,原因是请求库、解析库、反爬处理生态最全,写起来快,调试成本低。百度 AI 负责语义分析,原因是中文情绪分类自己训模型需要标注数据,而现成接口对口语化、带表情、带黑话的股吧文本鲁棒性明显好于通用词典法。MATLAB 负责数据处理和统计建模,原因是时间序列对齐、相关性检验、回归和绘图在 MATLAB 里几行就能出结果,做毕设答辩时图表也好看。
这里有个容易翻车的认知:不要把三段写成三个互不相干的脚本,靠手工导 CSV 串起来。我一般会约定一个中间格式,Python 输出date, stock_code, comment, sentiment_score, confidence这样的宽表,MATLAB 只认这个格式。这样任何一段出问题都能单独替换,不用推倒重来。
2.2 评论采集的字段设计与翻页策略
采集阶段最容易被忽略的是字段设计。只存评论文本是不够的,后面做对齐时你会后悔。通常的做法是至少保留:评论发布时间(精确到分钟)、所属股票代码、评论正文、点赞数、作者标识(脱敏后)、抓取时间戳。发布时间决定它归到哪个交易日,点赞数可以用来做加权,抓取时间戳用于排查数据缺口。
翻页策略上,股吧类页面一般是页码参数或游标参数。页码翻页简单但容易在数据更新时重复或漏抓,游标翻页更稳。下面是一个带重试和去重的采集骨架,参数都做了外置:
import time import hashlib import requests from requests.adapters import HTTPAdapter # 会话复用 + 重试,避免频繁建连被限流 session = requests.Session() session.mount("https://", HTTPAdapter(max_retries=3)) HEADERS = { "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64)", "Accept-Language": "zh-CN,zh;q=0.9", } def fetch_page(stock_code, page, page_size=50, sleep=1.5): """抓取单页评论,返回结构化列表""" url = "https://example-forum-api.com/comment/list" params = {"code": stock_code, "page": page, "size": page_size} resp = session.get(url, params=params, headers=HEADERS, timeout=10) resp.raise_for_status() items = resp.json().get("data", {}).get("list", []) result = [] for it in items: text = (it.get("content") or "").strip() if len(text) < 5: # 过滤“顶”“沙发”这类无信息短评 continue result.append({ "stock_code": stock_code, "publish_time": it.get("createTime"), "content": text, "like_count": it.get("likeCount", 0), "fingerprint": hashlib.md5(text.encode("utf-8")).hexdigest(), }) time.sleep(sleep) # 主动降速,别把对方打挂 return result这段代码的关键点有三个。fingerprint用正文做 MD5,是为了跨页去重——同一只股票的热帖会在多页重复出现,不去重会让情绪分被少数帖子带偏。sleep参数控制请求间隔,1.5 秒是经验值,太激进会触发限流,太保守采集效率低。raise_for_status让 HTTP 错误直接抛出,配合HTTPAdapter的重试,网络抖动不会静默丢数据。字段里保留like_count是为了后面做加权情绪,一条高赞帖和一条零赞帖对市场情绪的代表性显然不同。
2.3 百度 AI 语义接口的调用与限流控制
语义分析这一步,接口返回的一般是情绪极性(正向/负向/中性)加置信度。真正要落库的不是“正向”这个字符串,而是一个连续分数,方便后面做相关分析。通常的做法是把极性映射成 +1 / 0 / -1,再乘以置信度,得到sentiment_score ∈ [-1, 1]。这样一条“非常看多”的帖子得 0.9,一条“略微看空”的得 -0.4,粒度比三分类细。
import json import time import requests def analyze_sentiment(text, token, max_retry=3): """调用语义接口,返回 [-1,1] 的情绪分与置信度""" url = "https://aip.example.com/rpc/2.0/nlp/v1/sentiment_classify" payload = {"text": text[:500]} # 接口一般有长度上限,截断保护 for attempt in range(max_retry): try: resp = requests.post( url, params={"access_token": token}, data=json.dumps(payload, ensure_ascii=False).encode("utf-8"), headers={"Content-Type": "application/json"}, timeout=8, ) data = resp.json() item = data["items"][0] polarity = {"positive": 1, "negative": -1, "neutral": 0} score = polarity.get(item["sentiment"], 0) * item["confidence"] return round(score, 4), item["confidence"] except (KeyError, IndexError, requests.RequestException): time.sleep(2 ** attempt) # 指数退避,接口偶发超时很常见 return 0.0, 0.0 # 多次失败给中性分,不污染整体这里有两个设计取舍值得说清楚。截断到 500 字是因为长帖后半段往往是复读和情绪宣泄,对极性判断贡献不大,还能省调用量。失败返回 0 分而不是丢弃,是为了保持样本量——一条评论分析失败就丢掉,会让某些日期的样本骤减,统计时出现空洞。指数退避的2 ** attempt让重试间隔是 1、2、4 秒,避免接口抖动时连续打过去。
提示:接口有 QPS 和日调用量限制,正式跑之前先用几百条做一次全量试跑,估算总调用量和耗时,别等跑到一半额度没了。
2.4 MATLAB 侧的数据对齐与相关性建模
MATLAB 这一段的难点不在算法,在对齐。评论按自然日产生,周末和节假日也有大量讨论,但股价只在交易日变动。如果直接把周末评论算进周一,情绪会被稀释;如果直接丢掉周末评论,又会损失信息。我一般用“前向填充 + 交易日聚合”的做法:把每条评论按发布时间归到“下一个交易日”,也就是周五收盘后到周一开盘前的评论都算周一的预期情绪,这样逻辑上更贴近“盘前情绪影响当日走势”。
% 读取 Python 输出的宽表,按交易日聚合情绪 T = readtable('sentiment_wide.csv'); T.publish_time = datetime(T.publish_time, 'InputFormat', 'yyyy-MM-dd HH:mm:ss'); % 交易日历(示例:从行情数据里取真实交易日) tradeDays = readtable('trade_calendar.csv'); tradeDays.date = datetime(tradeDays.date, 'InputFormat', 'yyyy-MM-dd'); % 每条评论映射到其后的第一个交易日 T.trade_date = NaT(height(T),1); for i = 1:height(T) idx = find(tradeDays.date >= dateshift(T.publish_time(i),'start','day'), 1); if ~isempty(idx) T.trade_date(i) = tradeDays.date(idx); end end % 按交易日聚合:均值情绪 + 加权情绪 + 评论量 G = groupsummary(T, 'trade_date', {'mean','sum'}, 'sentiment_score'); G.weighted = splitapply(@(s,w) sum(s.*w)/sum(w), ... T.sentiment_score, T.like_count+1, findgroups(T.trade_date)); % 与次日涨跌幅做 Spearman 相关(不假设线性) ret = readtable('daily_return.csv'); M = innerjoin(G, ret, 'Keys','trade_date'); [rho, pval] = corr(M.mean_sentiment_score, M.next_day_return, 'Type','Spearman'); fprintf('情绪与次日收益 Spearman rho=%.3f, p=%.4f\n', rho, pval);这段代码里几个参数值得解释。like_count+1是加权时的平滑,避免零赞帖权重为 0 被完全忽略。用 Spearman 而不是 Pearson,是因为情绪分和收益率的关系大概率不是线性的,极端情绪日的影响会被 Pearson 放大。next_day_return而不是当日收益,是因为盘前和盘中评论对当日的影响已经被价格消化,真正有预测意义的是次日。innerjoin保证只保留两边都有的交易日,避免缺失值干扰检验。
3. 总体架构与模块划分:四层结构怎么落地
链路讲清楚之后,要把散落的脚本收成一套能交付的结构。毕设答辩时老师最常问的一句是“你的系统分几层、每层干什么”,答不上来就很被动。这套方案按四层划分:采集层、语义层、存储层、分析层,层与层之间只通过约定的数据格式通信。
3.1 四层架构与数据流
采集层负责请求调度、去重、断点续爬,输出原始评论表。语义层负责批量调用接口、失败重试、分数映射,输出带情绪分的评论表。存储层用一张主表加若干维表,主表是评论粒度,维表是股票信息和交易日历。分析层读主表做聚合、对齐、统计和绘图。数据流是单向的:采集 → 语义 → 存储 → 分析,不允许分析层回头去调采集层,否则一处改动全链路重跑。
这种分层的好处是替换成本低。比如语义接口换了供应商,只改语义层,存储和分析不动;比如想加一个“评论长度”特征,只在采集层加字段,下游按需读取。我见过不少同学把所有逻辑塞进一个 notebook,跑到一半内存爆了,连哪一步出的问题都定位不了,这就是没分层的代价。
3.2 目录结构与配置管理
工程目录建议按职责分,而不是按文件类型分。下面这个结构是我常用的,直接照搬也能用:
stock_sentiment/ ├── config/ │ ├── settings.yaml # 接口地址、token、限流参数 │ └── stocks.csv # 股票池与代码映射 ├── crawler/ │ ├── fetch.py # 翻页采集 │ └── dedup.py # 指纹去重 ├── nlp/ │ ├── analyze.py # 语义接口封装 │ └── batch_runner.py # 批量调度与断点 ├── storage/ │ ├── schema.sql # 建表语句 │ └── loader.py # 入库与校验 ├── analysis/ │ ├── align.m # 交易日对齐 │ └── correlation.m # 相关与回归 └── output/ ├── figures/ # 图表 └── reports/ # 统计结果配置外置是血泪经验。token、接口地址、限流间隔、股票池这些一变就要改代码的东西,全部放settings.yaml,代码里只读配置。这样换一只股票、换一个接口地址,不用碰逻辑代码,也避免了把密钥硬编码进脚本再传到公开仓库的尴尬。
3.3 数据库表结构设计
存储层用关系库就够,评论量在百万级以内,单机完全扛得住。核心是三张表:评论主表、股票维表、交易日历表。主表的设计要点是把“原始文本”和“分析结果”分开存,分析结果可以重跑覆盖,原始文本只写一次。
CREATE TABLE comment_raw ( id BIGINT PRIMARY KEY AUTO_INCREMENT, stock_code VARCHAR(10) NOT NULL, publish_time DATETIME NOT NULL, content TEXT NOT NULL, like_count INT DEFAULT 0, fingerprint CHAR(32) NOT NULL, crawl_time DATETIME DEFAULT CURRENT_TIMESTAMP, UNIQUE KEY uk_fp (fingerprint, stock_code), KEY idx_time (stock_code, publish_time) ); CREATE TABLE comment_sentiment ( comment_id BIGINT PRIMARY KEY, sentiment_score DECIMAL(6,4), -- [-1,1] confidence DECIMAL(6,4), model_version VARCHAR(32), update_time DATETIME DEFAULT CURRENT_TIMESTAMP, FOREIGN KEY (comment_id) REFERENCES comment_raw(id) );uk_fp唯一索引把去重下沉到数据库层,比在 Python 里维护集合更可靠,程序中断重启也不会重复入库。idx_time支撑按股票和时间范围查询,分析层聚合时走这个索引。情绪分单独一张表,是为了接口升级或换模型时可以整表重算,原始评论不受影响。model_version记录用的是哪版接口,答辩时被问到“结果可复现吗”,这个字段就是答案。
4. 关键实现细节:从采集到统计的四个硬骨头
架构搭好只是骨架,真正决定结论可信度的是几个实现细节。这一章挑四个最容易出问题、也最影响结果的地方展开:反爬与合规采集、情绪分的日聚合口径、涨跌标签的定义、以及统计显著性的判断。
4.1 采集稳定性:限流、断点与增量
采集跑一整晚是常态,中途断掉很常见。断点续爬的做法是记录每只股票已抓到的最大发布时间,下次从该时间之后继续。增量采集则是在每次运行前先查库里该股票的最新publish_time,只抓更新的部分。两者结合,既不会重复抓历史,也不会因为中断而从头再来。
限流方面,单 IP 的请求频率要有上限,通常的做法是给每只股票之间加随机间隔,而不是固定间隔。固定间隔容易被识别出机器节奏,随机间隔更像人。下面是一个带断点记录的调度片段:
import random import yaml with open("config/settings.yaml", encoding="utf-8") as f: cfg = yaml.safe_load(f) def crawl_stock(stock_code, last_time): page = 1 while True: rows = fetch_page(stock_code, page, cfg["page_size"], cfg["sleep"]) if not rows: break fresh = [r for r in rows if r["publish_time"] > last_time] save_to_db(fresh) if len(fresh) < len(rows): # 已翻到历史数据,停止 break page += 1 time.sleep(random.uniform(1.0, 2.5)) # 随机间隔last_time从数据库查,fresh过滤掉已入库的部分,len(fresh) < len(rows)是停止条件——一旦某页出现旧数据,说明已经翻过了增量边界,继续翻只是浪费请求。随机间隔用uniform(1.0, 2.5),比固定 1.5 秒更自然。
4.2 情绪聚合口径:均值、加权与分歧度
日度情绪不是一个数,而是一组数。均值反映整体倾向,加权均值反映“有影响力的声音”,标准差反映分歧程度。很多同学只算均值,结果发现相关性很弱就放弃了,其实分歧度往往比均值更有信息量——多空吵得越凶,波动越大,这个规律在实证里经常出现。
我一般会同时算三个指标:mean_score(简单均值)、weighted_score(按点赞加权)、std_score(情绪标准差)。在 MATLAB 里用groupsummary一次就能出,前面 2.4 的代码已经覆盖。要注意的是,评论量本身也是一个特征,comment_count突然放大往往对应事件驱动,把它作为控制变量放进回归,能减少遗漏变量偏误。
4.3 涨跌标签的定义:收益率还是方向
“涨跌”这两个字看着简单,定义不同结论可能完全相反。用当日收盘对前一日收盘算收益率,是最常见的做法;用次日开盘对当日收盘算,更贴近“盘前情绪”的假设;用方向标签(涨/跌/平)做分类,则要处理“平”的阈值。我的建议是主分析用连续收益率,辅助分析用方向标签,两者结论一致才敢下判断。
方向标签的阈值不能拍脑袋。A 股有涨跌停限制,日常波动在 1% 以内很常见,把 ±0.5% 以内都算“平”比较合理。阈值定太窄,样本几乎全是涨或跌,分类失去意义;定太宽,“平”占了大半,同样没信息。这个阈值要在报告里写清楚,答辩时被问到能答上来。
4.4 显著性检验与多重比较
跑出相关系数不等于有关系。样本量小的时候,rho=0.2 也可能 p<0.05,但换个时间段就消失了。稳妥的做法是分训练段和验证段:前 70% 交易日估参数,后 30% 看是否稳定。如果两段符号相反,基本可以判定是噪声。
还有一个坑是多重比较。如果你同时试了 5 只股票、3 种情绪口径、2 种收益定义,一共 30 个组合,总有几个“显著”的,这是概率问题不是发现。通常的做法是对 p 值做 Bonferroni 校正,把显著性门槛除以检验次数,或者干脆只报预先设定的那一组主结果,其余作为探索性分析标注清楚。
5. 避坑与常见问题排查:五个最容易翻车的地方
这套链路我前后调过几轮,下面五个问题几乎每次都会遇到,按“现象 → 原因 → 解决”写清楚,照着排查能省不少时间。
5.1 情绪分全是 0 或全是中性
现象:跑完几千条评论,sentiment_score大面积为 0,相关性自然算不出来。原因通常是接口返回结构和你解析的字段对不上,或者 token 失效返回了错误码,而代码里的except把异常吞掉后统一返回了 0。解决:先在analyze_sentiment里打印一次原始响应,确认items结构和字段名;把“接口报错”和“中性情绪”区分开,前者应该抛异常或记日志,不能静默变成 0 分。我一般会加一个失败计数器,失败率超过 5% 就中止任务。
5.2 评论时间和交易日对不齐
现象:MATLAB 里innerjoin之后样本量骤减,或者某些日期情绪分明显异常。原因多半是时区或格式问题——接口返回的时间可能是 UTC,也可能是字符串带毫秒,datetime解析失败后变成NaT,聚合时被静默丢弃。解决:入库前统一转成东八区、统一格式,解析失败的行单独落一张错误表,不要直接丢。对齐前先统计一下NaT的比例,超过 1% 就要回头查采集端。
5.3 重复评论把情绪带偏
现象:某只股票某天的情绪分极端偏高,点进去看是同一段话被转发了上百次。原因是只按正文去重,没考虑转发和复制粘贴。解决:指纹用“正文 + 作者 + 时间窗口”组合,或者对同一作者在短时间内的相似文本只保留一条。更简单的做法是对情绪分做截尾,把当日最高和最低各 1% 去掉再算均值,极端值的影响就压下去了。
5.4 相关性显著但方向反了
现象:算出来情绪越高、次日跌得越多,和直觉相反。先别急着改结论,检查是不是用了“当日收益”而不是“次日收益”——当日收益里已经包含了评论对价格的反向影响(涨了才有人吹),存在内生性。解决:明确用次日收益做因变量,并且把当日收益作为控制变量放进回归,看情绪项的系数是否还显著。如果还是反的,可能是样本期太短,换更长的时间段再看。
5.5 接口额度跑一半没了
现象:批量任务跑到中途大量失败,日志里是额度超限。原因是没做调用量预估,或者重试逻辑在额度耗尽时还在疯狂重试,把剩余额度也耗光。解决:跑之前用总评论数 / 单次批量上限估算调用次数,和日额度对比;重试只针对超时和 5xx,额度类错误(通常是特定错误码)直接中止并告警,不要重试。批量任务做成可断点续跑,额度恢复后从断点继续。
6. 验收与答辩:怎么证明这套方案站得住
验收这套东西,核心不是“代码跑通了”,而是“结论可信、过程可复现、边界说得清”。答辩时老师最爱问的三类问题——数据从哪来、方法为什么这么选、结论有多大把握——都要在验收环节准备好答案。我一般按下面这张表逐项过一遍,每项都有明确的通过标准,不靠感觉。
| 验收项 | 检查内容 | 通过标准 |
|---|---|---|
| 数据完整性 | 各股票评论量、时间覆盖 | 目标区间无超过 3 个交易日的空洞 |
| 去重有效性 | 重复率统计 | 去重后重复率低于 2% |
| 语义覆盖率 | 成功分析占比 | 失败率低于 5%,失败样本有记录 |
| 对齐正确性 | 交易日映射抽查 | 随机抽 20 条人工核对归属日期 |
| 统计稳健性 | 分训练/验证段 | 两段相关系数符号一致 |
| 可复现性 | 配置与版本记录 | 换机器按文档能重跑出同结果 |
表格之外,答辩时有两个技巧很实用。第一,主动交代局限:样本期多长、覆盖多少只股票、接口本身的准确率上限在哪,这些说在前面,比被问出来强。第二,准备一张“反例图”:故意展示一个相关性不显著的股票或时间段,说明你知道结论不是普适的,这反而增加可信度。评审最怕的是一上来就宣称“情绪能预测涨跌”,最欣赏的是“在什么条件下、多大幅度上、有多少把握”。
还有一个容易被忽略的点是复现文档。把settings.yaml的字段含义、每张表的字段说明、MATLAB 脚本的输入输出约定写成一页纸,附在报告附录里。评审翻到这一页,基本就认可你是按工程规范做的,而不是拼凑出来的。
从那以后我每次带这类“文本 + 行情”的交叉分析项目,都强制先跑一遍小样本全链路——几十条评论、三五天数据,从采集到相关系数完整走通,再放大规模。小样本跑通能提前暴露 80% 的格式和对齐问题,比跑到一半才发现字段对不上省太多时间。希望这套结构能帮到你,照着改改就能变成你自己的方案。
本文还有配套的精品资源,点击获取