--- 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 # 方式A:URL图片通过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') # 方式B:JS直接设置(并触发事件) 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 备选引擎 |