
简介这份资源面向对量化分析、自然语言处理与金融数据挖掘感兴趣的学习者提供一套从数据采集到结论呈现的完整实践素材用于探究股票评论情绪与股价涨跌之间的潜在关联。压缩包共10个文件约1.71MB包含4个png图表、2个xlsx数据表、2个py脚本、1个m脚本和1个csv评论数据分别对应可视化结果、情感与日期数据、爬虫及评论分析代码、MATLAB时间序列处理和原始评论样本。已有6466人学习下载说明该案例在爬虫与金融文本分析方向具有一定参考价值。读者可据此了解如何用Python抓取股票评论、调用百度AI完成情感倾向标注再借助MATLAB进行统计建模与关联分析并通过图表直观呈现评论情绪分布与股价走势的对应关系适合作为课程设计或小型研究项目的起步模板。1. 从一份“评论情绪 vs 涨跌曲线”的对照实验说起有段时间我特别想验证一个朴素的问题股吧里那些情绪化的评论到底和股价涨跌有没有可量化的关系这个项目就是围绕这个疑问搭起来的一条完整链路——用 Python 爬虫抓取股票评论调用百度 AI 开放平台的语义分析接口给每条评论打情绪分再把评论情绪和行情数据一起丢进 MATLAB 做数据处理和可视化最后看情绪曲线和涨跌曲线之间到底有没有相关性。它适合两类人一类是想学爬虫 第三方 AI 接口 数据分析全链路串起来的开发者另一类是想拿真实文本数据练手情感分析和时间序列对照的读者。整条链路不长但每一环都有坑下面按“能干什么 → 怎么跑 → 怎么改”拆开讲。2. 技术栈与整体架构三语言分工的流水线设计2.1 为什么是 Python 百度 AI MATLAB 三段式这个项目最反直觉的地方在于它没有强行统一语言。爬虫和接口调用用 Python是因为 requests、BeautifulSoup、pandas 这套生态抓页面、清洗文本、批量调 HTTP 接口最顺手语义分析不自己训模型直接调百度 AI 的情感倾向分析接口省掉标注数据和训练成本数据处理和画图交给 MATLAB是因为做时间序列对齐、相关性计算、多子图对照时MATLAB 的矩阵操作和绘图确实比手写 matplotlib 少写不少代码。三段式带来的代价是数据要在三种环境间流转所以整个项目的核心设计其实是一个“文件契约”Python 端负责产出结构化的 CSVMATLAB 端只认这个 CSV 的列名和格式。只要契约稳定两端可以各自独立开发和替换。常见做法是把中间产物统一成 UTF-8 编码的 CSV列名固定为日期、股票代码、评论数、平均情绪分、收盘价、涨跌幅这几列避免中文列名在跨工具读取时出现编码翻车。从数据流看整条流水线是这样的目标页面 → 爬虫抓取原始评论 → 文本清洗去重 → 百度 AI 情感打分 → 按「日期 股票代码」聚合 → 输出 CSV → MATLAB 读取对齐 → 相关性分析与绘图这个结构的好处是每一段都能单独跑、单独验证。爬虫挂了不影响已有数据做分析MATLAB 脚本改图也不用来回重爬。血泪经验是一定要先把 CSV 契约定死再写代码否则后面两端字段对不上排查起来就是黑匣子。2.2 目录结构与模块职责一个能跑起来的工程目录我一般会这样组织让每个环节的输入输出一目了然stock-sentiment/ ├── crawler/ │ ├── fetch_comments.py # 抓取评论落原始 JSON │ ├── clean_text.py # 去重、去表情、去广告 │ └── config.py # 目标股票、页码、请求头、延时 ├── ai/ │ └── sentiment.py # 调百度 AI批量打分带重试 ├── pipeline/ │ └── build_dataset.py # 聚合评论 合并行情产出最终 CSV ├── data/ │ ├── raw/ # 原始评论 JSON │ ├── market/ # 行情 CSV收盘价、涨跌幅 │ └── output/ # 最终分析用 CSV └── matlab/ ├── load_and_align.m # 读取 CSV、日期对齐、缺失值处理 ├── correlation.m # 情绪与涨跌相关性计算 └── plot_compare.m # 双轴对照图职责划分的关键点crawler只负责拿到文本不碰打分ai只负责把文本变成分数不关心文本从哪来pipeline是唯一的“汇合点”所有字段在这里对齐。这样任何一段出问题看它自己的输出文件就能定位不用整条链路重跑。2.3 数据契约CSV 字段设计中间 CSV 是整个项目的枢纽字段设计要提前想清楚不然后面 MATLAB 里做对齐会很难受。我用的字段如下字段名类型说明trade_dateYYYY-MM-DD交易日聚合主键之一stock_codestring股票代码聚合主键之一comment_countint当日有效评论条数avg_sentimentfloat当日评论平均情绪分范围约 0~1pos_ratiofloat正面评论占比neg_ratiofloat负面评论占比close_pricefloat当日收盘价pct_changefloat当日涨跌幅%avg_sentiment用百度接口返回的正向概率做加权pos_ratio和neg_ratio是辅助指标因为平均值会被极端评论拉偏占比更稳。注意日期一定要统一成YYYY-MM-DD字符串MATLAB 的readtable对日期格式敏感格式不统一会读成文本导致后续对齐失败。3. 爬虫与语义分析从页面到情绪分的落地实现3.1 评论抓取请求构造与反爬处理抓评论的核心不是解析而是“稳定地拿到页面”。目标页面通常是分页的列表页每页若干条评论翻页参数在 URL 或请求体里。我一般把目标股票、起始页、结束页、请求间隔都放进config.py方便换标的时只改配置。# crawler/fetch_comments.py import requests, time, json, random from config import HEADERS, BASE_URL, STOCK_CODE, START_PAGE, END_PAGE, DELAY_RANGE def fetch_page(page): params {code: STOCK_CODE, page: page, pageSize: 50} resp requests.get(BASE_URL, paramsparams, headersHEADERS, timeout10) resp.raise_for_status() return resp.json() # 假设接口返回 JSON若是 HTML 则改用 BeautifulSoup 解析 def crawl_all(): all_comments [] for page in range(START_PAGE, END_PAGE 1): try: data fetch_page(page) items data.get(data, {}).get(list, []) if not items: break # 没有更多数据提前结束 all_comments.extend(items) except Exception as e: print(fpage {page} failed: {e}) time.sleep(random.uniform(*DELAY_RANGE)) # 随机延时降低被封风险 with open(fdata/raw/{STOCK_CODE}.json, w, encodingutf-8) as f: json.dump(all_comments, f, ensure_asciiFalse, indent2) if __name__ __main__: crawl_all()逻辑说明fetch_page只做单页请求参数里page控制翻页pageSize控制每页条数crawl_all负责循环、异常兜底和落盘。DELAY_RANGE建议设成 1~3 秒的随机区间固定间隔反而容易被识别。raise_for_status()让 4xx/5xx 直接抛异常配合 try 保证单页失败不中断整体。参数上START_PAGE/END_PAGE决定抓取深度STOCK_CODE决定标的换股票只改这两个。如果目标页面是 HTML 而不是 JSON 接口解析部分换成 BeautifulSoup 即可选择器要按实际 DOM 结构调整这里不硬编码具体 class 名因为页面结构会变写死选择器是后期维护的主要痛点。3.2 文本清洗去噪比抓取更影响结果原始评论里混着大量表情符号、重复刷屏、广告链接和纯数字这些直接送进情感接口会污染分数。清洗环节我一般做四件事去 HTML 标签、去 URL、去连续重复字符、按文本去重。# crawler/clean_text.py import re URL_RE re.compile(rhttps?://\S) HTML_RE re.compile(r[^]) REPEAT_RE re.compile(r(.)\1{3,}) # 连续重复 4 次以上压缩 def clean(text): if not text: return text HTML_RE.sub(, text) text URL_RE.sub(, text) text REPEAT_RE.sub(r\1, text) text re.sub(r\s, , text).strip() return text def dedup(comments): seen, result set(), [] for c in comments: t clean(c.get(content, )) if len(t) 5: # 太短的评论信息量低丢弃 continue if t in seen: continue seen.add(t) result.append({**c, content: t}) return result逻辑说明clean按顺序去标签、去链接、压缩重复字符、归一空白dedup用集合做精确去重同时过滤长度小于 5 的短文本。参数上len(t) 5这个阈值可以按数据情况调评论普遍很短时可以降到 3。注意去重是精确匹配刷屏变体加空格、换同音字去不掉这是这套方案的已知局限后面在改造章节会讲怎么补。3.3 调用百度 AI 情感分析批量、限流与重试百度 AI 的情感倾向分析接口输入一段文本返回正向、负向、中性三个概率。批量调用时最需要处理的是 QPS 限制和偶发超时所以封装一个带重试和限流的调用函数是必须的。# ai/sentiment.py import time, requests from config import BAIDU_TOKEN, SENTIMENT_URL def score_one(text, retries3): payload {text: text[:500]} # 接口对长度有限制截断保护 for i in range(retries): try: resp requests.post( SENTIMENT_URL, params{access_token: BAIDU_TOKEN}, datapayload, timeout5, ) resp.raise_for_status() items resp.json().get(items, []) if items: return items[0].get(positive_prob, 0.5) except Exception as e: print(fretry {i}: {e}) time.sleep(1.5 * (i 1)) # 退避重试 return None # 多次失败返回 None由上层决定丢弃或填默认值 def score_batch(comments, qps2): results [] interval 1.0 / qps for c in comments: s score_one(c[content]) if s is not None: results.append({**c, sentiment: s}) time.sleep(interval) # 主动限流避免触发配额限制 return results逻辑说明score_one负责单条打分text[:500]是长度保护retries控制重试次数退避时间随重试次数递增score_batch用qps参数控制调用频率interval 1.0 / qps保证每秒不超过设定次数。参数上qps要按账号实际配额设设高了会连续报错positive_prob作为情绪分范围 0~1越接近 1 越正面。返回None的条目在上层聚合时直接跳过不要用 0.5 填充否则会把缺失伪装成中性污染均值。3.4 聚合与行情合并产出最终 CSV打分完成后要按“日期 股票代码”聚合再和行情数据按日期左连接。这一步是整条链路的汇合点字段对齐错了后面全白做。# pipeline/build_dataset.py import pandas as pd def build(comment_scores, market_csv, out_csv): df pd.DataFrame(comment_scores) df[trade_date] pd.to_datetime(df[create_time]).dt.strftime(%Y-%m-%d) agg df.groupby(trade_date).agg( comment_count(sentiment, size), avg_sentiment(sentiment, mean), pos_ratio(sentiment, lambda s: (s 0.6).mean()), neg_ratio(sentiment, lambda s: (s 0.4).mean()), ).reset_index() market pd.read_csv(market_csv, parse_dates[trade_date]) market[trade_date] market[trade_date].dt.strftime(%Y-%m-%d) merged pd.merge(agg, market, ontrade_date, howinner) merged.to_csv(out_csv, indexFalse, encodingutf-8-sig) return merged逻辑说明先把评论时间统一成日期字符串再按日期聚合出评论数、平均情绪、正负占比行情表读入后同样统一日期格式最后用inner join只保留两边都有的交易日。howinner是刻意的评论缺失或行情缺失的日子不参与相关性计算避免用插值制造虚假相关。encodingutf-8-sig是为了让 Excel 和 MATLAB 都能正确识别中文列名这个细节不注意打开就是乱码。4. MATLAB 数据处理对齐、相关性与可视化4.1 读取与日期对齐MATLAB 端的第一件事是把 CSV 读进来并把日期转成datetime然后按交易日排序、处理缺失。readtable默认会把日期列读成文本或datetime取决于格式所以显式指定更稳。% matlab/load_and_align.m T readtable(data/output/dataset.csv, TextType, string); T.trade_date datetime(T.trade_date, InputFormat, yyyy-MM-dd); T sortrows(T, trade_date); % 缺失值检查情绪列缺失用前值填充行情缺失直接剔除 T.avg_sentiment fillmissing(T.avg_sentiment, previous); T rmmissing(T, DataVariables, {close_price, pct_change}); % 计算情绪变化率和涨跌幅做同量纲对照 T.sentiment_chg [NaN; diff(T.avg_sentiment)];逻辑说明datetime指定InputFormat避免格式猜测出错fillmissing用前值填充情绪缺失因为情绪是连续变量前值填充比均值填充更贴近真实rmmissing剔除行情缺失行保证相关性计算样本干净。diff算情绪日变化和pct_change都是“日度变化”量纲可比这是做对照的前提。4.2 相关性计算别只看一个相关系数情绪和涨跌的关系不能只算一个 Pearson 系数就下结论我一般同时看同期相关和滞后相关判断情绪是领先还是同步。% matlab/correlation.m x T.sentiment_chg; y T.pct_change; valid ~isnan(x) ~isnan(y); x x(valid); y y(valid); r_same corr(x, y, Type, Pearson); r_spear corr(x, y, Type, Spearman); % 滞后 1~3 天情绪是否领先涨跌 lag_r zeros(1, 3); for k 1:3 lag_r(k) corr(x(1:end-k), y(1k:end), Type, Spearman); end fprintf(同期 Pearson%.3f, Spearman%.3f\n, r_same, r_spear); fprintf(滞后1~3天 Spearman: %.3f %.3f %.3f\n, lag_r);逻辑说明Pearson 对极端值敏感Spearman 基于秩两者一起看能判断关系是否被少数异常日带偏滞后相关用x(1:end-k)对y(1k:end)检验“今天的情绪能否预示后面几天的涨跌”。参数上滞后阶数 3 是经验值数据跨度长可以加大。注意相关系数绝对值低于 0.1 基本可以认为没有线性关系别过度解读。4.3 双轴对照图让关系一眼可见数字之外图更能说明问题。把情绪均值和收盘价画在双轴上再单独画一张情绪变化 vs 涨跌幅的散点图基本能看出有没有结构性关系。% matlab/plot_compare.m figure(Position, [100 100 900 400]); yyaxis left plot(T.trade_date, T.avg_sentiment, -o, LineWidth, 1.2); ylabel(平均情绪分); yyaxis right plot(T.trade_date, T.close_price, -s, LineWidth, 1.2); ylabel(收盘价); title(评论情绪与收盘价对照); grid on; figure; scatter(T.sentiment_chg, T.pct_change, 20, filled); xlabel(情绪日变化); ylabel(涨跌幅(%)); title(情绪变化 vs 涨跌幅); grid on;逻辑说明yyaxis让两个量纲不同的序列共享横轴、各用一侧纵轴避免量纲差异把其中一条压成直线散点图用来观察是否存在非线性或分簇关系比折线更直观。绘图前确保T已排序否则折线会来回穿插。5. 避坑与常见问题这条链路上最容易翻车的地方5.1 评论时间跨时区或格式混乱聚合全错现象聚合出来的每日评论数明显偏少或者某些日期凭空多出一大堆。原因原始评论时间有的是时间戳、有的是带时区的字符串pd.to_datetime解析后日期偏移一天。解决在清洗阶段统一转成 UTC 再转目标时区最后只取日期部分解析时显式传format或unit不要依赖自动推断。5.2 情感接口返回空 items均值被悄悄拉低现象某些天avg_sentiment异常低但评论内容看起来并不负面。原因接口对超长文本或特殊字符返回空items代码里用默认值填充把缺失当成了低分。解决score_one返回None并在聚合时dropna同时记录失败条数失败率超过阈值就停下来检查文本清洗是否漏了脏数据。5.3 CSV 中文列名在 MATLAB 里变乱码现象readtable读进来列名是乱码按列名取值全部报错。原因Python 端用utf-8写MATLAB 默认编码不匹配。解决Python 端统一encodingutf-8-sigMATLAB 端读取时确认系统区域设置或者干脆把列名改成英文从源头绕开编码问题这是最省心的做法。5.4 相关性算出来很高其实是共同趋势导致的伪相关现象情绪和收盘价的相关系数很高但散点图看不出关系。原因两条序列都有时间趋势直接算相关会把趋势当成关系。解决先对两条序列做差分用日变化而不是绝对值或者做去趋势处理再算相关同时看 Spearman 和滞后相关交叉验证别只信一个数。5.5 爬虫跑一半被封重跑又从第一页开始现象抓到几百页后请求全部失败重跑重复抓已抓过的页。原因没有断点续爬机制也没有失败页记录。解决每抓完一页就把页码写进进度文件启动时读进度跳过已完成页失败页单独记录最后统一补抓。这个改动不大但能省掉大量重复请求。6. 二次开发把这条链路改成你自己的实验台这套东西真正的价值不在“跑出某个结论”而在于它是一个可替换、可扩展的实验骨架。想换成别的标的、别的平台评论、别的情感接口改动点都很集中。第一个改造方向是换数据源。crawler/fetch_comments.py里fetch_page是唯一和具体页面绑定的函数换平台只需要重写它保持返回结构一致每条含content和create_time下游清洗、打分、聚合全部不用动。我一般会先写一个只抓一页的调试脚本确认字段名和分页逻辑再放开循环避免一上来就跑几百页才发现解析错了。第二个方向是换情感分析方案。ai/sentiment.py的score_one是接口适配层返回 0~1 的正面概率即可。想换成本地模型把函数体换成模型推理保持返回契约不变score_batch的限流逻辑可以保留也可以去掉。这里有个经验本地模型批量推理时限流反而拖慢速度应该改成批处理一次喂一批文本。第三个方向是加特征。现在只有情绪均值和正负占比可以加评论量变化率、情绪波动率标准差、极端评论占比然后在 MATLAB 里做多因子对照。加特征时记住一个原则新特征必须在build_dataset.py里算好写进 CSV不要让 MATLAB 去猜保持“Python 产出、MATLAB 消费”的单向依赖。验证改造是否成功我习惯用一个小样本回归测试固定抓 3 页评论、固定一批文本跑完整条链路比对输出的 CSV 行数和关键列数值是否在预期范围。这个测试脚本我一般放在tests/下改任何一环都先跑它。改造点改动文件需要保持的契约换评论平台fetch_comments.py返回含 content、create_time 的列表换情感接口sentiment.pyscore_one 返回 0~1 浮点或 None加新特征build_dataset.py新列写入最终 CSV换分析维度matlab/*.m只读 CSV不改上游最后说个我踩过的坑早期我图省事把清洗逻辑写在爬虫循环里结果想单独重洗已有数据时只能重新爬一遍。从那以后我每次做这类多段流水线都强制让每一段只读上一段的落盘文件、只写自己的输出文件中间结果全部留痕。这样任何一段想重跑、想替换、想对比新旧版本都不用动其他部分。希望这套骨架能帮到你把它改成你自己的实验台。本文还有配套的精品资源点击获取