ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

A股投资助手实战:研报爬虫+实时行情+RAG智能分析

A股投资助手实战:研报爬虫+实时行情+RAG智能分析 说实话我一开始做这个项目的动机特别朴素每天打开券商App看到几十上百份行业研报标题眼花缭乱等我一篇篇点开看完行情早就走完一轮了。花了两个星期把行业研报爬虫 实时行情 智能对话分析这套A股投资助手从0到1搭起来之后才发现真正值钱的不是那些花哨的AI功能而是把研报整合工具、股票行情分析系统、投资数据知识库这四件套跑通的那套工程能力。这篇文章不打算写成一个项目说明书我就按自己实际动手踩坑的顺序把整套系统的设计思路、核心代码、参数选择和翻车现场都摊开聊。无论你是想给自家投研流程提效的个人投资者还是正在找爬虫/行情/知识库练手项目的开发者这套东西的完整链路都能直接参考着抄。1. 整体架构从零搭建A股投资助手的模块划分先说结论这套系统本质上是一个数据管道不是一个大模型应用更不是一套荐股软件。它的核心价值在于把低价值密度、高信息量的数据研报内容和高价值密度、低延迟的数据实时行情汇集到同一个分析入口里让找信息变成问问题。1.1 四大核心模块的定位与关系我把系统拆成四个相对独立的模块先看整体关系模块职责技术要点输出物研报采集爬虫定时抓取行业研报的元数据与正文内容requests、Selenium兜底、PDF解析结构化研报记录实时行情模块获取A股沪深市场的行情快照与历史日线akshare/tushare类公共数据源行情表、复权因子表数据知识库统一存储研报、行情数据提供检索能力MySQL 向量检索可查询的结构化数据智能对话分析结合检索到的研报内容和实时行情生成回答大模型API RAG检索链路带来源引用的分析回答这四块是依次依赖的没有爬虫知识库是空的没有行情模块分析层的回答就是活在昨天没有检索链路大模型就开始一本正经地胡说八道。1.2 为什么非要爬虫知识库RAG而不是直接丢给大模型这里多说几句设计动机。刚开始有人劝我你直接把所有研报文本拼进Prompt里扔给大模型不就行了我实测过最多只能拼进一小部分一次问答的成本高得离谱而且回答质量随着文本变长急剧下降。更关键的是大模型的训练数据是有截止时间的它对最近三个月发布的研报内容一无所知。所以走了RAG路线先把研报切成小块、做向量化存进知识库用户提问时先检索出最相关的几段研报原文再把这些原文片段和实时行情快照一起作为上下文传给大模型。这样回答有依据、有出处还不会爆Prompt长度。2. 行业研报爬虫数据源选型与反爬实战爬虫是整套系统的上游水源也是最容易出现问题的环节。我一开始对着数据源选型纠结了很久后来确定了聚合公开信息的思路不碰任何需要登录权限的非公开接口。2.1 数据源怎么选从券商公开研报中心下手我用的是多家券商官方研报中心对外公开的研报列表接口和巨潮资讯的公开公告接口。这些接口初衷是给用户查询公开研报信息用的返回的数据字段通常包含研报标题、发布日期、机构名称、行业分类、评级、摘要部分还会给出目标价。核心抓取逻辑不复杂就是按时间范围拉取列表页再看详情页是否有PDF正文可下载。我用requests轮询关键点在于Headers伪装不能太敷衍import requests import time HEADERS { User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36, Accept: application/json, text/plain, */*, Accept-Language: zh-CN,zh;q0.9, Referer: https://data.eastmoney.com/report/, } def fetch_report_list(page: int, date_from: str, date_to: str): 抓取指定日期范围的研报列表页 url https://reportapi.eastmoney.com/report/list params { industryCode: *, # 全部行业 pageSize: 50, industry: *, rating: *, ratingChange: *, beginTime: date_from, endTime: date_to, pageNo: page, fields: , qType: 0, orgCode: , code: *, rcode: , p: page, pageNo: page, } r requests.get(url, paramsparams, headersHEADERS, timeout10) if r.status_code 200: return r.json() return None这里有个经验这类公开列表接口返回的JSON里每条记录通常包含infoCode研报唯一编号、title、orgName、publishDate这些字段infoCode很重要它是后面拼详情页URL的关键参数。2.2 研报正文抽取PDF解析才是真正的藏坑区列表页抓到的是标题和摘要但要做知识库必须拿到正文。实际操作里研报详情页会提供一个PDF文件地址下载后用pdfplumber或PyMuPDFfitz解析。pip install pdfplumber pymupdf解析时第一个大坑是双栏排版。券商研报PDF大量使用左右双栏直接按页面顺序抽取文字读出来全是乱的。处理办法是先用pdfplumber的extract_words拿到每个单词的坐标按x坐标分成左栏和右栏再分别按y坐标排序拼接import pdfplumber def extract_two_column_text(pdf_path: str) - str: 双栏PDF正文抽取 text_chunks [] with pdfplumber.open(pdf_path) as pdf: for page in pdf.pages: words page.extract_words( use_text_flowFalse, keep_blank_charsFalse, extra_attrs[size] ) if not words: continue # 按页面中间线分左右栏 mid_x page.width / 2 left [w for w in words if w[x0] mid_x] right [w for w in words if w[x0] mid_x] # 各自按y坐标排序 left.sort(keylambda w: (round(w[top] / 10), w[x0])) right.sort(keylambda w: (round(w[top] / 10), w[x0])) for col in (left, right): line_buf {} for w in col: line_key round(w[top] / 10) line_buf.setdefault(line_key, []).append(w[text]) for line_key in sorted(line_buf.keys()): text_chunks.append( .join(line_buf[line_key])) return \n.join(text_chunks)这段代码里的round把top坐标按10像素分组是为了解决同一行文字微小的y坐标抖动。实测下来对于排版规范的研报PDF抽取准确率在90%以上。另外所有下载的PDF都建议先存一份原始文件不要只存解析后的文本。原因很实际后面如果你的解析代码想换策略或者想用OCR补漏原文还在不用重新抓一遍。2.3 反爬与封禁的实测教训频率控制比代理池重要这半年我踩过最大的坑不是验证码而是自己手贱把频率调太高导致IP被封了整整一天。scrapy这类框架的并发配置天然适合大规模采掘但研报数据学不来的地方在于它根本不需要急每天新增研报就几十到一两百篇完全没必要高并发。我的实测做法是单线程 time.sleep(1.5~3秒)随机延时列表页和详情页分开限速每抓完50条记录主动sleep 5秒相当于给目标服务器喘息时间一旦遇到HTTP 403、449或验证码跳转立即停止当前循环进入退避状态而不是换UA硬冲单日总量控制在1万次请求以内这远低于触发风控的量级。重要提示不要为了研报采集去搞IP代理池轮换更不要并发攻击性采集。研报抓取本身不是时效性极强的数据每天定时增量拉取完全足够激进爬取只会让你的IP被网站风控系统重点标注。还有一点容易被忽略解析字段时不要只盯着标题和评级机构在研报里给出的目标价、行业评级变动、核心标签才是结构化分析的富矿。我统一把这些抽成JSON字段存入MySQL喂给后续分析层用。3. 实时行情模块接口选型与数据可靠性保障实时行情是整个系统的活水。研报告诉你一家公司的长期逻辑行情告诉你市场当下是否在验证这个逻辑。两者缺一不可。3.1 行情数据源选型A股开源库的横向对比我实测对比过主流的几个A股数据方案直接给结论数据源免费额度实时性字段丰富度稳定性akshare免费无需注册约1~3秒延迟高覆盖面广中依赖上游免费接口tushare pro注册有积分门槛部分接口积分要求高高较高baostock免费仅日线级别一般高东方财富公开API免费无需注册约1~3秒延迟高含盘口五档中我的选择是主用akshare/东方财富公开APItushare作为日线数据和基本面数据的补充源。原因很现实akshare包了一堆公共接口个人开发者接入成本最低可用的行情数据字段够全。3.2 分钟级行情更新策略增量分表实时行情不需要把所有历史数据都存在内存里。我设计了两张核心表quote_snapshot存最近一段时间的分钟快照stock_daily存日线数据。CREATE TABLE quote_snapshot ( ts_code VARCHAR(12) COMMENT 股票代码, trade_time DATETIME COMMENT 快照时间, price DECIMAL(10,2) COMMENT 最新价, pct_change DECIMAL(6,3) COMMENT 涨跌幅%, volume BIGINT COMMENT 成交量(手), amount DECIMAL(16,2) COMMENT 成交额(元), turnover_rate DECIMAL(6,3) COMMENT 换手率%, PRIMARY KEY (ts_code, trade_time) ) COMMENT 行情快照分表;增量更新的思路是交易日里每5分钟拉一次自选股和板块指数快照写入当日分表收盘后跑一个日线汇总任务把当天快照聚合进stock_daily。这样既保证盘中能看到实时状态又不会因为频繁写全量数据把磁盘撑爆。3.3 复权因子与数据对齐研报目标价对比的大前提做研报分析时最坑的一个细节是复权价和未复权价混用。研报里写目标价28元是基于报告发布日的未复权价格而你行情库里如果存的是前复权价格遇到中间经历过分红送转的股票这28元和当时的股价压根不在同一个坐标系里。所以我给stock_daily表加了一个adjust_factor字段每次拉日线时把复权因子一起存下来。对比研报目标价和当前价时统一换算成报告日视角def calc_target_upside(current_price, adj_factor_today, base_price, adj_factor_base): 计算相对研报发布日的目标空间 base_price是报告发布日股价adj_factor_base是当天的复权因子 adjusted_base base_price * adj_factor_base adjusted_now current_price * adj_factor_today upside (adjusted_base / adjusted_now - 1) * 100 return upside这样算出来的目标空间才是有意义的。我第一次跑出来某只股票距目标价还有80%空间吓一跳排查了半天发现是复权口径不一致导致的错误信号。3.4 边界场景处理停牌、涨停、新股行情模块最容易出隐性bug的边界情况有三个首先是停牌股票它的最新价可能是旧价格成交量是0涨跌幅也是0如果不加状态判断系统会把一只停牌三个月的股票当成横盘震荡。我做法是额外维护一张trade_status表每日更新停牌股票名单。其次是涨停股东财的公开API里封单量字段和涨停状态标识对短线情绪分析很有价值但如果你用的是普通的日线接口涨停和普通大阳线长得一模一样。需要额外拉资金流或盘口字段来辅助判断是否封死涨停。最后是新股上市前5日没有涨跌幅限制如果用统一规则计算距离均线的乖离率新股数据会剧烈失真。我直接在数据层加了个list_days字段过滤掉上市不足60日的次新股。4. 智能对话分析研报知识库的RAG实践前面的工作都是在攒数据到了这一步才真正进入分析环节。智能对话模块是我投入时间第二多的地方仅次于爬虫的数据清洗。4.1 RAG链路一次问答请求的完整流转用户问新能源车产业链现在的景气度怎么样系统实际做的是把问题转换成向量在研报向量库中检索最相关的4~6个文本片段同时从行情模块拉取新能源车ETF、宁德时代、比亚迪等核心标的的最新价和涨跌幅把检索到的研报片段 行情快照 用户问题一起塞进Prompt交给大模型返回带引用来源的答案。这里最关键的设计是Prompt里明确要求模型区分研报观点和行情事实回答时不能把两者混在一起。否则大模型很容易把研报里的主观判断当成已发生的行情事实。4.2 切块策略400字是最佳实践段落独立存储研报正文的切块直接影响检索质量。我试过把整篇研报作为一条向量记录效果很差因为检索出来的片段包含了太多无关内容而大模型会根据这些噪音生成不精准的回答。最终参数是正文按450~500字切块每个相邻块之间重叠80字保证跨块的关键信息不丢失。标题摘要评级单独作为一个短块存储这是最高权重的检索对象。每个文本块入库时都要带上研报ID、段落序号、发布日期以确保引用来源可追溯。4.3 Embedding模型与向量库选型Embedding选型我前后换过三次最终用的是bge-small-zh-v1.5的本地部署版本。如果你对精度有更高要求可以换bge-large-zh但中文研报场景下小模型的语义理解已经足够用而且向量维度低检索速度快很多。向量库的选择取决于你的数据量级方案数据量优势劣势Chroma万级以下零部署嵌入式运行并发查询能力弱Milvus十万级以上生产级性能部署运维成本高MySQLvector插件万级~十万级复用现有MySQL检索能力不如专用向量库我的知识库目前有近三万条研报片段用的还是Chroma单次检索耗时在80毫秒左右完全够用。4.4 Prompt设计约束模型不越边界这是问答效果最直接的调优点。我最终固定使用的系统提示词核心段落写明白了三点你是一名A股行业研究员助手。回答时优先依据提供的研报片段和行情数据不得编造研报中没有出现的具体数据如果研报片段不足明确说明当前知识库中未检索到相关内容。研报观点与实时行情分开呈现不构成投资建议。加上这个约束后模型答非所问的频率明显下降。这里特别提一下不要省略不构成投资建议这句话它不只是合规需要更是帮助模型在回答时区分事实判断和观点判断的边界。5. 实操复盘一次完整的研报分析请求链路前面把模块拆开讲了这里我把一条真实的请求链路串起来跑一遍你会发现整套系统其实没那么多玄学重点是每个环节的时效和容错设计。5.1 全流程时间线从提问到回答的3秒用户在前端输入新能源汽车产业链最近三个月的研报主要观点是什么具体处理过程如下请求到达API层我用的是FastAPI先做参数校验和用户行为记录调用检索模块本地Embedding模型把问题向量化在Chroma中检索top_k6的研报片段耗时约200ms同时行情模块从Redis中直接取新能源车相关标的的最新快照Redis中已有5分钟前刷新的缓存耗时约20ms组装Prompt把6段研报片段、行情快照、用户问题按模板拼接调用大模型API流式返回答案平均耗时2.5秒左右后端把回答中引用的研报片段ID转成来源链接指向原始研报地址返回前端。整条链路耗时控制在3秒内用户体感基本可以接受。实际上第5步大模型API耗时占总耗时的80%以上其他环节的优化空间不大。5.2 定时任务编排别让爬虫和行情抢资源任务调度我用的是APScheduler分三层安排每5分钟拉取自选股和关注的行业板块指数快照写入Redis和quote_snapshot当日表每60分钟增量抓取研报列表下载新增研报PDF解析正文并写入临时表每天凌晨2点把当天新入库的研报文本切块、生成向量、写入向量库同时清理异常数据和重复记录。这里有一个容易忽视的细节研报采集尽量避开交易时段高峰。虽然研报发布通常集中在早上8点到10点但你在这个时间去抓研报目标网站同时扛着大量交易用户的数据请求更容易触发限流。我策略是每15分钟增量检查一次如果检测到当天新增研报数量少于预期就减少抓取频率把高负载任务延后到午休或收盘后再跑。5.3 检索效果与回答质量的自测方法想判断RAG链路有没有调好不要只看一两个例子我每次改动后都会跑一组固定的测试集。自测集覆盖几类典型问题维度行业景气类光伏行业目前的供需格局如何个股逻辑类最近三个月有哪些机构调高了对某消费龙头的评级数据验证类某公司最新研报给出的目标价对应多少上涨空间边界试探类那只股票现在能不能买应拒绝回答并转为信息呈现特别关注第四类问题模型如果给出明确的买卖建议说明Prompt约束失效了要立刻调整。6. 常见问题排查与避坑清单实录这部分必须写因为这些坑我全都踩过写出来能帮后面做类似系统的人省下大量调试时间。6.1 爬虫模块的经典故障症状1列表页能抓到详情页拿不到正文。原因是详情页URL拼接参数名找错了。不同券商研报中心的信息代号规则不统一有的用infoCode有的用artCode还有的需要在URL里加orgId。我的排查方法是浏览器开发者工具里手动打开一篇研报的详情页对比请求参数和列表接口返回的字段确认对应关系后再写代码。症状2PDF解析出来一堆乱码。这是字体编码问题。少数研报PDF用了自定义字体编码pdfplumber直接提取会拿到乱码解决办法是改用PyMuPDF的page.get_text(text, flagspdf.TEXT_PRESERVE_WHITESPACE)重试如果还不行就上OCR兜底PaddleOCR。实测下来需要OCR的研报比例不到5%不用一开始就接OCR。症状3研报抓完去重把同一报告不同版本弄混了。券商在研究部更新研报时有时会做更新版发布标题一样但发布时间不同。如果单纯用标题做去重会把更新版丢掉。正确做法是机构ID报告标题发布日期三重组合做MD5更新版视为新增记录。6.2 行情模块的隐蔽错误症状盘中行情突然全部停住不动。排查后发现是上游接口返回了空数据但程序没有报错只是把旧数据重复写了一遍。解决办法是加一个数据新鲜度检查如果某只股票连续三次快照的价格完全相同而且当前不是停牌状态就视为数据异常切换到备用行情源。症状日线数据里复权价格跳动异常。这是复权因子的对齐问题。不同接口的复权因子基日算法不同我把stock_daily表里所有复权字段统一成后复权再换算前复权的算法后异常就消失了。强烈建议只依赖一个主数据源的复权因子不要混用。症状涨跌停状态与行情快照不一致。行情快照里涨停但封单量为0说明已经开板或即将开板这种短瞬状态用5分钟轮询很容易漏。我的策略是交易日期间对自选股列表里的高波动标的振幅超过5%提高采样频率到1分钟虽然接口压力会增加但数据完整性提升明显。6.3 智能问答的翻车现场症状1检索到的研报片段与问题完全无关。这通常是Embedding模型覆盖不足导致的。排查时先打印出检索到的Top-5文本看看语义相近度如果明显不相关检查切块时是否把标题和摘要信息混入了正文块导致向量方向跑偏。我在切块时把标题块的权重提高到1.5倍问题解决了。症状2回答信誓旦旦但数据是错的。这是RAG链路里最危险的错误。大模型很可能在上下文充足的情况下把检索片段里的研报数据和行情快照数据混在一起编造出精确数字。我在Prompt里加了所有具体数字必须与研报原文或行情快照一致无法确认时标注待核实后错误率大幅下降。症状3大模型API超时导致整个请求失败。这个问题在产品化阶段必须处理。FastAPI层要设置独立的超时时间大模型调用超时后不要直接报错而是降级返回检索到的研报摘要片段至少让用户拿到原始信息。6.4 数据合规与安全红线做A股相关工具有几条红线必须反复强调这是我做项目时给自己定的规矩第一只采集公开渠道的信息不碰任何需要破解、越权访问的数据。爬虫严格遵守目标网站的robots协议和访问频率限制数据仅用于个人学习研究不对外售卖或商用分发。第二不提供任何荐股类输出。系统里的所有内容本质上是信息的整合与检索最终决策必须由用户完成。UI上明确标注不构成投资建议。第三注意数据的版权问题。研报原文属于发布机构版权做知识库检索引用时只展示摘要和结论片段完整原文跳转原始链接查看不把全文字段存储后对外展示。6.5 避坑清单速查表场景错误做法正确做法研报抓取频率并发拉取、高频率轮询单线程随机延时每日总量控制PDF双栏解析直接按页顺序拼接按坐标分栏再排序拼接复权价格比较混用不同数据源复权因子统一复权口径存储复权因子停牌股票处理当成横盘数据独立维护交易状态表行情快照空数据直接写库增加新鲜度校验与备用源RAG检索无关盲目调top_k检查切块与标题独立加权大模型编数字完全信任回答Prompt强约束来源引用待核实标注数据合规直接存全文对外展示只存摘要完整原文跳转源站写在最后做完这套系统后我的几点体会如果让我重来一次我会把更多时间花在数据质量校验上而不是一开始就忙着调大模型。实际上在最终问答效果里检索质量对答案质量的贡献远大于模型本身的推理能力。有个很直观的经验当你的问答结果不理想时先检查检索到的片段是否精准在七八成情况下问题出在切块策略和数据清洗上而不是提示词没写对。另外这套系统到现在也只覆盖了研报和行情的整合分析下一步我打算把公告异动监控、持仓提醒和概率回测接进去让数据管道复用得更充分。做这类工具最怕的就是一个功能做完就扔把基础数据层做扎实后面每加一个新功能都是水到渠成的事。数据来源可靠、更新及时、引用可追溯这三点永远比算法花哨更重要。
返回列表