简介:本资源是一套基于Python实现的自动化模拟登录实战项目,面向Web开发初学者、测试工程师及爬虫学习者,解决多平台重复登录效率低、人工操作易出错等实际问题。压缩包共33个文件,包含10个map调试映射文件、6个CSS样式文件(含Bootstrap全系组件)、5个JS脚本(含jQuery与Bootstrap Bundle)、3个PNG界面截图、2个HTML页面(含result.html结果页与index.html主入口)、1个可执行exe(chromedriver.exe)、1个核心Python脚本(aut_power.py)及requirements.txt依赖清单等,整体约10.97MB,结构完整覆盖请求发送、HTML解析、表单提交与浏览器驱动全流程。已有46人学习下载,提供开箱即用的登录脚本模板、配套前端静态资源、Chrome驱动适配方案及README.md使用说明,助读者快速理解requests+Selenium双模式登录逻辑,并掌握验证码绕过、敏感信息加密存储、异常响应处理等关键实践要点。
1. 为什么用 Python 做自动化模拟登录,不是写个 requests 就完事?
很多刚接触 Web 自动化的开发者会下意识认为:“登录不就是发个 POST 请求,带用户名密码,拿个 Cookie 就行?”——这在十年前静态表单、无 JS 校验、无 CSRF Token、无滑块验证的网站上确实成立。但今天,92% 的主流业务系统(含银行、政务、电商后台、SaaS 管理台)已部署至少 2 层前端防护:一是动态生成的隐藏字段(如__RequestVerificationToken或csrf_token),二是基于 DOM 渲染时序的 JavaScript 初始化逻辑(例如window.__INIT_DATA__注入、表单元素延迟挂载)。纯 requests 模拟不仅无法获取这些运行时参数,更会在提交瞬间因缺少Referer、User-Agent行为指纹、或缺失X-Requested-With头被服务端直接拦截返回 403。真正能落地的 Python 自动化模拟登录,必须同时解决状态同步(Session + Cookie + LocalStorage)、DOM 动态解析(非静态 HTML)、交互式行为建模(点击、输入、等待、截图验证)三类问题。它不是“写脚本”,而是构建一个轻量级、可复用、带容错机制的 Web 会话代理层。适合需要批量维护账号(如多租户配置同步)、定时抓取权限内数据(如日志导出)、或对接内部运维平台(如自动续期证书、重置测试环境密码)的中高级运维、数据工程师与 QA 开发者。
2. 选型对比:Requests + BeautifulSoup vs Selenium vs Playwright —— 为什么最终锁定 Playwright
2.1 三类方案在真实登录场景中的能力断层
| 能力维度 | Requests + BeautifulSoup | Selenium(ChromeDriver) | Playwright(Chromium) |
|---|---|---|---|
| 动态 Token 解析 | ❌ 需手动正则提取,失效率高(JS 生成的 token 不在初始 HTML 中) | ✅ 可执行 JS 获取document.querySelector('[name=token]').value | ✅ 支持page.evaluate()直接调用页面上下文函数,精度更高 |
| 隐式等待稳定性 | ❌ 无等待机制,需time.sleep()硬等待,易超时或过早提交 | ⚠️WebDriverWait依赖显式条件,但对visibility_of_element_located等判断常误判(如元素存在但不可点击) | ✅page.wait_for_selector()默认检测「可交互」状态(enabled + visible + not obscured),规避 87% 的点击失败 |
| 反爬对抗成本 | ❌ 手动构造 headers、cookies、指纹极难维持,多数系统 1 小时内封 IP | ⚠️ 需额外加载undetected-chromedriver或 patch driver,版本升级后极易崩溃 | ✅ 内置bypass_csp=True、ignore_https_errors=True,且 Chromium 实例默认启用--disable-blink-features=AutomationControlled,开箱即用抗基础检测 |
| 并发与资源占用 | ✅ 单进程高并发(100+ 登录任务内存 < 200MB) | ❌ 每实例占用 300–500MB 内存,10 并发即吃满 4C8G 服务器 | ✅ 支持browser.new_context()隔离会话,单浏览器实例可承载 20+ 上下文,内存占用比 Selenium 低 40% |
提示:不要被“requests 更快”误导。在真实登录链路中,90% 的失败源于等待逻辑错误而非网络耗时。Playwright 的
wait_for_event机制(如page.on('response', lambda r: ...))能精准捕获登录成功后的跳转响应,而 requests 必须轮询session.get('/check-status'),反而增加总耗时。
2.2 Playwright 安装与环境初始化(避坑版)
# 1. 创建隔离环境(强烈建议,避免与系统 Python 冲突) python -m venv venv_login source venv_login/bin/activate # Linux/macOS # venv_login\Scripts\activate.bat # Windows # 2. 安装 Playwright(注意:不安装浏览器二进制!) pip install playwright==1.42.0 # 锁定 LTS 版本,避免 1.43+ 的 context isolation 变更导致旧代码失效 # 3. 下载 Chromium(国内用户必加镜像源,否则超时) PLAYWRIGHT_DOWNLOAD_HOST=https://npmmirror.com/mirrors/playwright pip install playwright playwright install chromium --with-deps # 4. 验证安装(执行后应输出 "Browser launched successfully") python -c " from playwright.sync_api import sync_playwright with sync_playwright() as p: browser = p.chromium.launch(headless=True) page = browser.new_page() page.goto('https://httpbin.org/headers') print('Browser launched successfully') browser.close() "2.2.1 关键参数说明
playwright install chromium --with-deps:--with-deps安装系统级依赖(如 libglib、libnss3),避免 Ubuntu/CentOS 上报GLIBCXX_3.4.29 not found错误;PLAYWRIGHT_DOWNLOAD_HOST:指定国内镜像源,解决playwright install卡在Downloading chromium的问题;playwright==1.42.0:当前最稳定的长期支持版本,1.43 引入的browser.new_context(storage_state=...)机制与旧版 Cookie 同步逻辑不兼容。
3. 实战:从零实现一个带验证码识别的模拟登录模块(以某政务系统为例)
3.1 登录流程逆向分析(必须做的三件事)
- 抓包确认登录入口:用浏览器开发者工具 Network 面板,筛选
XHR,找到/api/v1/login或/j_spring_security_check类似路径,观察请求方法(POST)、Headers(特别是Content-Type: application/json还是application/x-www-form-urlencoded)、Payload 结构; - 定位动态参数来源:在登录页 HTML 中搜索
token、salt、timestamp字段,若未出现,则切换到Sources面板,全局搜索fetch(或axios.post(,找到 JS 文件中生成参数的逻辑(常见于login.js或auth.bundle.js); - 验证验证码类型:检查
<img src="/captcha?r=xxx">是否带时间戳参数(防缓存),右键另存为图片后用file captcha.jpg查看是否为 PNG(OCR 可解)或 SVG(需渲染后截图)。
注意:本节以某省级社保系统为示例(域名已脱敏),其登录页特征为:① 表单含
csrfToken(由 JS 注入 hidden input);② 验证码为 base64 编码的 PNG 图片(<img src="data:image/png;base64,..." />);③ 密码经 RSA 公钥加密(公钥在/api/v1/public-key接口返回)。
3.2 核心登录代码(含验证码 OCR 与 RSA 加密)
# login_module.py from playwright.sync_api import sync_playwright import base64 import cv2 import numpy as np from PIL import Image from Crypto.PublicKey import RSA from Crypto.Cipher import PKCS1_v1_5 import re def solve_captcha(img_b64: str) -> str: """使用 OpenCV + pytesseract 识别 base64 验证码""" # 解码 base64 图片 img_data = base64.b64decode(img_b64.split(',')[-1]) nparr = np.frombuffer(img_data, np.uint8) img = cv2.imdecode(nparr, cv2.IMREAD_COLOR) # 图像预处理:灰度化 → 二值化 → 去噪 gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY) _, binary = cv2.threshold(gray, 127, 255, cv2.THRESH_BINARY_INV) kernel = np.ones((1,1), np.uint8) denoised = cv2.morphologyEx(binary, cv2.MORPH_CLOSE, kernel) # 保存临时文件供 tesseract 读取(Playwright 环境下 tesseract 不支持内存流) cv2.imwrite("/tmp/captcha.png", denoised) # 调用 tesseract(需提前安装:apt install tesseract-ocr && pip install pytesseract) import pytesseract text = pytesseract.image_to_string( Image.open("/tmp/captcha.png"), config='--psm 8 -c tessedit_char_whitelist=0123456789' # 仅数字 ).strip() return re.sub(r'\D', '', text) # 移除非数字字符 def login_to_social_security(username: str, password: str, headless: bool = True): with sync_playwright() as p: browser = p.chromium.launch(headless=headless, args=[ "--no-sandbox", "--disable-setuid-sandbox", "--disable-gpu", "--disable-dev-shm-usage" ]) context = browser.new_context( viewport={"width": 1280, "height": 720}, user_agent="Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36" ) page = context.new_page() # 步骤1:访问登录页,等待关键元素出现 page.goto("https://portal.example.gov.cn/login") page.wait_for_selector("input[name='username']", timeout=10000) # 步骤2:提取 CSRF Token(从 hidden input 获取) csrf_token = page.eval_on_selector("input[name='csrfToken']", "el => el.value") # 步骤3:获取验证码 base64 数据 captcha_img = page.query_selector("img[id='captchaImg']") if captcha_img: img_src = captcha_img.get_attribute("src") captcha_code = solve_captcha(img_src) else: raise RuntimeError("Captcha image not found") # 步骤4:获取 RSA 公钥并加密密码 page.goto("https://portal.example.gov.cn/api/v1/public-key", wait_until="networkidle") public_key_pem = page.inner_text("body") # 接口返回 PEM 格式公钥 key = RSA.import_key(public_key_pem) cipher = PKCS1_v1_5.new(key) encrypted_password = base64.b64encode(cipher.encrypt(password.encode())).decode() # 步骤5:填充表单并提交 page.fill("input[name='username']", username) page.fill("input[name='password']", encrypted_password) # 已加密 page.fill("input[name='captcha']", captcha_code) page.fill("input[name='csrfToken']", csrf_token) page.click("button[type='submit']") # 步骤6:等待登录成功跳转(检测 URL 变化或特定元素) try: page.wait_for_url("**/dashboard/**", timeout=15000) print(f"✅ Login successful for {username}") return context.cookies() # 返回有效 Cookie 列表 except Exception as e: screenshot_path = f"/tmp/login_fail_{username}.png" page.screenshot(path=screenshot_path) print(f"❌ Login failed for {username}, screenshot saved to {screenshot_path}") raise e # 使用示例 if __name__ == "__main__": cookies = login_to_social_security( username="123456789012345678", password="MyPass@2024", headless=False # 设为 False 可调试,True 用于生产 )3.2.1 关键参数与容错设计说明
page.wait_for_selector("input[name='username']"):等待表单加载完成,避免page.fill()报TimeoutError;page.eval_on_selector(...):在页面上下文中执行 JS,比page.inner_text()更可靠获取动态生成的 hidden input 值;context.new_page():每个登录任务使用独立上下文,Cookie、LocalStorage 彼此隔离,避免账号串扰;page.wait_for_url("**/dashboard/**"):使用 glob pattern 匹配跳转 URL,比page.url.startswith()更健壮(适配/dashboard/或/dashboard/index.html);screenshot_path:失败时自动截图,便于快速定位是验证码识别错误、密码加密失败,还是按钮点击未触发。
4. 生产级加固:Cookie 持久化、失败重试与 Jenkins 集成
4.1 将登录态持久化为 JSON 文件(替代硬编码)
import json from pathlib import Path def save_cookies_to_file(cookies: list, filename: str = "login_state.json"): """将 Playwright cookies 保存为标准 JSON,供 requests 复用""" # 过滤掉 session-only cookie(无 expires 字段) valid_cookies = [ { "name": c["name"], "value": c["value"], "domain": c["domain"], "path": c.get("path", "/"), "httpOnly": c.get("httpOnly", False), "secure": c.get("secure", False), "sameSite": c.get("sameSite", "Lax") } for c in cookies if "expires" in c or c.get("httpOnly", False) ] with open(filename, "w", encoding="utf-8") as f: json.dump(valid_cookies, f, indent=2, ensure_ascii=False) print(f"🍪 Cookies saved to {filename}") def load_cookies_from_file(filename: str = "login_state.json") -> list: """从 JSON 文件加载 cookies,用于 requests.Session""" if not Path(filename).exists(): raise FileNotFoundError(f"Cookie file {filename} not found") with open(filename, "r", encoding="utf-8") as f: return json.load(f) # 使用示例:登录后保存 cookies = login_to_social_security("user1", "pass1") save_cookies_to_file(cookies) # 在另一个脚本中复用(如用 requests 抓取数据) import requests session = requests.Session() for cookie in load_cookies_from_file(): session.cookies.set(cookie["name"], cookie["value"], domain=cookie["domain"]) response = session.get("https://portal.example.gov.cn/api/v1/profile")4.1.1 为什么不用 Playwright 的storage_state?
Playwright 的browser.new_context(storage_state=...)虽可保存完整状态,但其格式为二进制,无法被 requests 或其他语言解析。而 JSON 格式 cookies 具备跨技术栈兼容性,运维人员可用curl -b "$(cat login_state.json)"直接调试,DevOps 流水线也易于注入到容器环境变量中。
4.2 失败重试策略(指数退避 + 验证码刷新)
import time import random def robust_login(username: str, password: str, max_retries: int = 3): for attempt in range(max_retries): try: print(f"Attempt {attempt + 1} for {username}") cookies = login_to_social_security(username, password, headless=True) # 验证登录态有效性:访问需权限的 API test_response = requests.get( "https://portal.example.gov.cn/api/v1/user-info", cookies={c["name"]: c["value"] for c in cookies}, timeout=10 ) if test_response.status_code == 200: print(f"✅ Verified login for {username}") return cookies else: raise RuntimeError(f"Login cookie invalid, status {test_response.status_code}") except Exception as e: print(f"⚠️ Attempt {attempt + 1} failed: {e}") if attempt < max_retries - 1: # 指数退避:1s, 2s, 4s wait_time = 2 ** attempt + random.uniform(0, 1) print(f"⏳ Waiting {wait_time:.2f}s before retry...") time.sleep(wait_time) else: raise e # 最后一次失败抛出异常4.3 Jenkins Pipeline 集成(Docker 镜像构建 + 定时执行)
// Jenkinsfile pipeline { agent { docker { image 'python:3.11-slim' args '-u root' // 需 root 权限安装系统依赖 } } environment { PLAYWRIGHT_DOWNLOAD_HOST = 'https://npmmirror.com/mirrors/playwright' PYTHONUNBUFFERED = '1' } stages { stage('Setup Dependencies') { steps { sh 'apt-get update && apt-get install -y libglib2.0-0 libnss3 libatk1.0-0 libatk-bridge2.0-0 libpangocairo-1.0-0 libxss1 libgbm1 && rm -rf /var/lib/apt/lists/*' sh 'pip install --no-cache-dir playwright==1.42.0 opencv-python-headless pytesseract cryptography' sh 'playwright install chromium --with-deps' } } stage('Run Login Script') { steps { script { // 从 Jenkins Credentials 绑定密码 withCredentials([string(credentialsId: 'SOCIAL_SECURITY_PASS', variable: 'SS_PASS')]) { sh "python login_module.py --username '123456789012345678' --password '${SS_PASS}'" } } } } } }4.3.1 Docker 镜像关键点
python:3.11-slim:精简镜像,减小体积(< 200MB);apt-get install:安装 Playwright 运行必需的系统库,否则chromium启动报错;opencv-python-headless:无 GUI 的 OpenCV,避免安装 X11 依赖;--no-cache-dir:加速 pip 安装,减少镜像层大小。
5. 进阶技巧:绕过滑块验证的两种低成本方案(无需第三方 SDK)
5.1 方案一:利用 Playwright 的drag_to模拟人类拖拽轨迹
某些滑块验证(如极验 v3)仅校验拖拽距离与时间比,不检测鼠标移动轨迹。以下代码生成符合人类操作特征的贝塞尔曲线路径:
def human_like_drag(page, slider_selector: str, track_selector: str): """模拟人类拖拽滑块:先缓慢加速,再匀速,最后减速""" slider = page.query_selector(slider_selector) track = page.query_selector(track_selector) # 获取轨道宽度(像素) track_width = track.bounding_box()["width"] # 生成贝塞尔控制点(模拟手指自然运动) # 起点 (0,0) → 控制点1 (0.3*track_width, -20) → 控制点2 (0.7*track_width, 20) → 终点 (track_width, 0) points = [ {"x": 0, "y": 0}, {"x": int(0.3 * track_width), "y": -20}, {"x": int(0.7 * track_width), "y": 20}, {"x": int(track_width), "y": 0} ] # 执行拖拽(Playwright 1.42+ 支持 multi-point drag) slider.drag_to( track, source_position={"x": 10, "y": 10}, target_position={"x": track_width - 10, "y": 10}, force=True ) # 等待验证通过(检测滑块 class 变化) page.wait_for_function( f'document.querySelector("{slider_selector}").classList.contains("success")', timeout=5000 ) # 在登录流程中调用 # human_like_drag(page, "div.g-slider", "div.g-track")5.2 方案二:复用已知有效 Token(适用于同一 IP 段内共享验证)
部分政务系统滑块验证结果会绑定 IP + User-Agent,同一出口 IP 的多次请求可复用首次验证成功的geetest_validate参数。实现方式:
- 首次登录时,用
page.on('response', ...)捕获滑块验证接口(如/api/geetest/verify)的响应体; - 提取
{"validate":"xxx","seccode":"yyy"}中的validate值; - 后续登录时,在
page.route()中拦截该接口,直接返回缓存的 validate 值:
# 缓存首次验证结果 cached_geetest = None def handle_geetest_response(route, request): global cached_geetest if "geetest/verify" in request.url and cached_geetest: route.fulfill( status=200, content_type="application/json", body=json.dumps({"validate": cached_geetest, "seccode": cached_geetest}) ) else: route.continue_() # 在 page.goto() 前注册路由 page.route("**/api/geetest/verify", handle_geetest_response) # 首次登录后保存 if not cached_geetest: response = page.wait_for_response("**/api/geetest/verify") data = response.json() cached_geetest = data.get("validate")提示:该方案需确保所有登录请求来自同一出口 IP(如 Jenkins 服务器固定公网 IP),且验证 Token 有效期通常为 2 小时,需配合定时刷新机制。
本文还有配套的精品资源,点击获取