第4节提到一个事故:Cookie 失效后脚本静默失败了一整周。这一节彻底解决登录态问题——从 Cookie 保存到失效检测到验证码兜底,打造一套登录态不丢、丢了立刻知道、知道就能恢复的完整方案。
为什么登录态是第一道坎
大多数需要自动化的平台都有登录墙。OUCHN 国开学习网尤其麻烦:
| 特性 | 影响 |
|---|---|
| 短信验证码登录 | 无法全自动,必须人工介入 |
| Cookie 约 7-10 天过期 | 每周至少手动登录一次 |
| 登录后重定向 | Cookie 失效后被静默跳转到登录页,不报错 |
| 多终端使用 | 家里和办公室的 Cookie 需要同步 |
如果只是每次都手动登录,那不算自动化。核心目标是:把人工登录的频率从每天一次降到每周一次,且失效时立刻感知。
Cookie 持久化:保存与加载
基本流程
首次运行:人工登录 → 保存 Cookie 到 JSON → 后续加载复用
↓ ↓
start_edge_debug.bat cookie_file = "ouchn_cookies.json"
保存 Cookie
from playwright.sync_api import sync_playwright
import json
CDP_URL = "http://localhost:9222"
COOKIE_FILE = "ouchn_cookies.json"
def save_cookies(context):
"""从浏览器上下文提取 Cookie,保存到文件"""
cookies = context.cookies()
with open(COOKIE_FILE, "w", encoding="utf-8") as f:
json.dump(cookies, f, ensure_ascii=False, indent=2)
print(f"✅ Cookie 已保存: {len(cookies)} 条")
加载 Cookie
def load_cookies(context):
"""从文件加载 Cookie 到浏览器上下文"""
from pathlib import Path
if not Path(COOKIE_FILE).exists():
return False
with open(COOKIE_FILE, "r", encoding="utf-8") as f:
cookies = json.load(f)
context.add_cookies(cookies)
print(f"✅ Cookie 已加载: {len(cookies)} 条")
return True
完整连接流程
with sync_playwright() as p:
browser = p.chromium.connect_over_cdp(CDP_URL)
context = browser.contexts[0] if browser.contexts else browser.new_context()
# 1. 先加载已有 Cookie
load_cookies(context)
# 2. 访问目标页面,检测登录态
page = context.pages[0] if context.pages else context.new_page()
page.goto("https://lms.ouchn.cn", wait_until="domcontentloaded")
page.wait_for_timeout(3000)
# 3. 检查是否被重定向到登录页
if "login" in page.url or "auth" in page.url:
print("❌ Cookie 已失效,需要重新登录")
# 进入人工登录等待流程(见下文)
else:
print("✅ 登录态有效")
# 4. 任务完成后保存最新 Cookie(刷新过期时间)
save_cookies(context)
关键:每次运行结束都重新保存 Cookie。有些平台的 Cookie 会在每次访问时刷新过期时间,持续保存就能延长有效期。
Cookie 失效检测
第4节的"坑3"——Cookie 失效静默失败一周——根因就是没有检测逻辑。脚本被重定向到登录页后,拿不到数据就正常退出了,exit code 0,看起来一切正常。
检测函数
async def check_logged_in(page):
"""检测当前页面是否处于登录状态"""
await page.goto("https://lms.ouchn.cn", wait_until="domcontentloaded")
await asyncio.sleep(3)
# 方法1:检查 URL 是否包含 login/auth
if "login" in page.url or "auth" in page.url:
return False
# 方法2:检查页面是否有登录表单
has_login_form = await page.evaluate("""
() => {
return !!document.querySelector(
'input[type="password"], .login-form, #login'
);
}
""")
if has_login_form:
return False
# 方法3:检查是否有用户信息元素(已登录才有)
has_user_info = await page.evaluate("""
() => {
return !!document.querySelector(
'.user-info, .user-name, [class*="avatar"]'
);
}
""")
return has_user_info
在主流程中使用
async def main():
# ... 连接浏览器、加载 Cookie ...
if not await check_logged_in(page):
print("❌ 登录态失效!")
await wait_for_manual_login(page, context)
# 人工登录完成后继续
# 正常执行任务
await run_tasks(page)
三层检测的原因:有些平台重定向后 URL 不变(前端路由),有些登录页没有 password 输入框(验证码扫码),只靠一种方法会漏检。
短信验证码:人工登录兜底
OUCHN 平台需要手机短信验证码,这一步无法自动化。但可以做到:检测到失效后暂停,等用户在浏览器里手动登录,登录完成后自动继续。
async def wait_for_manual_login(page, context, timeout=300):
"""
等待用户手动登录。
timeout: 最大等待秒数(默认5分钟)
"""
print("=" * 50)
print("🔑 登录态已失效,请在浏览器中手动登录")
print(f" 调试浏览器端口: {CDP_URL}")
print(f" 等待超时: {timeout} 秒")
print("=" * 50)
# 确保浏览器停在登录页
await page.goto("https://lms.ouchn.cn/#/login", wait_until="domcontentloaded")
# 轮询检测登录状态
for i in range(timeout // 5):
await asyncio.sleep(5)
if await check_logged_in(page):
print("✅ 检测到登录成功!保存 Cookie...")
save_cookies(context)
return True
# 每30秒提示一次
if i % 6 == 0 and i > 0:
print(f" 仍在等待... ({i * 5}秒)")
print("❌ 等待超时,请稍后重试")
return False
用户体验
[08:40] 定时任务启动
[08:40] ❌ 登录态失效!
[08:40] 🔑 请在浏览器中手动登录
[08:40] 调试浏览器端口: http://localhost:9222
[08:40] 等待超时: 300 秒
(用户打开浏览器,输入账号密码,收到短信验证码,登录)
[08:43] ✅ 检测到登录成功!保存 Cookie...
[08:43] 开始执行任务...
设计原则:自动化不是消除人工,而是把人工降到最低。每周手动登录一次,比每天手动点 100 页强得多。
调试浏览器启动脚本
Cookie 持久化的前提是有一个带登录态的调试浏览器。每次手动启动太麻烦,写个批处理:
@echo off
REM start_edge_debug.bat — 启动带调试端口的 Edge
set EDGE_PATH="C:\Program Files (x86)\Microsoft\Edge\Application\msedge.exe"
set USER_DATA="C:\EdgeDebugProfile"
set PORT=9222
REM 如果端口已被占用,直接退出
netstat -ano | findstr ":%PORT%" >nul 2>&1
if %errorlevel%==0 (
echo 调试浏览器已在运行 (端口 %PORT%)
exit /b 0
)
echo 启动调试 Edge (端口 %PORT%)...
start "" %EDGE_PATH% --remote-debugging-port=%PORT% --user-data-dir=%USER_DATA%
timeout /t 3 >nul
echo ✅ 调试浏览器已启动
--user-data-dir指定独立的用户目录,避免污染日常浏览器。Cookie 和登录态存在这个目录里,重启不丢。
多终端 Cookie 同步
家里和办公室各有一台电脑,两边的 Cookie 可能不同步。方案:通过乐享知识库共享 Cookie 文件。
async def sync_cookies_via_lexiang(action="upload"):
"""
通过乐享知识库同步 Cookie。
action: "upload" 上传本机 Cookie / "download" 下载云端 Cookie
"""
if action == "upload":
# 读取本地 Cookie
with open(COOKIE_FILE, "r", encoding="utf-8") as f:
cookies = f.read()
# 上传到乐享(AI 调用 lexiang MCP)
print(f"上传 Cookie 到乐享 ({len(cookies)} 字符)")
# 实际操作:告诉 WorkBuddy "把 ouchn_cookies.json 上传到乐享"
elif action == "download":
# 从乐享下载 Cookie
print("从乐享下载 Cookie...")
# 实际操作:告诉 WorkBuddy "从乐享下载 ouchn_cookies.json"
# 下载后覆盖本地文件
if Path(COOKIE_FILE).exists():
print("✅ Cookie 已同步")
return True
return False
同步时机
| 场景 | 操作 |
|---|---|
| 本机登录成功后 | upload(把新 Cookie 推到云端) |
| 换电脑首次运行前 | download(拉取最新 Cookie) |
| Cookie 失效时 | download(试试云端的是否还有效) |
注意:Cookie 含敏感信息,只在私有乐享空间同步,不要用公开链接。
四个真实踩坑
坑 1 — Cookie 保存了但加载没用
现象:保存了 Cookie 文件,下次加载后访问页面还是未登录。
原因:context.add_cookies() 要求 Cookie 格式必须包含 name、value、domain、path 四个字段。Playwright 的 context.cookies() 返回的格式是兼容的,但如果你手动编辑过 JSON,可能漏掉字段。
修复:加载前校验格式:
def load_cookies_safe(context):
with open(COOKIE_FILE, "r", encoding="utf-8") as f:
cookies = json.load(f)
# 过滤掉缺少必要字段的 Cookie
valid = [c for c in cookies
if all(k in c for k in ("name", "value", "domain", "path"))]
if len(valid) < len(cookies):
print(f"⚠️ 过滤了 {len(cookies) - len(valid)} 条无效 Cookie")
context.add_cookies(valid)
坑 2 — Cookie 有效但页面还是跳登录
现象:Cookie 没过期,但访问页面还是被跳到登录页。
原因:有些平台除了 Cookie 还依赖 Session(服务端状态)。Session 过期了,Cookie 还在,但服务端不认。
修复:Cookie 不是万能的。检测到跳登录页就触发人工登录流程,别只依赖 Cookie 时间戳判断。
坑 3 — 调试浏览器关闭后 Cookie 丢了
现象:关了调试浏览器再开,之前的登录态没了。
原因:--user-data-dir 路径不对,或者被清理了。
修复:确保 --user-data-dir 指向一个不会被清理的稳定目录。不要用临时目录。
坑 4 — 两个终端同时跑,Cookie 互相覆盖
现象:家里和办公室同时跑脚本,一边的 Cookie 把另一边覆盖了。
修复:避免同时运行。定时任务错开时间。或者每台机器用独立的 Cookie 文件,不强制同步。
小结
| 学会的 | 要点 |
|---|---|
| Cookie 持久化 | context.cookies() 保存 → add_cookies() 加载 |
| 失效检测 | URL 检测 + 登录表单检测 + 用户元素检测,三层防漏 |
| 验证码兜底 | 检测到失效 → 暂停等待人工登录 → 检测到成功 → 继续 |
| 调试浏览器 | --remote-debugging-port + --user-data-dir 独立目录 |
| 多终端同步 | 乐享上传/下载 Cookie,登录后上传,换机前下载 |
| 踩坑预防 | 格式校验、Session 依赖、目录稳定、避免同时运行 |
下一篇预告
第 6 节:Angular 页面自动化进阶——Select2/MultiSelect 组件处理、scope 注入通用模式、$apply() 触发脏检查、表单提交与按钮匹配。解决"DOM 操作无效"的终极方案。
📺 系列文章:
- 第1节:WorkBuddy Skill 入门
- 第2节:CDP 浏览器操控原理
- 第3节:云端共享与多终端部署
- 第4节:WorkBuddy 自动化实战
- 第5节:登录态持久化(本文)
- 第6节:Angular 页面自动化进阶(待发布)
本文首发于稀土掘金。欢迎关注后续连载。