Steam评论爬虫提前终止、抓取量不稳定问题排查与解决咨询
我来帮你拆解这个问题,结合Steam API的机制和你用的aesuli/steam-crawler脚本的问题点,一步步分析原因和解决方案:
一、为什么会随机提前终止?
你用的正则endre = re.compile(r'({"success":2})|(no_more_reviews)')是核心问题所在:
{"success":2}并不是真的没有更多评论了,Steam API返回这个值通常是临时限流、服务器负载过高或者请求参数异常,属于可恢复的错误,不是终止信号。- 用正则匹配JSON字符串本身就不可靠,Steam的API返回格式可能有细微变化(比如空格、转义符差异),会导致误判
no_more_reviews或者success:2,从而提前终止抓取。 - 对于CS:GO这种百万级评论的游戏,请求频率很容易触发Steam的限流机制,这时候脚本直接终止,而不是重试,就会出现抓取页数随机的情况。
二、为什么Cursor会随机变化?
Steam的cursor不是固定的页码,而是一个动态生成的加密标记,它指向评论列表中的具体位置,并且会受到以下因素影响:
- 评论的实时更新(新评论发布、旧评论被删除);
- 服务器的负载均衡策略;
- 请求的时间窗口和用户代理等参数。
它的作用是让API准确返回下一页的评论,而不是基于固定页码——因为百万级的评论列表如果用页码,会因为实时更新导致数据重复或遗漏。所以每次请求返回的cursor都是唯一的,不能复用之前的,必须用当前页返回的cursor请求下一页。如果脚本没有正确提取和传递最新的cursor,就会请求到错误的页面,触发终止条件。
三、可行的解决方案
1. 替换正则判断,改用JSON解析终止条件
不要用正则匹配字符串,直接解析API返回的JSON数据,这是最可靠的方式:
import json import time # 假设你获取到了API的response文本 try: resp_data = json.loads(response.text) except json.JSONDecodeError: # 解析失败,重试当前页 time.sleep(3) continue # 处理不同的success状态 if resp_data.get("success") == 2: # 临时限流/错误,指数退避重试 time.sleep(5) # 可以根据情况递增等待时间 continue elif resp_data.get("success") != 1: # 其他错误,记录日志后跳过 print(f"Unexpected success code: {resp_data.get('success')}") continue # 判断是否真的没有更多评论 reviews = resp_data.get("reviews", []) next_cursor = resp_data.get("cursor") if not reviews or not next_cursor: # 没有评论或没有下一页cursor,终止抓取 break # 处理当前页的评论数据(日期、内容、用户ID等) # ... # 用最新的cursor请求下一页 next_url = f"https://store.steampowered.com/appreviews/{app_id}?cursor={next_cursor}&..."
2. 正确处理Cursor的传递
每次请求必须使用上一页返回的cursor参数,不能复用旧的URL或cursor。Steam的cursor是一次性的,用过之后就会失效,所以必须从当前页的返回数据中提取最新的cursor,作为下一页请求的参数。
3. 添加请求频率控制与重试机制
- 请求间隔:每页请求后添加1-2秒的等待时间,避免触发Steam的限流。对于百万级评论的游戏,甚至可以适当延长等待时间,降低被ban的风险。
- 指数退避重试:遇到
success:2或者请求失败时,不要直接终止,而是等待一段时间后重试,等待时间可以递增(比如第一次等3秒,第二次等6秒,最多重试5次)。
4. 放弃“保存已请求URL重复循环”的思路
这个方法不可行,因为每个URL的cursor是唯一的,重复请求同一个URL只会得到相同的内容,而且会增加触发限流的概率。正确的做法是跟踪当前的cursor,逐步推进抓取。
总结
你的核心问题是终止条件判断不准确,加上没有正确处理Steam API的动态cursor和限流机制。改用JSON解析判断终止状态、正确传递cursor、添加重试和请求间隔,就能解决CS:GO这类大评论量游戏的提前终止问题。
内容的提问来源于stack exchange,提问作者Jauhnax




