Playwright 第8章:数据提取与验证

数据提取是 Web 自动化的核心需求之一。本章介绍 Playwright 获取元素文本、属性和 HTML 内容的方法,以及如何使用 page.evaluate() 执行 JavaScript 进行批量数据提取。

文本内容提取

获取单个元素文本

import { chromium } from 'playwright';

const browser = await chromium.launch();
const page = await browser.newPage();
await page.goto('https://example.com/products');

// textContent() - 获取文本内容(包含隐藏文本)
const title = await page.locator('h1').textContent();
console.log('标题:', title?.trim());

// innerText() - 获取渲染后的可见文本
const description = await page.locator('.description').innerText();
console.log('描述:', description);

// 区别:textContent() 返回所有文本(含 display:none)
//      innerText() 只返回可见文本(考虑 CSS 样式)

// 获取输入框的值
const searchValue = await page.locator('#search').inputValue();

// 获取单个元素的所有文本
const allText = await page.locator('body').innerText();

批量获取文本

// allTextContents() - 获取所有匹配元素的 textContent
const allItems = await page.locator('.product-item').allTextContents();
console.log('所有产品:', allItems);

// allInnerTexts() - 获取所有匹配元素的 innerText
const visibleItems = await page.locator('.product-item').allInnerTexts();
console.log('可见产品:', visibleItems);

// 获取特定数据列表
const productNames = await page.locator('.product-name').allTextContents();
const productPrices = await page.locator('.product-price').allTextContents();
const productRatings = await page.locator('.product-rating').allTextContents();

console.log('产品名称:', productNames);
console.log('产品价格:', productPrices);
console.log('产品评分:', productRatings);

属性与 HTML 提取

获取元素属性

const page = await browser.newPage();
await page.goto('https://example.com');

// 获取单个属性
const href = await page.locator('a').getAttribute('href');
const src = await page.locator('img').getAttribute('src');
const alt = await page.locator('img').getAttribute('alt');
const dataId = await page.locator('.item').getAttribute('data-id');

// 批量获取属性
const allLinks = await page.locator('a').all();
for (const link of allLinks) {
  const href = await link.getAttribute('href');
  const text = await link.textContent();
  console.log(`链接: ${text} -> ${href}`);
}

// 使用 evaluateAll 批量获取
const linksData = await page.locator('a').evaluateAll(
  (elements) => elements.map(el => ({
    href: el.getAttribute('href'),
    text: el.textContent?.trim(),
  }))
);
console.log('链接数据:', linksData);

获取 HTML 内容

// innerHTML() - 获取元素内部 HTML
const innerHtml = await page.locator('.content').innerHTML();
console.log('内部 HTML:', innerHtml);

// 整页 HTML
const fullHtml = await page.content();
console.log('页面 HTML 长度:', fullHtml.length);

// 获取表单 HTML
const formHtml = await page.locator('#login-form').innerHTML();

page.evaluate() 执行 JS

page.evaluate() 可以在浏览器中执行任意 JavaScript 代码,返回序列化结果。

基础用法

const page = await browser.newPage();

// 返回简单值
const title = await page.evaluate(() => document.title);
const url = await page.evaluate(() => window.location.href);
const scrollHeight = await page.evaluate(() => document.body.scrollHeight);

// 传递参数(必须可序列化)
const text = await page.evaluate((selector) => {
  const el = document.querySelector(selector);
  return el ? el.textContent : null;
}, '.main-title');

// 传递复杂参数
const result = await page.evaluate(({ selector, attr }) => {
  const el = document.querySelector(selector);
  return el ? el.getAttribute(attr) : null;
}, { selector: 'img.main', attr: 'src' });

批量数据提取

// 提取产品列表
const products = await page.evaluate(() => {
  const items = document.querySelectorAll('.product-card');
  return Array.from(items).map(item => ({
    name: item.querySelector('.product-name')?.textContent?.trim(),
    price: item.querySelector('.product-price')?.textContent?.trim(),
    rating: item.querySelector('.product-rating')?.textContent?.trim(),
    reviewCount: parseInt(
      item.querySelector('.review-count')?.textContent?.replace(/[^0-9]/g, '') || '0'
    ),
    inStock: !item.querySelector('.out-of-stock'),
    url: item.querySelector('a')?.getAttribute('href'),
  }));
});

console.log(`提取到 ${products.length} 个产品`);
console.log(products);

高级数据提取

// 提取表格数据
const tableData = await page.evaluate(() => {
  const table = document.querySelector('table.data-table');
  if (!table) return null;

  const headers = Array.from(
    table.querySelectorAll('thead th')
  ).map(th => th.textContent?.trim());

  const rows = Array.from(
    table.querySelectorAll('tbody tr')
  ).map(row => {
    const cells = Array.from(row.querySelectorAll('td'));
    const rowData: Record<string, string> = {};
    cells.forEach((cell, index) => {
      rowData[headers[index] || `col${index}`] = cell.textContent?.trim() || '';
    });
    return rowData;
  });

  return { headers, rows };
});

console.log('表格数据:', JSON.stringify(tableData, null, 2));

// 提取页面统计信息
const pageStats = await page.evaluate(() => {
  return {
    links: document.querySelectorAll('a').length,
    images: document.querySelectorAll('img').length,
    scripts: document.querySelectorAll('script').length,
    forms: document.querySelectorAll('form').length,
    buttons: document.querySelectorAll('button').length,
    inputs: document.querySelectorAll('input').length,
    headings: {
      h1: document.querySelectorAll('h1').length,
      h2: document.querySelectorAll('h2').length,
      h3: document.querySelectorAll('h3').length,
    },
  };
});

截图与视觉验证

元素截图

// 元素截图
await page.locator('.product-card').screenshot({
  path: 'product.png',
});

// 多个元素截图
const products = page.locator('.product-card');
const count = await products.count();
for (let i = 0; i < count; i++) {
  await products.nth(i).screenshot({
    path: `product-${i}.png`,
  });
}

视觉对比

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

test('视觉回归测试', async ({ page }) => {
  await page.goto('/home');

  // 整页截图对比
  await expect(page).toHaveScreenshot('home.png', {
    fullPage: true,
    maxDiffPixels: 100,
    threshold: 0.2,
  });

  // 元素截图对比
  await expect(page.locator('.header')).toHaveScreenshot('header.png');

  // 遮罩动态内容
  await expect(page).toHaveScreenshot('home.png', {
    mask: [page.locator('.dynamic-banner')],
    maskColor: '#FF0000',
  });
});

数据验证

验证数据完整性

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

test('验证产品列表数据', async ({ page }) => {
  await page.goto('/products');

  // 验证产品数量
  const products = page.locator('.product-card');
  await expect(products).toHaveCount(24);

  // 验证每个产品都有名称和价格
  const names = await page.locator('.product-name').allTextContents();
  const prices = await page.locator('.product-price').allTextContents();

  for (let i = 0; i < names.length; i++) {
    expect(names[i]).toBeTruthy();
    expect(prices[i]).toMatch(/^¥\d+(\.\d{2})?$/);
  }

  // 验证排序
  const sortedPrices = [...prices].sort((a, b) => {
    const numA = parseFloat(a.replace(/[^0-9.]/g, ''));
    const numB = parseFloat(b.replace(/[^0-9.]/g, ''));
    return numA - numB;
  });
  expect(prices).toEqual(sortedPrices);
});

总结

Playwright 提供了灵活多样的数据提取方式。简单文本和属性提取使用定位器方法即可,复杂数据提取推荐使用 page.evaluate() 执行原生 JavaScript。批量提取时注意性能影响,建议使用 evaluateAll() 一次性提取全部数据。视觉验证为 UI 测试提供了额外的保障手段。