简介:这是一套面向个人学习者的京东商品库存监控与自动下单系统源码,适用于Windows/macOS双平台,帮助开发者解决热门商品秒杀场景下的实时盯盘与快速下单难题。资源包含61个文件,以9个Python核心脚本(如JdBuyer.py、JdSession.py、timer.py)、10个备份文件(.zbak)、36个文本类配置与区域ID列表(覆盖全国34个省级行政区及港澳台、海外、钓鱼岛等特殊编码)、以及图标(ico)、说明文档(md)、配置文件(ini/json)等为主,整体压缩包仅651KB,轻量易部署。目前已有55人学习下载,适合具备基础Python和HTTP会话管理能力的学习者深入理解电商自动化流程。读者可直接运行GUI版本(JdBuyerApp.py)获得可视化操作体验,或调用Shell模式实现7×24小时无人值守监控;配套完整日志记录、微信消息推送、异常捕获与区域ID映射机制,代码结构清晰,模块职责分明,是实践网络请求、会话保持、定时任务与跨平台GUI开发的优质学习样本。
1. 这不是抢购脚本,而是一套可调试、可验证、可复现的 GUI 库存监控闭环:它不承诺“秒杀成功”,但能让你看清页面加载逻辑、接口响应边界、状态轮询节奏——适合个人学习 Web 自动化中「感知-决策-执行」链路的完整建模过程
你见过凌晨三点还在刷新京东商品页的人吗?我见过。但更常见的是:写完自动下单代码,跑起来就报错ElementNotInteractableException;改完又卡在登录态失效,验证码弹窗没识别;再调参,发现库存接口返回{"code":403,"msg":"非法请求"},连请求头都对不上。这不是玄学,是缺一个带可视化反馈、可单步观察、状态可回溯的监控系统。这个源码包就是为此而生:它用 Python + PyQt6 构建 GUI 主界面,内嵌 Chromium 内核(通过 PyQtWebEngine)真实渲染京东商品页,同时并行发起轻量级 API 探测(绕过前端 JS 渲染依赖),双通道比对库存状态;所有操作日志实时输出到界面上方文本框,关键节点(如“检测到有货”“点击立即购买”“提交订单前校验”)均触发颜色高亮与声音提示。它不封装成黑匣子,所有核心模块(页面监听器、API 请求器、订单表单填充器、异常拦截器)全部解耦为独立.py文件,变量命名直白(如self.last_stock_api_response),注释标注了京东 PC 端当前(2024年中)实际返回字段结构。如果你正在学 Selenium 的局限性、想理解为什么纯接口调用会失败、或需要一份能讲清楚「浏览器自动化 vs 接口探测」适用边界的实操材料——它比教程视频更值得你 clone 下来逐行 debug。
2. 环境准备与项目结构解析:从 requirements.txt 到 main.py 的五层依赖关系
2.1 环境隔离:为什么必须用 Python 3.9+ 而非系统默认版本?
该项目对 Qt 绑定和 WebEngine 内核版本敏感。PyQt6 6.5+ 要求 Python ≥ 3.9,且 macOS 上若使用系统自带 Python(通常为 3.8 或更低),PyQtWebEngine安装时会因 SIP 机制缺失必要符号而编译失败。常见翻车点是:pip install PyQt6成功,但运行时报ImportError: cannot import name 'QWebEngineView'—— 这本质是 PyQtWebEngine 未正确链接到 QtWebEngine 框架。
正确做法:
# macOS 推荐用 pyenv 管理多版本 brew install pyenv pyenv install 3.9.18 pyenv global 3.9.18 # Windows 推荐从 python.org 下载 3.9.18 embeddable zip 版,解压后用其内置 pip提示:不要用
conda install pyqt,conda 渠道的 PyQt6 默认不包含 WebEngine 组件(需额外装pyqtwebengine包,但版本兼容性极差)。必须用pip安装官方 wheel。
2.2 requirements.txt 的真实作用:三类依赖的分工逻辑
打开requirements.txt,你会看到如下分组(已按功能重排,非原始顺序):
| 类别 | 包名 | 作用说明 | 关键版本约束 |
|---|---|---|---|
| GUI 核心 | PyQt6==6.5.3,PyQt6-WebEngine==6.5.3 | 提供窗口、按钮、Web 视图控件;WebEngine 必须与 PyQt6 主版本严格一致,否则QWebEngineView初始化失败 | ==6.5.3是经实测 macOS 13.6 / Windows 11 兼容的最高稳定版 |
| HTTP 客户端 | requests==2.31.0,urllib3==1.26.18 | 发起库存 API 探测;urllib3锁死 1.26.x 因requests 2.31.0对其有 ABI 依赖,新版urllib3 2.x会导致MaxRetryError | 若升级requests,必须同步验证urllib3兼容性 |
| 辅助工具 | lxml==4.9.3,beautifulsoup4==4.12.2 | 解析 HTML 页面中的 DOM 结构(如“加入购物车”按钮是否禁用);lxml用于 XPath 高速定位,bs4作 fallback | lxml 4.9.3是最后一个支持 Python 3.9 且无 C++17 编译错误的版本 |
安装命令必须带-r参数并指定 pip 版本:
python -m pip install --upgrade "pip>=22.0,<23.0" python -m pip install -r requirements.txt注意:
pip>=23.0会因PyQt6-WebEnginewheel 元数据问题跳过安装,这是 PyPI 上已知 issue(#12147),非本项目缺陷。
2.3 项目目录树:五个核心模块如何协同工作?
解压源码后,目录结构如下(删减测试/文档等非运行文件):
jd_monitor/ ├── main.py # GUI 主入口:创建窗口、启动事件循环、绑定信号槽 ├── monitor/ # 核心监控逻辑 │ ├── page_listener.py # 封装 QWebEngineView 加载完成事件,提取页面 DOM 中的库存文字(如“仅剩2件”) │ ├── api_prober.py # 封装 requests.Session,向京东商品详情页 API(/ware/detail.json)发 GET 请求,解析 stockState 字段 │ └── decision_engine.py # 双通道结果融合:当 page_listener 判“有货” AND api_prober 判“可购买”时,触发下单流程 ├── order/ # 下单动作执行 │ ├── cart_handler.py # 模拟点击“加入购物车”按钮(通过 evaluateJavaScript 注入 JS) │ └── submit_handler.py # 在结算页填充收货地址、选择支付方式、点击“提交订单”(需处理京东的防机器人 click 延迟) └── utils/ ├── logger.py # 自定义日志器:将时间戳、模块名、级别、消息写入 GUI 文本框,并支持保存到本地 jd_log_YYYYMMDD.log └── config_loader.py # 读取 config.json(含商品 URL、监控间隔、最大重试次数),支持热重载(按 Ctrl+R 重新加载配置)关键设计点:decision_engine.py不直接调用order/模块,而是通过QtCore.pyqtSignal发射order_required信号,由main.py中的槽函数接收并调用submit_handler.execute()。这种解耦让下单逻辑可被单独单元测试(无需启动 GUI)。
2.4 配置文件 config.json:四个必填字段与两个隐藏开关
config.json是唯一需要用户修改的文件,其结构强制校验(启动时若缺失字段则报错退出):
{ "target_url": "https://item.jd.com/1000XXXXXXX.html", "check_interval_ms": 3000, "max_retries": 5, "auto_submit": true, "debug_mode": false, "user_agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36" }target_url:必须是京东 PC 端商品页 URL(不能是移动端 m.jd.com),且需确保该商品支持“立即购买”(部分预售/定制商品无此按钮);check_interval_ms:页面 DOM 监听与 API 探测的间隔,不建议低于 2000ms—— 京东前端有防刷逻辑,高频请求会返回403;max_retries:下单失败后的重试次数,每次重试前强制等待check_interval_ms * 2,避免被风控;auto_submit:设为false时,系统只监控并高亮“有货”状态,不执行任何点击操作,适合调试阶段;debug_mode:设为true时,page_listener.py会将每次提取的 DOM 文本、api_prober.py会将完整 API 响应体(含 headers)打印到日志框,上线前务必关掉(暴露 cookies);user_agent:必须与你手动访问京东时浏览器 UA 一致,否则api_prober请求会被拒绝(京东服务端校验 UA 与 cookies 的关联性)。
提示:
user_agent获取方法——在 Chrome 打开京东,F12 → Console 输入navigator.userAgent回车,复制结果粘贴进 config.json。
3. GUI 界面交互与双通道监控实现:从页面加载到库存判定的完整链路
3.1 主窗口初始化:QWebEngineView 的三个关键配置项
main.py中创建QWebEngineView实例时,设置了三个影响监控稳定性的参数:
self.web_view = QWebEngineView() # 关键配置1:禁用图片加载,加速页面解析 self.web_view.settings().setAttribute(QWebEngineSettings.WebAttribute.AutoLoadImages, False) # 关键配置2:禁用 JavaScript 弹窗(防止 alert() 阻塞自动化流程) self.web_view.page().javaScriptAlertRequested.connect(lambda w, m: None) # 关键配置3:启用控制台日志捕获(用于调试 JS 错误) self.web_view.page().javaScriptConsoleMessageReceived.connect( lambda l, m, n, msg: self.logger.info(f"[JS] {msg}") )AutoLoadImages=False:京东商品页含大量高清图,加载耗时 2~5 秒,禁用后首屏渲染时间从 8s 降至 1.2s,且库存文字(DOM 文本)不受影响;javaScriptAlertRequested槽函数设为空 lambda:某些京东活动页会插入alert("活动火爆"),若不拦截,GUI 线程会挂起等待用户点击“确定”;javaScriptConsoleMessageReceived:当页面 JS 报错(如Cannot read property 'stock' of undefined)时,日志框会显示[JS] TypeError: Cannot read property 'stock' of undefined,帮助定位前端结构变更。
3.2 页面监听器:XPath 定位库存文字的鲁棒性策略
page_listener.py的核心是extract_stock_text()方法,它不依赖固定 CSS 选择器(易失效),而是用 XPath 动态匹配:
def extract_stock_text(self) -> str: # 策略1:优先匹配 <div class="stock"> 中的文本(京东标准库存容器) stock_div = self.web_view.page().runJavaScript( "document.evaluate('//div[contains(@class,\"stock\")]', document, null, XPathResult.FIRST_ORDERED_NODE_TYPE, null).singleNodeValue?.textContent || ''" ) if stock_div and "有货" in stock_div or "现货" in stock_div or "仅剩" in stock_div: return stock_div.strip() # 策略2:fallback 到按钮文字(“立即购买”是否禁用) buy_btn = self.web_view.page().runJavaScript( "document.querySelector('button.btn-buy')?.innerText || ''" ) if buy_btn and "立即购买" in buy_btn: return "有货" # 策略3:终极 fallback,检查 URL 是否含 /soldout/ 路径 url = self.web_view.url().toString() if "/soldout/" in url: return "无货" return "未知"runJavaScript()返回None表示 JS 执行失败(页面未加载完),此时方法返回"未知",decision_engine会忽略该次结果;- 三个策略按优先级降序执行,覆盖京东 2023Q4 至 2024Q2 的三次前端改版(class 名从
J-stock→stock→product-stock); || ''是 JS 空值安全操作,避免textContent为null时抛异常中断执行。
3.3 API 探测器:绕过前端渲染的轻量级库存验证
api_prober.py的probe_stock_api()方法直接调用京东商品详情接口,不依赖页面 DOM:
def probe_stock_api(self) -> dict: # 构造 API URL:从商品页 URL 提取 skuId sku_id = re.search(r'/(\d+)\.html', self.config["target_url"]).group(1) api_url = f"https://item.jd.com/ware/detail.json?skuId={sku_id}" # 关键:复用浏览器 cookies(登录态) cookies = self.get_browser_cookies() # 从 QWebEngineProfile 获取当前 cookies headers = { "User-Agent": self.config["user_agent"], "Referer": self.config["target_url"], "Origin": "https://item.jd.com" } try: resp = self.session.get(api_url, headers=headers, cookies=cookies, timeout=5) if resp.status_code == 200: data = resp.json() # 京东 API 返回结构示例:{"ware":{"stockState":30,"stockNum":2}} return { "stock_state": data.get("ware", {}).get("stockState", 0), "stock_num": data.get("ware", {}).get("stockNum", 0), "is_in_stock": data.get("ware", {}).get("stockState", 0) in [30, 31, 32] # 30=有货, 31=预售, 32=预约 } except Exception as e: self.logger.error(f"API 探测失败: {e}") return {"is_in_stock": False}get_browser_cookies()通过QWebEngineProfile.defaultProfile().cookieStore()同步浏览器 cookies,确保 API 请求携带有效登录态(否则返回{"code":403});stockState字段是京东后端真实库存状态码,比页面文字更可靠(页面文字可能缓存);timeout=5是硬性限制:若 API 响应超时,decision_engine会以page_listener结果为准,避免因网络抖动误判。
3.4 决策引擎:双通道结果融合的四种状态机
decision_engine.py的evaluate_stock_status()方法定义了监控状态机:
def evaluate_stock_status(self, page_result: str, api_result: dict) -> str: # 状态1:双通道确认有货 → 触发下单 if ("有货" in page_result or "现货" in page_result) and api_result.get("is_in_stock", False): return "ORDER_READY" # 状态2:页面显示有货但 API 返回无货 → 可能页面缓存,延迟 1s 后重查 if ("有货" in page_result) and not api_result.get("is_in_stock", False): self.logger.warning("页面显示有货但 API 无货,疑似缓存,延迟重查") QTimer.singleShot(1000, self.recheck) return "WAITING" # 状态3:页面显示无货但 API 有货 → 页面 JS 未执行完,强制刷新 if ("无货" in page_result or "售罄" in page_result) and api_result.get("is_in_stock", False): self.logger.warning("API 有货但页面无货,强制刷新页面") self.web_view.reload() return "REFRESHING" # 状态4:双通道均无货 → 继续监控 return "MONITORING"ORDER_READY状态触发order_required信号,进入下单流程;WAITING和REFRESHING是自适应纠错机制,避免因单通道瞬时异常导致误操作;- 所有状态转换均记录到日志框,例如:
[2024-06-15 02:14:22] INFO: 状态切换: MONITORING → WAITING。
4. 自动下单流程与防风控设计:从“加入购物车”到“提交订单”的七步动作链
4.1 购物车操作:为什么必须用 evaluateJavaScript 而非 Selenium click?
京东“加入购物车”按钮(<button class="btn-addtocart">)绑定了复杂的事件监听器,包括:
mousedown时添加activeclass;mouseup时触发addToCart()函数;click事件本身被preventDefault()阻止。
Selenium 的element.click()只触发click事件,无法模拟真实鼠标行为。而evaluateJavaScript可注入完整事件序列:
# cart_handler.py def add_to_cart(self): js_code = """ (function() { const btn = document.querySelector('button.btn-addtocart'); if (!btn) return '按钮未找到'; // 模拟真实鼠标按下-释放 btn.dispatchEvent(new MouseEvent('mousedown', {bubbles: true})); btn.dispatchEvent(new MouseEvent('mouseup', {bubbles: true})); // 等待 300ms 让 JS 处理异步逻辑 return new Promise(resolve => setTimeout(() => { resolve('已加入购物车'); }, 300)); })(); """ result = self.web_view.page().runJavaScript(js_code) return result # 返回 '已加入购物车' 或 None(超时)bubbles: true确保事件冒泡到 document,触发京东全局事件处理器;setTimeout是关键:京东 JS 在mouseup后需 200~400ms 完成购物车数量更新、弹窗提示,不等待则下一步“去购物车”按钮不可见。
4.2 结算页表单填充:地址与支付方式的 DOM 定位策略
submit_handler.py的fill_checkout_form()方法采用“可见性优先”原则定位元素:
def fill_checkout_form(self): # 步骤1:等待收货地址区域可见(class="address-item") self.wait_for_element_visible("//div[contains(@class,'address-item') and @style!='display: none;']") # 步骤2:选择第一个可用地址(排除“新增地址”按钮) address_items = self.web_view.page().runJavaScript( "Array.from(document.querySelectorAll('div.address-item')).filter(el => el.style.display !== 'none').length" ) if address_items > 0: # 执行 JS 点击第一个地址 self.web_view.page().runJavaScript( "document.querySelectorAll('div.address-item')[0].click()" ) # 步骤3:选择支付方式(微信支付) self.web_view.page().runJavaScript( "document.querySelector('li.pay-item[data-pay-type=\"weixin\"]')?.click()" ) # 步骤4:勾选“同意协议” self.web_view.page().runJavaScript( "document.querySelector('input[name=\"orderCommitAgreement\"]')?.click()" )wait_for_element_visible()是自定义方法,内部用QTimer每 200ms 检查一次 XPath,超时 5s 抛异常;>def submit_order(self): # 第一步:获取当前时间戳(注入到页面 JS 上下文) timestamp = int(time.time() * 1000) self.web_view.page().runJavaScript(f"window.__jda = {timestamp};") # 第二步:模拟鼠标移动到按钮(触发 hover 效果) self.web_view.page().runJavaScript( "const btn = document.getElementById('order-submit');" "if (btn) { btn.dispatchEvent(new MouseEvent('mousemove')); }" ) # 第三步:延迟 300ms 后点击(避开防刷窗口) QTimer.singleShot(300, lambda: self._actual_click_submit()) def _actual_click_submit(self): # 执行真实点击 self.web_view.page().runJavaScript( "document.getElementById('order-submit')?.click()" ) # 第四步:检查提交结果(URL 变为 success.jd.com) self.web_view.urlChanged.connect(self._on_url_changed) def _on_url_changed(self, url): if "success.jd.com" in url.toString(): self.logger.success("订单提交成功!") self.order_success_signal.emit()window.__jda是京东前端防刷的关键变量,必须在点击前设置为当前毫秒时间戳;mousemove事件是京东checkSubmit()的前置条件,缺失则onclick不执行;QTimer.singleShot确保点击发生在mousemove之后,且间隔可控。
5. 避坑指南:五个血泪经验总结的高频翻车点与排查路径
5.1 现象:GUI 启动后白屏,日志框无任何输出
原因:
PyQt6-WebEngine未正确安装,或系统缺少 QtWebEngine 框架依赖。macOS 上常见于未安装 Xcode Command Line Tools,导致libQt6WebEngineCore.dylib链接失败。
解决:# macOS 检查 Xcode 工具 xcode-select --install # 验证 dylib 是否可加载 otool -L $(python -c "import PyQt6.QtWebEngineCore; print(PyQt6.QtWebEngineCore.__file__)") | grep Qt6WebEngine # 若报错 "can't open file",重装 PyQt6-WebEngine pip uninstall PyQt6-WebEngine -y && pip install PyQt6-WebEngine==6.5.35.2 现象:页面显示“有货”,但 API 探测返回
{"code":403,"msg":"非法请求"}原因:
config.json中的user_agent与当前浏览器 UA 不一致,或 cookies 过期。京东服务端校验User-Agent与Cookie: pt_key=xxx的绑定关系。
解决:- 手动打开京东,F12 → Application → Cookies,复制
pt_key和pt_pin值; - 在
config.json中添加"cookies": {"pt_key": "xxx", "pt_pin": "yyy"}; - 修改
api_prober.py的get_browser_cookies()方法,优先读取 config 中的 cookies。
5.3 现象:点击“立即购买”后无反应,日志显示
[JS] TypeError: Cannot read property 'click' of null原因:京东商品页结构变更,
button.btn-buy选择器失效。2024 年部分商品页改用<a class="buy-btn">或<div># 策略4:匹配># 在选择支付方式前插入 self.wait_for_element_visible("//li[contains(@class,'pay-item') and @data-pay-type='weixin']") # 再执行点击 self.web_view.page().runJavaScript("document.querySelector('li.pay-item[data-pay-type=\"weixin\"]')?.click()")5.5 现象:订单提交后跳转到
failure.jd.com,提示“提交失败,请稍后重试”原因:京东风控系统判定为异常行为,常见于:
check_interval_ms设置过小(<2000ms);- 同一 IP 短时间内多次提交;
user_agent使用了爬虫 UA(如python-requests/2.31.0)。
解决:
- 将
check_interval_ms改为5000; - 在
config.json中设置真实浏览器 UA(见 2.4 节); - 提交失败后,
submit_handler.py的on_submit_failure()方法应调用self.web_view.back()返回商品页,并清空购物车(执行 JS:document.querySelector('a.clear-cart').click())。
6. 进阶技巧:用日志回放功能做状态归因分析,定位“假成功”场景
6.1 日志结构设计:时间戳 + 模块名 + 状态码 + 关键字段
系统日志不是简单字符串拼接,而是结构化 JSON 行(每行一个 JSON 对象),便于后续分析:
{"ts":"2024-06-15T02:14:22.123","module":"page_listener","status":"OK","text":"仅剩3件","xpath":"//div[contains(@class,'stock')]"} {"ts":"2024-06-15T02:14:22.456","module":"api_prober","status":"OK","stock_state":30,"stock_num":3} {"ts":"2024-06-15T02:14:22.789","module":"decision_engine","status":"ORDER_READY"} {"ts":"2024-06-15T02:14:25.012","module":"cart_handler","status":"SUCCESS","action":"add_to_cart"} {"ts":"2024-06-15T02:14:28.345","module":"submit_handler","status":"FAILURE","reason":"payment_not_selected"}ts是 ISO 8601 格式时间戳,精确到毫秒;module标识来源模块,方便 grep 过滤;status为OK/SUCCESS/FAILURE/WARNING,非字符串描述;text/stock_state/reason等字段为上下文关键数据,不冗余。
6.2 日志回放脚本:用 Python 分析“假成功”模式
创建
log_analyzer.py,读取jd_log_20240615.log,识别“页面显示有货 → API 确认有货 → 下单失败”的链路:import json from collections import defaultdict def analyze_log(log_path: str): events = [] with open(log_path) as f: for line in f: if line.strip(): events.append(json.loads(line)) # 按分钟聚合事件 by_minute = defaultdict(list) for e in events: minute = e["ts"][:16] # "2024-06-15T02:14" by_minute[minute].append(e) # 查找“假成功”模式:ORDER_READY 后 10s 内出现 FAILURE for minute, evts in by_minute.items(): order_events = [e for e in evts if e.get("status") == "ORDER_READY"] for order_evt in order_events: # 查找该 ORDER_READY 后 10s 内的 FAILURE order_ts = datetime.fromisoformat(order_evt["ts"]) failures = [ e for e in evts if e.get("status") == "FAILURE" and (datetime.fromisoformat(e["ts"]) - order_ts).total_seconds() < 10 ] if failures: print(f"[{minute}] 假成功:ORDER_READY 后 {len(failures)} 次 FAILURE") for f in failures: print(f" → {f.get('reason', 'unknown')}") if __name__ == "__main__": analyze_log("jd_log_20240615.log")运行后输出:
[2024-06-15T02:14] 假成功:ORDER_READY 后 2 次 FAILURE → payment_not_selected → address_not_selected这说明下单流程中地址/支付未正确选择,需检查
submit_handler.py的fill_checkout_form()是否被阻塞。6.3 GUI 内置日志过滤器:实时高亮关键状态
main.py的日志文本框支持正则过滤,点击右上角 🔍 图标可输入:status:ORDER_READY→ 只显示下单准备就绪事件;module:api_prober.*stock_state:30→ 显示 API 确认有货的所有记录;status:FAILURE→ 高亮所有失败事件(红色背景)。
过滤器语法为
key:value或key:regex,支持AND(空格)与OR(|),例如:status:FAILURE module:(api_prober|submit_handler)6.4 从那以后我每次部署新环境,都强制走一遍「三分钟压力测试」:
- 启动 GUI,加载商品页;
- 手动点击“加入购物车”,确认购物车数量+1;
- 修改
config.json的check_interval_ms为1000,观察是否触发403; - 将
auto_submit设为false,连续点击“刷新监控”按钮 5 次,验证日志中page_listener与api_prober时间戳差是否 <500ms; - 最后恢复
check_interval_ms为5000,auto_submit为true,开始正式监控。
这套流程帮我避开了 80% 的线上翻车,因为真正的稳定性不在代码里,而在你对每个毫秒级交互的理解深度里。希望帮到你。
本文还有配套的精品资源,点击获取