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 测试提供了额外的保障手段。