☰
Python爬虫+百度AI情感分析+MATLAB:股票评论与涨跌幅相关性分析实战
2026/10/23 5:09:40 网站建设 项目流程

简介:这是一套结合网络爬虫、自然语言处理与量化分析的实战资源,面向对金融舆情与股价联动感兴趣的 Python 与 MATLAB 初中级学习者。资源围绕“股票评论—情感—涨跌”这一主线,提供了从评论采集、百度AI语义标注、MATLAB统计建模到Excel图表呈现的完整分析处理流程。压缩包共10个文件,其中含2个Python脚本、1个MATLAB脚本、2个xlsx结果表、1个csv评论样本及4个走势/合并分析图,整体仅1.71MB,便于分步对照复现。目前已有6461人学习下载,尤其适合希望快速上手爬虫与文本情感分析、并尝试将评论情绪转化为市场判断依据的金融数据分析爱好者。通过该包可掌握评论数据清洗、情感倾向量化、股票涨跌相关性可视化等实操思路,为后续更深入的市场情绪建模打下坚实基础。

1. 股票评论与涨跌:先给“相关”定个边界

股票评论能不能预测涨跌,这个问题很容易吵起来。我的看法是:评论和涨跌之间的关系不在“准不准”,而在“有没有统计意义上的关联”,以及“这种关联在什么条件下才成立”。本文要拆的这套方案,用 Python 爬虫抓取某财经网站的股票评论,调用百度 AI 的语义分析接口给评论生成情感得分,再用 MATLAB 把情感得分和股价涨跌幅做相关性分析。适合想动手做文本情感分析、又不想只停留在理论层面的从业者,也适合已经被“评论预测股价”这类说法吸引、想知道真实计算长什么样的读者。我把整体流程和数据形态都放在后面,尽量让你看完就能自己复现一遍。

2. 评论采集模块:面向公开评论页的爬虫与清洗

2.1 爬虫的字段设计与数据规模预估

先定目标字段。每一次抓取需要记录三类信息:股票代码、评论内容、评论发布时间。这三项是后续所有分析的基石。股票代码用于对齐行情数据,评论内容用于情感分析,发布时间用于按日期聚合情绪。数据规模上,建议每只股票至少抓取过去 60 个交易日的评论,单日评论量少的股票可能只有几十条,多的可能有几百条,整体数据量不大,用 requests 就能扛住,不需要上 scrapy 或分布式方案。

页面的分页逻辑通常是按照时间倒序排列,每页固定条数。常见做法是直接在 URL 里改页码参数,先抓最后一页拿到总页数,再正向遍历。爬虫的设计目标不是快,而是稳。评论页的反爬压力通常比电商平台小,但请求频率如果过高,IP 还是会被临时封禁。我一般把请求间隔设置在 1.5 到 3 秒之间,并且给每次请求加上随机延时。

import requests import re import csv import time import random headers = { "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64)", "Referer": "https://finance.example.com/", } def fetch_comments(stock_code, max_pages=50): """ 抓取指定股票代码的评论数据。 返回列表,每个元素是 (股票代码, 评论时间, 评论内容) """ results = [] for page in range(1, max_pages + 1): url = f"https://finance.example.com/{stock_code}/comment_{page}.html" resp = requests.get(url, headers=headers, timeout=10) resp.encoding = resp.apparent_encoding # 从 HTML 中提取评论内容和时间 comment_pattern = re.compile(r'<div class="comment-text">(.*?)</div>', re.S) time_pattern = re.compile(r'<span class="comment-time">(.*?)</span>', re.S) comments = comment_pattern.findall(resp.text) times = time_pattern.findall(resp.text) for c, t in zip(comments, times): clean_text = re.sub(r'<.*?>', '', c).strip() if clean_text: # 过滤空评论 results.append((stock_code, t.strip(), clean_text)) if len(comments) < 10: break # 页面不足一页,说明已经抓到底了 time.sleep(random.uniform(1.5, 3.0)) return results if __name__ == "__main__": stock_code = "600000" data = fetch_comments(stock_code) with open("comments_raw.csv", "w", newline="", encoding="utf-8-sig") as f: writer = csv.writer(f) writer.writerow(["stock_code", "comment_time", "comment_text"]) writer.writerows(data) print(f"抓取完成,共 {len(data)} 条评论")

这里有三个细节值得说明。resp.encoding = resp.apparent_encoding是处理中文编码的关键,很多评论页用的是 GBK 编码,直接用默认的 UTF-8 解析会出现乱码。comment_pattern里的非贪婪匹配主要应对评论文字里可能出现的 HTML 标签嵌套,.findall一次拿全部匹配项比re.search逐条处理更高效。if len(comments) < 10这个提前终止逻辑,避免页面已经空了还在循环空转。

2.2 清洗规则与存储格式

评论数据清洗是整个链路里最枯燥但最重要的一环。原始评论里存在大量噪音:无意义字符、重复内容、广告文本、过短片段。清洗规则按优先级排列如下。

第一条规则是过滤长度过短的评论。少于 4 个字符的评论通常没有分析价值,比如“好的”“哈哈”“支持”,这些文本会让情感分析模型产生偏移。第二条规则是去重,同一用户重复发布的相同内容只保留一条,避免单条情绪被重复计数放大权重。第三条规则是移除 URL 和@用户标记,这些文本对情感判断没有帮助,反而会成为噪音。第四条规则是过滤纯数字和纯表情的内容。

import re def clean_comment(text): # 去 URL text = re.sub(r'http\S+', '', text) text = re.sub(r'www\.\S+', '', text) # 去 @用户 text = re.sub(r'@\w+', '', text) # 去 HTML 实体 text = re.sub(r'&[a-zA-Z]+;', '', text) # 去多余空格 text = re.sub(r'\s+', ' ', text).strip() # 过滤过短和纯数字 if len(text) < 4 or text.isdigit(): return None return text def deduplicate(data): seen = set() unique = [] for stock_code, t, text in data: cleaned = clean_comment(text) if cleaned is None: continue key = (stock_code, cleaned) if key not in seen: seen.add(key) unique.append((stock_code, t, cleaned)) return unique

去重时以“股票代码 + 清洗后文本”作为唯一键,而不是直接对全表去重。原因是不同股票下可能出现相同的模板评论,比如“学习一下”“关注中”,另外评论内容相同但时间不同的情况也应该保留一条。另外,清洗时不要把评论时间处理掉,后面按日期聚合情绪时,时间字段是分组的依据。

2.3 与行情数据的时间对齐问题

评论抓取完成只是第一步,真正的坑在于和行情数据对齐。股价数据按交易日记录,评论数据按自然日记录,两者并不天然一致。周六周日有评论但没有交易,节假日也有同样的问题。如果直接把评论日期当交易日日期做关联,数据会出现错位。

我一般用交易日历文件做映射。先构造一个交易日列表,然后把每条评论的发布时间提取出“年月日”,只保留交易日当天的评论,或者把非交易日的评论归到下一个交易日。这里建议按评论日期归属,因为评论者是在表达对当天或前一天行情的看法,把周六周日的评论归到周一,逻辑上更接近真实情绪状态。

import pandas as pd def attach_trade_date(comments_csv, calendar_csv): df = pd.read_csv(comments_csv) cal = pd.read_csv(calendar_csv, parse_dates=["trade_date"]) df["comment_date"] = pd.to_datetime(df["comment_time"]).dt.date cal["date"] = cal["trade_date"].dt.date # 方案1:只保留交易日当天的评论 df = df[df["comment_date"].isin(cal["date"])] # 方案2:非交易日评论归到下一个交易日 # 构造日期到最近交易日映射后 merge_asof return df

这里给出两种方案,按你的数据量选择。方案一简单直接,适合评论量大的股票。方案二适合评论稀疏的股票,强行丢弃周末评论会让样本量明显缩水。merge_asof 的具体写法需要先按日期排序,再指定 tolerance 参数限制向前或向后的最大天数,默认是向后找最近的交易日。

3. 情感分析模块:调用百度 AI 接口与情感分数标准化

3.1 百度 AI 情感倾向分析接口的参数配置

清洗后的评论文本,需要批量交给百度 AI 做语义分析。调用前要先在百度智能云平台开通自然语言处理服务,拿到 API Key 和 Secret Key,然后通过鉴权接口获取 access_token。新手最容易卡在这一步:拿到 token 后不看有效期,程序运行超过 30 天 token 过期,请求直接报 110 错误。建议每次运行脚本时重新拉取 token,不要缓存。

接口本身接受两个核心参数:text 是待分析文本,类型是字符串,长度限制通常在 2048 字节以内,超出需要截断。type 参数是情感分析的粒度,默认是整体分析,也可以按句分析。返回结果里最有价值的是 sentiment_label,取值范围 0、1、2,分别对应负向、中性、正向。同时返回的 confidence 字段表示该倾向的置信度,范围是 0 到 1,越高说明模型判断越确定。

import requests import json API_KEY = "your_api_key" SECRET_KEY = "your_secret_key" def get_access_token(): url = "https://aip.baidubce.com/oauth/2.0/token?grant_type=client_credentials" params = {"client_id": API_KEY, "client_secret": SECRET_KEY} resp = requests.post(url, params=params) return resp.json().get("access_token") def sentiment_score(text, access_token): url = "https://aip.baidubce.com/rpc/2.0/nlp/v1/sentiment_classify" params = {"access_token": access_token} payload = { "text": text[:500], # 防止超长文本截断报错 "type": 1 # 1 表示整体分析 } resp = requests.post(url, params=params, json=payload) result = resp.json() if "items" in result and len(result["items"]) > 0: item = result["items"][0] label = item["sentiment_label"] # 0负向 1中性 2正向 conf = item["confidence"] return label, conf return None, None

注意我在调用时把文本截断到了 500 字符,这是血泪经验。虽然接口文档说支持 2048 字节,但实际测试中超过 800 字符的评论,接口经常返回参数错误或者分析结果明显失真。截断并不是简单从头部切断,而是取开头 100 字和结尾 100 字的拼接,因为股票评论的关键情绪通常出现在开头和结尾,中间多是复述行情过程。type=1表示整体粒度,如果想要每条评论拆多个句子分别打分,要把这个参数改成 0,但返回的 items 会变成多条,处理和汇总逻辑都要跟着调整。

3.2 批量调用与失败重试策略

批量调用的核心约束是 QPS。百度 AI 的情感倾向分析接口免费额度下 QPS 限制是 1 到 2,也就是每秒只能请求 1 到 2 次。如果顺序 for 循环直接跑,很容易触发频率限制错误。这里要做一个带重试的批量处理封装。

import time def batch_sentiment(data_list, access_token, max_retries=3): results = [] for idx, (stock_code, date, text) in enumerate(data_list): label, conf = None, None for attempt in range(max_retries): label, conf = sentiment_score(text, access_token) if label is not None: break time.sleep(2 * (attempt + 1)) # 退避等待 results.append((stock_code, date, text, label, conf)) if idx % 50 == 0: print(f"已处理 {idx}/{len(data_list)} 条") time.sleep(1.0) # 控制 QPS return results

重试策略采用退避等待,第一次失败等 2 秒,第二次失败等 4 秒。连续三次失败就直接跳过该条评论,并把 label 记录为 None,标记为“未分类”。宁可让部分评论缺失,也不能让整个任务卡死在单条评论上。idx % 50 == 0的输出是为了方便观察进度,因为情感分析接口的处理速度远慢于爬虫阶段的抓取速度,没有进度输出很容易让人以为程序假死。

3.3 情感分数的标准化映射

接口返回的 sentiment_label 是 0、1、2 三个离散值。直接拿这三个值做数值计算是不合适的,因为 0 到 2 是类别编码,不是真实的情绪强度。“中性”不代表“0 分”,负向也不等于“负分”。需要把类别编码映射到连续区间。

我采用的映射方式是:正向映射为 +1,中立映射为 0,负向映射为 -1。然后乘以该条评论的置信度作为加权系数。这样每条评论的情感得分是一个在 -1 到 +1 之间的连续值,同时保留了模型对判断结果的确定程度。置信度低的评论被拉向 0,相当于对不确定样本做了平滑处理。

def map_to_score(label, confidence): if label == 2: # 正向 base = 1.0 elif label == 0: # 负向 base = -1.0 else: # 中性 base = 0.0 return base * (confidence if confidence else 0.5)

这里的 0.5 是置信度缺失时的默认值。在重试三次仍然失败、label 为 None 的样本上,情感得分直接设为 0.0,当作中性处理,因为缺失样本加入任何方向都会污染整体统计。标准化之后输出的 CSV 文件结构为:股票代码、评论日期、原始文本、情感类别、置信度、情感得分。这份文件就是 MATLAB 分析的输入。

4. MATLAB 数据处理:情感序列构建与涨跌幅相关性分析

4.1 MATLAB 读取与数据分组聚合

MATLAB 在这一环节的角色是数据处理与统计计算。先把 Python 输出的 CSV 读进来,按股票代码和评论日期分组,计算每个交易日的平均情感得分。这个聚合动作很关键,因为后续相关性分析的对象是“每日情绪值”,不是单条评论得分。

% 读取情感分析结果 opts = detectImportOptions('sentiment_scores.csv', 'Encoding', 'utf-8'); data = readtable('sentiment_scores.csv', opts); % 按股票代码和日期分组,计算每日平均情感得分 [group_id, date_id] = findgroups(data.stock_code, data.comment_date); daily_sentiment = splitapply(@mean, data.sentiment_score, group_id); % 构造汇总表 summary = table(date_id, daily_sentiment, 'VariableNames', {'Date', 'AvgSentiment'}); summary = sortrows(summary, 'Date');

detectImportOptions这一步经常被忽略。MATLAB 默认读取 CSV 时使用系统区域设置,如果系统是英文环境,中文日期格式很容易被误读为字符串或者直接解析失败。显式指定Encoding, utf-8可以避开这个坑。findgroups和splitapply的组合替代了传统的accumarray,好处是可以同时保留分组字段的值,便于后面拼接到表格里。

4.2 涨跌幅计算与两个序列的对齐

行情数据的格式通常是开盘价、最高价、最低价、收盘价、成交量。涨跌幅需要自己计算,公式是当前交易日收盘价相对于前一个交易日收盘价的百分比变化。用 MATLAB 的price2ret函数可以直接计算,也可以手动用 diff 计算。

% 读取行情数据,假设包含 Date, Close 两列 price_data = readtable('stock_price.csv'); price_data = sortrows(price_data, 'Date'); % 计算涨跌幅(百分比) close_price = price_data.Close; ret = diff(close_price) ./ close_price(1:end-1) * 100; ret = [NaN; ret]; % 第一天没有涨跌幅 % 对齐日期 price_data.Ret = ret; merged = innerjoin(summary, price_data, 'Keys', 'Date'); merged = rmmissing(merged, 'DataVariables', {'Ret', 'AvgSentiment'}); result_table = merged(:, {'Date', 'AvgSentiment', 'Ret'});

对齐操作使用了innerjoin,它只保留两个表都存在的日期。这样做的好处是自动丢弃了评论缺失的交易日和行情缺失的评论日。rmmissing会把 NaN 行删掉,包括第一天的涨跌幅缺失值和当天没有评论的日期。对齐之后的结果表,每一行都同时包含日期、当日平均情感得分、当日涨跌幅三个字段。

这里要特别说明:我用的对齐键是“当天评论对当天涨跌幅”。但在实际业务逻辑里,当天的评论大部分是在收盘后发出的,评论者看到的是当天的行情结果,评论内容带着当天涨跌的情绪,因此当天评论与当天涨跌幅的相关性相当于是“事后情绪”。更具参考价值的对齐方式是“当天评论对次日涨跌幅”,因为次日涨跌幅在评论发出时尚未发生,情绪对行情的关联才具备提前性。

4.3 Pearson 相关与滞后相关性分析

对齐完成后,直接调用corr函数计算情感得分与涨跌幅的相关系数。相关系数只能描述线性相关性,取值范围在 -1 到 1 之间,正数表示情绪越积极、涨跌幅越大,负数表示情绪越积极、涨跌幅越小。

% 同步相关:当日情绪 vs 当日涨跌幅 r_sync = corr(result_table.AvgSentiment, result_table.Ret); % 滞后相关:当日情绪 vs 次日涨跌幅 next_day_ret = [result_table.Ret(2:end); NaN]; result_table.NextRet = next_day_ret; r_lag1 = corr(result_table.AvgSentiment(1:end-1), result_table.NextRet(1:end-1)); fprintf('同步相关系数: %.3f\n', r_sync); fprintf('滞后1日相关系数: %.3f\n', r_lag1);

滞后相关的实现方式是手动构造 NextRet 列,把第 t+1 天的涨跌幅放到第 t 天的行上。这里有个容易出错的小地方:result_table.AvgSentiment(1:end-1)和NextRet(1:end-1)必须等长,而且删掉的都是最后一行,不是随机删。同步相关用全部数据,滞后相关用前 n-1 行,两个计算的样本数不同,并不影响corr` 函数自动处理。

相关系数在金融数据里的绝对值通常不会太高。股票涨跌幅受到基本面、大盘走势、资金流向等多种因素影响,评论情绪只能解释其中一小部分。0.15 到 0.3 之间的相关系数在文本情绪与涨跌关系的研究中已经算是有意义的发现。不要期望 0.8 以上的强相关,如果真算出这种数值,大概率是数据泄漏或者对齐错误。

5. 常见问题与排查:五个典型翻车场景

5.1 评论抓取件数为空,或 HTML 解析结果全是空文本

现象:爬虫正常跑完,CSV 文件里只有表头,没有一条数据。

原因:目标网页改版了。页面结构里的 class 名称被调整,正则表达式匹配不到任何内容。另一个常见原因是默认编码判断错误,网页实际是 GBK 编码,但resp.apparent_encoding返回了 UTF-8,导致中文字符全部变成乱码,清洗时全部被re.sub处理掉。

解决:抓取时先打印响应内容的前 1000 个字符,肉眼确认 HTML 结构里是否存在目标字段。如果resp.text里能看到评论内容但正则匹配不到,说明 class 名称需要同步更新。如果看到的是乱码,强制指定resp.encoding = 'gbk'再试。

5.2 情感分析“中性”比例过高,情绪序列没有波动

现象:统计分析时发现超过 60% 的评论被判定为中性,日平均情感得分全部落在 -0.1 到 0.1 之间,序列几乎没有区分度。

原因:股票评论里的情绪表达比较委婉,币圈类似的“冲”“拉盘”“出货”等词在财经语境下不是常规情感词,百度 AI 的通用情感模型对这些金融领域黑话的识别能力有限。

解决:把评论文本里的行情术语替换为通用情感词,例如“涨停”替换为“大涨”,跌停替换为“大跌”,“利好”替换为“好消息”,“利空”替换为“坏消息”。在调用接口之前做一次术语映射。同时,过滤中性评论,只保留正向和负向样本,重新计算情感得分——只计算“有明确态度”的评论的平均值,更能反映多空分歧。

5.3 MATLAB 读取 CSV 时中文乱码或日期解析失败

现象:MATLAB 读入数据后,评论日期列全部变成NaT,或者股票代码变成乱码。

原因:MATLAB 在 Windows 上的默认文本编码跟随系统区域设置,通常不是 UTF-8。Python 端输出的 CSV 如果没加utf-8-sig,Excel 和 MATLAB 都可能识别错误。加了utf-8-sig但 MATLAB 仍乱码,那就是readtable的自动检测机制没生效。

解决:Python 端写文件时统一用encoding='utf-8-sig',MATLAB 端用detectImportOptions显式指定编码。如果还是不行,直接在 MATLAB 里用readcell读取原始内容,检查cell内容是否正确,再手动构造表格。日期统一用datetime函数转换,不要依赖readtable的自动解析。

5.4 评论日期和行情日期错位,相关性计算结果异常

现象:计算出的同步相关系数高达 0.6 以上,明显不合理。

原因:数据合并时,日期对齐逻辑写错了。常见错误是把评论时间当成字符串直接和行情日期匹配,但评论时间可能带有时分秒,直接相等比较永远匹配不上。另一个错误是没处理节假日,周一开盘价和周末评论被错误地对应到了同一天。

解决:统一用datetime类型做对齐键,评论时间先转成datetime再取.Date字段去掉时间部分。用交易日历文件过滤,确保只有交易日参与内连接。验证方法是对比合并前后的行数:合并后的行数应该等于两个月内交易日的总数,而不是自然日的总数。

5.5 缺失值处理不当导致相关系数虚高

现象:删掉缺失值后,相关系数从 0.05 变成 0.4,变化大到让人怀疑人生。

原因:rmmissing默认按行删除,但如果你在构造滞后列时的代码是result_table.NextRet = [NaN; ret(1:end-1)],这里用了 NaN 填充第一行,rmmissing直接把第一行删掉,而第一行恰好是涨跌幅最大的极端点。删掉之后噪声减小,相关虚高。

解决:每次删除数据前先输出删除的行数和删除条件,确认到底删掉了什么。滞后构造用[ret(2:end); NaN]形式,NaN 放最后,这样rmmissing只删最后一行,不影响前面全部有效样本。然后再手动检查删除前后均值是否发生大幅变化,指标波动超过 20% 就说明删除逻辑有问题。

6. 进阶技巧:用滚动窗口观察情绪与涨跌的相关性稳定性

相关系数是一个静态值,它只能告诉你“整个时间范围内”情绪和涨跌幅的整体关联。但股票市场的情绪传导路径经常发生切换,某一阶段是情绪领先于行情,另一阶段是行情反作用于情绪。静态相关看不出这种切换。我通常在 MATLAB 里加一个滚动相关窗口,把 60 天作为一个窗口,每天计算一次窗口内的相关系数,然后观察 r 值序列的走向。

window = 60; n = height(result_table); rolling_r = NaN(n, 1); for i = window:n seg_sentiment = result_table.AvgSentiment(i-window+1:i); seg_ret = result_table.NextRet(i-window+1:i); valid_idx = ~isnan(seg_sentiment) & ~isnan(seg_ret); if sum(valid_idx) > window * 0.6 % 有效样本不能少于 60% rolling_r(i) = corr(seg_sentiment(valid_idx), seg_ret(valid_idx)); end end

window=60表示用过去两个多月的交易日数据计算一个相关系数。sum(valid_idx) > window*0.6这个条件很关键,如果窗口内有大量缺失值,算出来的相关很容易被少数几天带偏。窗口太小相关噪声大,窗口太大又看不到切换,60 个交易日是我试过比较能平衡两者的参数。跑完之后把rolling_r画出来,横轴是时间,纵轴是相关系数,负值区域表示情绪与次日涨跌呈反向关系,正值区域表示正向关系。

从滚动窗口里观察到的规律通常比单点相关系数更有业务价值。如果 r 值长期稳定在正区间,说明评论情绪是行情的同步指标;如果 r 值在 0 附近来回穿越,说明情绪和行情的关系不稳定,靠情绪做预测的可靠性很低。这个阶段的结论在报告里写出来,比单摆一个 r=0.2 要有说服力得多。

我从那以后做任何情绪与行情分析,都会强制走一遍滚动窗口,确认相关性的方向是否在样本期内发生过翻转。这种稳定的相关才有进一步建模的价值,翻转的相关只能算噪声。希望帮到你。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询