"""药九九浏览器自动化登录 — Playwright 填表登录,成功后 cookie 写入 accounts_platform""" import json import time import random from playwright.sync_api import sync_playwright, TimeoutError as PlaywrightTimeoutError from commons.Logger import get_spider_logger from commons.conn_mysql import MySQLPoolOn2 logger = get_spider_logger("yaojiujiu_login") LOGIN_URL = "https://www.yyjzt.com/login" PLATFORM_ID = 7 class YaoJiuJiuLogin: """Playwright 浏览器自动化登录药九九""" def __init__(self): self.db = MySQLPoolOn2() # ==================== 账号获取 ==================== def get_account(self): sql = """SELECT `id`, `phone`, `password` FROM `accounts_platform` WHERE `platform`=%s AND `status`=1 ORDER BY `cookie_timestamp` ASC LIMIT 1""" rows = self.db.select_data(sql, (PLATFORM_ID,)) return rows[0] if rows else None # ==================== 浏览器登录 ==================== def login(self, phone, password, headless=True): """ 打开浏览器 → 填表 → 点击登录 → 提取 cookie → 关闭浏览器 成功返回 yjj-token cookie 值,失败返回 None """ with sync_playwright() as p: browser = p.chromium.launch( headless=headless, channel="chrome", args=[ "--disable-blink-features=AutomationControlled", "--no-sandbox", ], ) context = browser.new_context( locale="zh-CN", timezone_id="Asia/Shanghai", viewport={"width": 1600, "height": 900}, ) page = context.new_page() try: # 1. 打开登录页 page.goto(LOGIN_URL, timeout=30000) page.wait_for_load_state("networkidle") # 2. 确保在"账户密码登录"tab(id="tab-first") pwd_tab = page.locator("#tab-first") if pwd_tab.count() > 0: pwd_tab.click() page.wait_for_timeout(500) # 3. 填账号 username_input = page.locator("input[name='username']") username_input.wait_for(state="visible", timeout=10000) username_input.click() username_input.fill("") # 模拟逐字输入 for ch in str(phone): username_input.type(ch, delay=random.randint(60, 180)) page.wait_for_timeout(random.randint(30, 80)) logger.info("已输入账号") # 4. 填密码 password_input = page.locator("input[name='password']") password_input.click() password_input.fill("") for ch in str(password): password_input.type(ch, delay=random.randint(60, 180)) page.wait_for_timeout(random.randint(30, 80)) logger.info("已输入密码") # 5. 点登录 login_btn = page.locator("button:has-text('登录')").first if login_btn.locator("..").first: # 等待按钮可用(Element UI 的验证需要输入完整后才解除 disabled) page.wait_for_timeout(500) login_btn.click() logger.info("已点击登录") # 6. 等待登录结果 — 页面跳转或报错 page.wait_for_timeout(3000) try: page.wait_for_load_state("networkidle", timeout=15000) except Exception: pass # 7. 检查登录状态 if "login" in page.url.lower(): # 检查是否有错误提示 err = page.locator(".el-message--error, .el-form-item__error, [class*='error']").first if err.count() > 0: logger.error("登录失败: %s", err.inner_text(timeout=3000)) else: logger.error("登录失败,仍在登录页") return None # 8. 提取 cookie cookies = context.cookies() browser.close() # 找 yjj-token token = "" for c in cookies: if c["name"] == "yjj-token": token = c["value"].replace("ssr_", "") break if not token: logger.error("登录后未找到 yjj-token cookie") return None logger.info("登录成功, token=%s...", token[:20]) return {"token": token, "cookies": cookies} except PlaywrightTimeoutError as e: logger.error("登录超时: %s", e) return None except Exception as e: logger.exception("登录异常: %s", e) return None finally: try: browser.close() except Exception: pass # ==================== Cookie 入库 ==================== def save_cookies(self, phone, cookies): """保存全套 cookie 到 accounts_platform,供快照模式注入""" # 提取关键 cookie cookie_map = {} for c in cookies: cookie_map[c["name"]] = c["value"] # Token 单独存,兼容 API 模式 token = cookie_map.get("yjj-token", "").replace("ssr_", "") cookie_map["Token"] = token cookie_str = json.dumps(cookie_map, ensure_ascii=False) now_ts = int(time.time()) sql = """UPDATE `accounts_platform` SET `cookie_str`=%s, `cookie_timestamp`=%s WHERE `phone`=%s AND `platform`=%s""" affected = self.db.execute(sql, (cookie_str, now_ts, str(phone), PLATFORM_ID)) if affected: logger.info("Cookie 已写入 accounts_platform (含 %s 个 key), phone=%s", len(cookie_map), phone) else: logger.warning("Cookie 写入失败, phone=%s", phone) def save_token(self, phone, token): """兼容旧调用""" self.save_cookies(phone, [{"name": "yjj-token", "value": "ssr_" + str(token)}]) # ==================== 入口 ==================== def run(self, headless=True): """执行登录:读数据库账号 → 浏览器登录 → 入库,返回 token""" acc = self.get_account() if not acc: logger.error("无可用药九九账号") return None phone = acc["phone"] password = acc["password"] # 先看缓存 cached = acc.get("cookie_str") or "" if cached: try: cd = json.loads(cached) t = cd.get("Token") or cd.get("token") if t: logger.info("使用缓存 token") return t except json.JSONDecodeError: pass # 浏览器登录 result = self.login(phone, password, headless=headless) if result: self.save_cookies(phone, result["cookies"]) return result["token"] return None if __name__ == "__main__": t = YaoJiuJiuLogin().run(headless=False) print("token:", t)