Files
atomk-hermes-skills/skills/browser-automation/captcha-auto-login/SKILL.md
T

428 lines
18 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
---
name: captcha-auto-login
title: ERP带验证码自动登录
description: 使用DrissionPage+ddddocr自动识别图形验证码并完成ERP系统登录。支持店小秘、妙手、通途等常见跨境ERP。
category: browser-automation
tags: [drissionpage, ddddocr, captcha, login, automation]
---
# 带验证码自动登录
## 依赖安装
```bash
pip install DrissionPage ddddocr requests
```
## 核心代码
### 1. 获取随机端口(防止BrowserConnectError
```python
import socket
def get_free_port():
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
s.bind(('', 0))
return s.getsockname()[1]
```
### 2. 创建浏览器(headless模式)
```python
from DrissionPage import ChromiumPage, ChromiumOptions
def create_browser(headless=True):
co = ChromiumOptions()
# 注意:根据环境查找正确的Chromium路径
co.set_browser_path('/home/ubuntu/.cache/ms-playwright/chromium-1217/chrome-linux64/chrome')
co.headless(headless)
co.set_argument('--headless=new')
co.set_argument('--no-sandbox')
co.set_argument('--disable-dev-shm-usage')
co.set_argument('--disable-gpu')
co.set_argument('--ignore-certificate-errors')
co.set_argument('--window-size=1440,900')
co.set_user_agent('Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36')
co.set_local_port(get_free_port())
return ChromiumPage(addr_or_opts=co)
```
### 3. OCR识别验证码
```python
import ddddocr
def recognize_captcha(image_path):
ocr = ddddocr.DdddOcr(show_ad=False)
with open(image_path, 'rb') as f:
image_bytes = f.read()
return ocr.classification(image_bytes)
def correct_ocr(raw_code):
"""修正ddddocr常见误识别——数字验证码场景"""
t = raw_code.strip().replace(' ', '').replace('\n', '')
t = t.translate(str.maketrans(
'oliOSsZzBbGgQq',
'01105522669999'
))
return t
```
### 4. 获取验证码图片(三种方式)
```python
import base64
# 方式AURL图片通过requests下载(共享cookie
def get_captcha_by_requests(page, captcha_url, save_path):
cookies = {c['name']: c['value'] for c in page.cookies()}
resp = requests.get(captcha_url, cookies=cookies, timeout=10, verify=False)
if resp.status_code == 200:
with open(save_path, 'wb') as f:
f.write(resp.content)
return True
return False
# 方式B:Base64内联图片(如妙手ERP)
def get_captcha_base64(page, img_selector, save_path):
captcha_src = page.run_js(f'''var img = document.querySelector('{img_selector}'); return img ? img.src : null;''')
if captcha_src and captcha_src.startswith('data:image'):
header, data = captcha_src.split(',', 1)
image_bytes = base64.b64decode(data)
with open(save_path, 'wb') as f:
f.write(image_bytes)
return True
return False
# 方式C:浏览器截图(确保session完全一致,适用于店小秘等严格校验session的站点)
def get_captcha_by_screenshot(page, img_selector, save_path):
img_info = page.run_js(f'''
var img = document.querySelector('{img_selector}');
if (!img) return null;
var rect = img.getBoundingClientRect();
return {{x: rect.left, y: rect.top, width: rect.width, height: rect.height}};
''')
if not img_info:
return False
result = page.driver.run('Page.captureScreenshot', format='png', clip={
'x': img_info['x'], 'y': img_info['y'],
'width': img_info['width'], 'height': img_info['height'], 'scale': 1
})
image_data = base64.b64decode(result['data'])
with open(save_path, 'wb') as f:
f.write(image_data)
return True
```
### 5. 填充表单
某些表单(如店小秘)需要使用DrissionPage的 `ele().input()` 方法,直接JS设置value可能不生效:
```python
# 方式A:使用ele API(更稳定)
user_ele = page.ele('css:input[name="account"]', timeout=3)
if user_ele:
user_ele.clear()
user_ele.input('your_account')
# 方式BJS直接设置(并触发事件)
page.run_js('''
function setValue(el, value) {
if (!el) return;
el.focus();
el.value = value;
el.dispatchEvent(new Event('focus', { bubbles: true }));
el.dispatchEvent(new KeyboardEvent('keydown', { bubbles: true }));
el.dispatchEvent(new KeyboardEvent('keypress', { bubbles: true }));
el.dispatchEvent(new KeyboardEvent('keyup', { bubbles: true }));
el.dispatchEvent(new Event('input', { bubbles: true }));
el.dispatchEvent(new Event('change', { bubbles: true }));
el.dispatchEvent(new Event('blur', { bubbles: true }));
}
setValue(document.querySelector('input[name="account"]'), 'your_account');
''')
```
### 4. 通用自动登录函数
```python
import time
import requests
def auto_login_with_captcha(login_url, username, password,
username_selector='input[name="account"],input[name="mobile"],input[name="username"]',
password_selector='input[name="password"]',
captcha_img_selector='img[src*="captcha"],img[src*="verify"],#verifyImgCode',
captcha_input_selector='input[name="verifyCode"],input[name="validateCode"],input[name="captcha"]',
login_btn_text='登录',
screenshot_path='/tmp/captcha.png',
wait_time=5):
page = create_browser(f'/tmp/dp_captcha_{time.time()}')
result = {'success': False, 'url': '', 'title': '', 'captcha': ''}
try:
page.get(login_url)
time.sleep(wait_time)
# 获取验证码图片src
captcha_src = page.run_js(f'''
var img = document.querySelector('{captcha_img_selector.replace("'", "\\'")}');
if (img) return img.src;
return null;
''')
if captcha_src:
# 使用requests下载验证码(共享cookie
cookies = {c['name']: c['value'] for c in page.cookies()}
resp = requests.get(captcha_src, cookies=cookies, timeout=10)
if resp.status_code == 200:
with open(screenshot_path, 'wb') as f:
f.write(resp.content)
captcha_code = recognize_captcha(screenshot_path)
result['captcha'] = captcha_code
# 填充表单
page.run_js(f'''
var userInp = document.querySelector('{username_selector.replace("'", "\\'")}');
var pwdInp = document.querySelector('{password_selector.replace("'", "\\'")}');
var codeInp = document.querySelector('{captcha_input_selector.replace("'", "\\'")}');
if (userInp) {{
userInp.value = '{username}';
userInp.dispatchEvent(new Event('input', {{ bubbles: true }}));
userInp.dispatchEvent(new Event('change', {{ bubbles: true }}));
}}
if (pwdInp) {{
pwdInp.value = '{password}';
pwdInp.dispatchEvent(new Event('input', {{ bubbles: true }}));
pwdInp.dispatchEvent(new Event('change', {{ bubbles: true }}));
}}
if (codeInp && '{captcha_code}') {{
codeInp.value = '{captcha_code}';
codeInp.dispatchEvent(new Event('input', {{ bubbles: true }}));
codeInp.dispatchEvent(new Event('change', {{ bubbles: true }}));
}}
''')
time.sleep(1)
# 点击登录按钮
page.run_js(f'''
var btns = document.querySelectorAll('button');
for (var i = 0; i < btns.length; i++) {{
if (btns[i].textContent.includes('{login_btn_text}')) {{
btns[i].click();
return true;
}}
}}
var form = document.querySelector('form');
if (form) {{ form.submit(); return true; }}
return false;
''')
time.sleep(8)
result['url'] = page.url
result['title'] = page.title
result['success'] = 'login' not in page.url.lower()
except Exception as e:
result['error'] = str(e)
finally:
page.quit()
return result
```
## 使用示例
### 妙手ERP (erp.91miaoshou.com)
特点:
- 登录 URL`https://erp.91miaoshou.com/`(主页,已登录自动跳转 `/welcome`
- ⚠️ `/auth/login` 返回 404「页面未找到」— 不要使用此路径
- 验证码是base64内联图片(`.captcha-img`
- 登录成功后跳转到 `/welcome`
- **多表单陷阱**:页面同时存在登录、注册、忘记密码、设备验证多个表单,每个都有独立的 `captcha-img`。如果直接用 `.captcha-text` 填写可能填到隐藏表单。
- **正确定位策略**:先通过 `input.J_captchaInput` 找到主登录表单的验证码输入框,再向上遍历 parent 查找关联的 `captcha-img`
```python
import re
from DrissionPage import ChromiumPage, ChromiumOptions
def login_miaoshou(username, password, max_retry=5):
co = ChromiumOptions()
co.set_browser_path('/home/ubuntu/.cache/ms-playwright/chromium-1217/chrome-linux64/chrome')
co.set_argument('--no-sandbox')
co.set_argument('--disable-dev-shm-usage')
co.set_argument('--disable-gpu')
co.headless()
co.auto_port()
page = ChromiumPage(co)
try:
for attempt in range(max_retry):
page.get('https://erp.91miaoshou.com/')
time.sleep(3)
# 填充账号密码
page.ele('css:.account-input', timeout=5).clear().input(username)
page.ele('css:.password-input', timeout=5).clear().input(password)
# 找到主登录表单的验证码输入框
cap_input = page.ele('css:input.J_captchaInput', timeout=5)
# 通过 parent 查找关联的验证码图片(避免填到隐藏表单)
parent = cap_input.parent()
cap_img = None
for _ in range(5):
cap_img = parent.ele('css:img.captcha-img', timeout=1)
if cap_img:
break
parent = parent.parent()
if not parent:
break
if not cap_img:
# fallback:找第一个带 data URI 的验证码图
for img in page.eles('css:img.captcha-img'):
if (img.attr('src') or '').startswith('data:'):
cap_img = img
break
# 解码验证码
src = cap_img.attr('src') or '' if cap_img else ''
code = None
if src.startswith('data:image/'):
b64 = src.split(',', 1)[1]
ocr = ddddocr.DdddOcr(show_ad=False)
raw = ocr.classification(base64.b64decode(b64))
code = correct_ocr(raw)
else:
path = f'/tmp/ms_captcha_{attempt}.jpg'
cap_img.get_screenshot(path=path)
with open(path, 'rb') as f:
code = correct_ocr(ddddocr.DdddOcr(show_ad=False).classification(f.read()))
if not code or len(code) < 3:
continue
cap_input.clear().input(code)
page.ele('css:.login.login-button', timeout=5).click()
time.sleep(4)
body = page('tag:body').text
if '图形验证码不正确' in body:
continue # 重试
elif '登录' in body and '账号' in body:
continue
# 成功检查:妙手跳转到 /welcome
if '/welcome' in page.url or '工作台' in body or '实时数据' in body:
cookies = page.cookies(all_domains=True)
with open('/home/ubuntu/.hermes/cookies/miaoshou_cookies_new.json', 'w') as f:
json.dump(cookies, f, indent=2)
return True
return False
finally:
page.quit()
```
### 店小秘 (dianxiaomi.com)
关键点:使用浏览器截图获取验证码以确保session一致。
**登录页 URL**`https://www.dianxiaomi.com/`(不是 `/index.htm`,虽然两个都可以跳转)
```python
def login_dianxiaomi(username, password):
page = create_browser()
page.get('https://www.dianxiaomi.com/')
time.sleep(8)
# 使用浏览器截图获取验证码
get_captcha_by_screenshot(page, '#verifyImgCode', '/tmp/dxm_captcha.png')
raw_code = recognize_captcha('/tmp/dxm_captcha.png')
captcha_code = correct_ocr(raw_code)
# 填写表单
account_ele = page.ele('css:#exampleInputName', timeout=3)
pwd_ele = page.ele('css:#exampleInputPassword', timeout=3)
code_ele = page.ele('css:#verifyCode', timeout=3)
if account_ele: account_ele.clear(); account_ele.input(username)
if pwd_ele: pwd_ele.clear(); pwd_ele.input(password)
if code_ele and captcha_code: code_ele.clear(); code_ele.input(captcha_code)
# 点击登录
login_btn = page.ele('css:.loginbnt', timeout=3)
if login_btn: login_btn.click()
time.sleep(10)
success = not page.run_js('return !!document.querySelector("#exampleInputName");')
page.quit()
return success
```
⚠️ **ddddocr 对店小秘验证码可靠性低**:多次实测(2026-06-08,含 CDP screenshot scale=2/3),ddddocr 持续误识别:`ncy``nCye``国x``三bnGM` 等均不正确。店小秘验证码字符风格与 ddddocr 训练数据不匹配。**建议**:
- 优先用 Bridge CDP 操控桌面浏览器,让用户肉眼识别验证码后手动输入
- 如必须自动识别,考虑 `onnxocr``paddleocr` 作为备选引擎
- 验证码失败时页面**无明确错误提示**,登录表单仍然可见 —— 需通过 `document.querySelector("#exampleInputName")` 是否存在来判断成功与否
- 9Commerce店小秘账号:9Commerce / Qq123456**用户名登录,非邮箱**)。已登录。用于跨境电商ERP管理及CDP自动化演示。
### 妙手ERP Bridge CDP 登录(替代 headless 方案)
当桌面 CDP 可用时,可直接通过 Bridge 操控用户桌面 Chrome 完成登录,无需 headless
```python
# 1. attach -> navigate -> evaluate
# 2. 提取 base64 captcha (img.captcha-img src="data:image/png;base64,...")
# 3. ddddocr 解码(妙手的数字验证码 ddddocr 识别准确,如 '6343' 一次成功)
# 4. fill: input[name="mobile"].account-input + input[name="password"].password-input
# + input[name="captcha"].captcha-text
# 5. click: button text="立即登录"
# 6. verify: URL 跳转到 /welcome 即成功
```
- 账号:17762501033,密码 `Tt123456!`
- 验证码:base64 内联图片,ddddocr 对妙手纯数字验证码识别较准确
- 登录成功标志:URL 包含 `/welcome`,标题 "妙手-欢迎使用"
## 关键要点
1. **端口管理**:必须使用随机端口,避免`BrowserConnectError`
2. **关联元素定位**:当页面存在多个同类表单(如登录/注册/忘记密码)时,不能直接用全局CSS选择器。应先定位到目标表单内的**特征输入框**(如 `input.J_captchaInput`),再向上遍历 `parent()` 查找关联的验证码图片,确保 captchaUuid 与验证码图片配对。
3. **验证码获取**:headless模式下直接截图可能失败,应通过JS获取`img.src`再用requests下载
3. **Cookie共享**:下载验证码时必须传递当前浏览器的cookies,否则验证码会刷新
4. **Session一致性**:部分站点(如店小秘)即使传递cookies,用requests下载验证码仍可能导致session不匹配。建议使用浏览器截图(CDP) 或浏览器内fetch获取验证码
5. **Base64内联图片**:如妙手ERP的验证码是`data:image/png;base64,...`格式,需split+解码
6. **事件触发**:仅设置`value`不够,必须触发`input``change``blur`等事件。某些表单需使用`ele().input()`替代JS设置
7. **OCR引擎**`ddddocr`是本地模型,无需联网,支持中英文验证码。需注意常见误识别修正(如o→0, l→1)
8. **多验证码字段**:部分页面有多个验证码输入框(如妙手同时有`captcha``validateCode`),需确认填写正确的字段
## 常见平台特殊处理
### 妙手ERP
- 验证码:base64内联图片(`.captcha-img`
- 登录字段:`mobile`(账号)、`password``captcha`(图形验证码)、`captchaUuid`hidden
- 登录按钮:`.login-button`(文字"立即登录"
### 店小秘
- 验证码:URL加载(`#verifyImgCode`src为`/verify/code.htm?t=TIMESTAMP`
- 建议用浏览器截图获取,避免session不一致
- 登录字段:`account`#exampleInputName**用户名非邮箱**)、`password`#exampleInputPassword)、`verifyCode`#verifyCode
- 登录按钮:`.loginbnt`
- **防止设备验证**:在非常用设备/环境登录时,店小秘会触发二次验证(邮箱或手机短信),无法通过纯自动化绕过。需先在常用设备上手动登录并勾选"信任此设备"
## 故障排查
| 问题 | 解决方案 |
|------|----------|
| BrowserConnectError | 更换端口或重启浏览器进程 |
| 验证码识别失败 | 检查图片是否下载完整,尝试调整页面等待时间,使用浏览器截图替代requests下载 |
| "验证码填写错误" | 检查session是否一致(用浏览器截图/fetch替代requests),检查OCR结果是否需修正(o→0, l→1) |
| "图形验证码不正确" 连续出现 | 检查是否填到了隐藏/错误表单的验证码框。使用 `input.J_captchaInput` 定位主登录表单,向上遍历 parent 查找关联的 captcha-img。 |
| "手机号/子账号/邮箱不能为空" | 表单填写未生效,尝试使用`ele().input()`替代JS设置value |
| 登录后仍跳转login页 | 检查账号密码是否正确,或增加等待时间 |
| 元素找不到 | 使用浏览器开发者工具确认selector是否正确 |
| 店小秘触发二次验证 | 在常用设备上手动登录一次并勾选"信任此设备" |
| ddddocr 持续识别错误(店小秘) | ddddocr 对店小秘验证码可靠性低。改用 Bridge CDP + 用户肉眼识别,或尝试 onnxocr/paddleocr 备选引擎 |