- 多账号管理(异步登录、状态轮询) - 购物车预售商品同步(倒计时/定时开售) - 定时抢购(自动刷新、SKU选择、重试机制) - 账号隔离调度(同账号顺序、跨账号并行) - Web面板(任务分组、实时倒计时、批量操作) - Dockerfile + docker-compose
114 lines
4.4 KiB
Python
114 lines
4.4 KiB
Python
import asyncio
|
||
import yaml
|
||
import time
|
||
from playwright.async_api import async_playwright
|
||
from utils.stealth import stealth_async
|
||
from utils.auth import Authenticator
|
||
from utils.timer import PrecisionTimer
|
||
|
||
async def snatch(config):
|
||
auth = Authenticator(config.get("auth_file", "auth_state.json"))
|
||
timer = PrecisionTimer()
|
||
timer.sync_time()
|
||
|
||
async with async_playwright() as p:
|
||
browser, context = await auth.get_context(p, headless=config.get("headless", False))
|
||
page = await context.new_page()
|
||
await stealth_async(page)
|
||
|
||
target_url = config.get("target_url")
|
||
if not target_url:
|
||
print("错误: 未配置 target_url")
|
||
return
|
||
|
||
# 1. 预热:先打开页面
|
||
print(f"正在打开商品页面: {target_url}")
|
||
await page.goto(target_url)
|
||
|
||
# 如果未登录,处理登录
|
||
if not auth.has_auth():
|
||
print("未发现登录状态,请手动操作并将状态保存...")
|
||
await auth.login(target_url)
|
||
await context.close()
|
||
await browser.close()
|
||
browser, context = await auth.get_context(p, headless=config.get("headless", False))
|
||
page = await context.new_page()
|
||
await stealth_async(page)
|
||
print("已重新加载登录状态,正在打开商品页面...")
|
||
await page.goto(target_url)
|
||
|
||
# 2. 等待抢购时间
|
||
snatch_time = config.get("snatch_time")
|
||
if snatch_time:
|
||
await timer.wait_until(snatch_time)
|
||
|
||
# 3. 抢购核心逻辑
|
||
max_retries = 3
|
||
for attempt in range(max_retries):
|
||
try:
|
||
# 刷新页面,让预售按钮变为可点击
|
||
if attempt > 0:
|
||
await asyncio.sleep(0.3)
|
||
await page.reload(wait_until='domcontentloaded', timeout=10000)
|
||
await asyncio.sleep(0.5)
|
||
|
||
# 点击购买按钮(兼容多种文案)
|
||
buy_btn = None
|
||
for text in ["立即抢购", "立即购买", "马上抢", "立即秒杀"]:
|
||
loc = page.get_by_text(text, exact=False)
|
||
if await loc.count() > 0:
|
||
buy_btn = loc.first
|
||
break
|
||
|
||
if not buy_btn:
|
||
if attempt < max_retries - 1:
|
||
print(f"第{attempt+1}次未找到购买按钮,重试...")
|
||
continue
|
||
print("错误: 未找到购买按钮")
|
||
break
|
||
|
||
await buy_btn.click(timeout=3000)
|
||
print("点击购买按钮")
|
||
|
||
# 处理 SKU 选择(如果弹出规格选择框)
|
||
await asyncio.sleep(0.5)
|
||
try:
|
||
confirm_btn = page.get_by_text("确定", exact=True)
|
||
if await confirm_btn.count() > 0 and await confirm_btn.first.is_visible():
|
||
# 自动选择第一个可用的 SKU 选项
|
||
sku_items = page.locator('.sku-item:not(.disabled), .sku_item:not(.disabled), [class*="sku"] [class*="item"]:not([class*="disabled"])')
|
||
if await sku_items.count() > 0:
|
||
await sku_items.first.click()
|
||
print("自动选择 SKU")
|
||
await asyncio.sleep(0.3)
|
||
await confirm_btn.first.click(timeout=3000)
|
||
print("点击确定(SKU)")
|
||
except Exception:
|
||
pass
|
||
|
||
# 提交订单
|
||
submit_btn = page.get_by_text("提交订单")
|
||
await submit_btn.wait_for(state="visible", timeout=8000)
|
||
await submit_btn.click()
|
||
print("点击提交订单!抢购请求已发送!")
|
||
break
|
||
|
||
except Exception as e:
|
||
if attempt < max_retries - 1:
|
||
print(f"第{attempt+1}次尝试失败: {e},重试...")
|
||
else:
|
||
print(f"抢购失败: {e}")
|
||
|
||
# 保持浏览器打开一段时间查看结果
|
||
await asyncio.sleep(10)
|
||
await browser.close()
|
||
|
||
|
||
def load_config():
|
||
with open("config.yaml", "r", encoding="utf-8") as f:
|
||
return yaml.safe_load(f)
|
||
|
||
if __name__ == "__main__":
|
||
config = load_config()
|
||
asyncio.run(snatch(config))
|