资讯动态

股吧舆情爬虫实战:Python四层架构与金融情感分析

发布时间:2026/9/10 13:01:57 来源:尧图企业网站定制
简介这是一套面向计算机、人工智能及金融相关专业学生与教师的实战型课程资源聚焦股市舆情挖掘与分析场景提供从股吧数据采集到情感识别与热点追踪的完整技术链路。资源包含12个文件涵盖4个核心Python脚本爬虫、解析、MongoDB存储及主控逻辑、6张关键界面与结果截图如发帖/评论界面、进度与报错展示、1个JS反爬绕过脚本及1份Markdown文档说明压缩包仅3.85MB轻量易部署。已有146人学习下载适合作为期末大作业、毕业设计选题或Python爬虫与NLP进阶实践项目。所有代码均经实测可运行附带真实采集数据与清晰执行流程特别提供反爬处理细节、情感分析简易实现方案及热点词频统计逻辑便于初学者理解工程落地难点也支持中高级用户基于模块化结构快速二次开发。1. 为什么股吧舆情不能只靠“关键词搜索”——一个真实可跑的东方财富股吧爬虫项目你有没有试过在股吧里搜“利好”“涨停”“抄底”结果翻了二十页全是水帖、段子和情绪宣泄单纯用关键词匹配做舆情统计误差率可能超过65%。这个项目不是教你怎么绕过反爬而是用真实调试过的 Python 爬虫链路把股吧页面结构、发帖/评论 DOM 规律、动态加载特征全拆开——它抓的是带时间戳、用户ID、楼层序号、文本长度、图片标记的原始交互数据再喂给轻量级情感分析模型TextBlob 自定义金融词典最后生成可排序的热点话题图谱。适合计算机、AI、金融工程方向的学生做课程设计或毕设代码结构清晰crawler → parser → mongodb → analysis 四层解耦所有依赖版本锁定在 requirements.txt连 MongoDB 连接失败时的 fallback 日志都打了 trace_id。如果你正卡在“爬下来但解析不准”“能存但不会算情绪分”“有数据但看不出热度变化”这个包里的main.py启动逻辑、parser.py的 XPath 定位策略、以及README.md里标注的 7 处易错点就是你缺的那块拼图。2. 股吧页面结构与反爬对抗从静态 HTML 到动态接口的三层解析策略东方财富股吧的页面呈现并非单一模式首页列表是服务端渲染的静态 HTML但点击某条帖子进入详情页后评论区采用 AJAX 异步加载而最新发帖则依赖 WebSocket 推送。本项目没有强行用 Selenium 模拟全部操作而是分层处理——对列表页用requestsBeautifulSoup解析对详情页评论用requests直接调用push2接口非官方文档接口但已验证稳定对实时发帖则通过监听https://guba.eastmoney.com/topic/xxxxx.html页面的script标签内嵌 JSON 数据提取。这种混合策略既规避了浏览器自动化带来的性能损耗又绕开了前端 JS 渲染导致的 XPath 失效问题。2.1 列表页抓取XPath 定位与分页参数构造股吧列表页 URL 形如https://guba.eastmoney.com/list,600519,f_1.html其中600519是股票代码f_1表示按“最新”排序。关键在于识别每页 80 条帖子的 DOM 结构# crawler.py 中的列表页解析核心片段 def parse_list_page(html_content: str) - List[Dict]: soup BeautifulSoup(html_content, lxml) items [] for li in soup.select(ul#stockList li): try: # 股票代码从URL中提取标题和链接从a标签获取 title_tag li.select_one(a.title) if not title_tag: continue url title_tag.get(href, ) title title_tag.get_text(stripTrue) # 发帖时间藏在 span.time 元素中格式为今天 14:23或05-12 09:17 time_tag li.select_one(span.time) post_time time_tag.get_text(stripTrue) if time_tag else # 阅读数和评论数在 div.count 下需分离数字 count_tag li.select_one(div.count) if count_tag: nums [int(x) for x in re.findall(r\d, count_tag.get_text())] read_count nums[0] if len(nums) 0 else 0 comment_count nums[1] if len(nums) 1 else 0 else: read_count comment_count 0 items.append({ title: title, url: url, post_time: post_time, read_count: read_count, comment_count: comment_count, crawl_timestamp: datetime.now().isoformat() }) except Exception as e: logger.warning(f解析列表项失败: {e}) return items注意ul#stockList li是股吧列表页的稳定容器选择器经测试在 2024 年 Q2 版本中未变更。若未来失效优先检查#stockList是否被替换为#articleList或增加 class 名称前缀如js-stock-list而非直接改用模糊的div.article-item。2.2 详情页评论抓取逆向push2接口与请求头构造股吧详情页评论不走常规 REST API而是调用https://guba.eastmoney.com/topic/xxxxx.html页面内嵌的push2接口其真实请求地址形如https://guba.eastmoney.com/topic/push2?code600519topicidXXXXXpage1perpage30该接口返回 JSON字段包括contentHTML 片段、nickname、level用户等级、timeUnix 时间戳。关键难点在于请求头必须携带Referer和User-Agent且Referer必须与目标股吧页面完全一致含协议、域名、路径否则返回空数据# crawler.py 中 push2 接口调用示例 def fetch_comments_by_push2(topic_id: str, stock_code: str, page: int 1) - Dict: url fhttps://guba.eastmoney.com/topic/push2 params { code: stock_code, topicid: topic_id, page: page, perpage: 30 } headers { User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124.0.0.0 Safari/537.36, Referer: fhttps://guba.eastmoney.com/topic/{topic_id}.html, # 必须精确匹配 Accept: application/json, text/plain, */*, X-Requested-With: XMLHttpRequest } try: resp requests.get(url, paramsparams, headersheaders, timeout10) resp.raise_for_status() return resp.json() except requests.exceptions.RequestException as e: logger.error(fpush2 接口请求失败 topic_id{topic_id} page{page}: {e}) return {data: []}2.2.1 topic_id 提取逻辑从 URL 到 DOM 的双重校验topic_id不是 URL 路径中的数字而是页面script标签内var topicId 123456789;的值。parser.py中使用正则提取并辅以 URL 路径回退机制# parser.py 中 topic_id 提取函数 def extract_topic_id_from_html(html_content: str, url: str) - str: # 优先从 script 标签中提取 match re.search(rvar\stopicId\s*\s*[\](\d)[\];, html_content) if match: return match.group(1) # 备用从 URL 路径提取如 /topic/123456789.html path_match re.search(r/topic/(\d)\.html, url) if path_match: return path_match.group(1) raise ValueError(f无法从 HTML 或 URL 中提取 topic_id: {url[:50]}...)提示push2接口无频率限制但单次最多返回 30 条需循环调用page1,2,3...直至data字段为空。项目中crawler.py的crawl_topic_comments()函数已内置分页终止判断。2.3 动态发帖监听DOM 内嵌 JSON 的增量捕获股吧首页的“最新发帖”区域由 JavaScript 动态注入其数据源是页面底部script标签中的一段 JSON 数组形如script typetext/javascript var latestTopics [{id:123456,title:茅台要出新品,author:股神小张,time:2024-05-20 10:22:31,read:1245,comment:89}, ...]; /scriptparser.py使用re.findall(rvar\slatestTopics\s*\s*(\[.*?\]);, html)提取并解析避免了等待 JS 执行的开销。该策略比 Puppeteer 更轻量且在main.py的--modelive参数下启用支持每 30 秒轮询一次首页仅对比id字段做增量去重。3. 数据清洗与情感分析金融语境下的 TextBlob 适配与词典增强爬取的数据若不经清洗直接喂给通用情感分析模型准确率会断崖式下跌——股吧里“跌停”是负面“跌停价买入”却是极端看多“缩量”常表谨慎“缩量涨停”却属强势信号。本项目采用“基础模型 金融词典微调”双轨策略先用 TextBlob 计算原始 polarity极性和 subjectivity主观性再根据自定义词典对关键词做权重偏移最终生成sentiment_score-1.0 ~ 1.0和sentiment_labelpositive/neutral/negative。3.1 文本清洗流水线去除干扰符号与标准化表达股吧文本常见干扰包括用户昵称提及如东方不败表情符号 Unicode如\U0001f602股票代码括号格式贵州茅台(600519)→贵州茅台重复标点→!???→?parser.py中的clean_text()函数按顺序执行# parser.py import re from textblob import TextBlob def clean_text(text: str) - str: if not isinstance(text, str): return # 1. 去除 提及 text re.sub(r\w, , text) # 2. 去除表情符号保留中文、英文字母、数字、常用标点 text re.sub(r[^\u4e00-\u9fa5a-zA-Z0-9\s\.\!\?\,\;\:\-\_\(\)\[\]\{\}], , text) # 3. 标准化股票代码括号如 (600519) → 空 text re.sub(r\(\d{6}\), , text) # 4. 合并连续空白符 text re.sub(r\s, , text).strip() # 5. 简化重复标点最多保留2个 text re.sub(r([!?.])\1{2,}, r\1\1, text) return text3.1.1 为什么不用正则直接删所有标点因为!和?在股吧语境中承载强烈情绪如“见顶了” vs “见顶了。”删除会损失 polarity 信号。项目保留!?.,;:-_()[]{}仅过滤 emoji 和控制字符。3.2 情感词典构建覆盖 137 个金融场景关键词项目附带finance_sentiment_dict.json包含三类词条极性反转词如“缩量”默认中性但“缩量涨停”整体为 positive程度副词加权如“超级”“史诗级”使 polarity ×1.5“略”“稍”使 ×0.7否定修饰链如“不是没可能”需识别双重否定 → positive。词典结构示例{ 缩量涨停: {polarity_offset: 0.4, label: positive}, 超级: {polarity_multiplier: 1.5}, 不是没: {polarity_offset: 0.3, negation: true} }analysis.py中的calculate_sentiment()调用流程def calculate_sentiment(text: str) - Dict: cleaned clean_text(text) if not cleaned: return {sentiment_score: 0.0, sentiment_label: neutral} # Step 1: TextBlob 基础分值 blob TextBlob(cleaned) base_polarity blob.sentiment.polarity base_subjectivity blob.sentiment.subjectivity # Step 2: 词典增强 enhanced_polarity base_polarity for term, config in FINANCE_DICT.items(): if term in cleaned: if polarity_offset in config: enhanced_polarity config[polarity_offset] if polarity_multiplier in config: enhanced_polarity * config[polarity_multiplier] if config.get(negation, False): # 简单否定检测检查 term 前是否有“不”“没”“未” start_idx cleaned.find(term) if start_idx 0 and cleaned[start_idx-1] in 不没未: enhanced_polarity * -1.0 # Step 3: 截断并打标 score max(-1.0, min(1.0, enhanced_polarity)) if score 0.2: label positive elif score -0.2: label negative else: label neutral return { sentiment_score: round(score, 3), sentiment_label: label, base_polarity: round(base_polarity, 3), subjectivity: round(base_subjectivity, 3) }提示FINANCE_DICT在analysis.py开头通过json.load(open(finance_sentiment_dict.json))加载确保路径正确。若新增词条需同步更新README.md中的词典维护说明。3.3 热点话题聚类TF-IDF 余弦相似度的轻量实现舆情分析不止于单条情绪打分更要识别“哪些讨论正在升温”。项目用sklearn.feature_extraction.text.TfidfVectorizer对标题首条评论做向量化再用scipy.spatial.distance.cosine计算相似度设定阈值 0.7 合并话题簇# analysis.py from sklearn.feature_extraction.text import TfidfVectorizer from scipy.spatial.distance import cosine import numpy as np def cluster_hot_topics(comments: List[Dict], threshold: float 0.7) - List[Dict]: # 构建文本集标题 首条评论若存在 texts [] for c in comments: title c.get(title, ) first_comment c.get(comments, [{}])[0].get(content, ) if c.get(comments) else texts.append(f{title} {first_comment}) if len(texts) 2: return [{topic: t, count: 1, sentiment_avg: 0.0} for t in texts] # TF-IDF 向量化 vectorizer TfidfVectorizer(max_features1000, stop_words[的, 了, 和, 是, 我, 有, 也, 不, 人, 都, 一, 一个, 上, 也, 很, 到, 说, 要, 去, 你, 会, 着, 没有, 看, 好, 自己, 这]) tfidf_matrix vectorizer.fit_transform(texts) # 两两计算余弦距离构建簇 clusters [] used [False] * len(texts) for i in range(len(texts)): if used[i]: continue cluster [i] used[i] True for j in range(i1, len(texts)): if used[j]: continue dist cosine(tfidf_matrix[i].toarray()[0], tfidf_matrix[j].toarray()[0]) if dist threshold: # 距离越小越相似 cluster.append(j) used[j] True # 计算簇内平均情绪分 cluster_scores [comments[k].get(sentiment_score, 0.0) for k in cluster] clusters.append({ topic: texts[cluster[0]][:30] ..., # 截断显示 count: len(cluster), sentiment_avg: round(np.mean(cluster_scores), 3), ids: [comments[k].get(id, ) for k in cluster] }) return sorted(clusters, keylambda x: x[count], reverseTrue)3.3.1 为什么不用 LDA 或 BERTLDA 需大量语料训练且主题数难调BERT 推理慢、显存高不适合学生作业级部署。TF-IDF 余弦在千条级数据下耗时 200ms且stop_words已预置金融高频停用词如“主力”“散户”“庄家”不作为停用平衡了精度与效率。4. MongoDB 存储设计与查询优化应对股吧数据的高写入低查询特性股吧数据具有典型“写多读少”特征单日爬取可达 5 万 条帖子、20 万 条评论但分析时通常只查最近 7 天或指定股票。本项目 MongoDB Schema 设计遵循两点原则写入不校验查询建索引。所有字段均设为nullable避免因某字段缺失导致插入失败关键查询字段stock_code,post_time,sentiment_label强制建立复合索引。4.1 Collection 结构与字段说明mongodb.py定义两个主 collectionCollection用途关键字段含类型posts存储帖子元数据stock_code(str),title(str),url(str),post_time(datetime),read_count(int),comment_count(int),crawl_timestamp(datetime),topic_id(str)comments存储评论详情topic_id(str),nickname(str),content(str),post_time(datetime),sentiment_score(float),sentiment_label(str),crawl_timestamp(datetime),floor(int)注意post_time字段存储为datetime类型非字符串便于$gte查询sentiment_score保留 3 位小数满足分析精度需求。4.2 索引创建脚本与性能验证mongodb.py中的init_indexes()函数在首次连接时自动创建索引# mongodb.py def init_indexes(db): # posts 集合按股票时间范围查询最频繁 db.posts.create_index([(stock_code, 1), (post_time, -1)]) # comments 集合按帖子ID情绪标签聚合 db.comments.create_index([(topic_id, 1), (sentiment_label, 1)]) # 补充按时间范围快速清理过期数据可选 db.posts.create_index([(crawl_timestamp, 1)], expireAfterSeconds2592000) # 30天TTL4.2.1 索引效果实测对比10 万条数据查询语句无索引耗时有索引耗时加速比db.posts.find({stock_code:600519, post_time:{$gte:ISODate(2024-05-01)}})1280 ms18 ms71×db.comments.find({topic_id:123456789, sentiment_label:positive})940 ms12 ms78×索引创建后main.py --stock600519 --days7的完整流程爬取解析存库分析从 42 分钟降至 6 分钟。4.3 数据导出与可视化CSV 与简易热度趋势图项目提供export_to_csv.py脚本支持按股票、日期范围、情绪标签导出结构化数据python export_to_csv.py --stock 600519 --start 2024-05-01 --end 2024-05-10 --label positive --output positive_600519.csv导出字段包括title,url,post_time,read_count,comment_count,sentiment_score,sentiment_label,avg_comment_sentiment该帖子下所有评论的平均分。此外plot_hot_trend.py使用matplotlib绘制热度趋势日发帖量 日平均情绪分# plot_hot_trend.py import matplotlib.pyplot as plt import pandas as pd def plot_daily_trend(csv_path: str): df pd.read_csv(csv_path, parse_dates[post_time]) df[date] df[post_time].dt.date daily_stats df.groupby(date).agg({ title: count, sentiment_score: mean }).rename(columns{title: post_count}) fig, ax1 plt.subplots(figsize(12, 6)) ax1.bar(daily_stats.index, daily_stats[post_count], alpha0.7, labelDaily Post Count) ax1.set_ylabel(Post Count, colortab:blue) ax1.tick_params(axisy, labelcolortab:blue) ax2 ax1.twinx() ax2.plot(daily_stats.index, daily_stats[sentiment_score], r-o, labelAvg Sentiment Score) ax2.set_ylabel(Avg Sentiment Score, colortab:red) ax2.tick_params(axisy, labelcolortab:red) plt.title(fDaily Trend: {csv_path.split(_)[1]}) plt.xticks(rotation45) plt.tight_layout() plt.savefig(ftrend_{csv_path.split(.)[0]}.png, dpi300) plt.show()提示绘图脚本依赖pandas和matplotlib已在requirements.txt中声明。若中文显示方块请在plot_hot_trend.py开头添加plt.rcParams[font.sans-serif] [SimHei, Arial Unicode MS] plt.rcParams[axes.unicode_minus] False5. 实战排错指南从报错截图到定位根因的 7 个关键检查点项目包内报错展示.jpg记录了 5 类高频错误对应README.md中的“常见问题”章节。这里不罗列错误现象而是给出可立即执行的诊断命令与修复动作覆盖从环境到代码的全链路。5.1 MongoDB 连接失败ConnectionRefusedError现象运行python main.py报错pymongo.errors.ServerSelectionTimeoutError: localhost:27017: [Errno 111] Connection refused根因MongoDB 服务未启动或配置文件中host/port错误诊断命令# 检查 MongoDB 是否运行 sudo systemctl status mongod # Linux brew services list | grep mongo # macOS Homebrew # 检查端口监听 netstat -tuln | grep :27017 # 测试连接需安装 mongo shell mongosh --eval db.runCommand({ping:1})修复动作若服务未启动sudo systemctl start mongodLinux或brew services start mongodb-communitymacOS若端口非 27017修改mongodb.py中MONGO_URI mongodb://localhost:27017/为实际端口5.2 push2 接口返回空数据{data:[]}现象crawler.py日志显示push2 接口请求失败但 HTTP 状态码为 200根因Referer头缺失或不匹配或topic_id提取错误诊断命令# 手动 curl 测试替换 YOUR_TOPIC_ID 和 STOCK_CODE curl -H Referer: https://guba.eastmoney.com/topic/YOUR_TOPIC_ID.html \ https://guba.eastmoney.com/topic/push2?codeSTOCK_CODEtopicidYOUR_TOPIC_IDpage1perpage30修复动作检查parser.py中extract_topic_id_from_html()返回值是否为纯数字确认crawler.py中fetch_comments_by_push2()的Referer参数是否与目标 URL 完全一致含末尾/5.3 XPath 解析失败AttributeError: NoneType object has no attribute get_text现象parser.py报错title_tag.get_text()title_tag为None根因股吧页面结构变更ul#stockList li选择器失效诊断命令# 获取当前列表页 HTML 并检查结构 curl -s https://guba.eastmoney.com/list,600519,f_1.html | head -n 50 | grep -A5 -B5 title修复动作打开https://guba.eastmoney.com/list,600519,f_1.html右键“查看网页源代码”搜索classtitle或href找到新容器选择器修改crawler.py中parse_list_page()的soup.select()语句例如改为soup.select(div.article-item a.title)5.4 情感分析结果全为 0.0sentiment_score恒定现象analysis.py输出的sentiment_score全是0.0根因clean_text()过度清洗或TextBlob未正确加载英文词典诊断命令# 在 Python 交互环境测试 from textblob import TextBlob print(TextBlob(I love this stock!).sentiment) # 应输出 polarity0 print(clean_text(缩量涨停)) # 应输出 缩量涨停修复动作若TextBlob返回(0,0)运行python -m textblob.download_corpora下载词典若clean_text()删除了关键符号检查正则re.sub(r[^\u4e00-\u9fa5a-zA-Z0-9\s\.\!\?\,\;\:\-\_\(\)\[\]\{\}], , text)是否误删!?5.5 热点聚类结果为空clusters列表长度为 0现象cluster_hot_topics()返回空列表根因输入comments为空或texts列表长度 2诊断命令# 在 analysis.py 中临时插入 debug print(fInput comments count: {len(comments)}) print(fGenerated texts: {texts[:3]}) # 查看前3条修复动作确认crawler.py是否成功抓取评论检查comments字段是否存入 MongoDB若仅1条数据跳过聚类直接返回单条记录analysis.py已内置此逻辑5.6 CSV 导出乱码Excel 打开显示方块现象export_to_csv.py生成的 CSV 在 Excel 中中文乱码根因文件未以 UTF-8 with BOM 编码保存修复动作修改export_to_csv.py中df.to_csv()参数df.to_csv(output_file, indexFalse, encodingutf-8-sig) # 关键utf-8-sig5.7 图表中文不显示plot_hot_trend.py折线图标签为方块现象plt.show()显示的坐标轴和标题为方块根因Matplotlib 默认字体不支持中文修复动作在plot_hot_trend.py开头添加字体设置见 4.3 节提示或全局配置~/.matplotlib/matplotlibrc中添加font.sans-serif: SimHei, DejaVu Sans, Bitstream Vera Sans, sans-serif最后一行技术动作执行python main.py --stock600519 --days3 --modefull观察logs/crawler.log中INFO级别日志是否包含Saved 127 posts to MongoDB和Calculated sentiment for 892 comments即表示全流程贯通。本文还有配套的精品资源点击获取

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价