1. 项目概述为什么是Playwright处理验证码在自动化测试或者数据采集的实战中验证码CAPTCHA就像一道横亘在路上的自动门它存在的核心目的就是区分人类和机器。无论是传统的数字字母验证码、滑块拼图、点选文字还是更复杂的旋转、推理验证码其本质都是增加自动化程序的交互成本。过去我们可能依赖专门的打码平台、OCR服务或者复杂的图像处理库如OpenCV来“硬刚”验证码。但这些方案要么有额外成本要么开发维护复杂且随着验证码技术的迭代识别成功率并不稳定。这时Playwright作为一个现代浏览器自动化框架为我们提供了一条不同的思路绕过识别模拟真人。它的核心优势在于能够精准地模拟人类在浏览器中的一切操作——鼠标移动轨迹、点击、拖拽、键盘输入甚至生成真实的人类行为指纹。对于很多非极端复杂的验证码尤其是行为验证码如滑块、点选我们完全可以通过Playwright模拟出“人类”的操作流程从而让系统“相信”操作者是真人进而绕过验证。这个教程要解决的就是如何利用Playwright的这一特性构建一套稳定、可复用的自动化验证码处理方案。它不适合需要极高识别精度的纯图像验证码那种情况下仍需结合OCR但对于市面上大量基于交互行为的验证码以及那些可以“绕过”的验证码场景如调试模式、测试环境禁用验证码Playwright是一把利器。无论你是测试工程师需要自动化登录流程还是开发者需要处理数据采集中的验证环节这套方法都能提供直接的参考。2. 核心思路模拟真人交互与合法“绕过”处理验证码无外乎“识别”和“绕过”两条路。Playwright的方案更倾向于后者或者说是两者的结合在无法绕过时尽可能逼真地模拟人类操作来完成验证。我们的核心思路可以拆解为以下几个层次。2.1 环境模拟让浏览器看起来像“人”验证码系统尤其是高级的行为验证码会收集大量浏览器环境信息如WebGL、Canvas指纹、字体列表、屏幕分辨率、时区、语言等。一个“赤裸”的自动化浏览器环境很容易被检测出来。Playwright的应对策略是提供丰富的上下文Context配置选项。在启动浏览器时我们可以创建一个高度定制化的浏览器上下文为其注入真实的人类环境特征。import asyncio from playwright.async_api import async_playwright async def main(): async with async_playwright() as p: # 使用带真实UI的浏览器而非无头模式初期调试更直观 browser await p.chromium.launch(headlessFalse) # 创建上下文并设置视口、用户代理、语言、时区等 context await browser.new_context( viewport{width: 1920, height: 1080}, user_agentMozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36, localezh-CN, timezone_idAsia/Shanghai, # 注入额外的HTTP头模拟更真实的流量 extra_http_headers{ Accept-Language: zh-CN,zh;q0.9, Sec-Ch-Ua: Not_A Brand;v8, Chromium;v120, } ) page await context.new_page() # ... 后续操作注意headlessFalse模式在开发和调试阶段非常有用你可以亲眼看到浏览器的操作过程。但在生产环境部署时建议切换为headlessTrue或headlessnew以提升性能并减少资源占用。new模式是Chromium较新版本的无头模式兼容性更好。2.2 交互模拟赋予操作“灵魂”简单的page.click()和page.type()是机械的。验证码系统会分析鼠标移动的速度曲线、点击的精确位置、按键的时间间隔等。Playwright提供了底层API让我们能精细控制这些行为。核心在于使用page.mouse和page.keyboard对象并引入随机性和人类化的延迟。例如模拟人类输入验证码时不会是瞬间完成的每个字符之间应有微小的时间差并且可能伴随退格修改。import random import asyncio async def human_type(page, selector, text): 模拟人类输入带有随机延迟和可能的错误修正 await page.click(selector) # 先点击输入框聚焦 for char in text: await page.keyboard.type(char, delayrandom.uniform(50, 150)) # 每个字符输入延迟50-150毫秒 # 小概率模拟输错后删除重输 if random.random() 0.05: # 5%的概率“打错字” await page.keyboard.press(Backspace, delayrandom.uniform(30, 80)) await page.keyboard.type(char, delayrandom.uniform(80, 180))对于滑块验证码模拟人类拖拽轨迹是关键。直接page.drag_and_drop()是直线运动极易被识别。我们需要生成一条包含加速、减速、甚至微小抖动的贝塞尔曲线或多项式轨迹。async def human_slide(page, slider_selector, track_width): 模拟人类滑动滑块 slider await page.wait_for_selector(slider_selector) box await slider.bounding_box() start_x box[x] box[width] / 2 start_y box[y] box[height] / 2 end_x start_x track_width await page.mouse.move(start_x, start_y) await page.mouse.down() # 生成一条带随机抖动的移动轨迹点 points generate_human_trajectory(start_x, start_y, end_x, start_y, num_points30) for point in points: await page.mouse.move(point[x], point[y]) await asyncio.sleep(random.uniform(0.02, 0.1)) # 移动间随机延迟 await page.mouse.up() def generate_human_trajectory(start_x, start_y, end_x, end_y, num_points): 生成模拟人类鼠标移动的轨迹点简化版 points [] for i in range(num_points): t i / (num_points - 1) # 使用三次贝塞尔曲线模拟先加速后减速 # 控制点引入一些随机偏移模拟手抖 cp1_x start_x (end_x - start_x) * 0.3 random.uniform(-5, 5) cp2_x start_x (end_x - start_x) * 0.7 random.uniform(-5, 5) # 计算贝塞尔曲线上的点 (简化计算实际应使用公式) x (1-t)**3 * start_x 3*(1-t)**2*t*cp1_x 3*(1-t)*t**2*cp2_x t**3*end_x y start_y random.uniform(-2, 2) # Y轴轻微抖动 points.append({x: x, y: y}) return points2.3 策略选择识别、绕过还是混合面对具体验证码第一步永远是分析。打开浏览器开发者工具Playwright可以录制或手动操作一次观察网络请求和页面元素。直接绕过有些网站在测试环境、携带特定Cookie或请求头、或访问频率不高时可能不会弹出验证码。或者验证码图片的URL是固定的可以提前获取并手动识别一次将结果缓存起来反复使用。检查是否有“免验证码”的API接口通常用于移动端或内部调用但这属于漏洞范畴需谨慎合规使用。模拟解决对于滑块、点选等交互式验证码采用上述的真人交互模拟方案。集成识别对于复杂的图文验证码模拟无效时需要集成第三方OCR。流程是用Playwright截图或获取验证码图片元素 - 调用OCR API如云打码平台、自建Tesseract模型- 获取文本结果 - 用模拟输入的方式填入。这里的关键是即使使用了OCR输入环节也应使用human_type来模拟而不是直接fill()。3. 实战演练攻克典型验证码场景让我们通过几个最常见的案例将上述思路具体化。假设我们使用Python版本的Playwright进行演示。3.1 场景一传统图文验证码数字字母混合这种验证码通常是一个图片里面包含扭曲的数字和字母。我们的策略是截图 - OCR识别 - 模拟输入。步骤拆解定位并获取验证码图片元素通常是一个img标签其src可能是Base64数据data:image...或一个动态URL。截图与预处理Playwright可以直接对元素截图。OCR前对图片进行灰度化、二值化、降噪等预处理能大幅提升识别率可使用PIL或opencv-python。调用OCR服务这里以免费的ddddocr库针对中文验证码优化为例也可以使用pytesseractTesseract的Python封装或付费API。模拟人类输入识别结果。import asyncio from playwright.async_api import async_playwright import ddddocr async def handle_image_captcha(page): 处理传统图像验证码 假设验证码图片的selector是 img.captcha-img输入框是 input#captcha # 1. 等待验证码图片加载 captcha_element await page.wait_for_selector(img.captcha-img) # 2. 截图并保存到内存 captcha_bytes await captcha_element.screenshot() # 3. 使用OCR识别 ocr ddddocr.DdddOcr(show_adFalse) # 禁用广告日志 captcha_text ocr.classification(captcha_bytes) print(f识别出的验证码为: {captcha_text}) # 4. 模拟人类输入到输入框 input_selector input#captcha await page.click(input_selector) # 清空输入框有些网站会自动填充旧值 await page.fill(input_selector, ) # 使用自定义的人类输入函数 await human_type(page, input_selector, captcha_text) # 5. 可选如果识别错误尝试刷新验证码重试 # 例如点击验证码图片本身或旁边的“刷新”按钮 # await captcha_element.click() # await asyncio.sleep(1) # 等待新验证码加载 # ... 重新执行识别流程 # 在主函数中调用 async def main(): async with async_playwright() as p: browser await p.chromium.launch(headlessFalse) page await browser.new_page() await page.goto(https://example.com/login) # ... 填写用户名密码 await handle_image_captcha(page) # ... 点击登录按钮 await page.click(button[typesubmit])实操心得ddddocr对于常见的扭曲数字字母验证码识别率不错且免费。但如果验证码背景干扰严重或字体特殊识别率会下降。此时可以考虑增加预处理用OpenCV进行阈值分割、形态学操作去除噪点。尝试多种OCR引擎组合使用ddddocr、pytesseract需单独安装Tesseract-OCR并配置语言包或商用API进行投票决策。加入人工回退当置信度低于某个阈值时将图片保存下来通过声音、弹窗或发送到手机等方式提醒人工识别并将正确结果反馈回系统用于后续模型训练如果自建OCR。3.2 场景二滑块验证码滑块验证码的核心是计算滑块需要拖动的距离。这个距离有时直接隐藏在页面元素属性中如背景图宽度与缺口位置的差值有时需要图像识别来计算缺口位置。步骤拆解获取滑块和背景图滑块是可拖动的元素背景图是包含缺口的图片。背景图可能以CSS背景、img标签或Canvas绘制的方式存在。计算滑动距离方案A元素属性检查网络请求或DOM属性看是否有包含距离信息的>import cv2 import numpy as np from io import BytesIO from PIL import Image import asyncio async def handle_slide_captcha(page): 处理滑块验证码 假设滑块按钮selector是 .slider-button背景图是 .captcha-bg 元素的CSS背景 # 1. 获取背景图片示例从CSS背景中提取 bg_element await page.wait_for_selector(.captcha-bg) # 获取元素的CSS background-image属性可能是url(...) bg_style await bg_element.get_attribute(style) # 这里简化处理实际中可能需要正则提取URL并用page.evaluate获取完整的base64或blob # 假设我们通过其他方式已经获得了背景图的字节数据 bg_bytes # 2. 获取滑块图片缺口图 slide_element await page.wait_for_selector(.slider-mask) # 假设缺口图是这个元素 slide_bytes await slide_element.screenshot() # 3. 使用OpenCV计算滑动距离 (需要预先有完整的背景模板 bg_template_bytes) # 这里演示一个简化的流程 distance calculate_slide_distance(bg_bytes, slide_bytes, bg_template_bytes) print(f计算出的滑动距离为: {distance} 像素) # 4. 模拟滑动 await human_slide(page, .slider-button, distance) # 5. 等待验证结果可能有一个成功的提示元素出现 try: await page.wait_for_selector(.verify-success, timeout5000) print(滑块验证成功) except: print(滑块验证可能失败需要重试或处理) # 可以在这里触发刷新验证码等操作 def calculate_slide_distance(bg_bytes, slide_bytes, template_bytes): 使用OpenCV模板匹配计算滑动距离简化示例 # 将字节数据转换为numpy数组 bg_img np.array(Image.open(BytesIO(bg_bytes)).convert(RGB)) slide_img np.array(Image.open(BytesIO(slide_bytes)).convert(RGB)) template_img np.array(Image.open(BytesIO(template_bytes)).convert(RGB)) # 灰度化 bg_gray cv2.cvtColor(bg_img, cv2.COLOR_RGB2GRAY) slide_gray cv2.cvtColor(slide_img, cv2.COLOR_RGB2GRAY) template_gray cv2.cvtColor(template_img, cv2.COLOR_RGB2GRAY) # 使用模板匹配寻找slide在bg中的位置 result cv2.matchTemplate(bg_gray, slide_gray, cv2.TM_CCOEFF_NORMED) _, _, _, max_loc cv2.minMaxLoc(result) # 假设缺口在模板中的位置是固定的计算偏移量 # 这里需要根据实际情况调整例如缺口在模板的x坐标是 known_gap_x known_gap_x 50 # 假设模板图中缺口的x坐标是50 slide_distance max_loc[0] - known_gap_x return slide_distance注意事项滑块验证码的对抗升级很快。高级的滑块会轨迹验证这正是我们花大力气模拟人类轨迹的原因。动态缺口缺口位置每次都在变甚至背景图是动态生成的。我们的图像识别方案需要能应对这种变化。背景干扰背景图有大量相似边缘干扰模板匹配。可能需要更复杂的算法如基于深度学习的缺口检测但成本高。无缺口滑块需要将滑块拖到指定区域而不是匹配缺口。这时距离可能是固定的或者需要通过其他方式如颜色、文字提示判断。一个更实用的技巧是先尝试用Playwright录制一次正确的手动滑动操作分析其网络请求。有时滑动距离或一个代表成功的token会直接通过API返回根本不需要计算像素距离。通过page.on(request)监听请求找到验证通过的接口然后尝试用Playwright直接模拟那个请求这可能是一种更稳定的“绕过”方式。3.3 场景三点选验证码如“点击图中所有的xxx”这类验证码需要识别图片中的特定物体如文字、交通标志、动物并点击。我们的策略是获取大图 - 获取需要点击的词语或提示 - 使用目标检测或图像分类模型找出所有目标位置 - 模拟点击。步骤拆解获取验证码大图和提示文字。目标检测这是难点。对于通用物体猫狗、汽车可以使用预训练的深度学习模型如YOLO但需要集成到Python环境中较重。对于文字点选可以尝试OCR识别出图中所有文字块然后筛选出与提示词匹配的。计算点击坐标将检测到的目标框的中心点坐标转换为相对于浏览器视口的坐标。模拟无序点击人类不会按顺序点击。需要将目标点列表打乱然后逐个用human_click包含移动和随机延迟的点击函数点击。import random async def handle_click_captcha(page): 处理点选验证码示例点击图中所有的‘自行车’ 假设大图selector是 .click-captcha-img提示文字在 .prompt 元素里 # 1. 获取提示文字 prompt_element await page.wait_for_selector(.prompt) prompt_text await prompt_element.text_content() # 例如: 请依次点击图中的 自行车 target_word prompt_text.split( )[-1] # 简单提取目标词实际需更健壮的解析 print(f需要点击的目标是: {target_word}) # 2. 获取大图 captcha_element await page.wait_for_selector(.click-captcha-img) captcha_bytes await captcha_element.screenshot() # 3. 目标检测这里是一个伪代码示例实际需要集成检测模型 # 假设 detect_objects 函数返回一个列表每个元素是 {label: 自行车, x: 100, y: 150, width: 50, height: 50} detected_objects await detect_objects_in_image(captcha_bytes, target_word) # 4. 获取图片元素在页面中的位置 box await captcha_element.bounding_box() img_x, img_y box[x], box[y] # 5. 将检测到的图片内坐标转换为页面坐标并打乱顺序 click_points [] for obj in detected_objects: # 计算目标中心点在页面中的坐标 center_x img_x obj[x] obj[width] / 2 center_y img_y obj[y] obj[height] / 2 click_points.append((center_x, center_y)) random.shuffle(click_points) # 打乱点击顺序 # 6. 模拟人类点击 for i, (point_x, point_y) in enumerate(click_points): # 先移动鼠标到目标附近的一个随机起始点 start_x point_x random.uniform(-30, 30) start_y point_y random.uniform(-30, 30) await page.mouse.move(start_x, start_y) await asyncio.sleep(random.uniform(0.1, 0.3)) # 再微调到精确位置并点击 await page.mouse.move(point_x, point_y, stepsrandom.randint(5, 15)) # steps参数让移动有轨迹 await asyncio.sleep(random.uniform(0.05, 0.15)) await page.mouse.down() await asyncio.sleep(random.uniform(0.02, 0.08)) # 按下后短暂停顿 await page.mouse.up() await asyncio.sleep(random.uniform(0.2, 0.5)) # 点击间隔 print(点选完成) async def detect_objects_in_image(image_bytes, target_label): 伪代码在图片中检测特定标签的物体。 实际实现可能需要 1. 使用云端视觉API如百度AI、阿里云视觉。 2. 本地运行轻量级模型如MobileNet SSD。 3. 对于文字点选使用OCR获取所有文字块及其位置然后匹配目标词。 # 此处为示例返回模拟数据 return [ {label: 自行车, x: 100, y: 150, width: 50, height: 50}, {label: 自行车, x: 300, y: 200, width: 55, height: 55}, ]实操心得点选验证码是当前对抗强度较高的一类。自建目标检测模型成本高。一个折中方案是使用付费视觉API准确率高但有调用成本和次数限制。适合商业项目。特征匹配如果验证码的图片库有限比如就几十张图可以提前收集所有图片和对应的点击坐标然后通过图像哈希如pHash或SIFT特征匹配来找到当前是哪张图直接使用预存的坐标。这属于“图库攻击”对于小型网站可能有效。半自动化当检测置信度不高时将图片和可能的点击区域标出显示给人工进行最终确认。这平衡了自动化效率和成功率。4. 工程化与稳定性提升单个验证码处理脚本写出来不难难的是将其集成到自动化流程中并保证长期稳定运行。这就需要工程化的思维。4.1 验证码处理器的抽象与封装我们应该将验证码处理逻辑抽象成一个独立的模块或类对外提供统一的接口例如CaptchaSolver.solve(page, captcha_type)。这样主流程代码会更清晰也便于更换不同的处理策略。# captcha_solver.py class CaptchaSolver: def __init__(self, ocr_enabledTrue, ocr_providerddddocr): self.ocr_enabled ocr_enabled if ocr_enabled and ocr_provider ddddocr: import ddddocr self.ocr_engine ddddocr.DdddOcr(show_adFalse) # 可以初始化其他引擎如Tesseract、付费API客户端等 async def solve(self, page, captcha_info): 统一解决入口 captcha_info: 字典包含验证码类型、元素选择器等信息 例如: {type: image, image_selector: img.captcha, input_selector: #captchaInput} captcha_type captcha_info.get(type) if captcha_type image: return await self._solve_image_captcha(page, captcha_info) elif captcha_type slide: return await self._solve_slide_captcha(page, captcha_info) elif captcha_type click: return await self._solve_click_captcha(page, captcha_info) else: raise ValueError(f不支持的验证码类型: {captcha_type}) async def _solve_image_captcha(self, page, info): # ... 实现图文验证码处理逻辑 pass async def _solve_slide_captcha(self, page, info): # ... 实现滑块验证码处理逻辑 pass # ... 其他私有方法 # 在主流程中使用 async def login_flow(page, solver): await page.goto(login_url) await page.fill(#username, myuser) await page.fill(#password, mypass) # 判断页面是否出现了验证码可以通过元素存在性判断 if await page.is_visible(img.captcha): captcha_info {type: image, image_selector: img.captcha, input_selector: #captchaCode} success await solver.solve(page, captcha_info) if not success: print(验证码处理失败可能需要重试或人工干预) return False await page.click(#submitBtn) return True4.2 重试机制与降级策略验证码处理不可能100%成功。必须有健全的重试和降级机制。重试当识别或验证失败时如登录后提示验证码错误自动刷新验证码并重试。设置最大重试次数如3次避免无限循环。降级多OCR引擎备选主引擎识别失败后自动切换备用引擎。人工介入当连续失败N次后将任务挂起通过通知机制如邮件、钉钉机器人提醒人工处理并将人工处理结果回填。甚至可以记录下失败的验证码图片用于后续标注和模型优化。切换方案例如点选验证码的AI识别失败后可以尝试寻找是否有“语音验证”的切换按钮转为处理语音验证码语音转文本相对简单。async def solve_with_retry(solver, page, captcha_info, max_retries3): for attempt in range(max_retries): try: print(f第 {attempt 1} 次尝试解决验证码...) success await solver.solve(page, captcha_info) if success: # 这里可以增加一个验证步骤比如检查是否跳转到了登录后的页面 await page.wait_for_load_state(networkidle) if await page.is_visible(#welcome): # 假设登录成功后的元素 print(验证码处理成功且登录验证通过) return True else: print(验证码处理可能成功但登录未成功触发重试) raise ValueError(Login failed after captcha) else: raise ValueError(Solver reported failure) except Exception as e: print(f尝试 {attempt 1} 失败: {e}) if attempt max_retries - 1: # 触发刷新验证码 if await page.is_visible(.refresh-captcha): await page.click(.refresh-captcha) await asyncio.sleep(1) # 等待新验证码加载 elif await page.is_visible(captcha_info.get(image_selector, )): # 有些网站点击验证码图片本身可以刷新 await page.click(captcha_info.get(image_selector)) await asyncio.sleep(1) else: # 刷新页面作为最后手段 await page.reload() await asyncio.sleep(2) else: print(已达到最大重试次数验证码处理失败) # 触发降级策略通知人工 await notify_human_for_help(page, captcha_info) return False return False4.3 性能优化与资源管理浏览器上下文复用不要为每个任务都启动和关闭一个浏览器。使用browser.new_context()创建独立的上下文会话它们共享浏览器进程但隔离Cookie、缓存等创建和销毁成本远低于启动新浏览器。并行处理如果有多账号或批量任务可以使用asyncio.gather控制多个页面Page或上下文Context并行操作但要注意目标网站的反爬策略如IP限制、频率限制。资源清理及时关闭不用的页面 (page.close()) 和上下文 (context.close())防止内存泄漏。使用async with语句块管理资源生命周期是最佳实践。4.4 反反爬策略集成Playwright本身可以模拟真人环境但还需要结合其他反反爬策略代理IP池通过browser await p.chromium.launch(proxy{server: http://proxy:port})设置代理并定期轮换IP避免因高频访问被封。请求频率控制在操作间增加随机等待时间await asyncio.sleep(random.uniform(1, 5))模拟人类浏览的随机间隔。Cookie管理使用context.storage_state(pathstate.json)保存登录状态下次启动时通过browser.new_context(storage_statestate.json)恢复避免重复登录和触发验证码。5. 常见问题排查与调试技巧在实际操作中你会遇到各种各样的问题。下面是一些典型问题的排查思路。5.1 元素定位失败或操作超时这是最常见的问题通常是因为页面加载速度、动态渲染或元素选择器不稳定。症状TimeoutError: Timeout 30000ms exceeded.排查增加超时时间await page.wait_for_selector(selector, timeout60000)。检查选择器使用浏览器的开发者工具F12检查元素是否唯一优先使用id、name或稳定的>browser await p.chromium.launch(headlessFalse, slow_mo1000)录制操作Playwright Codegen 是一个神器。通过命令playwright codegen https://example.com启动一个可交互的浏览器和代码录制器。你手动操作一遍登录和验证码过程它会自动生成对应的Playwright脚本。这是理解页面交互逻辑和获取稳定选择器的最快方式。Console Logging在脚本中监听Console日志和网络请求有助于理解页面JS逻辑。# 打印所有console日志 page.on(console, lambda msg: print(fconsole: {msg.text})) # 打印所有网络请求 # page.on(request, lambda req: print(f {req.method} {req.url})) # 仅打印包含特定关键词的请求 page.on(request, lambda req: print(f {req.method} {req.url}) if verify in req.url else None)保留Trace当脚本出错时可以通过Trace来复盘。await context.tracing.start(screenshotsTrue, snapshotsTrue, sourcesTrue) # ... 执行你的操作 ... await context.tracing.stop(pathtrace.zip)然后用Playwright的命令行工具playwright show-trace trace.zip打开这个zip文件你可以像看视频一样回放所有操作查看每个时间点的DOM快照、网络请求和Console日志是排查复杂问题的终极武器。处理自动化验证码是一场持久的“攻防战”。Playwright提供了强大的模拟真人交互的能力让我们在“绕过”和“模拟解决”两条路上都有了更先进的武器。但没有任何一种方法是万能的核心还是在于具体问题具体分析仔细研究目标验证码的实现选择最经济有效的策略并准备好完善的重试和降级机制。将验证码处理模块化、配置化你的自动化流程才能足够健壮应对真实世界中复杂多变的挑战。
Playwright自动化验证码处理:模拟真人交互与工程化实战
发布时间:2026/7/17 5:22:31
1. 项目概述为什么是Playwright处理验证码在自动化测试或者数据采集的实战中验证码CAPTCHA就像一道横亘在路上的自动门它存在的核心目的就是区分人类和机器。无论是传统的数字字母验证码、滑块拼图、点选文字还是更复杂的旋转、推理验证码其本质都是增加自动化程序的交互成本。过去我们可能依赖专门的打码平台、OCR服务或者复杂的图像处理库如OpenCV来“硬刚”验证码。但这些方案要么有额外成本要么开发维护复杂且随着验证码技术的迭代识别成功率并不稳定。这时Playwright作为一个现代浏览器自动化框架为我们提供了一条不同的思路绕过识别模拟真人。它的核心优势在于能够精准地模拟人类在浏览器中的一切操作——鼠标移动轨迹、点击、拖拽、键盘输入甚至生成真实的人类行为指纹。对于很多非极端复杂的验证码尤其是行为验证码如滑块、点选我们完全可以通过Playwright模拟出“人类”的操作流程从而让系统“相信”操作者是真人进而绕过验证。这个教程要解决的就是如何利用Playwright的这一特性构建一套稳定、可复用的自动化验证码处理方案。它不适合需要极高识别精度的纯图像验证码那种情况下仍需结合OCR但对于市面上大量基于交互行为的验证码以及那些可以“绕过”的验证码场景如调试模式、测试环境禁用验证码Playwright是一把利器。无论你是测试工程师需要自动化登录流程还是开发者需要处理数据采集中的验证环节这套方法都能提供直接的参考。2. 核心思路模拟真人交互与合法“绕过”处理验证码无外乎“识别”和“绕过”两条路。Playwright的方案更倾向于后者或者说是两者的结合在无法绕过时尽可能逼真地模拟人类操作来完成验证。我们的核心思路可以拆解为以下几个层次。2.1 环境模拟让浏览器看起来像“人”验证码系统尤其是高级的行为验证码会收集大量浏览器环境信息如WebGL、Canvas指纹、字体列表、屏幕分辨率、时区、语言等。一个“赤裸”的自动化浏览器环境很容易被检测出来。Playwright的应对策略是提供丰富的上下文Context配置选项。在启动浏览器时我们可以创建一个高度定制化的浏览器上下文为其注入真实的人类环境特征。import asyncio from playwright.async_api import async_playwright async def main(): async with async_playwright() as p: # 使用带真实UI的浏览器而非无头模式初期调试更直观 browser await p.chromium.launch(headlessFalse) # 创建上下文并设置视口、用户代理、语言、时区等 context await browser.new_context( viewport{width: 1920, height: 1080}, user_agentMozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36, localezh-CN, timezone_idAsia/Shanghai, # 注入额外的HTTP头模拟更真实的流量 extra_http_headers{ Accept-Language: zh-CN,zh;q0.9, Sec-Ch-Ua: Not_A Brand;v8, Chromium;v120, } ) page await context.new_page() # ... 后续操作注意headlessFalse模式在开发和调试阶段非常有用你可以亲眼看到浏览器的操作过程。但在生产环境部署时建议切换为headlessTrue或headlessnew以提升性能并减少资源占用。new模式是Chromium较新版本的无头模式兼容性更好。2.2 交互模拟赋予操作“灵魂”简单的page.click()和page.type()是机械的。验证码系统会分析鼠标移动的速度曲线、点击的精确位置、按键的时间间隔等。Playwright提供了底层API让我们能精细控制这些行为。核心在于使用page.mouse和page.keyboard对象并引入随机性和人类化的延迟。例如模拟人类输入验证码时不会是瞬间完成的每个字符之间应有微小的时间差并且可能伴随退格修改。import random import asyncio async def human_type(page, selector, text): 模拟人类输入带有随机延迟和可能的错误修正 await page.click(selector) # 先点击输入框聚焦 for char in text: await page.keyboard.type(char, delayrandom.uniform(50, 150)) # 每个字符输入延迟50-150毫秒 # 小概率模拟输错后删除重输 if random.random() 0.05: # 5%的概率“打错字” await page.keyboard.press(Backspace, delayrandom.uniform(30, 80)) await page.keyboard.type(char, delayrandom.uniform(80, 180))对于滑块验证码模拟人类拖拽轨迹是关键。直接page.drag_and_drop()是直线运动极易被识别。我们需要生成一条包含加速、减速、甚至微小抖动的贝塞尔曲线或多项式轨迹。async def human_slide(page, slider_selector, track_width): 模拟人类滑动滑块 slider await page.wait_for_selector(slider_selector) box await slider.bounding_box() start_x box[x] box[width] / 2 start_y box[y] box[height] / 2 end_x start_x track_width await page.mouse.move(start_x, start_y) await page.mouse.down() # 生成一条带随机抖动的移动轨迹点 points generate_human_trajectory(start_x, start_y, end_x, start_y, num_points30) for point in points: await page.mouse.move(point[x], point[y]) await asyncio.sleep(random.uniform(0.02, 0.1)) # 移动间随机延迟 await page.mouse.up() def generate_human_trajectory(start_x, start_y, end_x, end_y, num_points): 生成模拟人类鼠标移动的轨迹点简化版 points [] for i in range(num_points): t i / (num_points - 1) # 使用三次贝塞尔曲线模拟先加速后减速 # 控制点引入一些随机偏移模拟手抖 cp1_x start_x (end_x - start_x) * 0.3 random.uniform(-5, 5) cp2_x start_x (end_x - start_x) * 0.7 random.uniform(-5, 5) # 计算贝塞尔曲线上的点 (简化计算实际应使用公式) x (1-t)**3 * start_x 3*(1-t)**2*t*cp1_x 3*(1-t)*t**2*cp2_x t**3*end_x y start_y random.uniform(-2, 2) # Y轴轻微抖动 points.append({x: x, y: y}) return points2.3 策略选择识别、绕过还是混合面对具体验证码第一步永远是分析。打开浏览器开发者工具Playwright可以录制或手动操作一次观察网络请求和页面元素。直接绕过有些网站在测试环境、携带特定Cookie或请求头、或访问频率不高时可能不会弹出验证码。或者验证码图片的URL是固定的可以提前获取并手动识别一次将结果缓存起来反复使用。检查是否有“免验证码”的API接口通常用于移动端或内部调用但这属于漏洞范畴需谨慎合规使用。模拟解决对于滑块、点选等交互式验证码采用上述的真人交互模拟方案。集成识别对于复杂的图文验证码模拟无效时需要集成第三方OCR。流程是用Playwright截图或获取验证码图片元素 - 调用OCR API如云打码平台、自建Tesseract模型- 获取文本结果 - 用模拟输入的方式填入。这里的关键是即使使用了OCR输入环节也应使用human_type来模拟而不是直接fill()。3. 实战演练攻克典型验证码场景让我们通过几个最常见的案例将上述思路具体化。假设我们使用Python版本的Playwright进行演示。3.1 场景一传统图文验证码数字字母混合这种验证码通常是一个图片里面包含扭曲的数字和字母。我们的策略是截图 - OCR识别 - 模拟输入。步骤拆解定位并获取验证码图片元素通常是一个img标签其src可能是Base64数据data:image...或一个动态URL。截图与预处理Playwright可以直接对元素截图。OCR前对图片进行灰度化、二值化、降噪等预处理能大幅提升识别率可使用PIL或opencv-python。调用OCR服务这里以免费的ddddocr库针对中文验证码优化为例也可以使用pytesseractTesseract的Python封装或付费API。模拟人类输入识别结果。import asyncio from playwright.async_api import async_playwright import ddddocr async def handle_image_captcha(page): 处理传统图像验证码 假设验证码图片的selector是 img.captcha-img输入框是 input#captcha # 1. 等待验证码图片加载 captcha_element await page.wait_for_selector(img.captcha-img) # 2. 截图并保存到内存 captcha_bytes await captcha_element.screenshot() # 3. 使用OCR识别 ocr ddddocr.DdddOcr(show_adFalse) # 禁用广告日志 captcha_text ocr.classification(captcha_bytes) print(f识别出的验证码为: {captcha_text}) # 4. 模拟人类输入到输入框 input_selector input#captcha await page.click(input_selector) # 清空输入框有些网站会自动填充旧值 await page.fill(input_selector, ) # 使用自定义的人类输入函数 await human_type(page, input_selector, captcha_text) # 5. 可选如果识别错误尝试刷新验证码重试 # 例如点击验证码图片本身或旁边的“刷新”按钮 # await captcha_element.click() # await asyncio.sleep(1) # 等待新验证码加载 # ... 重新执行识别流程 # 在主函数中调用 async def main(): async with async_playwright() as p: browser await p.chromium.launch(headlessFalse) page await browser.new_page() await page.goto(https://example.com/login) # ... 填写用户名密码 await handle_image_captcha(page) # ... 点击登录按钮 await page.click(button[typesubmit])实操心得ddddocr对于常见的扭曲数字字母验证码识别率不错且免费。但如果验证码背景干扰严重或字体特殊识别率会下降。此时可以考虑增加预处理用OpenCV进行阈值分割、形态学操作去除噪点。尝试多种OCR引擎组合使用ddddocr、pytesseract需单独安装Tesseract-OCR并配置语言包或商用API进行投票决策。加入人工回退当置信度低于某个阈值时将图片保存下来通过声音、弹窗或发送到手机等方式提醒人工识别并将正确结果反馈回系统用于后续模型训练如果自建OCR。3.2 场景二滑块验证码滑块验证码的核心是计算滑块需要拖动的距离。这个距离有时直接隐藏在页面元素属性中如背景图宽度与缺口位置的差值有时需要图像识别来计算缺口位置。步骤拆解获取滑块和背景图滑块是可拖动的元素背景图是包含缺口的图片。背景图可能以CSS背景、img标签或Canvas绘制的方式存在。计算滑动距离方案A元素属性检查网络请求或DOM属性看是否有包含距离信息的>import cv2 import numpy as np from io import BytesIO from PIL import Image import asyncio async def handle_slide_captcha(page): 处理滑块验证码 假设滑块按钮selector是 .slider-button背景图是 .captcha-bg 元素的CSS背景 # 1. 获取背景图片示例从CSS背景中提取 bg_element await page.wait_for_selector(.captcha-bg) # 获取元素的CSS background-image属性可能是url(...) bg_style await bg_element.get_attribute(style) # 这里简化处理实际中可能需要正则提取URL并用page.evaluate获取完整的base64或blob # 假设我们通过其他方式已经获得了背景图的字节数据 bg_bytes # 2. 获取滑块图片缺口图 slide_element await page.wait_for_selector(.slider-mask) # 假设缺口图是这个元素 slide_bytes await slide_element.screenshot() # 3. 使用OpenCV计算滑动距离 (需要预先有完整的背景模板 bg_template_bytes) # 这里演示一个简化的流程 distance calculate_slide_distance(bg_bytes, slide_bytes, bg_template_bytes) print(f计算出的滑动距离为: {distance} 像素) # 4. 模拟滑动 await human_slide(page, .slider-button, distance) # 5. 等待验证结果可能有一个成功的提示元素出现 try: await page.wait_for_selector(.verify-success, timeout5000) print(滑块验证成功) except: print(滑块验证可能失败需要重试或处理) # 可以在这里触发刷新验证码等操作 def calculate_slide_distance(bg_bytes, slide_bytes, template_bytes): 使用OpenCV模板匹配计算滑动距离简化示例 # 将字节数据转换为numpy数组 bg_img np.array(Image.open(BytesIO(bg_bytes)).convert(RGB)) slide_img np.array(Image.open(BytesIO(slide_bytes)).convert(RGB)) template_img np.array(Image.open(BytesIO(template_bytes)).convert(RGB)) # 灰度化 bg_gray cv2.cvtColor(bg_img, cv2.COLOR_RGB2GRAY) slide_gray cv2.cvtColor(slide_img, cv2.COLOR_RGB2GRAY) template_gray cv2.cvtColor(template_img, cv2.COLOR_RGB2GRAY) # 使用模板匹配寻找slide在bg中的位置 result cv2.matchTemplate(bg_gray, slide_gray, cv2.TM_CCOEFF_NORMED) _, _, _, max_loc cv2.minMaxLoc(result) # 假设缺口在模板中的位置是固定的计算偏移量 # 这里需要根据实际情况调整例如缺口在模板的x坐标是 known_gap_x known_gap_x 50 # 假设模板图中缺口的x坐标是50 slide_distance max_loc[0] - known_gap_x return slide_distance注意事项滑块验证码的对抗升级很快。高级的滑块会轨迹验证这正是我们花大力气模拟人类轨迹的原因。动态缺口缺口位置每次都在变甚至背景图是动态生成的。我们的图像识别方案需要能应对这种变化。背景干扰背景图有大量相似边缘干扰模板匹配。可能需要更复杂的算法如基于深度学习的缺口检测但成本高。无缺口滑块需要将滑块拖到指定区域而不是匹配缺口。这时距离可能是固定的或者需要通过其他方式如颜色、文字提示判断。一个更实用的技巧是先尝试用Playwright录制一次正确的手动滑动操作分析其网络请求。有时滑动距离或一个代表成功的token会直接通过API返回根本不需要计算像素距离。通过page.on(request)监听请求找到验证通过的接口然后尝试用Playwright直接模拟那个请求这可能是一种更稳定的“绕过”方式。3.3 场景三点选验证码如“点击图中所有的xxx”这类验证码需要识别图片中的特定物体如文字、交通标志、动物并点击。我们的策略是获取大图 - 获取需要点击的词语或提示 - 使用目标检测或图像分类模型找出所有目标位置 - 模拟点击。步骤拆解获取验证码大图和提示文字。目标检测这是难点。对于通用物体猫狗、汽车可以使用预训练的深度学习模型如YOLO但需要集成到Python环境中较重。对于文字点选可以尝试OCR识别出图中所有文字块然后筛选出与提示词匹配的。计算点击坐标将检测到的目标框的中心点坐标转换为相对于浏览器视口的坐标。模拟无序点击人类不会按顺序点击。需要将目标点列表打乱然后逐个用human_click包含移动和随机延迟的点击函数点击。import random async def handle_click_captcha(page): 处理点选验证码示例点击图中所有的‘自行车’ 假设大图selector是 .click-captcha-img提示文字在 .prompt 元素里 # 1. 获取提示文字 prompt_element await page.wait_for_selector(.prompt) prompt_text await prompt_element.text_content() # 例如: 请依次点击图中的 自行车 target_word prompt_text.split( )[-1] # 简单提取目标词实际需更健壮的解析 print(f需要点击的目标是: {target_word}) # 2. 获取大图 captcha_element await page.wait_for_selector(.click-captcha-img) captcha_bytes await captcha_element.screenshot() # 3. 目标检测这里是一个伪代码示例实际需要集成检测模型 # 假设 detect_objects 函数返回一个列表每个元素是 {label: 自行车, x: 100, y: 150, width: 50, height: 50} detected_objects await detect_objects_in_image(captcha_bytes, target_word) # 4. 获取图片元素在页面中的位置 box await captcha_element.bounding_box() img_x, img_y box[x], box[y] # 5. 将检测到的图片内坐标转换为页面坐标并打乱顺序 click_points [] for obj in detected_objects: # 计算目标中心点在页面中的坐标 center_x img_x obj[x] obj[width] / 2 center_y img_y obj[y] obj[height] / 2 click_points.append((center_x, center_y)) random.shuffle(click_points) # 打乱点击顺序 # 6. 模拟人类点击 for i, (point_x, point_y) in enumerate(click_points): # 先移动鼠标到目标附近的一个随机起始点 start_x point_x random.uniform(-30, 30) start_y point_y random.uniform(-30, 30) await page.mouse.move(start_x, start_y) await asyncio.sleep(random.uniform(0.1, 0.3)) # 再微调到精确位置并点击 await page.mouse.move(point_x, point_y, stepsrandom.randint(5, 15)) # steps参数让移动有轨迹 await asyncio.sleep(random.uniform(0.05, 0.15)) await page.mouse.down() await asyncio.sleep(random.uniform(0.02, 0.08)) # 按下后短暂停顿 await page.mouse.up() await asyncio.sleep(random.uniform(0.2, 0.5)) # 点击间隔 print(点选完成) async def detect_objects_in_image(image_bytes, target_label): 伪代码在图片中检测特定标签的物体。 实际实现可能需要 1. 使用云端视觉API如百度AI、阿里云视觉。 2. 本地运行轻量级模型如MobileNet SSD。 3. 对于文字点选使用OCR获取所有文字块及其位置然后匹配目标词。 # 此处为示例返回模拟数据 return [ {label: 自行车, x: 100, y: 150, width: 50, height: 50}, {label: 自行车, x: 300, y: 200, width: 55, height: 55}, ]实操心得点选验证码是当前对抗强度较高的一类。自建目标检测模型成本高。一个折中方案是使用付费视觉API准确率高但有调用成本和次数限制。适合商业项目。特征匹配如果验证码的图片库有限比如就几十张图可以提前收集所有图片和对应的点击坐标然后通过图像哈希如pHash或SIFT特征匹配来找到当前是哪张图直接使用预存的坐标。这属于“图库攻击”对于小型网站可能有效。半自动化当检测置信度不高时将图片和可能的点击区域标出显示给人工进行最终确认。这平衡了自动化效率和成功率。4. 工程化与稳定性提升单个验证码处理脚本写出来不难难的是将其集成到自动化流程中并保证长期稳定运行。这就需要工程化的思维。4.1 验证码处理器的抽象与封装我们应该将验证码处理逻辑抽象成一个独立的模块或类对外提供统一的接口例如CaptchaSolver.solve(page, captcha_type)。这样主流程代码会更清晰也便于更换不同的处理策略。# captcha_solver.py class CaptchaSolver: def __init__(self, ocr_enabledTrue, ocr_providerddddocr): self.ocr_enabled ocr_enabled if ocr_enabled and ocr_provider ddddocr: import ddddocr self.ocr_engine ddddocr.DdddOcr(show_adFalse) # 可以初始化其他引擎如Tesseract、付费API客户端等 async def solve(self, page, captcha_info): 统一解决入口 captcha_info: 字典包含验证码类型、元素选择器等信息 例如: {type: image, image_selector: img.captcha, input_selector: #captchaInput} captcha_type captcha_info.get(type) if captcha_type image: return await self._solve_image_captcha(page, captcha_info) elif captcha_type slide: return await self._solve_slide_captcha(page, captcha_info) elif captcha_type click: return await self._solve_click_captcha(page, captcha_info) else: raise ValueError(f不支持的验证码类型: {captcha_type}) async def _solve_image_captcha(self, page, info): # ... 实现图文验证码处理逻辑 pass async def _solve_slide_captcha(self, page, info): # ... 实现滑块验证码处理逻辑 pass # ... 其他私有方法 # 在主流程中使用 async def login_flow(page, solver): await page.goto(login_url) await page.fill(#username, myuser) await page.fill(#password, mypass) # 判断页面是否出现了验证码可以通过元素存在性判断 if await page.is_visible(img.captcha): captcha_info {type: image, image_selector: img.captcha, input_selector: #captchaCode} success await solver.solve(page, captcha_info) if not success: print(验证码处理失败可能需要重试或人工干预) return False await page.click(#submitBtn) return True4.2 重试机制与降级策略验证码处理不可能100%成功。必须有健全的重试和降级机制。重试当识别或验证失败时如登录后提示验证码错误自动刷新验证码并重试。设置最大重试次数如3次避免无限循环。降级多OCR引擎备选主引擎识别失败后自动切换备用引擎。人工介入当连续失败N次后将任务挂起通过通知机制如邮件、钉钉机器人提醒人工处理并将人工处理结果回填。甚至可以记录下失败的验证码图片用于后续标注和模型优化。切换方案例如点选验证码的AI识别失败后可以尝试寻找是否有“语音验证”的切换按钮转为处理语音验证码语音转文本相对简单。async def solve_with_retry(solver, page, captcha_info, max_retries3): for attempt in range(max_retries): try: print(f第 {attempt 1} 次尝试解决验证码...) success await solver.solve(page, captcha_info) if success: # 这里可以增加一个验证步骤比如检查是否跳转到了登录后的页面 await page.wait_for_load_state(networkidle) if await page.is_visible(#welcome): # 假设登录成功后的元素 print(验证码处理成功且登录验证通过) return True else: print(验证码处理可能成功但登录未成功触发重试) raise ValueError(Login failed after captcha) else: raise ValueError(Solver reported failure) except Exception as e: print(f尝试 {attempt 1} 失败: {e}) if attempt max_retries - 1: # 触发刷新验证码 if await page.is_visible(.refresh-captcha): await page.click(.refresh-captcha) await asyncio.sleep(1) # 等待新验证码加载 elif await page.is_visible(captcha_info.get(image_selector, )): # 有些网站点击验证码图片本身可以刷新 await page.click(captcha_info.get(image_selector)) await asyncio.sleep(1) else: # 刷新页面作为最后手段 await page.reload() await asyncio.sleep(2) else: print(已达到最大重试次数验证码处理失败) # 触发降级策略通知人工 await notify_human_for_help(page, captcha_info) return False return False4.3 性能优化与资源管理浏览器上下文复用不要为每个任务都启动和关闭一个浏览器。使用browser.new_context()创建独立的上下文会话它们共享浏览器进程但隔离Cookie、缓存等创建和销毁成本远低于启动新浏览器。并行处理如果有多账号或批量任务可以使用asyncio.gather控制多个页面Page或上下文Context并行操作但要注意目标网站的反爬策略如IP限制、频率限制。资源清理及时关闭不用的页面 (page.close()) 和上下文 (context.close())防止内存泄漏。使用async with语句块管理资源生命周期是最佳实践。4.4 反反爬策略集成Playwright本身可以模拟真人环境但还需要结合其他反反爬策略代理IP池通过browser await p.chromium.launch(proxy{server: http://proxy:port})设置代理并定期轮换IP避免因高频访问被封。请求频率控制在操作间增加随机等待时间await asyncio.sleep(random.uniform(1, 5))模拟人类浏览的随机间隔。Cookie管理使用context.storage_state(pathstate.json)保存登录状态下次启动时通过browser.new_context(storage_statestate.json)恢复避免重复登录和触发验证码。5. 常见问题排查与调试技巧在实际操作中你会遇到各种各样的问题。下面是一些典型问题的排查思路。5.1 元素定位失败或操作超时这是最常见的问题通常是因为页面加载速度、动态渲染或元素选择器不稳定。症状TimeoutError: Timeout 30000ms exceeded.排查增加超时时间await page.wait_for_selector(selector, timeout60000)。检查选择器使用浏览器的开发者工具F12检查元素是否唯一优先使用id、name或稳定的>browser await p.chromium.launch(headlessFalse, slow_mo1000)录制操作Playwright Codegen 是一个神器。通过命令playwright codegen https://example.com启动一个可交互的浏览器和代码录制器。你手动操作一遍登录和验证码过程它会自动生成对应的Playwright脚本。这是理解页面交互逻辑和获取稳定选择器的最快方式。Console Logging在脚本中监听Console日志和网络请求有助于理解页面JS逻辑。# 打印所有console日志 page.on(console, lambda msg: print(fconsole: {msg.text})) # 打印所有网络请求 # page.on(request, lambda req: print(f {req.method} {req.url})) # 仅打印包含特定关键词的请求 page.on(request, lambda req: print(f {req.method} {req.url}) if verify in req.url else None)保留Trace当脚本出错时可以通过Trace来复盘。await context.tracing.start(screenshotsTrue, snapshotsTrue, sourcesTrue) # ... 执行你的操作 ... await context.tracing.stop(pathtrace.zip)然后用Playwright的命令行工具playwright show-trace trace.zip打开这个zip文件你可以像看视频一样回放所有操作查看每个时间点的DOM快照、网络请求和Console日志是排查复杂问题的终极武器。处理自动化验证码是一场持久的“攻防战”。Playwright提供了强大的模拟真人交互的能力让我们在“绕过”和“模拟解决”两条路上都有了更先进的武器。但没有任何一种方法是万能的核心还是在于具体问题具体分析仔细研究目标验证码的实现选择最经济有效的策略并准备好完善的重试和降级机制。将验证码处理模块化、配置化你的自动化流程才能足够健壮应对真实世界中复杂多变的挑战。