Playwright错误处理与重试机制实现

举报
霍格沃兹测试开发学社 发表于 2026/01/20 16:39:00 2026/01/20
【摘要】 在实际的自动化测试和网络爬虫开发中,稳定性是衡量脚本质量的重要指标。即使编写了最完善的Playwright脚本,也不可避免地会遇到各种运行时错误:元素加载延迟、网络波动、页面响应超时等。本文将分享如何构建健壮的Playwright错误处理与重试机制。为什么需要错误处理机制?我曾遇到过这样的场景:一个精心编写的爬虫脚本在本地运行完美,但放到服务器上却频繁失败。调查后发现,服务器与目标网站之间的...
在实际的自动化测试和网络爬虫开发中,稳定性是衡量脚本质量的重要指标。即使编写了最完善的Playwright脚本,也不可避免地会遇到各种运行时错误:元素加载延迟、网络波动、页面响应超时等。本文将分享如何构建健壮的Playwright错误处理与重试机制。

为什么需要错误处理机制?

我曾遇到过这样的场景:一个精心编写的爬虫脚本在本地运行完美,但放到服务器上却频繁失败。调查后发现,服务器与目标网站之间的网络延迟较高,导致元素加载时间超出预期。这就是缺乏错误处理机制的典型表现。

基础错误处理策略

1. 智能等待替代硬性等待

新手常犯的错误是使用page.waitForTimeout(5000)这样的固定等待。更好的做法是使用Playwright内置的智能等待方法:

// 不推荐 - 硬性等待
await page.waitForTimeout(5000);
await page.click('#submit');

// 推荐 - 智能等待
await page.waitForSelector('#submit', { state'visible' });
await page.click('#submit');

2. 异常捕获基础

最简单的错误处理是try-catch块:

async function safeClick(page, selector) {
  try {
    await page.click(selector);
    return true;
  } catch (error) {
    console.warn(`点击元素 ${selector} 失败:`, error.message);
    return false;
  }
}

实现重试机制

基础重试函数

下面是一个通用的重试函数,可以包装任何可能失败的操作:

async function withRetry(
  operation,
  maxAttempts = 3,
  delay = 1000,
  backoffFactor = 2
{
let lastError;

for (let attempt = 1; attempt <= maxAttempts; attempt++) {
    try {
      returnawait operation();
    } catch (error) {
      lastError = error;
      console.log(`尝试 ${attempt}/${maxAttempts} 失败:`, error.message);
      
      if (attempt === maxAttempts) break;
      
      const waitTime = delay * Math.pow(backoffFactor, attempt - 1);
      console.log(`等待 ${waitTime}ms 后重试...`);
      awaitnewPromise(resolve => setTimeout(resolve, waitTime));
    }
  }

throw lastError;
}

页面操作重试包装器

针对常见的页面操作,我们可以创建专门的重试包装器:

class RetryablePage {
constructor(page) {
    this.page = page;
  }

async clickWithRetry(selector, options = {}) {
    const maxAttempts = options.maxAttempts || 3;
    
    for (let attempt = 1; attempt <= maxAttempts; attempt++) {
      try {
        // 确保元素可见且可点击
        awaitthis.page.waitForSelector(selector, {
          state'visible',
          timeout10000
        });
        
        awaitthis.page.click(selector);
        return;
      } catch (error) {
        console.log(`点击 ${selector} 失败 (尝试 ${attempt}/${maxAttempts}):`, error.message);
        
        if (attempt === maxAttempts) {
          thrownewError(`多次尝试点击 ${selector} 均失败: ${error.message}`);
        }
        
        // 等待时间递增
        awaitthis.page.waitForTimeout(1000 * attempt);
      }
    }
  }

async navigateWithRetry(url, options = {}) {
    const maxAttempts = options.maxAttempts || 3;
    
    for (let attempt = 1; attempt <= maxAttempts; attempt++) {
      try {
        const response = awaitthis.page.goto(url, {
          waitUntil'networkidle',
          timeout30000
        });
        
        if (response && !response.ok()) {
          thrownewError(`HTTP ${response.status()}${response.statusText()}`);
        }
        
        return response;
      } catch (error) {
        console.log(`访问 ${url} 失败 (尝试 ${attempt}/${maxAttempts}):`, error.message);
        
        if (attempt === maxAttempts) {
          throw error;
        }
        
        // 如果是网络错误,尝试刷新页面
        if (error.message.includes('net::') || error.message.includes('Navigation')) {
          awaitthis.page.reload();
        }
        
        awaitthis.page.waitForTimeout(2000 * attempt);
      }
    }
  }
}

Playwright Test中的重试机制

如果你使用Playwright Test框架,它内置了重试功能:

// playwright.config.js
module.exports = {
// 全局重试配置
retries: process.env.CI ? 2 : 1,

use: {
    // 操作失败时的截图配置
    screenshot'only-on-failure',
    
    // 视频录制配置
    video'retain-on-failure',
  },

// 项目级别的重试配置
projects: [
    {
      name'chromium',
      retries2,
      use: { browserName'chromium' },
    },
  ],
};

自定义测试重试逻辑

对于需要特殊处理的重试场景,可以在测试内部实现:

import { test, expect } from'@playwright/test';

test('重要的支付流程测试'async ({ page }) => {
let paymentSuccessful = false;

for (let attempt = 1; attempt <= 3; attempt++) {
    try {
      // 执行支付流程
      await page.goto('/payment');
      await page.fill('#card-number''4111111111111111');
      await page.click('#pay-now');
      
      // 验证支付成功
      await expect(page.locator('.success-message')).toBeVisible({
        timeout10000
      });
      
      paymentSuccessful = true;
      break;
    } catch (error) {
      console.log(`支付测试尝试 ${attempt} 失败:`, error.message);
      
      if (attempt === 3) {
        throw error;
      }
      
      // 清理状态,准备重试
      await page.goto('/cart');
      await page.waitForTimeout(2000 * attempt);
    }
  }

  expect(paymentSuccessful).toBeTruthy();
});

高级错误处理策略

1. 错误分类与不同处理策略

class ErrorHandler {
static shouldRetry(error) {
    const retryableErrors = [
      'TimeoutError',
      'NetworkError',
      'net::',
      'Target closed',
      'Element not found'
    ];
    
    const errorMessage = error.toString();
    
    return retryableErrors.some(retryableError =>
      errorMessage.includes(retryableError)
    );
  }

static classifyError(error) {
    const message = error.toString();
    
    if (message.includes('Timeout')) {
      return'TIMEOUT';
    } elseif (message.includes('net::')) {
      return'NETWORK';
    } elseif (message.includes('not found') || message.includes('not visible')) {
      return'ELEMENT_NOT_FOUND';
    } else {
      return'UNKNOWN';
    }
  }
}

asyncfunction resilientOperation(operation) {
const maxAttempts = 3;
let lastError;

for (let attempt = 1; attempt <= maxAttempts; attempt++) {
    try {
      returnawait operation();
    } catch (error) {
      lastError = error;
      
      if (!ErrorHandler.shouldRetry(error) || attempt === maxAttempts) {
        break;
      }
      
      const errorType = ErrorHandler.classifyError(error);
      const delay = this.calculateDelay(attempt, errorType);
      
      console.log(`[${errorType}] 尝试 ${attempt} 失败,${delay}ms 后重试`);
      awaitnewPromise(resolve => setTimeout(resolve, delay));
    }
  }

throw lastError;
}

2. 上下文恢复与状态重置

某些错误需要重置浏览器上下文:

async function withContextRecovery(browser, operation) {
let context;
let page;

for (let attempt = 1; attempt <= 3; attempt++) {
    try {
      if (!context || context._closed) {
        context = await browser.newContext();
        page = await context.newPage();
      }
      
      returnawait operation(page);
    } catch (error) {
      console.log(`操作失败,尝试 ${attempt}/3:`, error.message);
      
      if (attempt === 3) {
        throw error;
      }
      
      // 清理旧上下文
      if (context && !context._closed) {
        await context.close();
      }
      
      // 短暂等待后继续
      awaitnewPromise(resolve => setTimeout(resolve, 1000 * attempt));
    }
  }
}

实战:完整的爬虫错误处理示例

const { chromium } = require('playwright');

class RobustCrawler {
constructor() {
    this.maxRetries = 3;
    this.requestTimeout = 30000;
  }

async crawl(url) {
    const browser = await chromium.launch();
    const results = [];
    
    try {
      for (let retry = 1; retry <= this.maxRetries; retry++) {
        try {
          const page = await browser.newPage();
          
          // 设置超时
          page.setDefaultTimeout(this.requestTimeout);
          
          // 监听请求失败
          page.on('requestfailed', request => {
            console.warn(`请求失败: ${request.url()} - ${request.failure().errorText}`);
          });
          
          // 访问页面
          console.log(`尝试 ${retry}/${this.maxRetries}: 访问 ${url}`);
          const response = await page.goto(url, {
            waitUntil'domcontentloaded',
            timeoutthis.requestTimeout
          });
          
          if (!response.ok()) {
            thrownewError(`HTTP ${response.status()}${response.statusText()}`);
          }
          
          // 提取数据
          const data = awaitthis.extractData(page);
          results.push(data);
          
          await page.close();
          break// 成功则退出重试循环
          
        } catch (error) {
          console.log(`尝试 ${retry} 失败:`, error.message);
          
          if (retry === this.maxRetries) {
            thrownewError(`爬取 ${url} 失败: ${error.message}`);
          }
          
          // 指数退避
          awaitnewPromise(resolve =>
            setTimeout(resolve, 1000 * Math.pow(2, retry - 1))
          );
        }
      }
    } finally {
      await browser.close();
    }
    
    return results;
  }

async extractData(page) {
    // 使用选择器重试提取数据
    const extractWithRetry = async (selector, extractor) => {
      for (let i = 0; i < 3; i++) {
        try {
          await page.waitForSelector(selector, { timeout5000 });
          returnawait extractor();
        } catch (error) {
          if (i === 2throw error;
          await page.waitForTimeout(1000);
        }
      }
    };
    
    const title = await extractWithRetry('h1'async () => {
      returnawait page.textContent('h1');
    });
    
    return { title };
  }
}

监控与日志记录

完善的错误处理还需要良好的监控:

class MonitoringErrorHandler {
constructor() {
    this.errors = [];
    this.stats = {
      totalOperations0,
      failedOperations0,
      retriedOperations0,
      recoveredOperations0
    };
  }

async trackOperation(operationName, operation) {
    this.stats.totalOperations++;
    const startTime = Date.now();
    
    try {
      const result = await operation();
      return result;
    } catch (error) {
      this.stats.failedOperations++;
      this.errors.push({
        operation: operationName,
        error: error.message,
        timestampnewDate().toISOString(),
        durationDate.now() - startTime
      });
      
      // 可以发送到监控系统
      this.reportToMonitoringSystem(error, operationName);
      
      throw error;
    }
  }

  reportToMonitoringSystem(error, operationName) {
    // 发送到Sentry, Datadog等
    console.error(`[MONITORING] ${operationName} failed:`, error.message);
  }

  getStats() {
    return {
      ...this.stats,
      successRate: ((this.stats.totalOperations - this.stats.failedOperations) / 
                   this.stats.totalOperations * 100).toFixed(2) + '%'
    };
  }
}

最佳实践总结

  1. 分级处理策略:根据错误类型采取不同的重试策略,网络错误可以立即重试,业务错误可能需要延迟重试。

  2. 避免无限重试:始终设置最大重试次数,避免陷入死循环。

  3. 指数退避算法:重试间隔应逐渐增加,避免对目标服务器造成压力。

  4. 上下文隔离:每次重试前清理状态,确保测试的独立性。

  5. 详细日志记录:记录每次重试的上下文信息,便于问题排查。

  6. 监控集成:将错误信息集成到现有监控系统,实现主动告警。

  7. 用户可配置:将重试参数(次数、延迟等)设计为可配置项,适应不同场景需求。

结语

错误处理不是Playwright脚本的事后考虑,而是应该在设计初期就纳入架构的重要部分。一个健壮的脚本不仅要能完成任务,更要能优雅地处理失败。通过实现智能的重试机制,你的自动化脚本将能够在生产环境中稳定运行,显著减少人工干预的需要。

记住,好的错误处理机制是透明的一一当它正常工作时,用户几乎感觉不到它的存在;当问题出现时,它又能提供足够的信息帮助快速定位问题。这才是真正有价值的自动化解决方案。

【声明】本内容来自华为云开发者社区博主,不代表华为云及华为云开发者社区的观点和立场。转载时必须标注文章的来源(华为云社区)、文章链接、文章作者等基本信息,否则作者和本社区有权追究责任。如果您发现本社区中有涉嫌抄袭的内容,欢迎发送邮件进行举报,并提供相关证据,一经查实,本社区将立刻删除涉嫌侵权内容,举报邮箱: cloudbbs@huaweicloud.com
  • 点赞
  • 收藏
  • 关注作者

评论(0

0/1000
抱歉,系统识别当前为高风险访问,暂不支持该操作

全部回复

上滑加载中

设置昵称

在此一键设置昵称,即可参与社区互动!

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。