Python爬虫和百度AI情感分析,如何让股吧评论变成股价信号
简介面向金融数据分析、Python爬虫与自然语言处理方向的读者这套资源把股票评论采集、百度AI情感标注、MATLAB统计建模串联成一个可复现的研究流程。压缩包共10个文件类型覆盖py爬虫与情感分析脚本、m格式的MATLAB处理程序、csv原始评论、xlsx情感与日期结果、png可视化图表整体仅1.71MB轻量易用。已有6461人学习。通过包内代码可以学会用requests和BeautifulSoup抓取东方财富评论调用百度AI接口为评论文本打上情感标签再借助MATLAB清洗数据统计评论情绪与股票涨跌之间的关联并用Excel图表呈现时间走势与情感分布形成从数据抓取、语义分析到统计展示的完整链路。对想快速搭建评论—股价分析原型、掌握跨工具协作的数据分析初学者和金融量化爱好者是一份能少走弯路的完整实操参考。1. 一个把「股吧评论」变成「涨跌信号」的三段式管线如果你做过股票相关的数据分析大概率会撞上同一个困惑网上铺天盖地的评论到底对股价有没有影响有人用词频统计有人用情绪打分结果往往是一团乱麻看着像有规律跑出来全是噪声。这个项目的切入点非常直接——用 Python 爬虫批量抓取股票评论调用百度AI的语义分析接口给每一条评论打上情感标签最后把清洗后的数据丢给 MATLAB 做股价关联性计算。整个流程跑完你能拿到一个可复现的结论评论情绪在什么时间窗口内与涨跌相关性最强哪些股票的特征更明显。它适合谁适合手里已有交易数据、想验证「舆情因子」是否有效的开发者也适合拿它当模板把评论分析能力迁移到其他场景的人。这条链路里每一步都是成熟组件真正的功夫在怎么把这些组件拼起来以及怎么处理拼装过程中的各种坑。2. 数据采集层用 Python 爬虫把评论从网页变成结构化数据2.1 数据源选择与接口探查做股票评论爬虫第一个要决策的不是代码怎么写而是数据从哪拿。常见的数据源有东方财富股吧、雪球、新浪财经个股评论页这个项目选择了东方财富股吧的公开页面原因是它的评论数据通过一个半公开的接口返回 JSON不需要处理复杂的 HTML 解析而且每条评论自带发布时间、用户名、评论内容、点赞数等字段省去了自己写字段提取逻辑的工作。接口的 URL 构造是这个样子的http://gbapi.eastmoney.com/comment/list?appidappid01fields1codefields2title,content,publishtime,likecountsort1pageindex1pagesize50codesh600000这里有几个关键参数需要说明sort1 表示按发布时间倒序pageindex 是页码pagesize 是每页条数这个项目里设置为 50code 是股票代码注意必须带交易所前缀sh 表示上交所sz 表示深交所。这个接口不需要登录请求头里带上一个正常的 User-Agent 就能直接返回 JSON 数据极大降低了爬虫的复杂度。不过要提醒一点这个接口不是官方文档里公开的属于前端页面内部调用的数据接口因此字段名有变化的可能。我在跑这个项目时遇到过一次fields2里的参数名变更导致解析出来的时间字段全是空值最后是抓取页面源码对比字段才发现的。应对办法很简单写代码前先打开浏览器开发者工具切到 Network 面板刷新评论页找到那个返回 JSON 的请求看一遍响应结构再动手写解析逻辑这能省掉后面大量的调试时间。2.2 爬虫主流程设计整个采集模块的思路是先通过一个股票代码列表批量获取每只股票的历史评论再逐页翻取最后统一落盘保存。代码结构分为三层请求层、解析层、存储层。请求层负责构造 URL 和伪装请求头解析层负责提取评论内容及元信息存储层把数据写入 CSV 文件方便后续 MATLAB 读取。import requests import pandas as pd import time from datetime import datetime def fetch_comments(stock_code, page1, page_size50): url http://gbapi.eastmoney.com/comment/list params { appid: appid01, fields1: code, fields2: title,content,publishtime,likecount, sort: 1, pageindex: page, pagesize: page_size, code: stock_code } headers { User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36, Referer: https://guba.eastmoney.com/ } resp requests.get(url, paramsparams, headersheaders, timeout10) resp.raise_for_status() data resp.json() return data.get(result, {}).get(list, []) def crawl_stock(stock_code, max_pages20): all_records [] for page in range(1, max_pages 1): try: items fetch_comments(stock_code, pagepage) if not items: break for item in items: all_records.append({ stock_code: stock_code, title: item.get(title, ), content: item.get(content, ), publish_time: datetime.fromtimestamp(item.get(publishtime, 0)), like_count: item.get(likecount, 0) }) print(f[INFO] {stock_code} 第{page}页累计{len(all_records)}条) time.sleep(1.5) # 控制请求频率避免被限流 except Exception as e: print(f[ERROR] 第{page}页失败: {e}) continue return pd.DataFrame(all_records) if __name__ __main__: stock_list [sh600000, sz000001, sh601318] df_all pd.DataFrame() for code in stock_list: df crawl_stock(code, max_pages30) df_all pd.concat([df_all, df], ignore_indexTrue) df_all.to_csv(stock_comments.csv, indexFalse, encodingutf-8-sig) print(f[DONE] 总共采集 {len(df_all)} 条评论)这段代码里有一个值得注意的细节crawl_stock内部使用了try...except包裹单页请求失败时不会中断整只股票的采集流程而是跳过当前页继续下一页。这个设计不是偷懒而是爬虫场景下非常实用的容错策略——网络抖动、请求超时、接口偶发报错都是常态如果一遇到异常就退出整个采集任务就毁了。实际的抓取任务往往跑在夜间无人值守的情况下每只股票 30 页、每页 50 条单只股票就是 1500 条评论多只股票累计上万条中途挂掉重跑的成本非常高。存储格式选 CSV 有两个原因一是 MATLAB 读取 CSV 是一行命令的事不需要额外安装数据库驱动二是这个项目的数据量在几万条规模远没到需要上数据库的程度用 CSV 反而更直观出了问题可以用 Excel 直接打开排查。注意编码用了utf-8-sig这是为了 Excel 打开时不乱码MATLAB 的readtable也能正确识别。2.3 字段清洗与质量校验评论数据拿到手并不意味着可以马上进入语义分析这里有个经常被忽视的步骤数据清洗。从股吧采集的评论噪音非常大主要问题包括广告内容、无意义的短文本比如只发一个表情或一个标点、重复评论、以及内容为空但点赞数为0的无效记录。清洗逻辑的核心是一句话长度太短的内容没有语义分析价值直接丢弃。百度AI的情感分析接口对纯表情、纯标点这类内容返回的置信度极低留着只会稀释有效信号。def clean_comments(df): # 去除内容为空或纯标点的记录 df df[df[content].str.len() 5] # 去除重复内容保留第一条 df df.drop_duplicates(subset[content]) # 去除广告特征明显的评论含加V 微信 q 等关键词 ad_keywords [加微信, 代操盘, 涨停板, 内部消息, 牛股] for kw in ad_keywords: df df[~df[content].str.contains(kw, naFalse)] return df.reset_index(dropTrue)这段清洗函数里用了三个过滤条件背后的逻辑分别对应三类问题短文本无分析价值、重复评论会拉偏情绪分布、广告会制造虚假的情绪信号。实际使用中你会发现清洗比例通常在 15% 到 30% 之间也就是说一万条原始评论里最后能用的大概七千到八千条。这不是坏现象反而说明数据质量是可控的。清洗完成后建议输出一份统计报告包括每只股票的评论总数、时间跨度、平均评论长度这份报告会在后续与股价数据关联时用来判断数据量是否足以支撑分析结论。如果一只股票只有两百条评论那它计算出来的情绪指标几乎没有任何统计意义。3. 语义分析层百度AI接口的接入与批量处理策略3.1 情感倾向分析接口的调用方式评论数据准备好了接下来是语义分析。这个项目选用的百度AI情感倾向分析接口属于自然语言处理中的文本分类能力每条文本会返回一个情感极性标签0 表示消极、1 表示中性、2 表示积极以及对应的置信度。接口本身不复杂有效的用法才是关键。调用前你需要先注册应用在控制台创建应用后获取 API Key 和 Secret Key然后换取访问令牌。这一步是固定的流程之后每次调用接口都要带上这个令牌令牌有效期大约一个月过期后需要重新获取。import requests import json import pandas as pd API_KEY 你的API_Key SECRET_KEY 你的Secret_Key def get_access_token(): url https://aip.baidubce.com/oauth/2.0/token params { grant_type: client_credentials, client_id: API_KEY, client_secret: SECRET_KEY } resp requests.post(url, paramsparams) return resp.json().get(access_token) def sentiment_analysis(text, access_token): url https://aip.baidubce.com/rpc/2.0/nlp/v1/sentiment_classify headers {Content-Type: application/json} payload { text: text[:200] # 接口限制单条文本长度超过时截断 } params {access_token: access_token} resp requests.post(url, headersheaders, paramsparams, datajson.dumps(payload)) result resp.json() if items in result and result[items]: return result[items][0] return {sentiment: 1, confidence: 0.0, positive_prob: 0.33, negative_prob: 0.33} def batch_analyze(df, access_token): sentiments [] for idx, row in df.iterrows(): result sentiment_analysis(row[content], access_token) sentiments.append({ sentiment: result[sentiment], confidence: result[confidence], positive_prob: result[positive_prob], negative_prob: result[negative_prob] }) if (idx 1) % 100 0: print(f[INFO] 已分析 {idx 1} 条) df_result df.copy() df_result[sentiment] [s[sentiment] for s in sentiments] df_result[confidence] [s[confidence] for s in sentiments] df_result[positive_prob] [s[positive_prob] for s in sentiments] df_result[negative_prob] [s[negative_prob] for s in sentiments] return df_result if __name__ __main__: token get_access_token() df pd.read_csv(stock_comments.csv, encodingutf-8-sig) df df.dropna(subset[content]) df_result batch_analyze(df, token) df_result.to_csv(stock_comments_sentiment.csv, indexFalse, encodingutf-8-sig)这段代码有几个值得拆解的细节。首先是text[:200]这个截断逻辑百度AI的情感分析接口对单条文本有长度上限超出的部分会被截断或者直接报错但评论内容通常不会那么长截断只是防御性写法。其次是return result[items][0]这行接口返回的是一个列表虽然目前只返回一个元素但按列表结构解析是最稳妥的。最后是sentiment_analysis函数内部对异常返回的兜底如果接口返回结构不符合预期返回一个中性情感的默认值避免整个批量任务中断。3.2 批量调用的性能瓶颈与优化如果把上面的代码直接跑一万条评论你会遇到一个非常现实的问题太慢了。每条评论都要发起一次 HTTP 请求平均耗时在 0.5 到 1 秒之间一万条就是一两个小时而且这还是理想状态。实际跑起来会出现两类问题一是请求频率过高触发接口限流返回错误码二是网络超时导致部分评论没有分析结果。针对限流问题百度AI的接口有 QPS 限制默认每秒只能处理几个请求。常见的做法是在请求之间加入延时控制下面的代码用了time.sleep(0.1)把请求频率控制在每 100 毫秒一个这样一万条评论大约需要 15 到 20 分钟虽然还是慢但至少稳定。# 批量分析时的限速控制 import time def batch_analyze_rate_limited(df, access_token): results [] for idx, row in df.iterrows(): retry 0 while retry 3: # 最多重试3次 try: result sentiment_analysis(row[content], access_token) results.append(result) break except Exception as e: retry 1 if retry 3: print(f[ERROR] 第{idx}条失败: {e}) results.append({sentiment: 1, confidence: 0.0}) time.sleep(2) # 失败后等待更长时间再重试 time.sleep(0.1) # 控制请求频率 if (idx 1) % 500 0: print(f[INFO] 进度{idx 1}/{len(df)}) return pd.DataFrame(results)这段代码把原来的循环重构成了带重试机制的版本核心变化是内层while retry 3的循环。它的作用是当单条请求失败时不会直接跳过而是重试最多三次每次失败后等待两秒再重试给了网络恢复的时间。这个机制让我在跑大批量数据时免去了反复人工重启的痛苦。实际项目里我还建议把分析结果每 1000 条增量保存一次到磁盘这样即使中途程序崩了已经分析完的结果不会丢重启后把已分析的评论过滤掉继续跑就行。这个习惯在长时间运行的脚本里价值极大。3.3 情绪聚合与时间窗口对应语义分析的结果还是逐条的要把它变成能与股价走势对齐的信号需要做聚合处理。聚合的核心逻辑是把分散在时间轴上的评论情绪按一定粒度聚合成情绪指标这个粒度就是后续分析的时间窗口。最常用的聚合方式是按天聚合。把某只股票一天内的所有评论情绪值加权平均权重的选择有两种流派一种是等权平均简单直接另一种是按点赞数加权认为高赞评论更有影响力。这个项目提供参数让你切换默认使用按点赞数加权的方式因为真实场景中一条获得大量点赞的评论对市场情绪的影响远超普通评论。def aggregate_daily(df): df[date] pd.to_datetime(df[publish_time]).dt.date grouped df.groupby([stock_code, date]).apply( lambda x: pd.Series({ sentiment_mean: (x[sentiment].astype(float) * (x[like_count] 1)).sum() / (x[like_count] 1).sum(), positive_ratio: (x[sentiment] 2).mean(), negative_ratio: (x[sentiment] 0).mean(), comment_count: len(x) }) ).reset_index() return grouped聚合结果是一个包含日期、个股代码、情感均值、积极比例、消极比例、评论数量的数据表。其中sentiment_mean的范围在 0 到 2 之间1 表示中性大于 1 偏积极小于 1 偏消极。这个指标就是后续 MATLAB 分析时与涨跌幅做相关的核心变量。4. MATLAB 数据处理层从 CSV 到相关性结论4.1 数据导入与时间对齐到了 MATLAB 这一步数据源就变成了两个文件stock_comments_sentiment.csv评论情绪聚合结果和股价历史数据用 MATLAB 的readtable从 CSV 或者直接从 MATLAB 内置的财经工具箱接口拉取。这里有一个关键问题评论数据的时间粒度是天股价数据也是天但两者可能存在交易日和非交易日的差异。评论在周六周日是有人发的但股市不开盘所以这些评论按天聚出来后没有对应的股价数据。处理方式是直接过滤掉非交易日的评论数据或者把周五和周末的评论合并到周一。这个项目采用的策略是简单过滤因为周末评论量本身就小直接剔除对结果影响不大。% 导入评论情绪数据 sentiment_data readtable(stock_comments_sentiment.csv); % 导入股价数据假设csv中包含日期和收盘价字段 price_data readtable(stock_prices.csv); % 转换日期格式 sentiment_data.date datetime(sentiment_data.date, InputFormat, yyyy-MM-dd); price_data.date datetime(price_data.date, InputFormat, yyyy-MM-dd); % 按日期内连接对齐到交易日 merged_data innerjoin(sentiment_data, price_data, Keys, date); % 过滤没有评论的交易日 merged_data merged_data(~isnan(merged_data.sentiment_mean), :);innerjoin在这里非常关键它的功能是取两个数据表的交集只保留两边同时存在的日期。这能避免一种尴尬情况你拿着某一天的评论情绪去对比当天的股价数据结果那天是周末股价是上一个交易日的对不上。4.2 相关性与领先滞后分析核心分析目标很明确验证评论情绪和涨跌之间到底有没有关系并且找出这种关系在时间上的方向性——评论是领先于股价变动还是同步还是滞后。领先意味着评论能作为预测因子同步意味着只能做解释滞后则说明评论只是股价的影子和回声。% 计算情绪均值与涨跌幅的皮尔逊相关系数 returns [0; diff(log(price_data.close))]; % 计算对数收益率 sentiment_mean merged_data.sentiment_mean; % 对齐长度 min_len min(length(returns), length(sentiment_mean)); returns returns(1:min_len); sentiment_mean sentiment_mean(1:min_len); % 同步相关性 [R_sync, P_sync] corrcoef(sentiment_mean, returns); % 计算领先相关用前一天的评论情绪对应当天的涨跌幅 sentiment_lag1 sentiment_mean(1:end-1); returns_lag1 returns(2:end); [R_lag1, P_lag1] corrcoef(sentiment_lag1, returns_lag1); % 输出结果 fprintf(同步相关系数%.4f, p值%.4f\n, R_sync(1,2), P_sync(1,2)); fprintf(领先1天相关系数%.4f, p值%.4f\n, R_lag1(1,2), P_lag1(1,2));这段代码的核心思想是滑动时间窗对齐。同步相关就是把同一日的情绪和同一日的涨跌幅对应起来领先1天相关则是把前一天的评论情绪和当天的涨跌幅对齐。如果领先1天的相关系数显著大于同步相关系数说明评论情绪确实具有一定的先行指标特征。注意这里有个常见的统计陷阱股价数据不是独立样本它带有自相关性所以对照组必须做随机化检验。怎么检验呢把评论情绪的时间序列随机打乱 1000 次再重新与涨跌幅计算相关得到相关性的随机分布。如果原始相关性落在随机分布的 95% 置信区间之外那才说明结果不是碰巧出来的。% 随机置换检验 num_perm 1000; perm_corr zeros(num_perm, 1); for i 1:num_perm perm_order randperm(length(sentiment_lag1)); perm_corr(i) corr(sentiment_lag1(perm_order), returns_lag1); end p_value mean(abs(perm_corr) abs(R_lag1(1,2))); fprintf(置换检验p值%.4f\n, p_value);置换检验的写法很简单但它的意义重大。你算出来的相关系数可能只有 0.1 左右看着不显著但如果置换检验显示这样的相关系数在随机情况下出现的概率低于 5%那它依然是有效的信号。反之如果你算出来一个 0.5 的相关系数但置换检验说不显著那大概率是数据量太少或者样本中有几个异常点拉高了相关系数要回到数据检查。4.3 结果的可视化输出分析结果光有数值还不够可视化能让人一眼看出情绪与涨跌是否存在肉眼可见的联动。MATLAB 在可视化方面几乎是零成本就能出图。% 绘制情绪均值与收盘价的双轴图 figure; yyaxis left plot(merged_data.date, merged_data.sentiment_mean, b-, LineWidth, 1.5); ylabel(情感均值); yyaxis right plot(merged_data.date, merged_data.close, r-, LineWidth, 1.5); ylabel(收盘价); xlabel(日期); legend({情感均值, 收盘价}); saveas(gcf, sentiment_vs_price.png);这张图能直观地展示一个现象情感均值的波动是否与股价走势存在对应关系。值得注意的是看了图之后不要急着下结论视觉上的「看起来有关系」往往经不住统计检验这也是我在这个项目里学到的血泪经验——有一组数据画出来曲线几乎同步相关系数只有 0.03完全经不起检验。视觉判断是真的会骗人的。5. 避坑指南跑通这条链路必须知道的 5 个问题这个项目涉及的环节多每个环节都有典型的坑。我把实际运行中撞过的几个问题列出来虽然看起来零碎但每一条都让人浪费过至少半天时间。问题1爬虫跑着跑着突然所有请求都返回 403原因请求频率过高目标服务器的反爬机制把 IP 临时封禁了。现象不是立刻发生而是累计请求到一定数量后突然失效。解决请求之间加入固定延时随机化不要用固定值。把time.sleep(1.5)改成time.sleep(random.uniform(1.0, 2.0))。另外设置请求失败后自动暂停 60 秒再继续给封禁时间窗口一个自然降温的机会。问题2百度AI接口返回error_code18提示 QPS 超限原因并发请求数超过接口上限免费配额通常只有每秒 2 个请求。如果你在代码里用了多线程加速撞限流的概率会急剧放大。解决放弃多线程回到单线程加固定 sleep。如果实在要加速去控制台申请提高 QPS 配额。对于这个项目的数据量来说单线程完全能接受没必要冒险。问题3MATLAB 读 CSV 后日期列变成一串数字而不是日期格式原因MATLAB 的readtable默认把日期列按文本读取而 CSV 里如果是2024-01-15这样的格式它会被识别成字符串转换成datetime时如果不指定InputFormat解析就会出问题。解决readtable读取时使用setvaropts指定日期列的格式或者在读取后用datetime(string_column, InputFormat, yyyy-MM-dd)显式转换。这里的教训是CSV 导出时日期格式一定要统一最好用 ISO 格式%Y-%m-%d避免出现 1 月 2 日被解析成 1 月 2 日还是 2 月 1 日的混乱。问题4情绪均值几乎恒等于 1看不出任何波动原因数据中包含大量中性评论也就是情感分析返回 sentiment1 的文本充塞了样本。股吧里大量评论是「打卡」「路过」「学习一下」这类无实质内容的短文本它们清洗时因为长度超过阈值而存活下来拉平了整体情绪值。解决在清洗阶段增加一个过滤条件剔除含「打卡」「路过」「签到」等典型中性词的评论或者更简单粗暴的将清洗长度阈值从 5 提到 10。实话说纯中性评论挤占样本是常见问题过滤后的情绪分布会更有区分度。问题5领先相关性分析结果不稳定换一只股票结果就大变原因单只股票的评论数量太少计算出的情绪均值标准差大导致相关系数置信区间过宽。这是小样本的天然问题不是代码写错了。解决设定最低评论量阈值。在聚合阶段过滤掉当日评论数少于 5 条的数据点这类数据点不可靠。同时分析结果按板块或行业合并计算行业层面的情绪信号通常比单只股票稳定得多。6. 二次开发把相关性变成可用的回调信号如果你不满足于「算出一个相关系数」那这套管线最自然的升级方向是把这个相关性结果转化成一个可回测的交易信号。具体来说需要做三件事构建一个情绪打分指标、定义信号生成规则、做历史回测统计胜率和盈亏比。先看情绪打分指标怎么升级。前面聚合得到的是一个 0 到 2 之间的均值这个数值不能直接用因为不同股票的情绪分布尺度不一样。有的股票评论区一直偏乐观均值长期在 1.4 附近有的股票评论区冷清情绪均值在 0.9 到 1.1 之间波动。直接拿原始均值做横向比较没有意义需要做标准化。标准化方式就看给定股票近 N 日情绪均值的 z-score# 将情绪均值转为z-score信号 import numpy as np import pandas as pd def add_zscore_signal(df, window20): df df.sort_values([stock_code, date]).reset_index(dropTrue) # 按股票分组计算滚动均值与标准差 grouped df.groupby(stock_code)[sentiment_mean] df[ma_sentiment] grouped.transform(lambda x: x.rolling(window, min_periods5).mean()) df[std_sentiment] grouped.transform(lambda x: x.rolling(window, min_periods5).std()) df[zscore] (df[sentiment_mean] - df[ma_sentiment]) / df[std_sentiment].replace(0, np.nan) return df.dropna(subset[zscore])z-score 大于 1.5 表示情绪显著偏高小于 -1.5 表示情绪显著偏低。信号生成规则可以是Z 值从低于 -1 回升穿过 -0.5 时发出买入信号Z 值从高于 1 回落穿过 0.5 时发出卖出信号。这本质上是均值回归的思路它的隐含假设是情绪极端化后会向均值回归而回归的方向会被股价走势提前兑现。回测时把信号与股价数据对齐计算每次信号发出后未来 1 天、3 天、5 天的累计收益。回测代码的骨架是这样的def backtest_signal(df, price_data, hold_days3): results [] for idx, row in df.iterrows(): if row[signal] 0: continue # 找到对应股票的后续股价 future_prices price_data[ (price_data[stock_code] row[stock_code]) (price_data[date] row[date]) ].sort_values(date) future_prices future_prices.iloc[:hold_days] if len(future_prices) hold_days: continue entry_price future_prices.iloc[0][close] exit_price future_prices.iloc[-1][close] ret (exit_price - entry_price) / entry_price results.append({ signal_date: row[date], stock_code: row[stock_code], signal: row[signal], return: ret }) result_df pd.DataFrame(results) return result_df回测结果输出后看一眼两个核心指标胜率收益大于 0 的信号占比和平均收益所有信号收益的均值。如果胜率在 60% 以上、平均收益为正值且经过了足够多的信号样本量验证那这套舆情因子就有实际参考价值了。回过头来复盘整个项目时我认为它最有价值的不是代码本身而是那条完整的数据处理链路从公开网页到爬虫从爬虫到结构化数据从结构化数据到 AI 语义分析从语义分析到统计验证每一步的决策都决定了最终结论的可信度。从那以后我每做一个类似的数据分析项目都会强制走一遍「数据质量检查 → 指标构建 → 随机性检验 → 可视化确认」这套流程不跳过任何一环因为跳过的环节会在最后用最难看的方式报复你。希望帮到你。本文还有配套的精品资源点击获取