在 AI 大模型火热的当下,智谱 AI 的 GLM Coding 套餐因为性价比高经常被抢空。本文分享我开发的 ZipuRobot 抢购机器人,从 0 到 1 实现自动监控库存、自动抢购的完整过程。
前言 智谱 AI 的 GLM Coding 套餐(Lite/Pro/Max)一直热销,尤其是 Pro 版本经常在补货后几分钟内售罄。作为一名程序员,与其每次准点守在电脑前刷新页面,不如用技术手段解决这个问题。
于是我开发了 ZipuRobot ——一个基于 Python + Playwright 的智谱 GLM Coding 抢购机器人。
项目介绍 ZipuRobot 是一个自动化抢购脚本,核心目标是:
自动监控库存 :实时检测目标套餐是否有货
自动完成抢购 :检测到有货后自动点击购买
静默登录 :通过 Cookie 实现免登录启动
定时任务 :支持设置定时运行
当前版本已经过实战验证,能够稳定完成「启动 → 登录 → 监控」的完整流程。
技术架构 技术栈
技术
用途
Python 3.10+
主语言
Playwright
浏览器自动化
asyncio
异步任务调度
unittest
单元测试
核心模块 1 2 3 4 5 6 7 8 9 10 11 12 zhipuglmbot/ ├── main.py # 主入口,包含 ZhipuRobot 类 ├── src/ │ ├── config.py # 配置管理 │ ├── cookie_manager.py # Cookie 管理 │ ├── inventory_monitor.py # 库存监控 │ ├── purchase_bot.py # 抢购执行 │ ├── scheduler.py # 定时任务 │ └── logger.py # 日志模块 ├── config.json # 配置文件 ├── cookie.json # 登录 Cookie └── logs/app.log # 运行日志
核心实现 1. 主入口与初始化 主程序入口在 main.py,核心类是 ZhipuRobot:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 class ZhipuRobot : """智谱 AI 抢购机器人。""" PRODUCT_URL = "https://bigmodel.cn/glm-coding" LOGIN_WARNING_THRESHOLD = 4 LOGIN_WAIT_TIMEOUT_MS = 400 LOGIN_WAIT_POLLING_MS = 100 def __init__ (self, config_path: str = "config.json" , verbose: bool = False ): self .config = Config(config_path) self .logger = setup_logger("zipurobot" , "DEBUG" if verbose else "INFO" ) self .cookie_manager = CookieManager() async def init_browser (self ): """初始化浏览器并应用本地 Cookie。""" from playwright.async_api import async_playwright self .playwright = await async_playwright().start() self .browser = await self .playwright.chromium.launch( headless=self .config.get("monitor.headless" , False ) ) self .context = await self .browser.new_context( viewport={"width" : 1440 , "height" : 900 }, 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" ), ) self .page = await self .context.new_page() self .cookie_manager.load_cookie() await self .cookie_manager.apply_cookie(self .page)
2. 配置管理 配置管理通过 Config 类实现,支持点号分隔的键访问:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 class Config : """配置管理""" def __init__ (self, config_path: str = None ): self .config_path = Path(config_path) self ._data: dict = {} self .load() def get (self, key: str , default: Any = None ) -> Any : """获取配置值,支持点号分隔的键""" keys = key.split('.' ) value = self ._data for k in keys: if isinstance (value, dict ): value = value.get(k) else : return default if value is None : return default return value
配置示例 config.json:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 { "target" : { "plan" : "pro" , "cycle" : "quarterly" } , "monitor" : { "headless" : false , "timeout" : 300 } , "retry" : { "max_attempts" : 3 , "base_interval" : 5 } }
3. Cookie 管理 Cookie 管理负责登录态的持久化:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 class CookieManager : """Cookie 管理""" def __init__ (self, cookie_path: str = None ): self .cookie_path = Path(cookie_path) self ._cookies: List [Dict ] = [] def load_cookie (self ) -> List [Dict ]: """加载 Cookie""" if self .cookie_path.exists(): with open (self .cookie_path, 'r' , encoding='utf-8' ) as f: self ._cookies = json.load(f) return self ._cookies def save_cookie (self, cookies: List [Dict ] ): """保存 Cookie""" self .cookie_path.parent.mkdir(parents=True , exist_ok=True ) with open (self .cookie_path, 'w' , encoding='utf-8' ) as f: json.dump(cookies, f, indent=2 , ensure_ascii=False ) self ._cookies = cookies async def apply_cookie (self, page ): """应用 Cookie 到页面""" if not self ._cookies: self .load_cookie() if self ._cookies: await page.context.add_cookies(self ._cookies)
4. 登录态识别 登录态识别是抢购的第一步,最初使用了不稳定的特征,后来根据实测改为更可靠的信号:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 async def login_check (self ) -> bool : """验证登录状态,并在登录成功后持久化 Cookie。""" await self .page.goto(self .PRODUCT_URL, wait_until="domcontentloaded" ) self .logger.info("正在验证登录状态..." ) retry_count = 0 max_retries = 120 login_signal_script = """ () => { const text = document.body.innerText || ''; const signals = { exitText: text.includes('退出') || text.includes('Logout'), profileText: text.includes('个人中心'), userProfileName: !!document.querySelector('.user-profile-name, [class*=username]'), loginRegister: text.includes('登录 / 注册'), myPlan: text.includes('我的编程套餐'), // 登录后顶部会出现 apiKey: text.includes('API Key') // 登录后可见 }; const isLoggedIn = !signals.loginRegister && ( signals.exitText || signals.profileText || signals.userProfileName || signals.myPlan || signals.apiKey ); return isLoggedIn ? signals : false; } """ while retry_count < max_retries: try : handle = await self .page.wait_for_function( login_signal_script, timeout=self .LOGIN_WAIT_TIMEOUT_MS, polling=self .LOGIN_WAIT_POLLING_MS, ) signals = await handle.json_value() self .logger.info(f"登录成功验证 (信号: {signals} ),正在同步 Cookie..." ) self .cookie_manager.save_cookie(await self .context.cookies()) return True except PlaywrightTimeoutError: retry_count += 1 if retry_count == self .LOGIN_WARNING_THRESHOLD: self .logger.warning("未检测到登录,请在弹出的浏览器中手动扫码/登录!" ) return False
[!tip] 关键发现 经过多次实测,发现最稳定的登录信号是「页面顶部出现 我的编程套餐 或 API Key」,而不是传统的「退出」按钮或个人中心入口。
5. 库存监控 库存监控是核心功能,采用双层策略:
快速 DOM 检查 :每轮循环快速检查按钮状态
自适应刷新 :根据超时时间动态调整页面刷新间隔
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 class InventoryMonitor : """库存监控""" BUY_BUTTON_SELECTOR = ".buy-btn.el-button--primary" PLAN_INDEX = { "lite" : 0 , "pro" : 1 , "max" : 2 , } @staticmethod def get_reload_interval_ms (elapsed_seconds: float , timeout_seconds: int ) -> int : """根据监控进度返回自适应刷新间隔。""" if timeout_seconds <= 0 : return 1000 progress = elapsed_seconds / timeout_seconds if progress >= 0.9 : return 1000 if progress >= 0.6 : return 3000 return 5000 async def check_stock (self, plan: str = "pro" ) -> bool : """精准检测目标方案的库存状态""" try : return await self .page.evaluate(f""" (targetPlan) => {{ const buttons = Array.from(document.querySelectorAll("{self.BUY_BUTTON_SELECTOR} ")); const indexMap = {{ lite: 0, pro: 1, max: 2 }}; const index = indexMap[targetPlan.toLowerCase()] ?? indexMap.pro; const btn = buttons[index]; if (!btn) return false; return !btn.classList.contains('is-disabled') && !btn.disabled; }} """ , plan) except Exception as e: self .logger.error(f"检查库存失败: {{e}}" ) return False async def wait_for_stock (self, plan: str = "pro" , timeout: int = 300 , check_interval_ms: int = 500 ) -> bool : """补货监控循环""" self .logger.info(f"🚀 启动【{plan} 】精准监控模式 (频率: {check_interval_ms} ms)" ) start_time = asyncio.get_event_loop().time() last_reload_at = start_time while True : now = asyncio.get_event_loop().time() elapsed = now - start_time reload_interval_ms = self .get_reload_interval_ms(elapsed, timeout) if now - last_reload_at >= reload_interval_ms / 1000.0 : await self .page.reload(wait_until="domcontentloaded" ) last_reload_at = asyncio.get_event_loop().time() if await self .check_stock(plan): self .logger.info(f"🎯 监测到【{plan} 】补货完成!" ) return True if elapsed > timeout: return False await asyncio.sleep(check_interval_ms / 1000.0 )
6. 主执行流程 完整的抢购流程:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 async def run (self ) -> bool : """执行主流程。""" self .logger.info("=" * 50 ) self .logger.info("开始执行抢购任务" ) self .logger.info("=" * 50 ) try : await self .init_browser() if not await self .login_check(): self .logger.error("登录超时,任务终止" ) return False target_plan = self .config.get("target.plan" , "pro" ) monitor = InventoryMonitor(self .page, self .logger) self .logger.info(f"开始进入库存监控状态 (方案: {target_plan} )..." ) stock_start = asyncio.get_event_loop().time() if not await monitor.wait_for_stock( plan=target_plan, timeout=self .config.get("monitor.timeout" , 300 ), check_interval_ms=300 , ): self .logger.warning("监控结束,未检测到补货" ) return False self .logger.info(f"发现库存!准备下单..." ) if not await monitor.click_buy_button(plan=target_plan): self .logger.error(f"点击 {target_plan} 订阅按钮失败" ) return False purchase_bot = PurchaseBot(self .page, self .logger, self .config) if await purchase_bot.execute_purchase(): self .logger.info("抢购流程执行完成!请在浏览器中确认支付结果。" ) return True self .logger.error("购买流程中途失败" ) return False finally : await self .close_browser()
使用教程 环境准备 1 2 3 4 5 6 7 8 9 git clone <repo-url> cd zhipuglmbotpip install -r requirements.txt playwright install chromium
配置说明 编辑 config.json:
1 2 3 4 5 6 7 8 9 10 { "target" : { "plan" : "pro" , "cycle" : "quarterly" } , "monitor" : { "headless" : false , "timeout" : 300 } }
运行方式 1 2 3 4 5 6 7 python main.py python main.py -v
套餐定位 页面上有 3 个购买按钮(Lite/Pro/Max),通过固定索引定位:
索引
套餐
选择器
0
Lite
.buy-btn.el-button--primary (第1个)
1
Pro
.buy-btn.el-button--primary (第2个)
2
Max
.buy-btn.el-button--primary (第3个)
经验总结 踩过的坑
Cookie 过期 :智谱的 Cookie 会定期失效,需要定时刷新
页面结构变化 :购买按钮的选择器可能随页面更新变化,需要定期维护
弹窗结构不稳定 :真实购买弹窗的 DOM 结构难以稳定获取,建议补货时手动抓取
登录信号误判 :最初使用「退出」「个人中心」作为登录信号,实测发现不稳定,改为「我的编程套餐」「API Key」更可靠
注意事项
请确保 Cookie 有效,支付时可能需要手动确认
抢购成功后请及时完成支付
本工具仅供学习交流,请遵守网站服务条款
下一步计划
在真实补货时抓取购买弹窗 DOM
校准周期选择器和最终确认按钮
完成一次完整的实战抢购验证
如果你对这个项目感兴趣,欢迎在评论区交流讨论!
相关文档:[[2026-03-14-zhipurobot-usage]] [[2026-03-14-zhipurobot-status]]