ARTICLE DETAIL

资讯详情

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

东方财富股吧评论爬虫实战:Scrapy+Playwright稳定采集方案

东方财富股吧评论爬虫实战:Scrapy+Playwright稳定采集方案 简介本资源是一套基于Scrapy框架实现的东方财富网股票评论数据爬取项目面向Python初学者及金融数据分析从业者解决金融舆情数据自动化采集与初步情感分析的实际需求。压缩包共17个文件含7个核心Python源码如spiders、pipelines、items、middlewares等模块、7个编译缓存文件pyc、2个配置类文本txt及1个Scrapy项目配置文件cfg整体仅71KB轻量易部署目录结构完整体现Scrapy标准工程规范。已有4061人学习下载涵盖从Spider定义、XPath解析规则、Item数据建模到Pipeline中集成中文分词jieba与基础情感判断的全流程实现。读者可直接运行调试快速掌握反爬应对策略、结构化数据存储方法并为后续接入NLTK/spaCy等NLP工具开展深度情绪量化分析奠定坚实基础。1. 项目概述为什么盯上东方财富的股票评论数据做量化分析、舆情监控或者个股情绪研究的朋友几乎都绕不开东方财富股吧这个“民间信息集散地”。它不是交易所的官方数据源但胜在真实——散户怎么想、主力资金有没有异动、突发消息后市场第一反应是什么全藏在那些带情绪、有错别字、甚至夹杂方言的评论里。我最早接触这个需求是帮一个做短线择时策略的朋友补数据缺口他发现某只股票连续三天在股吧出现大量“抄底”“梭哈”“等反弹”的高频词次日果然放量上涨而另一只票评论区突然刷屏“跑路”“清仓”当天就跌停。这种非结构化文本里的信号传统财务指标根本抓不到。所以当标题里出现“东方财富股票评论数据 Python爬虫代码 Scrapy框架”核心要解决的从来不是“能不能爬”而是“怎么稳定、可持续、合规地拿到高质量评论流”。这里必须划重点Scrapy不是万能钥匙它只是把爬虫工程化的工具真正决定成败的是对东方财富前端渲染逻辑的理解、反爬机制的应对策略、以及数据清洗的颗粒度设计。很多人一上来就写Scrapy Spider结果跑两天就被封IP或者爬下来全是“加载中…”的空评论问题根本不在框架而在没看清页面背后的水有多深。我实测过三种主流方案requestsBeautifulSoup硬解析、Selenium模拟浏览器、ScrapyPlaywright组合。最终选Scrapy不是因为它最简单而是它在批量调度、中间件扩展、去重机制、分布式支持这四点上天然适配股票评论这种高频、增量、需长期运行的场景。比如股吧每分钟新增几百条评论你不可能每次全量重爬必须用增量模式只抓新内容又比如不同股票的评论页URL结构不统一有的带分页参数有的靠滚动加载Scrapy的Rule规则和CrawlSpider类能快速适配。这些细节新手常忽略但恰恰是项目能否从“能跑通”升级到“能落地”的分水岭。关键词里反复出现的“python”“scrapy”“爬虫”“东方财富”“股票评论”其实指向一个明确的用户画像有一定Python基础、熟悉HTTP协议但可能没实战过大型爬虫、需要可复用代码而非理论教程的从业者或学生。他们最痛的点不是学不会Scrapy语法而是不知道东方财富的评论接口到底藏在哪、哪些字段必须保留、如何避免被识别为机器流量、爬下来的数据怎么直接喂给NLP模型做情感分析。这篇内容就从真实踩坑现场出发把从页面分析到数据入库的完整链路拆开给你看不讲概念只说怎么做、为什么这么做、哪里最容易翻车。2. 整体架构设计与技术选型逻辑2.1 为什么放弃RequestsSelenium坚定选择Scrapy先说结论Requests适合单页快照Selenium适合强交互页面Scrapy适合持续性数据管道。东方财富股吧的评论页表面看是静态HTML实际是“伪静态”——初始HTML只渲染前20条评论后续内容通过Ajax请求动态加载且加载逻辑随版本迭代频繁变动。我最初用Requests抓首页发现response.text里只有骨架DOM关键评论数据全在XHR请求里改用Selenium模拟滚动虽能触发加载但速度慢单页耗时30秒、资源占用高每个实例吃1G内存、稳定性差ChromeDriver版本稍不匹配就报错。而Scrapy配合Playwright中间件既能精准拦截XHR请求又能复用浏览器上下文效率提升3倍以上。更关键的是工程化能力。股票评论数据不是爬一次就完事需要7×24小时运行。Scrapy的Scheduler能按优先级队列管理请求Downloader Middleware可统一处理User-Agent轮换、代理池接入、Cookie自动续期Item Pipeline则把清洗、去重、存储逻辑模块化。举个例子股吧评论常含广告“加微信XXX领牛股”、灌水“666”“楼主好人”、重复帖同一用户10分钟内发5条相同内容如果用Selenium这些过滤得写在回调函数里代码臃肿Scrapy则用独立Pipeline处理后续想加情感打分、敏感词过滤只需新增Pipeline类不影响主流程。提示Scrapy的CrawlSpider比Spider更适合此场景。因为股吧URL有规律可循如https://guba.eastmoney.com/list,600519,f_1.htmlCrawlSpider的Rule规则能自动提取下一页链接和详情页URL省去手动构造分页逻辑的麻烦。但要注意Rule的正则表达式必须严格否则会漏爬或误爬无关页面比如把公告页也当成评论页。2.2 Playwright中间件破解动态加载的核心钥匙东方财富近年全面升级前端框架评论加载从jQuery Ajax转向ReactGraphQL传统XPath定位经常失效。Scrapy原生不支持等待动态元素必须引入Playwright。这里不推荐PuppeteerNode.js生态因为整个项目用Python混用语言增加部署复杂度Playwright的Python版API更简洁且支持多浏览器Chromium/Firefox/WebKit能快速验证是否是浏览器指纹问题。具体实现上我封装了一个PlaywrightMiddleware类核心逻辑分三步请求拦截监听所有fetch和xhr请求捕获包含comment或postlist关键词的URL智能等待不依赖固定sleep时间而是用page.wait_for_selector(.comment-item, stateattached)等评论容器节点出现即停止等待数据提取直接从XHR响应JSON中取data.list字段比解析DOM快5倍且不受CSS类名变更影响。实测对比纯Scrapy抓取一页20条评论需1.2秒加Playwright中间件后升至3.8秒但成功率从62%提升到99.3%。多花的2.6秒换来的是不用每天半夜起来修爬虫——这才是工业级应用的底线。2.3 数据存储方案为什么用SQLite而非MySQL看到“股票评论”就想到大数据大错特错。单只股票日均评论量约5000条A股5000只股票全量爬就是2500万条/天但90%的用户实际只关注20只以内标的。用MySQL建表、设索引、配主从运维成本远超收益。我最终选SQLite理由很实在单文件存储无需安装服务pip install pysqlite3后直接import sqlite3支持INSERT OR IGNORE语法天然防重复插入股吧评论ID是数字可设为主键用CREATE VIRTUAL TABLE comments USING fts5(content)开启全文检索查“宁德时代 涨停”比LIKE快20倍本地开发调试时用DB Browser for SQLite直接打开.db文件看数据比连MySQL客户端快10倍。当然如果要做全市场舆情分析再迁移到ClickHouse也不迟。但起步阶段过度设计是最大陷阱。3. 核心细节解析与实操要点3.1 页面结构逆向找到真正的数据源头很多人以为爬股吧就是解析div classcomment-content结果发现爬下来全是空的。真相是东方财富把评论数据存在window.__INITIAL_STATE__这个全局变量里这是React服务端渲染SSR的典型特征。你用浏览器开发者工具看Network→XHR会发现一个/api/stock/comment/list请求响应体是标准JSON{ status: 0, msg: success, data: { list: [ { id: 123456789, content: 这票明天必涨已满仓, user: {nick: 股神小张, level: 5}, time: 2023-10-25 14:22:33, like_count: 12 } ], total: 12500 } }这个API才是黄金入口。但直接请求会返回{status: -1, msg: 非法请求}因为需要两个关键HeaderReferer: 必须是对应股票的股吧首页URL如https://guba.eastmoney.com/list,600519,0.htmlX-Requested-With: 固定值XMLHttpRequest。Scrapy中这样构造请求yield scrapy.Request( urlfhttps://guba.eastmoney.com/api/stock/comment/list?code{stock_code}page{page}, headers{ Referer: fhttps://guba.eastmoney.com/list,{stock_code},0.html, X-Requested-With: XMLHttpRequest }, callbackself.parse_api_response )注意stock_code不是股票代码本身而是东财的内部编码。600519对应600519但创业板300750对应300750科创板688001对应688001——没有转换规则需从股吧URL中直接提取。我写了个正则rlist,(\d),\d.html从Referer里抓比查映射表更可靠。3.2 反爬对抗IP、User-Agent、Cookie的三层防御东方财富的反爬不是摆设。我测试过不用任何防护单IP每分钟请求超15次就会返回403。解决方案是三层叠加IP层用免费代理池如快代理、芝麻代理不如自建住宅代理。我用树莓派3G USB网卡搭建了5个家庭宽带出口每个IP每天限1000次请求成本不到200元/月User-Agent层不随机生成而是从真实浏览器UA池中轮换。我收集了Chrome 115-118、Edge 116-117、Firefox 115-117的20个UA存成列表每次请求随机取一个Cookie层最关键的一步。股吧要求Cookie中必须有em_hq_cid渠道ID和em_hq_uid用户ID这两个值在首次访问股吧首页时由JS生成并写入Cookie。Scrapy默认不执行JS所以必须用Playwright先访问首页获取Cookie后传给后续请求。Playwright中间件中提取Cookie的代码def get_cookies(self, page): cookies page.context.cookies() return {c[name]: c[value] for c in cookies if c[name] in [em_hq_cid, em_hq_uid]}然后在Scrapy Request中带上cookies self.get_cookies(page) # 从Playwright获取 yield scrapy.Request(url, cookiescookies, callbackself.parse_api)3.3 数据清洗从原始JSON到可用字段的蜕变爬下来的JSON看着干净但实际充满噪声。我定义了Item类强制规范字段class EastMoneyCommentItem(scrapy.Item): comment_id scrapy.Field() # 评论ID唯一标识 stock_code scrapy.Field() # 股票代码600519 content scrapy.Field() # 原始评论内容 clean_content scrapy.Field() # 清洗后内容去广告、去表情、标准化 user_nickname scrapy.Field() # 用户昵称 user_level scrapy.Field() # 用户等级1-10 post_time scrapy.Field() # 发布时间转为datetime like_count scrapy.Field() # 点赞数 is_original scrapy.Field() # 是否楼主首发帖True/False清洗Pipeline的核心逻辑去广告用正则r加[微信|QQ|vx]\s*[a-zA-Z0-9\u4e00-\u9fa5]{5,15}匹配替换为空字符串去表情股吧评论常用[呲牙][旺柴][加油]用re.sub(r\[.*?\], , content)清除时间标准化股吧时间格式混乱“刚刚”“1小时前”“2023-10-25 14:22”统一转为datetime.now()减去对应秒数或直接用dateutil.parser.parse()识别楼主帖对比评论用户昵称与股吧页面顶部的“楼主”昵称一致则设is_originalTrue。实测清洗后有效评论占比从73%提升到92%尤其广告过滤让后续情感分析准确率提高18%。4. 实操过程与核心环节实现4.1 环境搭建5分钟完成ScrapyPlaywright初始化别被“Scrapy框架”吓住实际初始化比装Python还简单。我的环境是Ubuntu 22.04 Python 3.10步骤如下创建虚拟环境并安装核心包python -m venv em_crawler_env source em_crawler_env/bin/activate pip install scrapy playwright beautifulsoup4 lxml安装Playwright浏览器关键很多教程漏这步playwright install chromium # 只装Chromium够用且体积小注意Playwright必须单独安装浏览器pip install playwright只装Python API不包含浏览器二进制文件。若跳过此步运行时会报BrowserType.connect: Failed to connect。生成Scrapy项目骨架scrapy startproject eastmoney_crawler cd eastmoney_crawler scrapy genspider guba guba.eastmoney.com配置Settings.py关键参数# settings.py BOT_NAME eastmoney_crawler SPIDER_MODULES [eastmoney_crawler.spiders] NEWSPIDER_MODULE eastmoney_crawler.spiders # 关键启用中间件 DOWNLOADER_MIDDLEWARES { eastmoney_crawler.middlewares.PlaywrightMiddleware: 543, } # 并发控制防封IP CONCURRENT_REQUESTS 1 DOWNLOAD_DELAY 3 # 每次请求间隔3秒 RANDOMIZE_DOWNLOAD_DELAY False # 启用Pipeline ITEM_PIPELINES { eastmoney_crawler.pipelines.EastMoneyCommentPipeline: 300, }4.2 Spider编写从URL构造到数据提取的完整链路spiders/guba.py是核心我把它拆成四个逻辑块第一块起始URL生成def start_requests(self): # 从配置文件读取关注股票列表 with open(stocks.json, r) as f: stocks json.load(f) # [{code: 600519, name: 贵州茅台}] for stock in stocks: # 构造股吧首页URL url fhttps://guba.eastmoney.com/list,{stock[code]},0.html yield scrapy.Request( urlurl, meta{stock_code: stock[code], page: 1}, callbackself.parse_first_page )第二块首页解析提取总评论数和首屏数据def parse_first_page(self, response): stock_code response.meta[stock_code] # 从HTML中提取总评论数用于计算分页 total_str response.css(span#zwcnt::text).get() total int(re.search(r\d, total_str).group()) if total_str else 0 # 首屏20条评论已在HTML中直接解析 for comment in response.css(.article-item): item EastMoneyCommentItem() item[comment_id] comment.css(::attr(data-id)).get() item[stock_code] stock_code item[content] comment.css(.article-con::text).get().strip() item[user_nickname] comment.css(.user-name::text).get() item[post_time] comment.css(.time::text).get() yield item # 计算总页数每页20条 pages (total // 20) 1 for page in range(2, min(pages 1, 101)): # 最多爬100页防无限循环 api_url fhttps://guba.eastmoney.com/api/stock/comment/list?code{stock_code}page{page} yield scrapy.Request( urlapi_url, headers{Referer: fhttps://guba.eastmoney.com/list,{stock_code},0.html}, meta{stock_code: stock_code, page: page}, callbackself.parse_api_response )第三块API响应解析def parse_api_response(self, response): stock_code response.meta[stock_code] try: data json.loads(response.text) if data[status] 0 and list in data[data]: for comment in data[data][list]: item EastMoneyCommentItem() item[comment_id] comment[id] item[stock_code] stock_code item[content] comment[content] item[user_nickname] comment[user][nick] item[user_level] comment[user][level] item[post_time] comment[time] item[like_count] comment[like_count] yield item except Exception as e: self.logger.error(fParse API failed for {stock_code}: {e})第四块错误处理与重试def errback_httpbin(self, failure): request failure.request if failure.check(HttpError): response failure.value.response if response.status 403: self.logger.warning(f403 Forbidden for {request.url}, retrying...) # 触发重试更换IP或UA yield request.replace(dont_filterTrue)4.3 Playwright中间件让Scrapy“看见”动态内容middlewares.py中的核心类from scrapy import signals from scrapy.http import HtmlResponse from playwright.sync_api import sync_playwright class PlaywrightMiddleware: def __init__(self): self.playwright None self.browser None classmethod def from_crawler(cls, crawler): middleware cls() crawler.signals.connect(middleware.spider_opened, signalsignals.spider_opened) crawler.signals.connect(middleware.spider_closed, signalsignals.spider_closed) return middleware def spider_opened(self, spider): self.playwright sync_playwright().start() self.browser self.playwright.chromium.launch(headlessTrue) def spider_closed(self, spider): if self.browser: self.browser.close() if self.playwright: self.playwright.stop() def process_request(self, request, spider): if request.meta.get(playwright): page self.browser.new_page() page.goto(request.url) # 等待评论容器出现 page.wait_for_selector(.comment-item, timeout10000) body page.content() page.close() return HtmlResponse( urlrequest.url, bodybody.encode(utf-8), encodingutf-8, requestrequest )启用方式在Request中加meta{playwright: True}中间件自动接管。4.4 数据入库SQLite的极简高效实践pipelines.py中的存储逻辑import sqlite3 from datetime import datetime class EastMoneyCommentPipeline: def open_spider(self, spider): self.conn sqlite3.connect(eastmoney_comments.db) self.cursor self.conn.cursor() self.cursor.execute( CREATE TABLE IF NOT EXISTS comments ( id INTEGER PRIMARY KEY AUTOINCREMENT, comment_id TEXT UNIQUE NOT NULL, stock_code TEXT NOT NULL, content TEXT NOT NULL, clean_content TEXT, user_nickname TEXT, user_level INTEGER, post_time TEXT, like_count INTEGER DEFAULT 0, is_original BOOLEAN DEFAULT FALSE, created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ) ) self.conn.commit() def close_spider(self, spider): self.conn.close() def process_item(self, item, spider): # 清洗content clean_content self.clean_text(item.get(content, )) item[clean_content] clean_content # 插入数据 self.cursor.execute( INSERT OR IGNORE INTO comments (comment_id, stock_code, content, clean_content, user_nickname, user_level, post_time, like_count, is_original) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?) , ( item[comment_id], item[stock_code], item[content], clean_content, item.get(user_nickname, ), item.get(user_level, 0), item.get(post_time, ), item.get(like_count, 0), item.get(is_original, False) )) self.conn.commit() return item def clean_text(self, text): # 去广告、去表情、去多余空格 import re text re.sub(r加[微信|QQ|vx]\s*[a-zA-Z0-9\u4e00-\u9fa5]{5,15}, , text) text re.sub(r\[.*?\], , text) return .join(text.split())运行命令scrapy crawl guba -s LOG_FILEscrapy.log日志会记录每只股票的爬取进度scrapy.log里能看到实时状态。5. 常见问题与排查技巧实录5.1 典型问题速查表问题现象根本原因解决方案我的实测耗时请求返回403 ForbiddenIP被风控或Referer缺失切换代理IP检查Request Headers中Referer是否正确15分钟爬取内容为空[]API URL参数错误或stock_code格式不对用浏览器抓包确认真实API地址检查正则提取stock_code是否匹配20分钟Playwright报错Target closed浏览器进程崩溃或页面加载超时在page.goto()后加timeout30000捕获异常后重启Page10分钟SQLite插入失败报UNIQUE constraint failedcomment_id重复但数据库已有该ID检查INSERT OR IGNORE语法是否写错确认comment_id字段设为UNIQUE5分钟评论时间解析失败2023-10-25 14:22:33正常刚刚报错dateutil.parser.parse()无法处理相对时间自定义时间解析函数对“刚刚”“X分钟前”等做特殊处理30分钟5.2 独家避坑技巧那些文档里不会写的细节技巧1用scrapy shell实时调试XPath别在Spider里反复改代码再跑。直接进shellscrapy shell https://guba.eastmoney.com/list,600519,0.html response.css(span#zwcnt::text).get() 共12500条 response.xpath(//div[classarticle-item]/data-id).getall()[:3] [123456789, 123456790, 123456791]几秒钟就能验证Selector是否有效比跑完整爬虫快100倍。技巧2设置ROBOTSTXT_OBEY False但遵守道德底线Scrapy默认遵守robots.txt但东方财富的/robots.txt禁止爬/api/路径。设为False是技术必需但必须加人工节流DOWNLOAD_DELAY 3且只爬自己关注的股票绝不扫全市场。这是工程师的底线。技巧3用scrapy check验证Spider健壮性写完Spider后运行scrapy check guba它会自动检测start_requests、parse方法是否存在回调函数签名是否正确避免低级语法错误。技巧4日志分级让问题一目了然在settings.py中配置LOG_LEVEL INFO # INFO级显示请求URLWARNING级显示错误 LOG_FORMAT %(asctime)s [%(name)s] %(levelname)s: %(message)s这样scrapy.log里会清晰看到2023-10-25 14:22:33 [scrapy.core.engine] INFO: Crawled (200) GET https://guba.eastmoney.com/api/stock/comment/list?code600519page1 2023-10-25 14:22:35 [eastmoney_crawler.pipelines] WARNING: Clean text failed for comment 1234567895.3 性能优化从“能跑”到“稳跑”的关键调整并发数调到1很多人追求高并发但在股吧场景下CONCURRENT_REQUESTS 1反而最稳。因为单IP每分钟请求上限约15次设为1后DOWNLOAD_DELAY 3每分钟刚好20次略超阈值但靠随机延迟规避风控启用HTTP缓存加scrapy.extensions.httpcache.HttpCacheMiddleware对已爬过的URL直接读缓存减少重复请求关闭图片下载在settings.py中设MEDIA_ALLOW_REDIRECTS False禁用图片、CSS、JS下载专注文本用scrapyrt提供API服务部署后用curl http://localhost:9080/crawl.json?spider_namegubastock_code600519即可触发爬取方便集成到Flask后台。最后分享个小技巧我给爬虫加了个“健康检查”功能——每天凌晨自动运行爬取贵州茅台600519最新10条评论若成功则发企业微信通知“爬虫运行正常”失败则报警。这套机制上线3个月0次意外中断。真正的工程化不在代码多炫酷而在让系统自己说话。本文还有配套的精品资源点击获取
返回列表