尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

基于Playwright的雪球股票情绪数据采集实战与避坑指南

基于Playwright的雪球股票情绪数据采集实战与避坑指南 雪球网上的讨论区是我个人认为中文投资社区里情绪信号最密集的地方之一。一只票的讨论热度、多空措辞的变化、大V发言后的跟帖节奏这些东西拼起来往往比单纯的行情数据更能反映短期市场情绪的走向。问题在于这些内容靠人工翻页去记录一天两天还行时间一长根本扛不住——帖子在滚动、评论在折叠、页面还在不停异步加载。我最初用 requests 直接怼接口前几次还算顺利后来发现返回的 HTML 里关键列表是空的数据全靠前端 JS 渲染纯静态抓取这条路走不通。于是我把方案换成了 Playwright用真实浏览器内核去驱动页面让内容先渲染出来再取。这篇就把我整套基于 Playwright 的雪球股票情绪数据采集流程拆开讲清楚包括为什么选它、页面结构怎么摸、数据怎么落库、以及我踩过的那些坑。1. 为什么情绪数据采集最终落在了 Playwright 上1.1 情绪数据的价值到底在哪先把采什么说清楚不然工具选型就是空谈。我做这个采集目标不是把雪球上所有文字都搬下来而是围绕特定股票代码抓取讨论区的帖子标题、正文摘要、发布时间、作者、阅读数、评论数以及评论区的文本内容。这些字段组合起来能支撑几类分析一是讨论热度曲线看某只票在什么时间点被集中讨论二是情绪极性分布通过文本判断看多、看空、中性的比例三是关键节点回溯比如某天股价异动回头去看当天讨论区在聊什么。这三类分析对数据的要求是不一样的。热度曲线只关心数量和时间的对应关系对文本质量要求低情绪极性要求文本完整、不能缺字漏字关键节点回溯则要求时间戳精确到分钟且评论要能跟主帖关联上。所以采集方案必须保证字段完整、时间准确、层级关系不丢这三点直接决定了后面能不能做分析。1.2 requests 方案为什么在雪球上折戟我一开始的想法很朴素找到列表接口用 requests 循环翻页解析 JSON 就完事。实际操作下来问题出在几个地方。第一讨论区首屏的 HTML 里确实有内容但往下滚动加载的部分是异步请求回来的接口地址带了动态签名参数这个签名跟时间戳、页面状态都有关系硬解成本很高。第二就算拿到了接口返回里面的字段是经过裁剪的正文经常只给摘要完整内容还要再发一次请求。第三也是最要命的一点频繁直接请求接口很容易触发风控返回空数据或者要求验证。这里我要强调一个判断当目标站点的数据依赖前端渲染、且接口有动态签名时继续死磕接口解析的投入产出比会急剧下降。你花三天逆向出来的签名逻辑对方改一行代码就废了。这不是技术能力问题是维护成本问题。1.3 Playwright 相比直接解码爬虫的真实优势Playwright 的核心价值是它驱动的是一个真实的浏览器实例。页面该怎么渲染就怎么渲染JS 该怎么执行就怎么执行我拿到的就是用户在浏览器里看到的那份 DOM。这带来几个直接好处不用逆向签名请求由浏览器自己发出签名参数由页面脚本生成我完全不关心它怎么算的。动态内容天然可见滚动加载、点击展开、懒加载图片这些在 Playwright 里就是模拟用户操作等元素出现即可。调试成本低可以直接截图、可以打印当前 DOM、可以在控制台里试选择器定位问题比对着接口返回猜要快得多。当然代价也有速度比纯接口慢资源占用高。所以我的策略是能用接口拿的静态字段就用接口渲染相关的核心内容才走 Playwright两者结合。但对雪球这种渲染重、风控严的场景Playwright 是主力。提示Playwright 不是更高级的爬虫它是一个浏览器自动化工具。把它当爬虫用前提是你接受它慢但稳的特性。追求极致速度的场景它不合适。2. 动手前必须摸清的雪球页面结构2.1 讨论区列表的 DOM 特征在写任何代码之前我花了大概两个小时纯手动操作页面配合浏览器开发者工具观察结构。这一步不能省省了后面全是返工。雪球个股讨论区的列表每一条帖子通常是一个独立的容器元素容器里包含作者信息区、正文区、底部互动区。我需要的是给这些容器找一个稳定的、不随内容变化的定位锚点。我的做法是优先找带有语义化class或者>pip install playwright playwright install chromium这里有个选择装 chromium 还是 firefox 还是 webkit。我的建议是优先 chromium兼容性最好社区资料最多遇到问题好查。除非目标站点对 chromium 有特殊检测才考虑换内核。安装完记得跑一下playwright install把内核下载全不然运行时报找不到浏览器很常见。3.2 用 storage_state 保存登录态登录态持久化是这套方案能不能长期跑的关键。Playwright 提供了storage_state机制可以把 cookies 和 localStorage 存成 JSON 文件。我的流程是第一次运行一个登录脚本打开有头浏览器手动完成登录。登录成功后调用context.storage_state(pathstate.json)保存状态。后续采集脚本启动时用browser.new_context(storage_statestate.json)加载状态。这样每次采集都不用重新登录。但要注意登录态是有有效期的一般能撑几天到一两周。所以我的采集脚本里加了一个检测如果发现页面跳转到登录页就抛出明确异常提醒我重新登录而不是默默采一堆空数据。from playwright.sync_api import sync_playwright with sync_playwright() as p: browser p.chromium.launch(headlessFalse) context browser.new_context(storage_statestate.json) page context.new_page() page.goto(https://xueqiu.com) # 检测是否掉登录 if 登录 in page.title() or page.locator(text登录).count() 0: raise RuntimeError(登录态已失效请重新登录并更新 state.json)3.3 无头模式与有头模式的取舍开发调试阶段我一定用有头模式headlessFalse因为能看到页面在干什么选择器对不对、有没有弹窗遮挡一目了然。等逻辑稳定了再切无头模式跑批量。但有个坑有些站点对无头浏览器的检测更严如果切无头后突然采不到数据八成是被识别了。这时候要么加一些反检测配置要么干脆保持有头模式跑在服务器上配合虚拟显示。我的实际选择是长期跑的任务保持有头模式虽然占资源但稳定性明显更好省下来的排错时间远比那点资源值钱。3.4 依赖版本锁定Playwright 更新比较频繁不同版本的行为偶尔有差异。我建议在项目里用requirements.txt锁死版本避免某天自动升级后脚本突然跑不通。这个教训是我在另一个项目里用血换来的——升级后某个等待 API 的行为变了排查了大半天。4. 采集主流程的代码拆解与关键等待策略4.1 页面导航与首屏就绪判断导航到讨论区页面后不能立刻开始抓要等首屏列表渲染完成。我的判断方式是等待某个必然出现的列表项元素出现而不是等load事件。因为load事件触发时异步内容可能还没渲染。page.goto(fhttps://xueqiu.com/S/{stock_code}) page.wait_for_selector(.讨论列表项的选择器, timeout15000)这里的超时设置也有讲究。设太短网络慢时误判失败设太长真出问题时干等。我一般设 15 秒配合重试机制。4.2 滚动加载的循环控制这是整个采集的核心逻辑。我的实现思路是def scroll_and_collect(page, max_scroll50): last_count 0 stable_rounds 0 for i in range(max_scroll): items page.locator(.讨论列表项的选择器) current_count items.count() if current_count last_count: stable_rounds 1 if stable_rounds 3: # 连续3次没增长判定到底 break else: stable_rounds 0 last_count current_count page.mouse.wheel(0, 3000) page.wait_for_timeout(random.randint(2000, 4000)) return last_count关键点是stable_rounds这个计数器。连续多次数量不增长才判定到底单次不增长可能只是加载慢直接停会漏数据。这个细节决定了数据完整性。4.3 单条数据的字段提取列表项拿到之后逐条提取字段。这里用相对定位从每个列表项容器内部找子元素避免全局选择器串数据。for i in range(items.count()): item items.nth(i) title item.locator(.标题选择器).inner_text() author item.locator(.作者选择器).inner_text() publish_time item.locator(.时间选择器).get_attribute(title) or \ item.locator(.时间选择器).inner_text() content item.locator(.正文选择器).inner_text() # 阅读数、评论数类似处理时间字段我特别处理了一下优先取title属性因为雪球列表上显示的是3小时前这种相对时间而title里往往是精确时间戳。相对时间对分析毫无价值必须拿到绝对时间这是很多人会忽略的点。4.4 异常捕获与断点续采采集过程中难免遇到某条数据提取失败元素结构变了、内容为空。我的做法是单条 try-except失败就跳过并记录绝不让一条数据的异常中断整个任务。同时把已采集的数据实时写入支持断点续采——记录最后成功采集的页码或时间戳下次从那里继续。try: # 提取逻辑 save_to_db(record) except Exception as e: log_error(f提取失败: {e}, 跳过该条) continue这个设计让采集任务可以长时间稳定运行即使中途挂了重启也能接着来不用从头开始。5. 数据落库、去重与情绪字段的预处理5.1 存储选型SQLite 起步够用再升级数据存哪我的建议是先用 SQLite。单机采集、数据量在百万级以内SQLite 完全够用零配置、单文件、方便迁移。等数据量真的上来了或者需要多机并发写入再考虑换 PostgreSQL 或 MySQL。一上来就上重型数据库对个人项目是过度设计。表结构我设计得比较简单主帖一张表评论一张表用帖子 ID 关联。字段包括股票代码、帖子 ID、标题、正文、作者、发布时间、阅读数、评论数、采集时间。5.2 去重策略帖子 ID 是唯一锚点去重是采集系统必须解决的问题否则重复跑几次数据就脏了。我的做法是用帖子 ID 做唯一约束插入时用INSERT OR IGNORESQLite 语法重复的直接忽略。帖子 ID 从哪来从列表项的链接里提取雪球的帖子链接里带唯一 ID这是最可靠的去重依据。注意不要用标题作者做去重键因为同一作者可能发相似标题也可能编辑标题。ID 才是唯一且稳定的。5.3 时间字段的标准化前面提到相对时间的问题这里展开说。采集到的时间可能是3小时前昨天 14:302024-01-15 09:20等多种格式。我写了一个解析函数统一转换成标准时间戳。对于相对时间用采集时刻倒推。这个转换必须在入库前完成存原始字符串会让后续分析非常痛苦。5.4 情绪分析前的文本清洗如果后续要做情绪分析文本清洗不能少。我的清洗步骤包括去除 HTML 残留标签、去除多余空白和换行、过滤掉纯表情和纯符号的内容、统一全半角。清洗完的文本再送去做极性判断准确率会明显提升。这一步很多人偷懒跳过结果分析出来的情绪分布全是噪音。6. 那些让我熬夜的坑与对应的解法6.1 选择器失效从随机类名到语义定位最开始我用浏览器直接复制的选择器全是css-xxxx这种随机类名。跑了两天雪球发版全废了。后来我改成优先用稳定的语义属性实在没有再退回结构定位。这个转变让脚本的存活周期从几天延长到了几个月。教训就是永远不要用构建工具生成的类名做选择器。6.2 滚动到底判定错误导致漏采前面提过我一开始用单次不增长就停结果经常漏掉最后几页。改成连续三次不增长才停之后数据完整性明显改善。这个坑很隐蔽因为漏的数据不会报错你根本不知道少了。6.3 登录态过期后的静默失败有一次脚本跑了一晚上第二天发现采了几千条空数据——登录态半夜过期了页面跳转到登录页但我的提取逻辑没报错只是提取到了空字符串。后来我加了前置登录检测和内容非空校验双保险。这个坑的教训是采集系统必须对异常但没报错的情况保持警惕。6.4 频率过高触发的访问限制早期我为了快把间隔设得很短结果采到一半开始返回空列表。降低频率、加随机间隔后恢复正常。采集节奏要像真人浏览而不是机器扫荡。这个道理说起来简单但真到自己写的时候总想快一点然后就被教做人了。6.5 内存占用随采集时长增长长时间运行后浏览器实例内存会持续增长。我的解法是分批处理每采集一定页数关闭当前页面重新开一个或者定期重启浏览器实例。这样内存能保持稳定不会跑着跑着把机器拖垮。7. 从原始数据到可用情绪指标的加工链路7.1 讨论热度的量化原始数据里每条帖子有发布时间。我把时间按小时或按天聚合统计每个时间窗口内的帖子数量就得到了讨论热度曲线。再结合评论数加权能区分很多人发帖和少数帖子引发大量讨论这两种不同的热度形态。7.2 情绪极性的初步判断情绪判断我分两步走。第一步用关键词词典做快速打标比如看好加仓起飞归为正面割肉跑路垃圾归为负面。这个方法简单、快、可解释缺点是覆盖不全。第二步再用文本模型做补充判断处理词典覆盖不到的复杂表达。两步结合兼顾速度和准确率。7.3 数据质量的持续监控采集不是跑完就完事得有监控。我每天会检查几个指标当天采集条数是否在正常范围、时间字段是否有异常值、空内容比例是否突然升高。任何一个指标异常都说明采集链路可能出了问题。把数据质量监控当成采集系统的一部分这是我做了几个采集项目后养成的习惯。8. 长期稳定运行的一些个人经验采集这套东西写代码可能只占三成精力剩下七成都在跟稳定性较劲。我现在的做法是把采集任务拆成登录态维护数据采集数据入库质量检查几个独立环节每个环节单独跑、单独监控。哪个环节出问题一眼就能定位不用在一大坨代码里找。另外我强烈建议给采集脚本加详细的日志记录每一步的时间、结果、异常。出问题时日志就是你的现场记录。我吃过没日志的亏出了问题只能靠猜有了日志之后排查效率完全不是一个量级。最后说个心态上的事。做数据采集尤其是针对有风控的站点不要追求一次采全、采快。可持续、可恢复、数据干净比单次采得多重要得多。我见过太多人追求速度结果账号被限、脚本被封反而什么都采不到。慢一点稳一点长期看反而效率最高。这套基于 Playwright 的方案我从最初跑通到现在稳定运行中间迭代了十几版核心逻辑没变变的全是稳定性和容错。如果你也在做类似的事希望这些踩坑经验能帮你少走点弯路。
返回列表