From 7aea2ca2a8cbff45cf62f0ffa08e37467fabc9dd Mon Sep 17 00:00:00 2001 From: Zhaojie Date: Mon, 2 Feb 2026 09:27:49 +0800 Subject: [PATCH] Update markdown and initial code --- README.md | 43 ++++++++++ auth_state.json | 1 + config.yaml | 9 +++ main.py | 91 ++++++++++++++++++++++ resolve_url.py | 83 ++++++++++++++++++++ utils/__pycache__/auth.cpython-313.pyc | Bin 0 -> 3717 bytes utils/__pycache__/stealth.cpython-313.pyc | Bin 0 -> 557 bytes utils/__pycache__/timer.cpython-313.pyc | Bin 0 -> 2667 bytes utils/auth.py | 57 ++++++++++++++ utils/stealth.py | 9 +++ utils/timer.py | 49 ++++++++++++ 11 files changed, 342 insertions(+) create mode 100644 README.md create mode 100644 auth_state.json create mode 100644 config.yaml create mode 100644 main.py create mode 100644 resolve_url.py create mode 100644 utils/__pycache__/auth.cpython-313.pyc create mode 100644 utils/__pycache__/stealth.cpython-313.pyc create mode 100644 utils/__pycache__/timer.cpython-313.pyc create mode 100644 utils/auth.py create mode 100644 utils/stealth.py create mode 100644 utils/timer.py diff --git a/README.md b/README.md new file mode 100644 index 0000000..edde67d --- /dev/null +++ b/README.md @@ -0,0 +1,43 @@ +# Weidian Snatch (微店抢购脚本) + +这是一个基于 [Playwright](https://playwright.dev/) 的微店自动抢购工具,支持精确计时、自动登录、隐身模式等功能。 + +## 功能特性 + +- **自动登录**:支持保存和加载登录状态 (`auth_state.json`)。 +- **精确计时**:内置 `PrecisionTimer` 进行时间同步和倒计时等待。 +- **隐身模式**:使用 stealth 脚本隐藏自动化特征,降低防爬虫检测风险。 +- **自动抢购**:自动执行点击购买、确认规格(SKU)、提交订单的流程。 + +## 文件结构 + +- `main.py`: 主程序入口,包含抢购的核心逻辑。 +- `config.yaml`: 配置文件,设置商品链接、抢购时间、浏览器模式等。 +- `resolve_url.py`: URL 解析工具(如果有)。 +- `utils/`: + - `auth.py`: 处理用户认证和 Session 管理。 + - `stealth.py`: 反爬虫隐身处理。 + - `timer.py`: 时间同步与控制。 + +## 使用方法 + +1. **安装依赖** + 请确保已安装 Python,并安装所需的库: + ```bash + pip install playwright pyyaml + playwright install + ``` + +2. **配置 config.yaml** + 修改 `config.yaml` 文件,填入目标商品 URL (`target_url`) 和抢购时间 (`snatch_time`)。 + +3. **运行脚本** + ```bash + python main.py + ``` + 如果是首次运行且无登录状态,脚本会提示登录。请手动登录后,脚本会自动保存状态供下次使用。 + +## 注意事项 + +- 请确保网络畅通,以保证时间同步和抢购请求的及时发送。 +- 抢购成功率受多种因素影响(网络延迟、库存数量、平台风控等),本脚本仅辅助操作,不保证 100% 成功。 diff --git a/auth_state.json b/auth_state.json new file mode 100644 index 0000000..974c3f8 --- /dev/null +++ b/auth_state.json @@ -0,0 +1 @@ +{"cookies": [{"name": "wdtoken", "value": "ac506552", "domain": ".weidian.com", "path": "/", "expires": -1, "httpOnly": false, "secure": false, "sameSite": "Lax"}, {"name": "__spider__visitorid", "value": "5dbb14b15c5ee844", "domain": ".weidian.com", "path": "/", "expires": 1804428790.96553, "httpOnly": false, "secure": false, "sameSite": "Lax"}, {"name": "v-components/clean-up-advert@private_domain", "value": "1711911458", "domain": ".weidian.com", "path": "/", "expires": -1, "httpOnly": false, "secure": false, "sameSite": "Lax"}, {"name": "v-components/clean-up-advert@wx_app", "value": "1711911458", "domain": ".weidian.com", "path": "/", "expires": -1, "httpOnly": false, "secure": false, "sameSite": "Lax"}, {"name": "v-components/tencent-live-plugin@wfr", "value": "BuyercopyURL", "domain": ".weidian.com", "path": "/", "expires": -1, "httpOnly": false, "secure": false, "sameSite": "Lax"}, {"name": "__spider__sessionid", "value": "b1d5cfc65d7b5426", "domain": ".weidian.com", "path": "/", "expires": 1769870594, "httpOnly": false, "secure": false, "sameSite": "Lax"}, {"name": "visitor_id", "value": "f3e575ae-e31b-4f84-abcc-375b58566172", "domain": ".weidian.com", "path": "/", "expires": 1772460791.264752, "httpOnly": false, "secure": false, "sameSite": "Lax"}], "origins": [{"origin": "https://shop1711911458.v.weidian.com", "localStorage": [{"name": "__kernel__owl_visit", "value": "{\"timestamp\":1769868790968,\"caches\":[{\"c\":1,\"h\":\"87b0e9a2b49f7c87\"}]}"}]}]} \ No newline at end of file diff --git a/config.yaml b/config.yaml new file mode 100644 index 0000000..96194f5 --- /dev/null +++ b/config.yaml @@ -0,0 +1,9 @@ +target_url: https://weidian.com/fastorder.html?itemID=7565508011&wfr=BuyercopyURL +item_id: '7565508011' +shop_id: '' +sku_id: '' +snatch_time: '2026-01-31 22:21:00' +headless: false +use_stealth: true +browser_type: chromium +auth_file: auth_state.json diff --git a/main.py b/main.py new file mode 100644 index 0000000..e56620b --- /dev/null +++ b/main.py @@ -0,0 +1,91 @@ +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("未发现登录状态,请手动操作并将状态保存...") + # 注意:login 会启动一个新的浏览器窗口 + await auth.login(target_url) + + # 登录完成后,我们需要关闭当前空的 context 并重新加载带有 cookie 的 context + 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. 抢购核心逻辑 (H5 流程) + # 注意:这里的选择器需要根据微店 H5 实际页面结构进行微调 + # 这里演示一般的微店抢购流程:点击购买 -> 选择规格 -> 确定 -> 提交订单 + try: + # 刷新页面以获取最新状态(可选,视具体页面倒计时逻辑而定) + # await page.reload() + + # 点击“立即购买”按钮 + # 微店 H5 常见的购买按钮类名类似于 .buy-btn, .footer-buy + # 我们尝试使用 text 匹配 + buy_button = page.get_by_text("立即购买") + await buy_button.click() + print("点击立即购买") + + # 处理 SKU 选择(如果弹出 SKU 选择框) + # 这里简单起见,如果弹出了规格选择,点击第一个选项并确定 + # 实际需要根据 config 中的 sku_id 进行精准点击 + confirm_btn = page.get_by_text("确定") + if await confirm_btn.is_visible(): + await confirm_btn.click() + print("点击确定(SKU)") + + # 进入确认订单页面后,点击“提交订单” + # 提交订单按钮通常在底部,文字为“提交订单” + submit_btn = page.get_by_text("提交订单") + await submit_btn.wait_for(state="visible", timeout=5000) + await submit_btn.click() + print("点击提交订单!抢购请求已发送!") + + except Exception as e: + 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)) diff --git a/resolve_url.py b/resolve_url.py new file mode 100644 index 0000000..3c446b4 --- /dev/null +++ b/resolve_url.py @@ -0,0 +1,83 @@ +import asyncio +from playwright.async_api import async_playwright +import re +import yaml + +async def resolve_url_and_extract(short_url): + print(f"正在解析链接: {short_url}") + async with async_playwright() as p: + # 使用 headless=True 和 禁用GPU + browser = await p.chromium.launch(headless=True, args=['--disable-gpu']) + # 模拟手机,有的跳转可能依赖UA + device = p.devices['iPhone 13'] + context = await browser.new_context(**device) + page = await context.new_page() + + try: + await page.goto(short_url) + # 等待跳转完成 + await page.wait_for_load_state('networkidle') + await asyncio.sleep(2) # 额外等待确保 URL 稳定 + + final_url = page.url + print(f"最终链接: {final_url}") + + item_id = "" + shop_id = "" + + # 尝试从 URL 提取 + # 常见的 param 是 itemID=xxx, shopId=xxx + # 或者路径中 /item.html?itemID=... + + item_id_match = re.search(r"[?&]itemID=(\d+)", final_url, re.IGNORECASE) + if item_id_match: + item_id = item_id_match.group(1) + + shop_id_match = re.search(r"[?&]shopId=(\d+)", final_url, re.IGNORECASE) + if shop_id_match: + shop_id = shop_id_match.group(1) + + # 如果 URL 里没有,尝试在页面内容里找(有时候是全局变量) + if not item_id or not shop_id: + content = await page.content() + if not item_id: + # 匹配 "itemID":"123123" 或 itemID = '123123' + m = re.search(r'["\']?itemID["\']?\s*[:=]\s*["\']?(\d+)["\']?', content, re.IGNORECASE) + if m: item_id = m.group(1) + + if not shop_id: + m = re.search(r'["\']?shopId["\']?\s*[:=]\s*["\']?(\d+)["\']?', content, re.IGNORECASE) + if m: shop_id = m.group(1) + + print(f"解析结果 -> itemID: {item_id}, shopId: {shop_id}") + return final_url, item_id, shop_id + + except Exception as e: + print(f"解析出错: {e}") + return None, None, None + finally: + await browser.close() + +def update_config(url, item_id, shop_id): + config_path = "config.yaml" + with open(config_path, "r", encoding="utf-8") as f: + config = yaml.safe_load(f) + + config['target_url'] = url + if item_id: + config['item_id'] = item_id + if shop_id: + config['shop_id'] = shop_id + + with open(config_path, "w", encoding="utf-8") as f: + yaml.dump(config, f, allow_unicode=True, sort_keys=False) + print("配置 config.yaml 已更新。") + +if __name__ == "__main__": + # URL to resolve + target = "https://k.youshop10.com/cTO2VL6s?a=b&p=iphone&wfr=BuyercopyURL&share_relation=c03c72974993c056_1767112998_1" + + final_url, item_id, shop_id = asyncio.run(resolve_url_and_extract(target)) + + if final_url: + update_config(final_url, item_id, shop_id) diff --git a/utils/__pycache__/auth.cpython-313.pyc b/utils/__pycache__/auth.cpython-313.pyc new file mode 100644 index 0000000000000000000000000000000000000000..546f6216f3e2c36fad2c517a3845e01fb0a6e9a0 GIT binary patch literal 3717 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 literal 0 HcmV?d00001 diff --git a/utils/__pycache__/timer.cpython-313.pyc b/utils/__pycache__/timer.cpython-313.pyc new file mode 100644 index 0000000000000000000000000000000000000000..cf14d4d03fd1825a30286e4c7759d22424434913 GIT binary patch literal 2667 zcmai0>u(!H5a0Fr?6d2{P8%m~tR^PZZLW|wX;KQNB_u7-QZSHGo4uL& z?e6T%tQRUOoCrwghLVyEp&#j>MAk)NVH*e|NI(MKgNXUY1RG-uvB2B1hq;j% zBNCW8Bv`jtOmAXMV4j_)g9wjxu20frEjb{^O|{AK;$Y!&5Ju1)h@t~BCX8UIL&Qok z2`gBE+XNQ4U9b@@Vdpu$@>U|plWH=p^a3oIhh3m}36SE_g)s0etOf?#z)_R}$)p70 zEfGRZ-p0Bu-QTau8c0@EPW7u4C7_2wQB@{t=x$k3)5_poQaZF#R`1c$nb3W5Us6&+ zSuL4TLv&Z9C9_}WL@}u(HBr=SFJ4ni$lx7~9gAz8le&#i4?uxQ8v*nX-Tdi@;oEQ7lnQ4@F-=F|PEXXGto3DaUVy zjDZj?%@C_7SqauC>M`=E^u>G&tQ!foc4o`su>?@UaS4i5maObZT8ogvlp)!pOeGSy za-RkyI25LfNjS`RKki4{nFiF4d1p2VSkB^+!?VZV2t;~s4a|;yJU@Ek=TnEzjlVKC zK3Y6DJonbg`NxNygRV?EooZ3EOiP+1=4pf>E$#c@gdv5p0T`RUCRlO&@NDh~;4C48 z1A|OUd%up=Y!w*8WQ&%se{k-^8{t6K2Vbef#A82w{^i_r$L2mdId}496Ep$|YlFE{ z$vbrixb8?LWku8NM7}pGs~~9ii!_gPtC3AKMRYbpl3?Sw?*6!((O{9hm8_x)Tio>K zh^%JPiYn_^R%y`&fQXBlIMn@0+(h)P$i*cMJuvkkumk8kXLIbpGhY9cH~5V=IO*M( zyMDTM-RRKMLu1OP(vhKj?X|gG({+tgb(;!xo8IevfA2ec-;EXOuFUN`yl2K$m9OrY z==~)2LF}tF`Ht(px-Gx@hVNXv{+Mz577(V;)Fe12jS2atYxAqSrd-_xSNGrN8LIqC zrJ;MMYeN^0zUH0XF6QfYcekB6ZD&FL_nN*QJVwe(1I8<9xila>|D|-A!C^gQL4>yu zx&lJOPK>Y^9l#8wJ0WW#Gy=#JGobY=0=hz910ZTGumfnuy=Hj-llu=naJ2X3*z>W1 zyD85#5jW`K<;$T*_b@yQcf+)oqQJ!)E4KtAN{uLr1zhU1MqMverD;L=QeR+VwC@-s zSfaQRwOf`evL0%>hi-fYEqPmS@1T{huu)cE!eN9Y$gna*>oQit4mND~Ik0I2o28D> zLvwNk!5&z_Mv+VYyOChO3+i$o9$_M^3*vCg2C(4_yoUeB6CD46gzM4G`w-$#q!}LP zR>tTaSHhy4bH|@3e(~tsizkY?iMbcYXU85lYc|j{J9esgYB(I&+`9RS=GM*4t!;s} z)^IER@y@}j;NIroKy$D!5Zo0G-W(1J**d_RJ^otprDG+~Ww>y_xNfrR!KQqg;wK-% z#6O=pcE!K%b}^k( zH3IzsnumjORg(rX1o|o>mr%7`q7|P^>#UlR# zcimKV@Ed3l>HO9kC#(159Mk^wQ~s8Mzh%fk{wCm8&jK`a=+4ikx`?SY<-iADDkG36paHeM6 z=)f}rW4kAqS8kcC>6r3#6g(Z%Rdq*&XB$e&os%{El!q^Pc<`(8{?dTlYksLlRa<`W z@PDfG^d9Q!>~6=WG42joPg~Ygc?0flv!34QpmG}qPUuw{;T`l(fg(YOMcpZi1L?kO zipnlgyf-VQ7By~B>`xL^OC=RqNsA((H!G>9j2 1: + await asyncio.sleep(remaining - 0.5) + elif remaining > 0.1: + await asyncio.sleep(0.01) + else: + # 最后一刻进入忙等以获取最高精度 + pass