验证码(CAPTCHA)一直是自动化测试中最让人头疼的环节之一。每次碰到那些扭曲的文字、点选图片的挑战,自动化脚本就像撞上了一堵墙。我负责的电商项目最近就卡在了登录自动化这个环节——那个该死的滑动验证码让我们的回归测试屡屡失败。
经过几周的实战踩坑和方案对比,我总结出几种用Playwright处理验证码的可行方案。这些方案各有适用场景,没有绝对的“银弹”,但足够帮你绕过大多数验证码障碍。
方案一:最直接的方式——测试环境关闭验证码
如果你们公司有测试环境的管理权限,这是最干净利落的解决方案。
// 在测试环境中,通过修改配置或调用管理接口禁用验证码
asyncfunction disableCaptchaInTestEnv(page) {
// 方式1:如果有管理后台接口
await page.goto('http://test-admin.example.com/features');
await page.click('#toggle-captcha');
// 方式2:通过设置测试用户白名单
await page.evaluate(() => {
localStorage.setItem('bypass_captcha', 'true');
});
// 方式3:修改hosts或使用mock服务(需运维配合)
console.log('验证码已在测试环境关闭');
}
优点:零成本,100%稳定,执行速度快。
缺点:仅限测试环境,生产环境模拟不了完整流程。
方案二:半自动方案——人工介入一次,重复使用凭证
对于无法关闭验证码但又不频繁变更的场景,这个方案很实用。
const fs = require('fs');
const path = require('path');
class CaptchaHandler {
constructor() {
this.tokenFile = path.join(__dirname, '.auth_token');
}
async handleCaptcha(page) {
// 检查是否有缓存的登录凭证
if (fs.existsSync(this.tokenFile)) {
const token = fs.readFileSync(this.tokenFile, 'utf8');
awaitthis.useCachedToken(page, token);
returntrue;
}
// 首次需要人工处理
console.log('请手动完成验证码验证...');
// Playwright会暂停,等待人工操作
await page.pause(); // 这是关键!手动完成后按回车继续
// 保存获取到的凭证(如cookie、token)
const cookies = await page.context().cookies();
const authToken = cookies.find(c => c.name === 'auth_token');
if (authToken) {
fs.writeFileSync(this.tokenFile, authToken.value);
console.log('凭证已保存,后续测试将自动使用');
}
returntrue;
}
async useCachedToken(page, token) {
// 使用缓存的token设置cookie
await page.context().addCookies([{
name: 'auth_token',
value: token,
domain: 'your-domain.com',
path: '/'
}]);
// 刷新页面使cookie生效
await page.reload();
}
}
// 使用示例
const handler = new CaptchaHandler();
await handler.handleCaptcha(page);
await page.goto('https://your-app.com/dashboard');
优点:平衡了自动化与可靠性,只需人工介入一次。
缺点:凭证过期后需要重新人工处理。
方案三:全自动方案——第三方OCR服务
当需要完全自动化且验证码不算太复杂时,可以考虑OCR方案。
const axios = require('axios');
const fs = require('fs');
asyncfunction solveCaptchaWithOCR(page) {
// 1. 定位并截图验证码元素
const captchaElement = await page.$('.captcha-image');
const screenshotPath = 'captcha.png';
await captchaElement.screenshot({ path: screenshotPath });
// 2. 读取图片并编码为base64
const imageBuffer = fs.readFileSync(screenshotPath);
const base64Image = imageBuffer.toString('base64');
try {
// 3. 调用OCR API(这里以2Captcha为例,需注册获取API key)
const apiKey = process.env.CAPTCHA_API_KEY;
const response = await axios.post('https://2captcha.com/in.php', {
key: apiKey,
method: 'base64',
body: base64Image,
json: 1
});
const requestId = response.data.request;
// 4. 轮询获取结果
let result = null;
for (let i = 0; i < 30; i++) {
await page.waitForTimeout(2000);
const checkResponse = await axios.get(
`https://2captcha.com/res.php?key=${apiKey}&action=get&id=${requestId}&json=1`
);
if (checkResponse.data.status === 1) {
result = checkResponse.data.request;
break;
}
}
if (!result) thrownewError('OCR识别超时');
// 5. 输入识别结果
await page.fill('#captcha-input', result);
await page.click('#submit-btn');
returntrue;
} catch (error) {
console.error('OCR识别失败:', error.message);
// 失败时保存截图供后续分析
fs.renameSync(screenshotPath, failed_${Date.now()}.png);
returnfalse;
}
}
费用提示:2Captcha每1000次识别约$1-3,具体看复杂度。对于大型测试套件,这是笔不小的开销。
方案四:智能等待与重试机制
有时候验证码的出现是有条件的,可以通过优化测试逻辑来减少触发。
async function smartLogin(page, username, password) {
const MAX_RETRIES = 3;
for (let attempt = 1; attempt <= MAX_RETRIES; attempt++) {
try {
await page.goto('https://example.com/login');
// 填写登录表单
await page.fill('#username', username);
await page.fill('#password', password);
// 检测验证码是否出现
const captchaVisible = await page.isVisible('.captcha-container');
if (captchaVisible) {
console.log(`第${attempt}次尝试出现验证码,尝试绕过...`);
// 尝试刷新验证码(有时新的验证码更简单)
await page.click('.refresh-captcha');
await page.waitForTimeout(1000);
// 这里可以集成上述的任一解决方案
// await solveCaptchaWithOCR(page);
// 或者使用备用账号
if (attempt > 1) {
await page.fill('#username', `${username}_backup`);
}
}
// 提交登录
await page.click('#login-btn');
// 等待登录成功标志
await page.waitForSelector('.user-dashboard', { timeout: 5000 });
console.log('登录成功!');
returntrue;
} catch (error) {
console.log(`第${attempt}次登录尝试失败: ${error.message}`);
if (attempt === MAX_RETRIES) {
thrownewError(`登录失败,已重试${MAX_RETRIES}次`);
}
// 等待一段时间后重试
await page.waitForTimeout(2000);
}
}
}
方案五:针对特定类型验证码的专项处理
滑动验证码处理
async function handleSlideCaptcha(page) {
const slider = await page.$('.slider');
const sliderBox = await slider.boundingBox();
const target = await page.$('.slider-target');
const targetBox = await target.boundingBox();
// 计算需要滑动的距离
const slideDistance = targetBox.x - sliderBox.x;
// 模拟人类滑动(先快后慢)
await slider.hover();
await page.mouse.down();
// 分段滑动,模拟真实轨迹
const steps = 10;
const stepDistance = slideDistance / steps;
for (let i = 0; i < steps; i++) {
// 越靠近目标越慢
const speed = 50 + Math.random() 100 - i 10;
await page.mouse.move(
sliderBox.x + stepDistance (i + 1),
sliderBox.y + (Math.random() 10 - 5), // 加入微小垂直抖动
{ steps: 1 }
);
await page.waitForTimeout(speed);
}
await page.mouse.up();
}
点选文字验证码(简单版)
async function handleClickCaptcha(page) {
// 获取需要点击的文字
const promptText = await page.$eval('.captcha-prompt', el => el.textContent);
const wordsToClick = promptText.match(/点击【(.*?)】/)[1].split('');
// 获取所有可点击的文字元素
const charElements = await page.$$('.captcha-char');
for (const char of wordsToClick) {
for (const element of charElements) {
const text = await element.textContent();
if (text === char) {
// 随机延迟后点击,模拟人类反应时间
await page.waitForTimeout(300 + Math.random() * 500);
await element.click();
break;
}
}
}
}
最佳实践建议
根据我们项目的经验,我推荐以下策略:
分层处理策略:
// 策略优先级:禁用 > 缓存 > OCR > 重试
class CaptchaStrategy {
async solve(page) {
if (awaitthis.tryDisableCaptcha(page)) return;
if (awaitthis.tryCachedToken(page)) return;
if (awaitthis.tryOCR(page)) return;
if (awaitthis.tryAlternativeAccount(page)) return;
// 最后手段:标记测试失败并保存截图
awaitthis.saveDebugInfo(page);
thrownewError('无法处理验证码');
}
}
验证码监控:
// 记录验证码出现频率,用于优化测试策略
const captchaStats = {
totalAttempts: 0,
captchaShown: 0,
successRate: 0,
lastCaptchaTime: null
};
环境感知配置:
// 根据环境选择不同策略
const CAPTCHA_CONFIG = {
development: { strategy: 'disable' },
staging: { strategy: 'cached_token' },
production: { strategy: 'mixed', fallback: 'ocr' }
};
总结
验证码的处理没有一劳永逸的方案,但通过组合策略,我们基本能保证自动化测试的稳定性。我们团队目前的方案是:测试环境完全禁用,预发环境使用缓存令牌,只有少量的生产环境监控脚本会使用OCR服务。
最后提醒一点:尊重网站的验证码机制。这些措施旨在提升测试效率,而不是滥用或攻击服务。对于特别复杂的验证码(如行为验证),与其花费大量精力破解,不如考虑与开发团队协商,为自动化测试提供专门的测试接口或令牌。
如果你有更好的验证码处理方案,欢迎在评论区分享——毕竟,每个项目的验证码实现都可能不一样,多交流才能少踩坑。