news 2026/10/24 4:52:19

Python爬虫+大模型+MATLAB:股票评论情绪分析与股价相关性实战

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
Python爬虫+大模型+MATLAB:股票评论情绪分析与股价相关性实战

1. 项目缘起与整体设计思路

最早动这个念头,是因为身边做量化研究的朋友总在抱怨一件事:每天收盘后翻遍各大财经社区的评论区,想看看散户情绪到底偏多还是偏空,结果翻了两个小时,眼睛都花了,最后还是凭感觉拍脑袋。我当时就想,这套流程完全可以用代码串起来——评论区的文本抓下来,交给模型打分,再把分数和行情数据做相关性计算,最后出一张能直接放进周报的图。整个链条听起来环节不少,但拆开看每一步都不复杂,难的是把它们顺畅地接在一起。

这个项目的核心目标很明确:把非结构化的股票评论文字,转化成可量化、可统计、可验证的情绪指标。具体来说,输入是某只股票在财经社区下的评论文本,输出是一张情绪得分与股价走势的对照图,外加一份相关系数表。适合谁来参考?我认为有三类人比较对口:一是做量化研究但懒得自己搭文本处理管线的朋友,二是想给自己的交易策略加一个"情绪因子"的独立开发者,三是单纯想练手"爬虫+AI+数据分析"全链路的学生或转行者。哪怕你只对其中某一个环节感兴趣,也可以把对应部分单独拆出来用。

为什么选这条技术路线?我对比过几种方案。纯人工标注情绪当然最准,但一天几百上千条评论,人力成本扛不住,而且主观偏差大。用传统词典法(比如基于情感词典匹配正负词)速度快,但股票评论里充斥着"利好出尽""利空落地""这票要起飞"这类反讽和黑话,词典法误判率很高。所以最终我选了大模型判分作为核心环节,它在理解语境和反讽上明显更靠谱。至于相关性计算用MATLAB、出图用Excel,纯粹是因为目标用户群体里这两样工具普及率最高,交付出去别人能直接改,不用再装一堆环境。

整个流程我把它拆成四个阶段,用一张表先理清楚每个阶段干什么、用什么工具、产出什么:

阶段核心任务主要工具产出物
数据采集抓取股票评论区文本Python爬虫原始评论CSV
情感判分逐条评论打情绪分大模型API带分数的评论表
相关性计算情绪分与行情做相关MATLAB相关系数矩阵
可视化出图生成对照图表Excel走势对照图

这个拆法的好处是每个阶段解耦。爬虫挂了不影响判分,判分换模型也不用动后面的代码。我踩过的坑是:一开始图省事把所有逻辑写在一个脚本里,结果调试的时候改一处崩三处,后来老老实实按阶段拆成独立模块,维护成本立刻降下来。

提示:阶段解耦不只是代码组织问题,更重要的是数据格式要提前约定好。我在第一阶段就固定了输出CSV的列名和日期格式,后面三个阶段全部按这个约定来读,省了无数对齐字段的时间。

2. 数据采集环节的实操要点

2.1 目标页面分析与请求构造

采集这一步,核心是搞清楚评论数据到底藏在哪。财经社区的评论通常有两种加载方式:一种是服务端渲染,评论直接写在HTML里,用常规的解析库就能抠出来;另一种是前端异步加载,评论通过接口返回JSON,你得找到那个接口地址。我遇到的大部分社区属于后者,所以思路是先抓接口,再解析JSON,比硬啃HTML稳得多。

具体操作上,打开浏览器的开发者工具,切到网络面板,筛选XHR或Fetch请求,然后手动翻一页评论,观察哪个请求的响应里带着评论内容。找到之后,重点看三样东西:请求URL、请求方法(GET还是POST)、以及请求头里有没有必须带的字段(比如Referer、User-Agent、某些自定义token)。把这些原样复制到代码里,基本就能复现请求了。

请求构造我一般用requests库,配合一个会话对象保持连接。下面是一个通用的请求骨架,参数需要你按实际接口替换:

import requests import time import random session = requests.Session() session.headers.update({ "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36", "Referer": "https://example-community.com/stock/000001", "Accept": "application/json, text/plain, */*" }) def fetch_comments(stock_code, page): url = "https://example-community.com/api/comments" params = { "code": stock_code, "page": page, "page_size": 50, "sort": "time" } resp = session.get(url, params=params, timeout=10) resp.raise_for_status() return resp.json()

这里有几个细节值得说。分页参数一定要先手动翻几页确认清楚,有的社区用page从1开始,有的从0开始,还有的用游标(cursor)而不是页码。page_size别贪大,我试过设成200,结果接口直接返回空,设成50就正常,说明服务端有上限。超时必须设,不然网络一抖整个脚本就卡死。

2.2 反爬应对与采集节奏控制

采集环节最容易被忽视、也最容易翻车的就是节奏控制。很多人写完请求就直接for循环狂发,结果没几十条就被限流甚至封IP。我的经验是:把采集当成"模拟真人浏览"来设计,而不是"尽可能快地拉数据"。

具体做法有三条。第一,请求间隔加随机抖动。固定间隔(比如每秒一次)反而容易被识别出机器特征,我一般用random.uniform(1.5, 3.5)这样的随机区间。第二,控制单次会话的采集量。一次别抓太多页,抓个十几二十页就停一停,隔几分钟再继续。第三,准备好失败重试机制,遇到超时或非200状态码,退避几秒再重试,重试超过三次就跳过这一页并记录下来。

def fetch_with_retry(stock_code, page, max_retry=3): for attempt in range(max_retry): try: data = fetch_comments(stock_code, page) return data except Exception as e: wait = (attempt + 1) * 3 + random.uniform(0, 2) print(f"第{page}页第{attempt+1}次失败: {e}, 等待{wait:.1f}秒") time.sleep(wait) return None

采集节奏上,我实测下来一个比较稳的参数组合是:每页间隔2到4秒,每抓10页休息30到60秒,单只股票一次采集不超过500条评论。这个量级对于日频情绪分析已经够用了,再多边际收益很低,反而增加风险。

注意:采集到的评论里往往混着大量噪声,比如纯表情、无意义的"顶""沙发"、广告刷屏、重复灌水。这些在采集阶段可以先不管,但一定要在CSV里保留原始文本,清洗放到判分阶段之前做,这样万一清洗规则有问题还能回溯。

2.3 数据落盘与字段设计

采集完的数据怎么存,直接决定了后面几个阶段顺不顺。我强烈建议用CSV而不是数据库,因为这个项目的规模(单只股票几百到几千条)用CSV完全够,而且Excel能直接打开,交付给不写代码的同事也方便。

字段设计上,我固定了这么几列:comment_id(评论唯一标识)、stock_code(股票代码)、publish_time(发布时间,统一成YYYY-MM-DD HH:MM:SS格式)、content(评论文本)、like_count(点赞数,可选但有用)。其中publish_time的格式统一特别重要,因为后面要和行情数据按日期对齐,格式不一致会导致匹配失败。

import csv def save_comments(comments, filename): with open(filename, "w", newline="", encoding="utf-8-sig") as f: writer = csv.DictWriter(f, fieldnames=[ "comment_id", "stock_code", "publish_time", "content", "like_count" ]) writer.writeheader() for c in comments: writer.writerow(c)

这里encoding="utf-8-sig"是个小技巧,带BOM的UTF-8能让Excel直接正确识别中文,不然打开就是乱码。这个坑我踩过不止一次,后来养成习惯,凡是给Excel用的CSV一律加-sig。

3. AI情感判分的核心实现

3.1 为什么用大模型而不是词典

情感判分这一步,是整个项目里技术含量最高、也最影响最终结果质量的一环。前面提过,股票评论的语言特点决定了词典法不好使。我举个真实例子:一条评论写"这走势,怕是要上天台了",词典法看到"上天"可能判成正面,但实际是极度悲观。再比如"主力终于舍得拉一把了",字面没有明显情感词,但结合语境是偏正面的期待。这类需要语境理解的句子,只有大模型能处理得比较准。

大模型判分的另一个好处是可以自定义评分维度。我不满足于简单的正负二分,而是让模型输出一个-1到1之间的连续分数,-1代表极度悲观,0代表中性,1代表极度乐观。这样后续做相关性计算时,信息量比二分类大得多。如果你还想更细,可以让模型同时输出"情绪强度"和"情绪类型"(比如恐慌、贪婪、观望),不过维度一多,判分稳定性和成本都会上升,我建议先从单一情绪分做起。

3.2 判分提示词的设计与调优

判分准不准,八成看提示词。我前后改了七八版,总结出几个关键点。第一,给模型明确的角色和任务边界,别让它自由发挥。第二,给出评分标准和示例,尤其是边界情况(比如纯提问、纯转发新闻)怎么打分。第三,要求模型只输出分数,不要输出解释,否则解析起来很麻烦。

下面是我最终用的提示词模板,效果比较稳定:

PROMPT_TEMPLATE = """你是一个股票社区情绪分析助手。请对下面这条股票评论进行情绪打分。 评分标准: - 1.0:极度乐观,强烈看涨 - 0.5:偏乐观,温和看涨 - 0.0:中性,无明显情绪或纯信息陈述 - -0.5:偏悲观,温和看跌 - -1.0:极度悲观,强烈看跌 注意: 1. 只输出一个数字,范围-1到1,保留两位小数 2. 不要输出任何解释文字 3. 遇到反讽、黑话要结合语境判断 4. 纯提问、纯转发新闻按0.0处理 评论内容:{content} 分数:"""

这个模板里,**"只输出一个数字"**这条约束非常关键。我早期没加这条,模型动不动就回一段"这条评论表达了……因此我给出0.3分",解析的时候还得写正则去抠数字,徒增麻烦。加上之后,直接float()就能转,干净利落。

3.3 批量判分与结果校验

批量判分的时候,有两个问题必须处理:并发控制和失败兜底。并发太高容易被限流,太低又慢。我一般用线程池,并发数控制在5到10之间,配合重试机制。失败兜底的意思是,某条评论判分失败(比如超时、返回格式异常),不能让它把整个流程卡住,而是给个默认值(我一般给0.0)并记录下来,事后可以单独复查。

from concurrent.futures import ThreadPoolExecutor import re def score_comment(content): prompt = PROMPT_TEMPLATE.format(content=content[:500]) try: resp = call_llm_api(prompt) match = re.search(r"-?\d+\.?\d*", resp) if match: score = float(match.group()) return max(-1.0, min(1.0, score)) except Exception as e: print(f"判分失败: {e}") return 0.0 def batch_score(comments, max_workers=8): with ThreadPoolExecutor(max_workers=max_workers) as executor: scores = list(executor.map(score_comment, comments)) return scores

注意content[:500]这个截断,评论一般不会太长,截断到500字符既能覆盖绝大部分内容,又能控制token消耗。另外max(-1.0, min(1.0, score))是防止模型偶尔输出超范围的分数,做个钳位保护。

判分完成后,我强烈建议抽样人工校验。随机抽20到30条,自己看一眼分数合不合理。如果发现系统性偏差(比如整体偏正),说明提示词需要调整。我做过一次校验,发现模型对"割肉""套牢"这类词判得偏轻,后来在提示词里补了几个示例,偏差就纠正过来了。

提示:判分结果一定要和原始评论存在同一张表里,别分开存。后面做相关性分析时,你需要按日期聚合情绪分,如果分数和文本分离,聚合时还得做一次join,容易出错。

4. MATLAB相关性计算与Excel出图

4.1 数据对齐与日频聚合

到了这一步,你手上有两份数据:一份是带情绪分的评论表,一份是股票行情数据(日期、开盘、收盘、成交量等)。要做相关性计算,第一步是把两份数据按日期对齐。评论是逐条的,行情是逐日的,所以得先把评论按天聚合,算出每天的平均情绪分和评论数量。

聚合逻辑很简单:按publish_time的日期部分分组,对情绪分求均值。但有个细节要注意——非交易日怎么办。周末和节假日没有行情数据,但评论可能照样有。我的处理方式是:非交易日的评论情绪,归并到下一个交易日。这样既保留了信息,又不会因为日期对不上而丢数据。

% 读取评论数据 opts = detectImportOptions('comments_scored.csv'); opts.VariableNamingRule = 'preserve'; T = readtable('comments_scored.csv', opts); % 提取日期部分 T.date = dateshift(T.publish_time, 'start', 'day'); % 按日期聚合情绪分 [G, dateGroups] = findgroups(T.date); dailySentiment = splitapply(@mean, T.score, G); dailyCount = splitapply(@numel, T.score, G); % 组装成表 sentimentTable = table(dateGroups, dailySentiment, dailyCount, ... 'VariableNames', {'date', 'avg_sentiment', 'comment_count'});

MATLAB的findgroups和splitapply组合是处理分组聚合的利器,比写循环清爽得多。dateshift函数把时间戳截断到天,也是常用操作。

4.2 相关系数的计算与解读

数据对齐之后,就可以算相关系数了。我一般算三个:情绪分与当日涨跌幅的相关性、情绪分与次日涨跌幅的相关性、评论数量与成交量的相关性。第一个反映的是"情绪和当天走势同步性",第二个反映的是"情绪有没有预测性",第三个反映的是"关注度和交易活跃度的关系"。

% 读取行情数据 priceTable = readtable('price_data.csv'); priceTable.date = datetime(priceTable.date, 'InputFormat', 'yyyy-MM-dd'); % 合并两张表 merged = innerjoin(sentimentTable, priceTable, 'Keys', 'date'); % 计算涨跌幅 merged.pct_change = [NaN; diff(merged.close) ./ merged.close(1:end-1)]; % 计算相关系数 [r1, p1] = corr(merged.avg_sentiment, merged.pct_change, 'Rows', 'complete'); [r2, p2] = corr(merged.avg_sentiment(1:end-1), merged.pct_change(2:end), 'Rows', 'complete'); [r3, p3] = corr(merged.comment_count, merged.volume, 'Rows', 'complete'); fprintf('情绪与当日涨跌相关性: r=%.3f, p=%.3f\n', r1, p1); fprintf('情绪与次日涨跌相关性: r=%.3f, p=%.3f\n', r2, p2); fprintf('评论量与成交量相关性: r=%.3f, p=%.3f\n', r3, p3);

解读相关系数时,有几个经验值可以参考。r的绝对值在0.3以下算弱相关,0.3到0.6算中等,0.6以上算强相关。股票情绪和涨跌的相关性,通常落在0.2到0.5之间,能到0.4以上就算不错了。p值小于0.05说明统计显著,但如果样本量很小(比如只有二三十个交易日),即使r看着不小,p值也可能不显著,这时候结论要谨慎。

我实测下来,情绪分与当日涨跌的相关性通常比与次日的高,这符合直觉——评论大多是对当天走势的反应。但偶尔也会出现次日相关性更高的情况,那可能说明市场情绪有一定的惯性或滞后反应,值得进一步研究。

4.3 Excel图表的制作与美化

算完相关系数,最后一步是出图。为什么用Excel而不是MATLAB直接画?因为交付对象往往是不写代码的人,Excel图表他们能直接改颜色、调坐标轴、加标注,灵活性高。我的做法是:MATLAB把合并好的数据导出成CSV,Excel打开后做双轴折线图。

具体操作:把日期放X轴,情绪分和收盘价各放一个Y轴(次坐标轴),这样两条量纲完全不同的曲线能画在一张图上。情绪分用折线,收盘价用面积图或折线,视觉上区分开。再叠加一条情绪分的移动平均线(比如5日均线),能过滤掉单日的噪声,趋势更清晰。

Excel出图有几个美化技巧值得分享。坐标轴标签别用默认的"系列1""系列2",改成"情绪分""收盘价",读者一眼就懂。网格线调淡一点,别喧宾夺主。数据点标记如果太密就关掉,只保留线条。图例放右上角或底部,别压在数据上。这些细节看着小,但直接决定了图表能不能放进正式报告。

注意:Excel打开CSV时,日期列有时会被识别成文本,导致X轴排序错乱。解决办法是在Excel里选中日期列,用"分列"功能强制转成日期格式,或者干脆在MATLAB导出时就写成Excel格式(writetable直接写.xlsx),省去转换麻烦。

5. 常见问题与排查技巧实录

5.1 采集与判分阶段的典型故障

这个项目跑下来,遇到的问题五花八门,我挑几个最有代表性的整理成速查表,方便你对号入座。

问题现象可能原因排查与解决
请求返回403或空数据请求头缺失或过期重新抓包,更新Referer和token
采集到一半突然全失败触发限流加大间隔,换时间段重试
判分结果全是0.0API调用失败或解析异常检查API返回内容,确认正则匹配
判分整体偏正/偏负提示词示例不足补充边界示例,重新校准
日期对不齐时间格式不一致统一转成YYYY-MM-DD再聚合
相关系数异常高数据泄漏检查是否用了未来数据

其中数据泄漏这个坑最隐蔽也最致命。什么叫数据泄漏?举个例子,你算"情绪分与次日涨跌的相关性",但如果情绪分里混入了次日才发布的评论,那算出来的相关性就是假的,因为用了未来信息。避免方法是:聚合每日情绪时,严格按评论的发布时间截断,只用到当天收盘为止的评论。这个细节不注意,回测结果会好得离谱,实盘一用就亏。

5.2 提升结果可信度的几个经验

跑通流程只是第一步,让结果可信才是难点。我总结了三条经验。第一,样本量要够。二三十个交易日的数据算出来的相关系数,波动极大,至少得攒够两三个月的交易日数据再下结论。第二,做分组验证。把数据按时间段切成两半,分别算相关性,如果两段结果差异很大,说明关系不稳定,不能当策略依据。第三,控制混杂因素。情绪和涨跌的相关性,可能被大盘整体走势带偏,最好把个股涨跌减去大盘涨跌(算超额收益)再算相关性,结果更干净。

还有个小技巧:情绪分的分布要先看一眼。如果大部分评论都集中在0附近,说明这个社区的用户发言比较理性中性,情绪因子的区分度就低;如果分布很分散,两头都有不少极端值,那这个因子的信息量就大。用histogram画一下分布,心里就有数了。

5.3 流程自动化与后续扩展

如果你打算长期跑这套流程,建议把它做成定时任务。每天收盘后自动采集、判分、更新数据、重算相关性、刷新图表。用系统的定时任务工具(比如Linux的cron或Windows的任务计划)挂一个入口脚本就行。入口脚本里做好日志记录,哪天哪步失败了,翻日志一目了然。

后续扩展方向,我想到几个。一是多股票对比,把同一套流程套到多只股票上,横向比较哪只股票的情绪因子更有效。二是情绪因子的回测,把情绪分做成一个简单的择时信号(比如情绪分高于某阈值就持有),看看历史收益如何。三是换更细的判分维度,比如区分"理性分析"和"情绪宣泄",因为后者的噪声更大。这些扩展都不需要推翻现有框架,在对应阶段加模块就行。

我个人在实际操作中的体会是:这套流程最大的价值不在于某个环节多精妙,而在于它把一件原本靠感觉的事,变成了可重复、可验证的流程。哪怕相关系数只有0.2,只要它稳定显著,就比拍脑袋强。最后再分享一个小技巧——每次调整提示词或清洗规则后,别急着看最终相关性,先抽样看判分结果有没有变好,因为最终相关性受太多因素影响,用它来调中间环节,反馈太慢。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/24 4:51:27

Linux I2C驱动实战:从总线模型到设备树与调试技巧

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/10/24 4:50:44

C# EWS访问Exchange邮箱避坑指南:从证书到搜索的全链路排错

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/10/24 4:50:13

基于书签操作Word文档:POI实现内容替换、删除与插入的完整指南

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/10/24 4:49:13

Hi3516EV200 VENC取流超时排查实战:从RTMP推流断流到线程解耦修复

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/10/24 4:48:52

RLC电路深度解析:从谐振、阻尼到工程避坑

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/10/24 4:48:03

RLC电路本质:从谐振频率到阻尼特性,用能量视角理解二阶系统

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华