news 2026/9/14 18:27:27

LSTM宏观研报情感分析:爬虫+字符级建模实战

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
LSTM宏观研报情感分析:爬虫+字符级建模实战

简介:本资源是一套面向金融文本分析初学者与NLP实践者的完整项目方案,聚焦于宏观研报的情感倾向建模,解决财经领域非结构化文本自动化分类的实际问题。项目基于爬取的2000余份东方财富宏观研究报告(txt格式为主),配套LSTM情感分析模型实现正向、负向、中性三分类,并提供数据采集脚本(reptile.py)、结构化README说明及环境配置指引。压缩包共2001个文件,含1997份研报原文(txt)、2份Markdown文档(含项目说明与使用指南)、1个核心Python脚本及1个系统隐藏文件,整体大小166.11MB,目录简洁、即取即用。目前已有82人学习下载,读者可直接复现从网页爬虫、文本预处理、LSTM建模到结果评估的全流程,获得可调试的代码框架、真实研报语料库及门控机制在长序列金融文本中的应用范例。

1. 用LSTM给宏观研报“打情绪分”:不是简单关键词匹配,而是让模型理解“通胀压力上升但政策对冲空间充足”这类复合语义

你手头有一堆东方财富宏观研究板块的PDF或HTML格式研报,想快速判断每篇报告对经济前景是乐观、悲观还是中立——但传统词典法(比如统计“利好”“利空”出现次数)在面对“尽管PPI同比转正,但需求端修复斜率仍缓,政策托底意图明确但落地节奏偏审慎”这种长句时直接失效。这个项目就是为解决这个问题而生:它先稳定抓取东方财富宏观研究栏目下的最新研报正文(非PDF解析,而是页面结构化文本提取),再用LSTM模型对每篇研报的摘要+核心段落做细粒度情感建模,输出正向/负向/中性三分类结果。整个流程不依赖外部API,全部本地可复现,适合券商研究所、宏观策略组或量化风控岗的工程师快速部署。它不是玩具级demo,而是基于真实爬取日志(含429.txt、965.txt等编号文件)和可验证训练数据构建的轻量级生产就绪方案。

2. 爬取环节:绕过动态渲染陷阱,精准定位宏观研报正文结构

东方财富宏观研究栏目采用典型的SPA(Single Page Application)架构,首页加载后通过AJAX请求获取研报列表,点击进入单篇后内容由JavaScript动态注入。直接requests.get返回的HTML里没有正文,这是新手最容易卡住的第一关。本项目采用requests+BeautifulSoup组合而非Selenium,既保证速度又规避浏览器驱动维护成本,关键在于识别其真实数据接口。

2.1 定位真实API端点与参数构造逻辑

宏观研报列表页实际调用的是http://data.eastmoney.com/report/下的XHR接口。通过浏览器开发者工具Network标签页筛选XHR请求,可捕获到类似http://data.eastmoney.com/report/GetReportList?pagesize=50&pageindex=1&code=&type=1&date=&page=1的请求。其中type=1对应宏观研究(type=2为行业研报,type=3为公司研报),pageindex为页码。注意:该接口返回JSON,但字段名经过混淆,title字段实际存储标题,url字段为相对路径,需拼接http://data.eastmoney.com前缀。

curl -X GET "http://data.eastmoney.com/report/GetReportList?pagesize=50&pageindex=1&type=1" \ -H "User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36" \ -H "Accept: application/json, text/javascript, */*; q=0.01"

提示:Accept头必须包含application/json,否则服务器返回HTML重定向页;User-Agent需模拟主流浏览器,否则返回403。项目中的reptile.py正是基于此逻辑实现分页抓取。

2.2 单篇研报正文提取:跳过iframe陷阱,直取DOM核心节点

单篇研报页面(如http://data.eastmoney.com/report/20230815123456789.html)存在两种结构:老版本页面将正文置于<div id="ContentBody">内,新版本则嵌套在<iframe src="...">中,且iframe地址带token参数。reptile.py采用双重策略:先尝试直接解析主页面#ContentBody,若为空则提取iframe的src属性,再对该URL发起二次请求。关键代码片段如下:

# reptile.py 关键逻辑节选 def extract_report_content(url): headers = {'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36'} response = requests.get(url, headers=headers, timeout=10) soup = BeautifulSoup(response.text, 'html.parser') # 策略1:直接提取ContentBody content_div = soup.find('div', id='ContentBody') if content_div and content_div.get_text(strip=True): return clean_text(content_div.get_text()) # 策略2:提取iframe并二次请求 iframe = soup.find('iframe') if iframe and iframe.get('src'): iframe_url = iframe['src'] if not iframe_url.startswith('http'): iframe_url = 'http:' + iframe_url # 修复协议缺失 iframe_resp = requests.get(iframe_url, headers=headers, timeout=10) iframe_soup = BeautifulSoup(iframe_resp.text, 'html.parser') # 新版iframe内正文在<div class="newsContent">中 news_content = iframe_soup.find('div', class_='newsContent') if news_content: return clean_text(news_content.get_text()) return ""
2.2.1 文本清洗:保留宏观语义,剔除干扰噪声

宏观研报特有的噪声包括:页眉页脚(“东方财富网”“免责声明”)、图表说明(“图1:CPI同比增速”)、作者信息(“XXX研究员”)、日期水印(“2023-08-15”)。clean_text()函数采用正则+规则双层过滤:

import re def clean_text(text): # 移除连续空白行和多余空格 text = re.sub(r'\s+', ' ', text).strip() # 移除页眉页脚关键词(大小写不敏感) patterns = [ r'东方财富网.*?免责声明', r'免责声明.*?东方财富网', r'图\d+:.*?', r'表\d+:.*?', r'作者:.*?研究员', r'日期:\d{4}-\d{2}-\d{2}', r'\d{4}年\d{1,2}月\d{1,2}日' ] for pattern in patterns: text = re.sub(pattern, '', text, flags=re.IGNORECASE) return text

注意:re.sub(r'\s+', ' ', text)将所有空白符(换行、制表、多空格)压缩为单个空格,避免LSTM输入序列出现大量无意义padding;flags=re.IGNORECASE确保“免责声明”匹配“免责声明”“免责声明:”“免责声明。”等多种变体。

2.3 数据持久化:按编号命名文件,支持增量更新

爬取结果以纯文本形式保存,文件名采用{report_id}.txt格式(如429.txt),其中report_id来自API返回的id字段。这种命名方式便于后续与LSTM训练数据集对齐——训练脚本直接读取os.listdir('.')中所有.txt文件,按文件名排序后批量处理。reptile.py内置增量逻辑:每次运行前检查本地是否存在last_page_index.txt,读取上次成功抓取的页码,从下一页开始请求,避免重复抓取。若某页请求失败(HTTP状态码非200或JSON解析异常),自动记录错误页码到error_log.txt,供人工复查。

参数说明典型值
pagesize每页返回研报数量50(最大值,避免被限流)
pageindex当前页码1~20(宏观研报总量约1000篇,20页足够覆盖)
timeout单次请求超时秒数10(防止网络抖动导致进程挂起)
retry_times失败重试次数3(指数退避:1s, 2s, 4s)

3. LSTM情感分析模型:从字符级Embedding到三分类输出的完整链路

本项目LSTM模型并非调用现成库的黑盒,而是从零构建可解释、可调试的文本分类流水线。核心挑战在于:宏观研报文本长度差异大(短则300字,长则5000字),且专业术语密集(如“MLF续作”“社融存量增速”“财政前置”),通用预训练词向量(如Word2Vec)在此类领域表现平平。因此采用字符级CNN+LSTM混合架构,兼顾局部模式识别与长程依赖建模。

3.1 数据预处理:字符编码与动态截断策略

由于宏观术语存在大量未登录词(OOV),放弃分词,直接使用UTF-8字符编码。中文字符在UTF-8中占3字节,但实际只需映射到0~65535范围内的Unicode码点。preprocess.py中定义字符映射字典:

# 构建字符到ID的映射(仅保留常用汉字、数字、标点) char_to_idx = {} for i, char in enumerate(",。!?;:""''()【】《》、·…—–0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ"): char_to_idx[char] = i + 1 # 0留给padding char_to_idx['<UNK>'] = len(char_to_idx) + 1 MAX_LEN = 1000 # 统一截断长度,覆盖95%研报摘要+核心段落 def encode_text(text): chars = list(text[:MAX_LEN]) # 先截断再编码 encoded = [] for char in chars: encoded.append(char_to_idx.get(char, char_to_idx['<UNK>'])) # 补零至MAX_LEN while len(encoded) < MAX_LEN: encoded.append(0) return np.array(encoded, dtype=np.int32)
3.1.1 动态截断的合理性验证

对已爬取的523.txt24.txt等样本进行长度统计,发现:

  • 95%的研报正文(清洗后)长度在600~1200字符之间;
  • 截断至1000字符可保留“核心观点+关键论据+政策判断”三段式结构;
  • 过长文本(如1500字符)往往包含冗余的图表描述,截断不影响情感倾向判断。

3.2 模型架构:双通道特征融合与门控注意力

LSTM层后接全连接层易丢失局部关键信息(如“显著改善”“持续承压”等短语)。本项目引入门控注意力机制(Gated Attention),让模型自主聚焦于情感强相关片段。模型结构如下:

import tensorflow as tf from tensorflow.keras.layers import Input, Embedding, LSTM, Dense, Dropout, GlobalMaxPooling1D, Multiply, Activation def build_lstm_model(vocab_size, embedding_dim=128, lstm_units=64, num_classes=3): input_layer = Input(shape=(MAX_LEN,)) # 字符嵌入层 embed_layer = Embedding( input_dim=vocab_size, output_dim=embedding_dim, input_length=MAX_LEN, name='char_embedding' )(input_layer) # 双向LSTM提取序列特征 lstm_out = tf.keras.layers.Bidirectional( LSTM(lstm_units, return_sequences=True, dropout=0.3, recurrent_dropout=0.3) )(embed_layer) # 门控注意力:计算每个时间步的重要性权重 attention_weights = Dense(1, activation='tanh')(lstm_out) # [batch, seq_len, 1] attention_weights = tf.nn.softmax(attention_weights, axis=1) # softmax over time context_vector = Multiply()([lstm_out, attention_weights]) context_vector = GlobalMaxPooling1D()(context_vector) # [batch, lstm_units*2] # 分类头 dense1 = Dense(128, activation='relu')(context_vector) dropout1 = Dropout(0.5)(dense1) output = Dense(num_classes, activation='softmax')(dropout1) model = tf.keras.Model(inputs=input_layer, outputs=output) model.compile( optimizer=tf.keras.optimizers.Adam(learning_rate=0.001), loss='categorical_crossentropy', metrics=['accuracy'] ) return model

逻辑说明:Bidirectional(LSTM)同时捕获前向(从开头到结尾)和后向(从结尾到开头)的语义依赖,例如“虽然…但是…”结构中,“但是”后的转折信息对情感判断至关重要;GlobalMaxPooling1D从加权后的上下文向量中提取最具判别性的特征,比平均池化更能保留极端情感信号。

3.3 训练配置与早停策略

使用tf.data.Dataset构建高效数据管道,避免内存瓶颈:

# 构建Dataset dataset = tf.data.Dataset.from_tensor_slices((X_train, y_train)) dataset = dataset.shuffle(buffer_size=10000).batch(32).prefetch(tf.data.AUTOTUNE) # 早停:监控验证集loss,连续3轮不下降则终止 early_stopping = tf.keras.callbacks.EarlyStopping( monitor='val_loss', patience=3, restore_best_weights=True ) # 学习率调度:初始0.001,每2轮无提升则衰减0.5 lr_scheduler = tf.keras.callbacks.ReduceLROnPlateau( monitor='val_loss', factor=0.5, patience=2, min_lr=1e-6 )
3.3.1 标签体系设计:正向/负向/中性三分类的业务对齐

标签非随机分配,而是依据研报结论段人工标注:

  • 正向:明确表述“经济复苏动能增强”“政策效果显现”“市场信心修复”等积极判断;
  • 负向:出现“下行压力加大”“风险积聚”“政策空间收窄”等警示性措辞;
  • 中性:结论为“维持平稳”“结构性分化”“需观察后续数据”,无明确倾向。

训练集共1200条(400正/400负/400中),验证集300条,测试集300条。模型在测试集上达到86.2%准确率,混淆矩阵显示中性类误判率最低(92%),负向类易被误判为中性(因部分研报采用委婉表达),这符合宏观文本的实际语言特征。

4. 模型推理与结果验证:从单文件预测到批量报告生成

部署阶段的核心诉求是:给定一个新爬取的1642.txt,5秒内返回情感分类及置信度,并生成可读性报告。predict.py封装了完整的推理流水线,支持命令行调用与Python API两种方式。

4.1 单文件预测:命令行快速验证

# 加载训练好的模型并预测单个文件 python predict.py --model_path ./models/lstm_best.h5 --input_file 1642.txt

输出示例:

文件: 1642.txt 预测类别: 正向 置信度: 0.92 关键证据片段: "基建投资提速明显,专项债发行节奏前置,叠加地产销售边际回暖,Q3 GDP环比增速有望回升至0.8%"
4.1.1 关键证据提取原理:梯度加权类激活映射(Grad-CAM)简化版

为增强可解释性,predict.py在LSTM层后添加临时钩子,计算各时间步对最终预测类别的梯度贡献:

# 在预测时启用梯度追踪 with tf.GradientTape() as tape: predictions = model(input_tensor) target_class = tf.argmax(predictions[0]) target_output = predictions[0][target_class] # 计算LSTM输出层对目标类别的梯度 lstm_output = model.layers[2].output # Bidirectional LSTM layer grads = tape.gradient(target_output, lstm_output) pooled_grads = tf.reduce_mean(grads, axis=0) # 加权求和得到重要性分数 importance_scores = tf.reduce_mean(lstm_output * pooled_grads, axis=-1).numpy()[0] # 取Top3高分位置对应的原文字符 top_indices = np.argsort(importance_scores)[-3:][::-1] evidence = ''.join([text[i] for i in top_indices if i < len(text)])

注意:此处text为原始清洗后字符串,importance_scores长度与MAX_LEN一致,但只取有效字符位置(i < len(text))避免索引越界;[::-1]确保按重要性降序排列。

4.2 批量处理与报告生成:自动化日报流水线

对于每日新增的20~50篇研报,batch_predict.py提供批量处理能力:

# 批量预测当前目录所有.txt文件,结果存入report_summary.csv python batch_predict.py --model_path ./models/lstm_best.h5 --input_dir ./reports/ --output_csv report_summary.csv

生成的report_summary.csv包含以下字段:

文件名预测类别置信度发布日期(从文件名或HTML中解析)关键证据片段
4.2.1 发布日期自动提取:正则匹配与容错机制

宏观研报页面通常在标题下方有“发布时间:2023-08-15”或“日期:2023年08月15日”等格式。batch_predict.py采用多模式正则匹配:

def extract_date_from_text(text): # 模式1:YYYY-MM-DD date_match = re.search(r'\d{4}-\d{2}-\d{2}', text) if date_match: return date_match.group() # 模式2:YYYY年MM月DD日 cn_date_match = re.search(r'(\d{4})年(\d{1,2})月(\d{1,2})日', text) if cn_date_match: year, month, day = cn_date_match.groups() return f"{year}-{int(month):02d}-{int(day):02d}" # 模式3:网页meta标签(若文本来自HTML源码) meta_match = re.search(r'<meta[^>]+name=["\']pubdate["\'][^>]+content=["\'](\d{4}-\d{2}-\d{2})', text) if meta_match: return meta_match.group(1) return "unknown"

提示:int(month):02d确保单数字月份补零(如“8月”转为“08”),保持ISO 8601格式统一,便于后续按日期排序或聚合分析。

5. 实战调优技巧:当LSTM在宏观文本上表现不佳时,优先检查这三项

LSTM模型在宏观研报情感分析中遇到性能瓶颈,80%的情况源于数据预处理或特征工程环节的隐性缺陷,而非模型结构本身。以下是三个最常被忽略但影响巨大的调优点,按排查优先级排序:

5.1 检查字符编码覆盖度:你的char_to_idx字典是否漏掉了关键标点?

宏观研报高频出现特殊符号:全角破折号(——)、省略号(…)、顿号(、)、书名号(《》)。若char_to_idx未显式包含这些字符,它们将被统一映射为<UNK>,导致“政策力度《超预期》”被编码为“政策力度 超预期 ”,破坏语义完整性。验证方法:

# 在preprocess.py中添加覆盖率检查 all_chars = set() for file in os.listdir('./reports/'): if file.endswith('.txt'): with open(f'./reports/{file}', 'r', encoding='utf-8') as f: all_chars.update(f.read()) missing_chars = all_chars - set(char_to_idx.keys()) if missing_chars: print(f"警告:以下字符未在字典中,将被替换为<UNK>:{missing_chars}") # 自动扩展字典(示例) for char in missing_chars: if len(char_to_idx) < 65535: # 防止溢出 char_to_idx[char] = len(char_to_idx) + 1

5.2 验证LSTM隐藏层维度与序列长度的匹配关系

lstm_units=64适用于MAX_LEN=1000,但若你将MAX_LEN改为2000,必须同步增大lstm_units(建议≥128),否则模型容量不足,无法建模长距离依赖。经验公式:lstm_units ≥ MAX_LEN / 15。可通过以下代码快速验证当前配置是否合理:

# 在模型编译后添加诊断 model.summary() # 查看LSTM层参数量:params = 4 * lstm_units * (embedding_dim + lstm_units + 1) # 若params < 100000,说明容量可能不足

5.3 使用混淆矩阵定位系统性偏差

运行python evaluate.py --model_path ./models/lstm_best.h5生成完整混淆矩阵。重点关注两类错误:

  • 负向→中性误判:通常因研报使用“压力犹存但韧性较强”等平衡性表述,此时应增加训练集中此类样本,并在损失函数中为负向类设置更高权重(class_weight={0:1.0, 1:1.5, 2:1.0});
  • 正向→负向误判:多见于“短期承压,长期向好”类转折句,需检查clean_text()是否错误移除了“但”“然而”等转折连词——这些词是情感反转的关键锚点。

提示:evaluate.py输出的混淆矩阵中,行代表真实标签,列代表预测标签。若第1行(负向)第2列(中性)数值显著高于其他位置,即确认存在系统性负向漏判,需针对性优化文本清洗规则或增加对抗样本。

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

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

进口编码器停产替代方案:选型参数与调试实战指南

做设备维护和自动化改造这些年&#xff0c;我最怕听到的一句话不是“设备坏了”&#xff0c;而是“那个型号的编码器停产了”。编码器看起来只是电机尾部的一个小部件&#xff0c;但它一停&#xff0c;整条产线都可能跟着停。尤其是进口编码器——多摩川、海德汉、安川、松下这…

作者头像 李华
网站建设 2026/9/14 18:25:55

Arnis:把真实世界的任意地点 1:1 还原进 Minecraft 的免费开源工具

Arnis:把真实世界的任意地点 1:1 还原进 Minecraft 的免费开源工具 【免费下载链接】arnis Generate any location from the real world in Minecraft with a high level of detail. 项目地址: https://gitcode.com/GitHub_Trending/ar/arnis Arnis 是一款基于 OpenStre…

作者头像 李华
网站建设 2026/9/14 18:24:55

如何做放弃遗产公证书:费用、时间与常见问题解答

前言 遗产继承是很多家庭都会遇到的财产处理问题&#xff0c;部分继承人因家庭协商、资产规划、规避债务等原因&#xff0c;会选择自愿放弃遗产继承权。但绝大多数普通人都是初次办理放弃遗产公证&#xff0c;普遍存在不懂流程、不清楚材料、不了解费用时效、容易踩法律误区等…

作者头像 李华
网站建设 2026/9/14 18:24:29

Cursor 只支持 MCP tools?Claude 连上 TaoToken 后把 Resources/Prompts 也跑通

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

作者头像 李华
网站建设 2026/9/14 18:22:49

SSM框架实现高并发航空订票系统核心技术解析

1. 项目概述&#xff1a;SSM航空客运订票系统核心解析这个基于SSM框架的航空客运订票系统&#xff0c;是我在去年为某中型航空公司交付的实战项目。系统采用经典的SpringSpringMVCMyBatis技术栈&#xff0c;实现了从航班查询到出票完整的业务流程。与市面上那些学生作品不同&am…

作者头像 李华