简介:这份资源面向需要采集淘宝平台数据的Python爬虫开发者,提供定时登录淘宝并获取有效Cookie的自动化方案,解决爬虫请求中登录态维持与Cookie失效的痛点。包内共5个文件,以Python脚本、ctp模板、说明文档和许可证为主,压缩包约12KB,其中login_robot.py作为服务入口,通过Restful接口对外提供登录能力,template目录下的配置模板与爬虫脚本模板经简单配置即可生成对应店铺的登录脚本。资源依赖Python 2.6+与CasperJS 1.0+,并需PhantomJS 1.8.2以上版本支撑,缺省服务端口为9080,API请求方法在TaobaoLoginAPI类中已附注释说明。目前已有3403人学习下载,适合具备一定Python基础、希望快速搭建淘宝数据采集登录环节的开发者参考,可借此理解定时登录、Cookie获取与服务化封装的整体思路。
1. 定时登录淘宝获取有效 Cookie:一个被低估的自动化基础设施
做过电商数据采集的人都有一个共识:淘宝的登录态维持是整个链路里最玄学的一环。你昨天跑得好好的脚本,今天早上起来一看,Cookie 全部失效,页面跳转到登录页,采集任务全军覆没。手动扫码重新登录?一天两天还行,长期跑下去根本不现实。taobao_cookieman 这个方向要解决的就是这个问题——通过定时自动登录淘宝,持续获取并刷新有效 Cookie,让下游的采集、监控、比价等任务不再因为登录态过期而中断。
这个方案适合谁?如果你在做淘宝商品价格监控、店铺数据追踪、竞品分析,或者任何需要维持淘宝登录态的自动化任务,并且已经被 Cookie 过期问题反复折磨过,那这套思路值得认真看完。它不依赖任何第三方打码平台,核心是用 Playwright 驱动真实浏览器完成扫码登录,再把登录后的 Cookie 持久化下来供后续复用。整条链路我跑了大半年,踩过的坑比想象中多,下面把选型、实现、参数和排错一次讲透。
2. 为什么选 Playwright 而不是 Selenium 或纯请求模拟
2.1 淘宝登录态的技术本质
淘宝的登录态不是简单的一个 session_id。它由多层 Cookie 组成,包括但不限于_m_h5_tk、_m_h5_tk_enc、cookie2、_tb_token_、sgcookie、unb等。这些 Cookie 有不同的过期策略:_m_h5_tk通常只有几个小时的有效期,cookie2可以维持数天甚至数周,而sgcookie跟设备指纹绑定。更麻烦的是,淘宝会在你发起请求时动态校验这些 Cookie 的组合是否合理,任何一个关键字段缺失或过期,都会触发登录页跳转。
纯 requests 模拟登录的路线在几年前还能走通,现在基本被封死了。原因在于淘宝的风控体系会检测浏览器指纹、TLS 指纹、鼠标轨迹、页面渲染行为等几十个维度。你用 requests 发出去的请求,header 再像浏览器,TLS 握手特征也对不上。所以必须用真实浏览器内核来跑登录流程。
2.2 Playwright 相比 Selenium 的实际优势
我最早用的是 Selenium + ChromeDriver,后来切到 Playwright,主要原因是三个:
第一,Playwright 自带 browser context 隔离机制。每个 context 有独立的 Cookie 存储、localStorage、缓存,互不干扰。这意味着你可以在同一台机器上维护多个淘宝账号的登录态,不会串号。
第二,Playwright 的storage_state方法可以直接把整个浏览器上下文的登录态导出成 JSON 文件,下次启动时直接加载,省去了手动逐条设置 Cookie 的麻烦。Selenium 要做同样的事得自己遍历driver.get_cookies()再逐条add_cookie(),而且经常丢字段。
第三,Playwright 对异步的支持更自然。定时任务场景下,你需要同时管理浏览器生命周期、页面等待、Cookie 导出等多个异步操作,Playwright 的 async API 写起来比 Selenium 的同步阻塞模型舒服得多。
2.3 最小可运行环境搭建
先装依赖。Python 版本建议 3.10 以上,Playwright 对 3.8 以下的支持越来越差。
# 创建虚拟环境 python -m venv venv source venv/bin/activate # Windows 用 venv\Scripts\activate # 安装 Playwright 和异步支持 pip install playwright>=1.40.0 pip install asyncio aiofiles # 安装 Chromium 内核(Playwright 自带,不需要系统装 Chrome) playwright install chromium # 如果需要用系统 Chrome 的指纹特征,也可以装 playwright install chrome这里有个细节:playwright install chromium装的是 Playwright 定制编译的 Chromium,它的指纹特征和官方 Chrome 有细微差别。淘宝的风控对这一点目前没有明显区分,但如果你发现登录时频繁触发滑块验证,可以试试换成playwright install chrome然后用channel="chrome"启动。
2.4 登录流程的核心代码骨架
下面是一个最小可运行的登录 + Cookie 导出脚本。注意,这里用的是扫码登录,不是账号密码登录——账号密码登录在淘宝上几乎必然触发滑块,扫码是目前最稳定的方案。
import asyncio import json import time from pathlib import Path from playwright.async_api import async_playwright # Cookie 存储路径 COOKIE_DIR = Path("./cookies") COOKIE_DIR.mkdir(exist_ok=True) async def login_and_save_cookie(account_name: str = "default"): """ 打开淘宝登录页,等待用户扫码,登录成功后导出 storage_state """ async with async_playwright() as p: # headless=False 必须,扫码需要可见的浏览器窗口 browser = await p.chromium.launch( headless=False, args=[ "--disable-blink-features=AutomationControlled", # 隐藏自动化特征 "--no-sandbox", "--disable-dev-shm-usage", ] ) # 创建独立上下文,设置真实 UA 和视口 context = await browser.new_context( viewport={"width": 1280, "height": 800}, 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" ), locale="zh-CN", timezone_id="Asia/Shanghai", ) page = await context.new_page() # 注入脚本隐藏 webdriver 特征 await page.add_init_script(""" Object.defineProperty(navigator, 'webdriver', { get: () => undefined }); """) # 打开淘宝登录页 await page.goto("https://login.taobao.com/member/login.jhtml", wait_until="domcontentloaded") print(f"[{account_name}] 请在弹出的浏览器中扫码登录...") # 等待登录成功——判断依据是 URL 跳转或特定元素出现 # 这里用轮询方式,每 2 秒检查一次,最多等 120 秒 max_wait = 120 waited = 0 logged_in = False while waited < max_wait: current_url = page.url # 登录成功后通常会跳转到 i.taobao.com 或 www.taobao.com if "login.taobao.com" not in current_url: logged_in = True break await asyncio.sleep(2) waited += 2 if not logged_in: print(f"[{account_name}] 登录超时,未检测到跳转") await browser.close() return None # 再等几秒,确保所有 Cookie 都写入完毕 await asyncio.sleep(3) # 导出 storage_state(包含 cookies 和 localStorage) state = await context.storage_state() # 保存到文件 cookie_file = COOKIE_DIR / f"{account_name}_state.json" with open(cookie_file, "w", encoding="utf-8") as f: json.dump(state, f, ensure_ascii=False, indent=2) print(f"[{account_name}] Cookie 已保存到 {cookie_file}") # 额外导出纯 Cookie 列表,方便下游 requests 使用 cookies = await context.cookies() cookie_jar = {c["name"]: c["value"] for c in cookies} jar_file = COOKIE_DIR / f"{account_name}_cookies.json" with open(jar_file, "w", encoding="utf-8") as f: json.dump(cookie_jar, f, ensure_ascii=False, indent=2) await browser.close() return state if __name__ == "__main__": asyncio.run(login_and_save_cookie("account_a"))这段代码的逻辑链条是:启动有头浏览器 → 创建带真实指纹的上下文 → 注入反检测脚本 → 打开登录页 → 轮询等待 URL 跳转 → 导出 storage_state 和纯 Cookie 字典。几个关键参数说明:
headless=False:扫码必须可见窗口,无头模式下用户看不到二维码。--disable-blink-features=AutomationControlled:这是 Chromium 的一个启动参数,能去掉navigator.webdriver等自动化标志。不加这个,淘宝登录页可能直接拒绝加载。add_init_script:在每个页面加载前注入脚本,把navigator.webdriver覆盖为 undefined。这是对抗基础检测的标配。storage_state():Playwright 的杀手锏,导出的 JSON 包含 cookies 和 origins 下的 localStorage,下次用browser.new_context(storage_state=...)直接恢复。- 轮询间隔 2 秒、超时 120 秒:扫码一般 30 秒内完成,120 秒是给网络慢或用户临时走开留的余量。
3. 定时调度与 Cookie 有效性验证的工程化
3.1 定时任务的三种调度方案对比
拿到 Cookie 只是第一步,关键是让它持续有效。淘宝的_m_h5_tk过期时间通常在 6 到 12 小时之间,cookie2可以撑几天。但实际观察下来,即使 Cookie 没过期,淘宝也可能因为风控策略变化而提前失效。所以定时刷新是必须的。
| 方案 | 实现方式 | 优点 | 缺点 | 适用场景 |
|---|---|---|---|---|
| cron + 脚本 | 系统 crontab 定时执行 Python 脚本 | 简单、稳定、不依赖额外服务 | 无法动态调整、日志分散 | 单机、少量账号 |
| APScheduler | Python 库,进程内调度 | 灵活、支持多种触发器 | 进程挂了就没了 | 常驻服务、中等规模 |
| 消息队列 + 消费者 | Redis/RabbitMQ 投递任务 | 可分布式、可重试 | 架构复杂 | 多账号、大规模 |
我目前用的是 APScheduler 常驻进程方案,配合一个简单的健康检查。原因是 cron 方案在需要动态调整刷新频率时很麻烦,而消息队列对个人项目来说太重了。
3.2 用 APScheduler 实现定时刷新
from apscheduler.schedulers.asyncio import AsyncIOScheduler from apscheduler.triggers.interval import IntervalTrigger import asyncio # 假设上面的 login_and_save_cookie 已经定义 async def refresh_cookie_job(account_name: str): """定时刷新任务:先检查 Cookie 是否还有效,无效则重新登录""" is_valid = await check_cookie_valid(account_name) if is_valid: print(f"[{account_name}] Cookie 仍然有效,跳过刷新") return print(f"[{account_name}] Cookie 已失效,开始重新登录") await login_and_save_cookie(account_name) async def check_cookie_valid(account_name: str) -> bool: """ 用保存的 Cookie 发一个轻量请求,判断登录态是否有效 这里用 Playwright 加载 storage_state 后访问一个需要登录的页面 """ cookie_file = COOKIE_DIR / f"{account_name}_state.json" if not cookie_file.exists(): return False async with async_playwright() as p: browser = await p.chromium.launch(headless=True) context = await browser.new_context( storage_state=str(cookie_file), 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" ), ) page = await context.new_page() try: # 访问淘宝首页,如果跳转到登录页说明 Cookie 失效 await page.goto("https://www.taobao.com", wait_until="domcontentloaded", timeout=15000) await asyncio.sleep(2) current_url = page.url # 如果 URL 包含 login 说明被踢到登录页了 if "login" in current_url.lower(): return False # 进一步检查页面是否有登录用户元素 # 比如检查是否存在用户昵称元素 has_user = await page.query_selector(".site-nav-user") return has_user is not None except Exception as e: print(f"检查 Cookie 时出错: {e}") return False finally: await browser.close() async def main(): scheduler = AsyncIOScheduler() # 每 4 小时检查一次 account_a 的 Cookie scheduler.add_job( refresh_cookie_job, trigger=IntervalTrigger(hours=4), args=["account_a"], id="refresh_account_a", max_instances=1, # 防止任务重叠 misfire_grace_time=300, # 错过执行时间后 5 分钟内仍可触发 ) scheduler.start() print("调度器已启动,按 Ctrl+C 退出") # 保持主协程运行 try: await asyncio.Event().wait() except (KeyboardInterrupt, SystemExit): scheduler.shutdown() if __name__ == "__main__": asyncio.run(main())这段代码的核心设计思路是「先验证再刷新」。直接定时重新登录有两个问题:一是频繁登录会增加被风控标记的概率,二是扫码需要人工介入,不可能全自动。所以正确做法是先用保存的 Cookie 发一个轻量请求验证有效性,只有失效了才触发重新登录。
参数说明:
IntervalTrigger(hours=4):每 4 小时检查一次。这个频率是根据_m_h5_tk的过期时间反推的,太频繁浪费资源,太稀疏可能在下游任务执行时刚好过期。max_instances=1:防止上一次检查还没结束下一次又启动,导致浏览器实例堆积。misfire_grace_time=300:如果因为系统休眠等原因错过了执行时间,5 分钟内仍然补执行一次。check_cookie_valid里用 headless=True:验证不需要人工介入,无头模式更快更省资源。
3.3 Cookie 有效性判断的三种信号
判断 Cookie 是否有效,不能只看一个指标。我一般会组合三个信号:
第一,URL 是否跳转到登录页。这是最直接的信号,但有时候淘宝不会立即跳转,而是先加载页面再异步跳转,所以需要等 2 到 3 秒再判断。
第二,页面是否包含登录用户专属元素。比如.site-nav-user这个 class 在未登录状态下是不存在的。但淘宝的前端 class 名会变,所以这个判断需要定期维护。
第三,发一个需要登录态的 API 请求,看返回码。比如访问https://i.taobao.com/my_taobao.htm,如果返回 200 且内容包含用户信息,说明有效;如果返回 302 跳转到登录页,说明失效。这种方式最准确,但需要处理请求头里的 Cookie 拼接。
实际用的时候,我会把三种信号做成一个评分机制:URL 跳转权重最高,元素检查次之,API 请求作为兜底。任意两个信号同时指向失效,才触发重新登录。
4. 避坑指南:定时登录淘宝时最容易翻车的五个地方
4.1 坑一:无头模式下二维码不显示
现象:脚本用headless=True启动,打开登录页后截图发现二维码区域是空白的,或者页面直接报错。
原因:淘宝的登录页二维码是通过 Canvas 或 WebGL 渲染的,部分无头模式下的 Chromium 缺少 GPU 加速支持,导致渲染失败。另外,淘宝会检测无头特征,直接拒绝加载二维码组件。
解决:扫码登录必须用headless=False。如果服务器没有图形界面,用xvfb-run虚拟显示:
# Ubuntu 下安装 xvfb sudo apt-get install xvfb # 用虚拟显示运行脚本 xvfb-run -a python login_script.py但注意,xvfb 环境下截图能看到二维码,但用户没法扫码。所以实际做法是:在本地机器上跑扫码登录,把 Cookie 文件同步到服务器上供采集任务使用。或者用 VNC 远程连接到服务器桌面完成扫码。
4.2 坑二:Cookie 导出后下游 requests 用不了
现象:Playwright 导出的 Cookie 文件,用 requests 加载后请求淘宝接口,返回 403 或跳转登录页。
原因:Playwright 的storage_state导出的 Cookie 包含了很多字段,但 requests 加载时如果只取了name和value,会丢失domain、path、expires、httpOnly、secure等属性。淘宝的某些 Cookie 对 domain 和 path 有严格要求,缺失这些属性会导致 Cookie 不被发送。
解决:导出时保留完整字段,下游用requests.cookies.RequestsCookieJar逐条设置:
import json import requests from requests.cookies import RequestsCookieJar def load_cookies_for_requests(cookie_file: str) -> RequestsCookieJar: """把 Playwright 导出的 Cookie 转成 requests 可用的 CookieJar""" with open(cookie_file, "r", encoding="utf-8") as f: state = json.load(f) jar = RequestsCookieJar() for cookie in state.get("cookies", []): jar.set( name=cookie["name"], value=cookie["value"], domain=cookie.get("domain", ".taobao.com"), path=cookie.get("path", "/"), ) return jar # 使用示例 jar = load_cookies_for_requests("./cookies/account_a_state.json") session = requests.Session() session.cookies = jar resp = session.get("https://i.taobao.com/my_taobao.htm") print(resp.status_code)关键点是domain和path必须保留。淘宝的_m_h5_tk通常 domain 是.taobao.com,path 是/,如果设置错了,requests 不会带上这个 Cookie。
4.3 坑三:频繁登录触发滑块验证
现象:第一次扫码登录成功,几小时后 Cookie 失效,再次扫码时弹出滑块验证,而且滑块怎么拖都过不了。
原因:淘宝的风控系统会记录设备指纹和登录频率。短时间内同一设备多次登录同一账号,会被标记为异常行为,触发滑块验证。如果滑块验证连续失败,账号可能被临时限制登录。
解决:三个措施。第一,降低登录频率,不要一失效就立刻重新登录,可以设置一个最小间隔,比如 30 分钟内不重复登录。第二,每次登录时清理浏览器上下文,但保留一个固定的user_data_dir,让淘宝认为你是同一台设备。第三,如果已经触发滑块,不要硬刚,等 24 小时后再试,或者换一个网络环境。
# 使用持久化用户目录,保持设备指纹一致 context = await browser.new_context( user_data_dir="./browser_data/account_a", # 持久化目录 viewport={"width": 1280, "height": 800}, user_agent="...", )注意,user_data_dir和storage_state不要同时用,会冲突。用user_data_dir的话,Cookie 会自动保存在目录里,不需要额外导出。
4.4 坑四:Cookie 文件被并发读写导致损坏
现象:定时任务和采集任务同时读写同一个 Cookie 文件,偶尔出现 JSON 解析错误,或者读到的 Cookie 是半截的。
原因:多个进程同时写同一个文件,没有加锁。Python 的json.dump不是原子操作,写入过程中如果另一个进程来读,就会读到不完整的内容。
解决:用文件锁或者原子写入。最简单的方式是先写到临时文件,再os.replace原子替换:
import os import json import tempfile def atomic_write_json(filepath: str, data: dict): """原子写入 JSON,避免并发读写损坏""" dir_name = os.path.dirname(filepath) fd, tmp_path = tempfile.mkstemp(dir=dir_name, suffix=".tmp") try: with os.fdopen(fd, "w", encoding="utf-8") as f: json.dump(data, f, ensure_ascii=False, indent=2) os.replace(tmp_path, filepath) # 原子替换 except Exception: os.unlink(tmp_path) raiseos.replace在大多数文件系统上是原子操作,要么替换成功,要么保持原文件不变,不会出现中间状态。
4.5 坑五:服务器时间不同步导致 Cookie 提前过期
现象:Cookie 文件里记录的过期时间还没到,但实际请求已经失效了。
原因:Cookie 的过期判断依赖客户端时间。如果服务器时间比标准时间慢了或快了,会导致 Cookie 被提前判定为过期,或者过期了还在用。
解决:确保服务器开启了 NTP 时间同步:
# 检查时间同步状态 timedatectl status # 如果 NTP 未启用 sudo timedatectl set-ntp true # 手动同步一次 sudo ntpdate pool.ntp.org另外,在代码里判断 Cookie 是否过期时,不要完全依赖expires字段,而是以实际请求结果为准。因为淘宝服务端有自己的过期判断逻辑,可能和 Cookie 里的expires不一致。
5. 进阶技巧:用 Cookie 池 + 代理轮换把稳定性拉满
5.1 多账号 Cookie 池的调度策略
单账号的 Cookie 刷新总有极限,一旦触发风控,整个链路就断了。更稳的做法是维护一个 Cookie 池,多个账号轮流使用。调度策略我一般用加权轮询:每个账号根据历史成功率分配权重,成功率高的多用,成功率低的少用甚至暂时剔除。
import random from collections import defaultdict class CookiePool: def __init__(self): self.accounts = {} # account_name -> {"state": ..., "weight": 1.0, "fail_count": 0} def add_account(self, name: str, state: dict): self.accounts[name] = { "state": state, "weight": 1.0, "fail_count": 0, } def get_cookie(self) -> tuple: """按权重随机选一个账号,返回 (name, state)""" if not self.accounts: raise RuntimeError("Cookie 池为空") names = list(self.accounts.keys()) weights = [self.accounts[n]["weight"] for n in names] chosen = random.choices(names, weights=weights, k=1)[0] return chosen, self.accounts[chosen]["state"] def report_result(self, name: str, success: bool): """上报使用结果,动态调整权重""" if name not in self.accounts: return acc = self.accounts[name] if success: acc["weight"] = min(acc["weight"] * 1.1, 5.0) # 成功加权,上限 5 acc["fail_count"] = 0 else: acc["fail_count"] += 1 acc["weight"] = max(acc["weight"] * 0.5, 0.1) # 失败降权,下限 0.1 if acc["fail_count"] >= 3: acc["weight"] = 0.01 # 连续失败 3 次,几乎不再使用这个池子的核心逻辑是:成功就加权,失败就降权,连续失败直接雪藏。权重上限 5.0 防止某个账号被过度使用,下限 0.01 保证它还有极小的概率被选中,以便在恢复后重新证明自己。
5.2 代理 IP 的绑定与轮换
淘宝对同一 IP 的请求频率有监控。如果所有账号都从同一个 IP 出去,很容易被关联。常见做法是给每个账号绑定一个独立的代理 IP,或者用一个代理池轮换。
代理的配置在 Playwright 里通过launch的proxy参数设置:
browser = await p.chromium.launch( headless=False, proxy={ "server": "http://proxy_host:port", "username": "user", "password": "pass", } )注意,代理的质量直接影响登录成功率。免费代理基本不能用,淘宝会直接拒绝。建议用住宅代理或移动网络代理,成功率会高很多。另外,代理的 IP 地理位置最好和账号的常用登录地一致,否则可能触发异地登录验证。
5.3 用健康检查脚本提前发现失效
与其等下游任务报错才发现 Cookie 失效,不如主动做健康检查。我写了一个简单的检查脚本,每 30 分钟跑一次,把结果写到一个状态文件里,下游任务执行前先读状态文件,如果标记为失效就跳过或切换账号。
import asyncio import json import time from pathlib import Path HEALTH_FILE = Path("./cookies/health.json") async def health_check_all(pool: CookiePool): """检查池子里所有账号的 Cookie 有效性""" results = {} for name, acc in pool.accounts.items(): is_valid = await check_cookie_valid(name) results[name] = { "valid": is_valid, "checked_at": time.time(), "weight": acc["weight"], } with open(HEALTH_FILE, "w", encoding="utf-8") as f: json.dump(results, f, ensure_ascii=False, indent=2) return results这个健康检查的价值在于:它把「Cookie 是否有效」这个信息从隐式变成了显式。下游任务不需要自己去试错,直接读状态文件就行。而且健康检查的频率可以比刷新频率高,比如每 30 分钟检查一次,但每 4 小时才刷新一次,这样既能及时发现问题,又不会频繁登录触发风控。
5.4 一个我踩了最久的坑:storage_state 的 localStorage 丢失
最后说一个我踩了最久的坑。Playwright 的storage_state()导出时,默认只包含 cookies 和 origins 下的 localStorage。但淘宝的登录态有一部分存在 sessionStorage 里,而 sessionStorage 是不被storage_state捕获的。这导致一个问题:用导出的 state 恢复上下文后,页面看起来是登录状态,但某些需要 sessionStorage 的接口调用会失败。
解决方式是在导出前手动把 sessionStorage 的内容也 dump 出来,恢复时再注入回去:
# 导出时 session_storage = await page.evaluate("() => JSON.stringify(sessionStorage)") state["session_storage"] = session_storage # 恢复时 await context.add_init_script(f""" sessionStorage.setItem('__restored__', '1'); const data = {state.get('session_storage', '{}')}; for (const [k, v] of Object.entries(JSON.parse(data))) {{ sessionStorage.setItem(k, v); }} """)这个技巧不常用,但在某些特定接口上能救命。我现在的习惯是:每次导出 Cookie 时,顺手把 sessionStorage 也存一份,恢复时按需注入。多存一份数据不占多少空间,但能省下很多排查时间。
希望帮到你。
本文还有配套的精品资源,点击获取