做量化或者盯盘的朋友,应该都听说过一句话:股市是情绪的放大器。但情绪这东西看不见摸不着,怎么量化?今天分享的这个小项目,就是直接用 Python 爬虫去监听东方财富股吧的帖子热度,把散户的讨论热度变成一个个可以排序、可以计算的数字。我给这玩意起了个名字叫“情绪挖掘机”。
不是什么高大上的东西,本质上就是:把股吧里某个股票、某个板块的帖子标题、阅读量、评论数、发布时间这些公开信息定时抓下来,再通过一个简单的评分公式,算出每一只股票当前的热度分和情绪倾向——到底是看多的人多,还是看空的人多,讨论是在升温还是在降温。
别小看这个数据,你盯盘的时候觉得“今天这票好像突然有人聊了”,其实就是热度在起变化。机器能帮你把这种“好像”变成具体的数值和曲线。这项目适合谁?一是 Python 基础刚学完、想找个真实案例练手的朋友,二是做量化交易、想引入情绪面辅助判断的玩家,三就是纯粹满足好奇心——看看哪只股票最近在股吧被讨论得最凶。
1. 项目拆解:情绪挖掘机到底在挖什么
1.1 核心需求与数据维度
先说清楚这个项目要解决什么问题。东方财富股吧是国内散户聚集度最高的股票社区之一,尤其是个股吧,里面讨论的基本都是实时交易情绪。有人发帖建仓、有人发帖骂庄、有人发帖问“还能拿吗”,这些内容天然就是情绪指标。
我把“情绪”拆成了两个维度:热度和情绪倾向。热度看的是帖子讨论的活跃程度,参考三个指标——阅读量、评论数、发布时间;情绪倾向看的是发帖人看多看空的立场,这个用自然语言处理的简单办法做,不搞大规模模型,就先通过一组正负向关键词加规则判断。
数据维度上,每个帖子主要采集以下字段:
| 字段 | 含义 | 用途 |
|---|---|---|
| post_title | 帖子标题 | 情绪分析文本来源 |
| post_id | 帖子唯一标识 | 去重和增量更新 |
| read_count | 阅读数 | 热度计算 |
| comment_count | 评论数 | 热度计算 |
| like_count | 点赞数 | 热度计算 |
| create_time | 发布时间 | 新鲜度计算 |
| stock_code | 所属股票代码 | 聚合分组 |
有了这些字段,就可以按股票代码分组,计算每只股票的整体热度,也能进一步看每只股票下面热门帖子都在聊什么方向。
1.2 技术路线选型:为什么是 requests 而不是 selenium
说实话,市面上很多人一提到爬虫就想到 selenium,觉得“能渲染网页就万能”,但这个项目我第一反应就是用 requests 直接抓接口。
原因有三:
- 股吧的数据是接口返回的 JSON,不是纯网页渲染。网页上看到的帖子列表,实际上是通过一个 AJAX 接口动态加载的,数据本身是一段结构化的 JSON。直接请求这个 JSON 接口,拿到的数据干净、完整、好解析,完全没必要让浏览器去渲染一遍。
- Selenium 的开销太大了。每启动一个浏览器实例,内存和 CPU 占用都不小,而且速度慢。如果是定时任务,每 5 分钟跑一次,用 Selenium 就是给自己找罪受。
- 反爬的定位不同。股吧的核心数据接口并没有做太复杂的校验,主要靠频率限制和部分 header 校验。requests 加上合理的请求头模拟,实测下来完全能稳定抓取。
当然,如果哪天对方把整个页面改成完全的 JS 渲染、接口加了复杂的动态 token,再考虑上 selenium 或者 playwright 不迟。但那是后话,永远不要让“可能的未来复杂化”拖慢今天的项目进度。
2. 抓包分析与协议还原:最关键的 30 分钟
2.1 找到正确的数据接口
很多人写爬虫死在第一步:不知道从哪里拿数据。这一步其实不靠猜,打开浏览器开发者工具,全部靠看。
操作路径是这样的:
- 打开东方财富某个个股吧页面(比如 600519 的贵州茅台吧)。
- 按 F12 打开开发者工具,切到 Network(网络)面板。
- 刷新页面,在筛选框输入
ajax或者直接找返回类型是 JSON/XHR 的请求。 - 逐个看响应内容,找到包含帖子列表数据的那个请求。
核心接口结构大概是这样的一个 GET 请求:
https://guba.eastmoney.com/list/600519,1,f.html不过这个是网页版列表页,直接抓这个拿到的是 HTML,不是最理想的。更推荐的是找到真正的 JSON 数据接口。在开发者工具里看到的实际请求是一系列带参数的 URL,形如:
https://gbapi.eastmoney.com/stock/list/600519?pageIndex=1&pageSize=20&sort=1&type=1&marketCode=1具体参数名在不同时期会有调整,但大致就这几类:股票代码、页码、每页条数、排序方式。响应数据里会有一个re列表,里面就是每条帖子记录的字典,字段正好对应前面表格里的那几个。
这一步的“为什么”很重要:找到一个稳定的接口,比写一万行抓取代码都值钱。接口找到了,爬虫就完成了一半;接口没找到,后面全是白搭。
2.2 请求参数与风控伪装
拿到接口之后,先在浏览器里复制成 cURL,自己用 Postman 或者直接在 Python 里试一把。如果复制出来的 cURL 一跑就通,说明这个接口对 cookie 的依赖不强,可以直接用 requests 模拟。
但直接用裸 requests 去请求,大概率会出问题。我实测中发现它至少会校验几个东西:
- User-Agent:必须伪装成真实浏览器的 UA,默认的
python-requests/2.x很容易被识别。 - Referer:得带上你从哪个页面进过来的,一般填对应的股吧 URL,表示你是从股吧页面跳转过来的正常访问。
- Cookie:部分接口不校验 cookie 也能通,但建议冷启动时先通过 requests.Session 访问一次股吧首页,拿到初始 cookie,再带着这个 session 去请求数据接口。
完整的请求头我做成了下面这样的配置:
headers = { "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 " "(KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36", "Referer": "https://guba.eastmoney.com/", "Accept": "application/json, text/plain, */*", "Accept-Language": "zh-CN,zh;q=0.9", }这里有一个细节:把 Accept 明确设置成 JSON 格式,有些接口会根据这个字段决定返回 HTML 还是 JSON 数据,反而是很多人忽略的点。
3. 代码实现:数据采集与热度评分
3.1 采集器实现
讲思路不如给代码。我写了一个轻量版的采集器,核心逻辑分三层:
- 第一层,
fetch_page:请求接口,解析 JSON,返回帖子列表。 - 第二层,
parse_post:清洗和转换数据,把时间字符串转成时间戳,把数字字符串转成 int。 - 第三层,
crawl_stock:按股票代码循环翻页,拼接完整列表。
伪代码大概长这样:
import requests import time import json import logging HEADERS = { "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 " "(KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36", "Referer": "https://guba.eastmoney.com/", "Accept": "application/json, text/plain, */*", } logging.basicConfig(level=logging.INFO, format="%(asctime)s - %(name)s - %(levelname)s - %(message)s") def fetch_page(session, stock_code, page_index=1, page_size=20): url = f"https://gbapi.eastmoney.com/stock/list/{stock_code}" params = { "pageIndex": page_index, "pageSize": page_size, "sort": "1", "type": "1", "marketCode": "1", } resp = session.get(url, params=params, headers=HEADERS, timeout=10) if resp.status_code != 200: logging.warning(f"请求失败, status={resp.status_code}") return [] data = resp.json() return data.get("re", [])这段代码注意两个点。第一,session.get里的params参数会自动把字典序列化成 URL 后面的查询字符串,不用自己拼 URL。第二,每个股票翻页时,用session.get而不是requests.get,目的就是维持 cookie 和连接池,减少握手次数,减少被风控的概率。
抓取的主循环再加一个time.sleep(random.uniform(0.5, 1.5))的随机延时,把访问节奏模拟得接近真人,同时避免对服务器造成压力。
3.2 热度评分模型
数据到手了,怎么变成“热度分”?我试过不少方案,最后沉淀下来一套计算公式。
先说为什么直接用阅读数不行。原因很简单:阅读数的量级远大于评论数和点赞数。如果直接把三个数相加,评论和点赞就被阅读数完全淹没了。所以我做了一步对数变换,把极端的大数值压缩到合理区间。
核心公式:
score = 0.40 * log1p(read_count) + 0.30 * log1p(comment_count) + 0.20 * freshness + 0.10 * log1p(like_count)逐项解释:
log1p(x)就是log(1 + x),在 Python 里是math.log1p。添加 1 是为了避免 x=0 时对数为负无穷。这个变换的逻辑是:阅读量从 100 涨到 1000,对热度的影响远大于从 10000 涨到 100000,对数能体现这种边际递减效应——一篇文章从没人看到有人看是质变,爆款之后再涨只是量变。freshness是新鲜度得分,按照发布时间衰减。我的做法是:
hours_age = (now - create_ts) / 3600 freshness = max(0.0, 1.0 - hours_age / 24.0)也就是说,24 小时内的帖子新鲜度得分在 0 到 1 之间线性递减,超过 24 小时统一为 0。这是模拟“热帖效应”:刚发出来的帖子讨论热度天然高于旧帖。
- 四个权重加起来正好 1.0,这样每篇帖子的基础分在 0 到几分的区间内。单帖分数看起来不高,但几千篇帖子聚合到股票维度之后,差距就拉开了。
聚合打分公式对应到代码里就是:
def calc_post_score(post): import math read_score = math.log1p(post["read_count"]) comment_score = math.log1p(post["comment_count"]) like_score = math.log1p(post["like_count"]) freshness = max(0.0, 1.0 - (time.time() - post["create_ts"]) / 86400) return 0.40 * read_score + 0.30 * comment_score + 0.20 * freshness + 0.10 * like_score至于情绪的看多看空判断,我用的是一组正负向关键词字典,配合几个简单规则,比如标题中“涨停”“突破”“利好”“抄底”视为看多,“跌停”“破位”“利空”“割肉”视为看空。对标题做一次分词匹配,看命中了哪些词,汇总正负得分。这个模型简单,但胜在稳定可解释,后续要换大模型或者接入成熟的情感分析库,接口也方便替换。
3.3 增量更新与主流程编排
定时任务最怕重复抓全量数据,所以增量更新的核心是去重。对于每篇帖子,post_id是天然的唯一键。我在内存里维护一个已见集合,每抓到一个新帖子,如果post_id已经在集合里,就跳过,否则更新数据并记录。
首次运行时是全量抓取,之后每隔 5 分钟增量抓取一次。5 分钟这个间隔是我实测后定的。太频繁容易被限流,而且 1 分钟内的讨论量变化没有统计意义;太稀疏又抓不到短时爆发,比如突发利空后半小时内的情绪骤变。5 分钟算是一个临界线,数据分辨率和请求频率之间比较平衡。
主流程的结构,我用的是最简单但可靠的方案:schedule库做定时,每 5 分钟触发一次任务函数,任务函数先抓增量数据,再重算热度榜,最后把结果写入 CSV 文件。逻辑清晰、排错方便,不建议一上来就上 scrapy 这类重型框架,杀鸡不要用牛刀。
4. 结果产出:从数据到可用信息
4.1 榜单输出
数据抓下来,不能堆在那里落灰。我给输出设计了三个榜单,分别对应不同使用场景:
第一张是热度榜。按股票代码聚合所有帖子的热度分,取前 20 名,展示哪些股票当前讨论最激烈。这张榜适合开盘前快速扫一眼,看看今天的市场焦点在哪里。
第二张是情绪榜。统计每只股票下面看多帖子和看空帖子的数量比,计算出情绪偏多、偏空还是中性。我把它定义为:
sentiment = (bull_count - bear_count) / (bull_count + bear_count + 1)结果范围在 -1 到 1 之间。1 表示全盘看多,-1 表示全盘看空,0 表示分歧很大。这个数值配合热门股票代码,能辅助判断一只股票当前的市场预期。
第三张是趋势榜。对比上一周期和当前周期的热度分,找出增幅最大的股票。方法就是每个周期结束时拍一张快照,下次计算时用当前分减去上一次的快照分。这个榜单的价值在于发现“正在被讨论”而不是“已经被讨论很久”的标的,对短线敏感度高。
三个榜单一出来,数据就不是一堆乱糟糟的帖子了,而是可直接浏览的决策素材。我把三个榜分别导出到 CSV,用 pandas 处理,然后直接打印在终端,运行完之后一眼就能看到全局情况。
4.2 简单可视化与记录历史
我强烈建议,爬虫抓到的数据一定要落盘。不要只存在内存里,因为程序一重启,历史数据就全丢了,而情绪分析最值钱的就是历史对比。
落盘方案我用的是 CSV 文件,按日期分文件夹存放:
data/ 2024-01-15/ 600519.csv 300750.csv ...每个 CSV 的字段对应帖子完整信息,包括标题、阅读数、评论数、发布时间、热度分、情绪倾向。代码层面,用csv.DictWriter追加写入就够,不需要上数据库。等数据量积累到几万条级别,再考虑迁移到 SQLite 或 MySQL 也不迟。
如果是想做个简易可视化,pandas + matplotlib 画个热度的曲线图完全够用。我常用的就三行:
df = pd.read_csv("600519.csv", parse_dates=["create_time"]) df.set_index("create_time")["heat_score"].plot() plt.show()当然,如果你想把这份数据接入到自己的量化回测系统里,建议输出格式直接用 JSON 或者 pandas 的 pickle 格式,后续处理更灵活。
5. 反爬对抗与合规红线:边界要清楚
5.1 常见的反爬手段与排查经验
聊到反爬,先说一个我踩过的坑:第一次写完代码,本地运行一切正常,开心地挂到服务器上,结果第二天早上起来一看,请求全部超时,被限制访问了。
排查下来的原因很简单:服务器 IP 段被重点关注了。数据中心 IP 的访问行为特征和普通住宅 IP 差异明显,单纯靠请求头伪装是骗不过的。
后来我的调整方案是三管齐下:
- 控制频率。每两次请求之间随机延迟 1 到 2 秒,绝不并发。
- 加长周期。把 1 分钟一次的频率调低到 5 分钟一次,采集密度完全够用。
- 增加随机性。除了固定 UA,还准备了一个浏览器列表,每次请求时随机抽取一个,从行为模式上打散指纹。
另外一个常被忽略的细节是深夜维护窗口。东方财富的系统每天会有固定的维护时段,凌晨 3 点左右容易出现接口异常,返回空数据或者直接超时。遇到这个情况不要慌,写个重试机制,失败 3 次后跳到下一轮,不要原地死磕。
有一些现象能提前预警风控,总结成表:
| 现象 | 可能原因 | 应对措施 |
|---|---|---|
| 接口返回 403 | IP 被临时限制 | 停止采集,等待 10-30 分钟 |
| 返回数据为空但状态码 200 | 接口变动或维护窗口 | 检查接口字段,观察返回内容 |
| read_count 全部异常为 0 | 触发了测试版页面 | 重置 session,更换 UA |
| 请求超时率高 | 频率过高被限制 | 降低频率,加延时 |
5.2 合规与道德:哪些事绝对不能做
这个话题我必须单独拿出来说,因为踩线的代价远大于收益。
东方财富股吧的帖子数据是公开数据,任何人打开网页都能看到,所以页面信息的采集,原则上属于公开信息收集。但这不代表可以无限度地薅:
- 绝不绕过登录验证,股吧的帖子列表无需登录也能看,那就不要搞什么自动登录、模拟密码,那已经构成对服务条款的违反。
- 绝不高频抓取,公开数据也要讲究克制。把对方服务器打挂了,最后修复接口的是人家,蒙受损失的是所有依赖这个平台的人。
- 绝不用于骚扰或商业侵权,抓下来的帖子数据里含有发帖人的 ID、内容,重新打包成所谓“用户画像”往外卖,这是明显的违法边界,想都不要想。
我自己的使用范围很简单:本地运行,数据自己看,最多拿来写写量化策略的研究分析。不公开分享原始数据,不做商业化,不对服务器增加不合理负担。
一句话总结合规准则:访问频率低到像个人在浏览网页,数据使用严格限定在自我分析,不碰任何非公开信息和用户隐私。
6. 常见问题与排查技巧实录
这个项目跑起来之后,你大概率会遇到下面这些问题,我提前把坑填好。
6.1 问题速查表
| 问题 | 核心原因 | 解决方案 |
|---|---|---|
| 安装依赖报错 | 环境没有 requests/pandas | pip install requests pandas schedule,注意 pip 版本 |
| 返回 JSON 解析失败 | 接口返回了 HTML 而非 JSON | 检查请求头 Accept 字段,检查 cookie 是否过期 |
| 帖子列表为空 | 股票代码格式错误 | 确认代码是 6 位数字,不要带“.SH”后缀再请求 |
| 阅读数字段为空 | 接口字段名变化 | 到开发者工具里重新看接口返回字段名,做一次映射更新 |
| 时间解析报错 | 时间格式不是标准 ISO | 用datetime.strptime按实际格式解析,或先用正则提取 |
| 长时间运行后请求全部失败 | session 过期 | 定时重建 session,重新访问首页获取 cookie |
| 服务器上运行代码乱码 | 编码环境问题 | 顶部加# -*- coding: utf-8 -*-,输出时指定encoding="utf-8" |
6.2 我的几个独家调试技巧
技巧一:先缓存,后解析。我最初是拿到响应就直接解析,结果接口一旦变动,连原始数据长什么样都不知道。后来改成了先把响应 body 存成.json文件,再写解析代码,调试效率翻倍。接口出问题了,直接打开缓存文件看原始结构,一眼就能定位是字段名变了还是数据格式换了。
技巧二:小规模验证再跑全量。写爬虫的直觉是做任务就全量跑,但正确的姿势是先跑 1 页、2 页,确认解析正确、字段完整,再放开循环跑全量。很多时候你以为的问题,在小规模测试里根本复现不出来。
技巧三:日志要留痕。不要只用print输出,建议用logging。跑定时任务的时候,print 的输出刷掉就没了,而logging能写到文件里。出了问题,翻日志是最快的排查路径。日志里建议至少记录每个股票代码的抓取页数、成功条数、耗时,这三个指标足够判断系统的健康状态。
7. 后续扩展:从情绪挖掘到策略联动
别把项目停在“看看热度”这一步,再往下走几步,价值就完全不一样了。
我目前做的一个扩展是:把热度榜和情绪榜数值导出到 CSV 之后,用一个小脚本和行情数据的涨跌幅做关联分析。具体做法是每天收盘后,对比前一天的股吧情绪分和当天的股价涨跌幅,看两者有没有统计上的相关性。虽然不是严格意义上的因果验证,但长期积累下来,已经能找到一些规律,比如某些股票在情绪分快速拉升后的 1-2 天,往往出现明显的放量走势。
更进一步,这个数据可以接入量化交易系统。常见的量化策略大多基于技术指标,但技术指标是滞后的——价格涨了 MACD 才金叉,成交量放大了量能指标才变化。而股吧情绪数据有一个特殊的优势:它领先于成交量的变化。主力资金进场之前,讨论热度往往先起来。如果能把情绪因子作为选股的一个前置过滤器,配合技术指标确认入场点,策略的胜率理论上能得到改善。
还有一种玩法是做“情绪预警”——监控特定持仓股的情绪分,一旦出现异常暴增(比如从 1 分飙到 10 分以上的极端波动),就触发提醒,让你及时关注盘面。这比一直盯盘轻松多了,机器替你盯着散户的情绪变化,你来判断该不该操作。
但这里必须说清楚一点:情绪数据是参考因子,不是稳赚信号。股吧里也存在水军、控评、垃圾帖干扰,你看到的情绪分暴涨可能是真实利好,也可能是有人在刻意制造热度。所以任何结论都要结合行情、基本面和技术面做交叉验证,不要把一个粗糙的情绪分当圣旨。
这个项目本身的代码量不大,核心逻辑也不复杂,但麻雀虽小,五脏俱全:接口分析、数据清洗、增量更新、频率控制、结果产出、合规边界,爬虫工程里该涉及的知识点全都覆盖到了。跑通一遍,你对 Python 爬虫的整个工作流会有一个完整的体感,比刷十篇教程都管用。
最后分享一个我个人的小习惯:每天早上开盘前,我先看一眼自己维护的那个热度榜,再打开行情软件看盘。机器给我的是一份客观事实——哪只股票正在被讨论;盘面给我的是一份市场价格——哪只股票正在被交易。把这两份信息放在一起对照,有时候能发现一些很有意思的预期差,这大概就是情绪挖掘最大的乐趣所在。