Puppeteer和Playwright是流行的浏览器自动化和网页抓取工具。在处理大量请求或解析受保护的网站时,使用代理对于避免IP封锁至关重要。在本指南中,我们将探讨将代理集成到这两个工具中的所有方法,从基本设置到高级场景,包括轮换和错误处理。
无头浏览器中代理的基础知识
Puppeteer和Playwright通过DevTools协议控制真实的浏览器(Chromium、Firefox、WebKit)。这意味着代理在浏览器启动时进行设置,而不是在单个请求中。两个工具都支持HTTP、HTTPS和SOCKS5代理,但在设置上有不同的API。
与普通的HTTP库(如axios、fetch)相比,主要区别在于:
- 代理在浏览器启动时设置 — 不能在同一浏览器会话中动态更改代理
- 支持JavaScript和渲染 — 代理适用于页面的所有资源(图像、脚本、XHR)
- 自动处理重定向和cookies — 浏览器表现得像真实用户
- 指纹识别 — 即使使用代理,网站仍然可以通过浏览器特征识别自动化
重要: 对于需要频繁更换IP的任务(解析数千个页面、大规模注册),使用带轮换的住宅代理更有效 — 它们允许在每个请求中更改IP,而无需重启浏览器。
在Puppeteer中设置代理
Puppeteer是Google提供的用于控制Chrome/Chromium的库。代理通过浏览器启动参数进行设置,使用标志--proxy-server。
基本的HTTP/HTTPS代理设置
const puppeteer = require('puppeteer');
(async () => {
const browser = await puppeteer.launch({
headless: true,
args: [
'--proxy-server=http://proxy.example.com:8080'
]
});
const page = await browser.newPage();
// 检查IP地址
await page.goto('https://api.ipify.org?format=json');
const content = await page.content();
console.log('当前IP:', content);
await browser.close();
})();
这段代码通过代理服务器启动浏览器。所有HTTP和HTTPS请求将通过指定的代理进行。使用ipify服务检查可用性,该服务返回外部IP地址。
SOCKS5代理设置
const browser = await puppeteer.launch({
headless: true,
args: [
'--proxy-server=socks5://proxy.example.com:1080'
]
});
// 其余代码类似
SOCKS5代理在更低的层次上工作,并支持UDP流量,这对于某些Web应用程序可能有用。语法与HTTP代理相同,只是URL中的协议不同。
为特定域使用代理
const browser = await puppeteer.launch({
args: [
'--proxy-server=http://proxy1.example.com:8080',
'--proxy-bypass-list=localhost;127.0.0.1;*.internal.com'
]
});
// 本地请求和对*.internal.com的请求将直接发送
// 其他所有请求将通过代理
标志--proxy-bypass-list允许将特定域排除在代理之外。这在需要组合直接请求和代理请求时非常有用。
在Playwright中设置代理
Playwright是Microsoft提供的更现代的库,支持Chromium、Firefox和WebKit。代理通过配置对象进行设置,这使得API更加清晰和类型化。
基本的代理设置
const { chromium } = require('playwright');
(async () => {
const browser = await chromium.launch({
proxy: {
server: 'http://proxy.example.com:8080'
}
});
const context = await browser.newContext();
const page = await context.newPage();
await page.goto('https://api.ipify.org?format=json');
const ip = await page.textContent('body');
console.log('通过代理的IP:', ip);
await browser.close();
})();
Playwright使用proxy对象,而不是命令行参数。这在TypeScript中提供了更好的类型化和更干净的代码。
在上下文级别设置代理
const browser = await chromium.launch();
// 上下文1 - 使用代理
const context1 = await browser.newContext({
proxy: {
server: 'http://proxy1.example.com:8080'
}
});
// 上下文2 - 使用另一个代理
const context2 = await browser.newContext({
proxy: {
server: 'http://proxy2.example.com:8080'
}
});
const page1 = await context1.newPage();
const page2 = await context2.newPage();
// 每个页面使用自己的代理!
Playwright的一个关键优势是能够在一个进程中创建多个具有不同代理的浏览器上下文。这在处理多个IP地址时节省了资源。
排除域名不使用代理
const browser = await chromium.launch({
proxy: {
server: 'http://proxy.example.com:8080',
bypass: 'localhost,127.0.0.1,*.internal.com'
}
});
// 对localhost和*.internal.com的请求将直接发送
基于用户名和密码的身份验证
大多数商业代理都需要身份验证。两个工具都支持授权,但实现方式不同。
在Puppeteer中的身份验证
const browser = await puppeteer.launch({
args: ['--proxy-server=http://proxy.example.com:8080']
});
const page = await browser.newPage();
// 设置代理凭据
await page.authenticate({
username: 'your_username',
password: 'your_password'
});
await page.goto('https://httpbin.org/ip');
const content = await page.content();
console.log(content);
方法page.authenticate()为该页面上的所有后续请求设置凭据。重要的是在第一次page.goto()之前调用它。
在Playwright中的身份验证
const browser = await chromium.launch({
proxy: {
server: 'http://proxy.example.com:8080',
username: 'your_username',
password: 'your_password'
}
});
const page = await browser.newPage();
await page.goto('https://httpbin.org/ip');
Playwright允许直接在代理配置对象中指定凭据 — 这更方便和安全,因为不需要额外的方法调用。
替代方法:在URL中包含凭据
// 在两个框架中都有效
const proxyUrl = 'http://username:password@proxy.example.com:8080';
// Puppeteer
const browser = await puppeteer.launch({
args: [`--proxy-server=${proxyUrl}`]
});
// Playwright
const browser = await chromium.launch({
proxy: { server: proxyUrl }
});
这种方法是可行的,但不推荐用于生产环境 — 凭据可能会被记录到日志中。使用环境变量存储敏感数据。
建议: 在解析商业网站时,建议使用住宅代理 — 它们具有真实的家庭用户IP,并且更少被反机器人系统封锁。
代理轮换和IP池管理
对于大规模抓取,需要定期更换IP地址。由于代理在浏览器启动时设置,轮换需要重启浏览器会话。
使用代理数组的简单轮换(Puppeteer)
const puppeteer = require('puppeteer');
const proxyList = [
'http://user1:pass1@proxy1.example.com:8080',
'http://user2:pass2@proxy2.example.com:8080',
'http://user3:pass3@proxy3.example.com:8080'
];
async function scrapeWithRotation(urls) {
for (let i = 0; i < urls.length; i++) {
const proxyUrl = proxyList[i % proxyList.length];
const browser = await puppeteer.launch({
args: [`--proxy-server=${proxyUrl}`]
});
try {
const page = await browser.newPage();
await page.goto(urls[i], { waitUntil: 'networkidle0' });
const data = await page.evaluate(() => {
return {
title: document.title,
url: window.location.href
};
});
console.log(`URL ${i + 1}:`, data);
} catch (error) {
console.error(`在${urls[i]}上出错:`, error.message);
} finally {
await browser.close();
}
}
}
const urlsToScrape = [
'https://example.com/page1',
'https://example.com/page2',
'https://example.com/page3',
'https://example.com/page4'
];
scrapeWithRotation(urlsToScrape);
这段代码循环遍历代理列表,并为每个URL启动一个新的浏览器。方法i % proxyList.length确保循环轮换。
使用上下文池的轮换(Playwright)
const { chromium } = require('playwright');
const proxyList = [
{ server: 'http://proxy1.example.com:8080', username: 'user1', password: 'pass1' },
{ server: 'http://proxy2.example.com:8080', username: 'user2', password: 'pass2' },
{ server: 'http://proxy3.example.com:8080', username: 'user3', password: 'pass3' }
];
async function scrapeWithContextPool(urls) {
const browser = await chromium.launch();
// 创建具有不同代理的上下文池
const contexts = await Promise.all(
proxyList.map(proxy => browser.newContext({ proxy }))
);
for (let i = 0; i < urls.length; i++) {
const context = contexts[i % contexts.length];
const page = await context.newPage();
try {
await page.goto(urls[i], { waitUntil: 'networkidle' });
const title = await page.title();
console.log(`URL ${i + 1}(代理 ${i % contexts.length}):`, title);
} catch (error) {
console.error(`在${urls[i]}上出错:`, error.message);
} finally {
await page.close();
}
}
await browser.close();
}
const urls = [
'https://example.com/page1',
'https://example.com/page2',
'https://example.com/page3'
];
scrapeWithContextPool(urls);
Playwright允许在一个浏览器中创建多个上下文,每个上下文都有自己的代理。这比完全重启浏览器节省了内存和启动时间。
带错误跟踪的智能轮换
class ProxyRotator {
constructor(proxyList) {
this.proxyList = proxyList;
this.currentIndex = 0;
this.failedProxies = new Set();
}
getNext() {
const availableProxies = this.proxyList.filter(
(_, index) => !this.failedProxies.has(index)
);
if (availableProxies.length === 0) {
throw new Error('所有代理不可用');
}
const proxy = this.proxyList[this.currentIndex];
this.currentIndex = (this.currentIndex + 1) % this.proxyList.length;
return { proxy, index: this.currentIndex - 1 };
}
markFailed(index) {
this.failedProxies.add(index);
console.log(`代理 ${index} 被标记为不可用`);
}
resetFailed() {
this.failedProxies.clear();
}
}
// 使用
const rotator = new ProxyRotator(proxyList);
async function scrapeWithSmartRotation(url, maxRetries = 3) {
for (let attempt = 0; attempt < maxRetries; attempt++) {
const { proxy, index } = rotator.getNext();
const browser = await chromium.launch({ proxy });
const page = await browser.newPage();
try {
await page.goto(url, { timeout: 30000 });
const data = await page.content();
await browser.close();
return data;
} catch (error) {
console.error(`代理 ${index} 出错:`, error.message);
rotator.markFailed(index);
await browser.close();
if (attempt === maxRetries - 1) {
throw new Error(`在 ${maxRetries} 次尝试后无法加载 ${url}`);
}
}
}
}
这个类跟踪不可用的代理,并将其排除在轮换之外。在出错时自动切换到列表中的下一个代理。
错误处理和可用性检查
在使用代理时会出现特定的错误:连接超时、授权拒绝、目标网站IP被封锁。正确的错误处理对于解析器的稳定运行至关重要。
常见错误及其处理
async function safePageLoad(page, url, options = {}) {
const defaultOptions = {
timeout: 30000,
waitUntil: 'networkidle0',
maxRetries: 3,
retryDelay: 2000
};
const config = { ...defaultOptions, ...options };
for (let attempt = 1; attempt <= config.maxRetries; attempt++) {
try {
await page.goto(url, {
timeout: config.timeout,
waitUntil: config.waitUntil
});
return { success: true, attempt };
} catch (error) {
console.error(`尝试 ${attempt} 失败:`, error.message);
// 分析错误类型
if (error.message.includes('ERR_PROXY_CONNECTION_FAILED')) {
throw new Error('代理不可用');
}
if (error.message.includes('ERR_TUNNEL_CONNECTION_FAILED')) {
throw new Error('代理隧道错误');
}
if (error.message.includes('407')) {
throw new Error('代理授权错误 (407)');
}
if (error.message.includes('Navigation timeout')) {
console.log(`加载超时,${config.retryDelay}ms后重试`);
await new Promise(resolve => setTimeout(resolve, config.retryDelay));
continue;
}
// 如果这是最后一次尝试 - 抛出错误
if (attempt === config.maxRetries) {
throw error;
}
}
}
}
检查代理的可用性
async function testProxy(proxyConfig) {
const browser = await chromium.launch({ proxy: proxyConfig });
const page = await browser.newPage();
const result = {
working: false,
ip: null,
responseTime: null,
error: null
};
const startTime = Date.now();
try {
await page.goto('https://api.ipify.org?format=json', { timeout: 10000 });
const content = await page.textContent('body');
const data = JSON.parse(content);
result.working = true;
result.ip = data.ip;
result.responseTime = Date.now() - startTime;
} catch (error) {
result.error = error.message;
} finally {
await browser.close();
}
return result;
}
// 测试代理列表
async function validateProxyList(proxyList) {
console.log('检查代理...');
const results = await Promise.all(
proxyList.map(async (proxy, index) => {
const result = await testProxy(proxy);
console.log(`代理 ${index + 1}:`, result.working ? `✓ ${result.ip} (${result.responseTime}ms)` : `✗ ${result.error}`);
return { proxy, ...result };
})
);
const workingProxies = results.filter(r => r.working);
console.log(`\n可用代理: ${workingProxies.length}/${proxyList.length}`);
return workingProxies.map(r => r.proxy);
}
该函数在使用之前检查每个代理,测量响应时间并仅返回可用的代理。建议在应用程序启动时运行验证。
监控和日志记录
class ProxyMonitor {
constructor() {
this.stats = {
totalRequests: 0,
successfulRequests: 0,
failedRequests: 0,
proxyErrors: 0,
averageResponseTime: 0,
requestsByProxy: new Map()
};
}
recordRequest(proxyIndex, success, responseTime, error = null) {
this.stats.totalRequests++;
if (success) {
this.stats.successfulRequests++;
this.stats.averageResponseTime =
(this.stats.averageResponseTime * (this.stats.successfulRequests - 1) + responseTime) /
this.stats.successfulRequests;
} else {
this.stats.failedRequests++;
if (error && error.includes('proxy')) {
this.stats.proxyErrors++;
}
}
// 每个代理的统计信息
if (!this.stats.requestsByProxy.has(proxyIndex)) {
this.stats.requestsByProxy.set(proxyIndex, { success: 0, failed: 0 });
}
const proxyStats = this.stats.requestsByProxy.get(proxyIndex);
success ? proxyStats.success++ : proxyStats.failed++;
}
getReport() {
const successRate = (this.stats.successfulRequests / this.stats.totalRequests * 100).toFixed(2);
return {
...this.stats,
successRate: `${successRate}%`,
averageResponseTime: `${this.stats.averageResponseTime.toFixed(0)}ms`
};
}
}
// 使用
const monitor = new ProxyMonitor();
async function monitoredScrape(url, proxyIndex) {
const startTime = Date.now();
try {
// ... 抓取代码 ...
const responseTime = Date.now() - startTime;
monitor.recordRequest(proxyIndex, true, responseTime);
} catch (error) {
monitor.recordRequest(proxyIndex, false, 0, error.message);
throw error;
}
}
高级场景:地理定位和指纹识别
现代反机器人系统不仅检查IP地址,还检查地理位置、时区、浏览器语言和其他参数。在使用来自其他国家的代理时,正确设置所有这些参数非常重要。
根据代理设置地理位置和语言
const { chromium } = require('playwright');
async function createContextWithGeo(proxy, geoData) {
const browser = await chromium.launch({ proxy });
const context = await browser.newContext({
locale: geoData.locale, // 'en-US', 'de-DE', 'fr-FR'
timezoneId: geoData.timezone, // 'America/New_York', 'Europe/Berlin'
geolocation: {
latitude: geoData.latitude,
longitude: geoData.longitude
},
permissions: ['geolocation']
});
return { browser, context };
}
// 示例:来自德国的代理
const germanyProxy = {
server: 'http://de-proxy.example.com:8080',
username: 'user',
password: 'pass'
};
const germanyGeo = {
locale: 'de-DE',
timezone: 'Europe/Berlin',
latitude: 52.520008,
longitude: 13.404954
};
const { browser, context } = await createContextWithGeo(germanyProxy, germanyGeo);
const page = await context.newPage();
await page.goto('https://www.google.com');
// Google将显示德语版本并返回柏林的结果
这段代码设置浏览器,使其看起来像是来自德国的真实用户:德语界面、柏林时区和柏林中心的坐标。
完整的指纹设置
async function createStealthContext(proxy, profile) {
const context = await chromium.launch({ proxy }).then(b =>
b.newContext({
locale: profile.locale,
timezoneId: profile.timezone,
userAgent: profile.userAgent,
viewport: profile.viewport,
deviceScaleFactor: profile.deviceScaleFactor,
isMobile: profile.isMobile,
hasTouch: profile.hasTouch,
colorScheme: profile.colorScheme,
geolocation: profile.geolocation,
permissions: ['geolocation']
})
);
return context;
}
// 美国Windows 10 + Chrome的配置文件
const desktopUSProfile = {
locale: 'en-US',
timezone: 'America/New_York',
userAgent: 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36',
viewport: { width: 1920, height: 1080 },
deviceScaleFactor: 1,
isMobile: false,
hasTouch: false,
colorScheme: 'light',
geolocation: { latitude: 40.7128, longitude: -74.0060 }
};
// 英国iPhone的配置文件
const mobileUKProfile = {
locale: 'en-GB',
timezone: 'Europe/London',
userAgent: 'Mozilla/5.0 (iPhone; CPU iPhone OS 17_0 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/17.0 Mobile/15E148 Safari/604.1',
viewport: { width: 390, height: 844 },
deviceScaleFactor: 3,
isMobile: true,
hasTouch: true,
colorScheme: 'light',
geolocation: { latitude: 51.5074, longitude: -0.1278 }
};
完整的指纹设置降低了被识别为自动化的可能性。确保所有参数相互匹配是很重要的:例如,移动的User-Agent应与移动屏幕分辨率一起使用。
重要: 在处理广告平台(Google Ads、Facebook Ads)或金融服务时,建议使用移动代理 — 它们具有真实移动运营商的信任评分,几乎不被封锁。
绕过WebRTC泄漏
// Puppeteer: 禁用WebRTC
const browser = await puppeteer.launch({
args: [
'--proxy-server=http://proxy.example.com:8080',
'--disable-webrtc',
'--disable-webrtc-hw-encoding',
'--disable-webrtc-hw-decoding'
]
});
// Playwright: 重定义WebRTC API
const context = await browser.newContext({ proxy: proxyConfig });
await context.addInitScript(() => {
// 禁用RTCPeerConnection
window.RTCPeerConnection = undefined;
window.RTCDataChannel = undefined;
window.RTCSessionDescription = undefined;
// 重定义getUserMedia
navigator.mediaDevices.getUserMedia = undefined;
navigator.getUserMedia = undefined;
});
WebRTC可能会泄露您的真实IP地址,即使在使用代理时。此代码完全禁用浏览器中的WebRTC API。
Puppeteer和Playwright在代理工作中的比较
| 标准 | Puppeteer | Playwright |
|---|---|---|
| 代理设置 | 通过命令行参数 | 通过配置对象 |
| 身份验证 | 在启动后使用page.authenticate() | 在创建时在proxy对象中 |
| 在同一浏览器中使用多个代理 | 不可以,需要单独的浏览器 | 可以,通过不同的上下文 |
| 浏览器支持 | 仅支持Chromium/Chrome | 支持Chromium、Firefox、WebKit |
| 性能 | 快速启动单个浏览器 | 在多个上下文中更高效 |
| TypeScript | 通过@types/puppeteer提供类型 | 内置TypeScript支持 |
| 文档 | 良好,有很多示例 | 优秀,更加结构化 |
| 生态系统 | 更多插件和扩展 | 发展更快,新功能 |
选择建议
如果您选择Puppeteer:
- 仅与Chrome/Chromium一起使用
- 一次使用一个代理
- 需要与现有工具的最大兼容性
- 已经有大量Puppeteer代码库
如果您选择Playwright:
- 需要与Firefox或Safari(WebKit)一起使用
- 需要同时使用多个代理
- 在扩展时性能很重要
- 使用TypeScript编写
- 从头开始新项目
代理轮换的性能
测试:在MacBook Pro M1上使用10个代理轮换解析100个页面:
| 方法 | 执行时间 | RAM消耗 |
|---|---|---|
| Puppeteer(重启浏览器) | 8分钟23秒 | ~1.2 GB峰值 |
| Playwright(重启浏览器) | 7分钟54秒 | ~1.1 GB峰值 |
| Playwright(上下文池) | 4分钟12秒 | ~800 MB稳定 |
使用上下文池的Playwright几乎快两倍,因为没有浏览器启动的开销。这在解析数千个页面时至关重要。
结论
将代理与Puppeteer和Playwright集成是网页抓取、测试和自动化的标准实践。Puppeteer提供了简单性和广泛的生态系统,而Playwright则提供了现代API和在多个代理通过浏览器上下文工作时的最佳性能。
我们讨论的关键点包括:
- 在两个框架中基本设置HTTP、HTTPS和SOCKS5代理
- 基于用户名和密码的身份验证
- 大规模抓取的代理轮换
- 错误处理和使用前的代理验证
- 地理定位和指纹识别的设置以绕过反机器人系统
- 不同方法的性能比较
对于生产解决方案,建议将高质量的代理与正确的指纹设置、错误处理和监控相结合。这将确保解析器在受保护的网站上稳定运行。
如果您计划抓取商业网站、市场或与广告平台合作,建议使用住宅代理 — 它们提供最大程度的匿名性和最小的封锁风险,因为它们具有真实家庭用户的IP地址。