1. 为什么选择Playwright+智能代理做旅行比价爬虫?
去年帮朋友做酒店比价工具时,我用过Requests+BeautifulSoup的传统方案,结果在携程和Booking上连续触发验证码。后来测试发现,这些平台对无头浏览器的指纹检测越来越严格,而Playwright的自动化浏览器环境能完美模拟人类操作轨迹。
Playwright相比Selenium的核心优势在于:
- 原生支持多语言(Python/Node.js/C#/Java)
- 自动等待机制避免异步加载问题
- 内置网络拦截和Mock功能
- 跨浏览器支持(Chromium/WebKit/Firefox)
智能代理则是应对反爬的第二道防线。实测某家酒店平台对单一IP的访问阈值是15次/分钟,使用Luminati等商业代理服务可以:
- 自动切换住宅IP
- 按目标网站地理位置分配出口节点
- 智能调整请求频率
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 环境搭建与核心依赖安装
2.1 基础环境配置
推荐使用Python 3.8+版本,太新的版本可能遇到依赖冲突:
bash复制# 创建虚拟环境
python -m venv travel_spider
source travel_spider/bin/activate # Linux/Mac
travel_spider\Scripts\activate.bat # Windows
# 安装Playwright核心包
pip install playwright
playwright install # 下载浏览器二进制文件
2.2 代理服务集成
以BrightData代理为例,需要额外安装:
python复制pip install brightdata-sdk
代理配置示例(实际key需替换):
python复制from brightdata_sdk import BrightDataSDK
proxy = BrightDataSDK(
zone="your_zone_name",
username="your_username",
password="your_password"
)
3. 爬虫架构设计与实现
3.1 页面导航逻辑
旅行平台通常有三级页面结构:
- 列表页(如酒店搜索结果)
- 详情页(单个酒店信息)
- 价格日历页(动态加载)
python复制async def crawl_hotel(browser, proxy, keyword):
context = await browser.new_context(
proxy={"server": proxy.get_next_proxy()}
)
page = await context.new_page()
# 第一层:搜索列表
await page.goto("https://www.booking.com")
await page.fill('input[name="ss"]', keyword)
await page.click('button[type="submit"]')
# 第二层:酒店详情
hotel_links = await page.query_selector_all('div[data-testid="property-card"] a')
for link in hotel_links[:5]: # 限制爬取数量
await link.click()
await page.wait_for_selector('div#hp_hotel_name')
# 第三层:价格数据
await page.click('button[data-testid="price-calendar-button"]')
await extract_calendar_data(page)
await context.close()
3.2 反反爬策略组合拳
- 指纹伪装 - 修改浏览器特征
python复制context = await browser.new_context(
user_agent="Mozilla/5.0 (Windows NT 10.0) AppleWebKit/537.36...",
locale="zh-CN",
timezone_id="Asia/Shanghai"
)
- 行为模拟 - 随机化操作间隔
python复制import random
await page.wait_for_timeout(random.randint(800, 1500))
- 验证码处理 - 自动识别+人工回填
python复制if await page.is_visible('img#captcha-image'):
captcha = await solve_captcha(page)
await page.fill('input#captcha', captcha)
4. 数据解析与存储方案
4.1 结构化数据抽取
使用Playwright内置的文本提取方法比XPath更稳定:
python复制hotel_data = {
"name": await page.inner_text('div#hp_hotel_name'),
"rating": float(await page.get_attribute('div[data-testid="review-score"]', 'data-review-score')),
"price": await extract_dynamic_price(page),
"facilities": [
await item.inner_text()
for item in await page.query_selector_all('div.facilitiesChecklist li')
]
}
4.2 价格日历的特殊处理
遇到动态渲染的价格表格时:
python复制async def extract_calendar_data(page):
await page.evaluate("""() => {
window.scrollBy(0, 200);
document.querySelector('div[data-testid="calendar"]').style.display = 'block';
}""")
dates = await page.locator('td[data-date]').all()
for date in dates:
if 'data-price' in await date.get_attribute('data-date'):
yield {
"date": await date.get_attribute('data-date'),
"price": await date.get_attribute('data-price')
}
4.3 数据存储优化
针对高频更新的价格数据,采用增量存储策略:
python复制import sqlite3
def save_to_db(data):
conn = sqlite3.connect('prices.db')
cursor = conn.cursor()
# 只存储价格变动记录
latest = cursor.execute(
"SELECT price FROM hotels WHERE hotel_id=? ORDER BY timestamp DESC LIMIT 1",
(data['hotel_id'],)
).fetchone()
if not latest or latest[0] != data['price']:
cursor.execute("""
INSERT INTO hotels VALUES
(?, ?, ?, ?, ?)
""", (data['hotel_id'], data['name'], data['price'], data['date'], datetime.now()))
conn.commit()
5. 实战中的七个致命陷阱
- 代理质量检测 - 每次请求前检查代理可用性:
python复制async def check_proxy(proxy):
try:
async with aiohttp.ClientSession() as session:
async with session.get(
"http://httpbin.org/ip",
proxy=f"http://{proxy}",
timeout=5
) as resp:
return resp.status == 200
except:
return False
- 页面跳转检测 - 有些平台会故意返回200但跳转到验证页:
python复制if "security_check" in page.url:
raise Exception("触发反爬验证")
- 元素定位失效 - 使用双重选择器策略:
python复制price_element = await page.query_selector(
'div[data-testid="price"] || div.price-value || span.final-price'
)
- 请求指纹泄露 - 禁用部分Web API:
python复制await context.add_init_script("""
delete navigator.webdriver;
window.chrome = undefined;
""")
- 内存泄漏处理 - 定期重启浏览器实例:
python复制if len(context.pages) > 20:
await context.close()
context = await browser.new_context()
-
验证码服务选择 - 对比主流方案:
| 服务商 | 识别率 | 价格/千次 | 响应时间 |
|--------------|--------|-----------|----------|
| 2Captcha | 85% | $0.5 | 8-12s |
| Anti-Captcha | 92% | $1.2 | 5-8s |
| DeathByCaptcha| 78% | $0.3 | 15-20s | -
法律风险规避 - 必须遵守:
- 在robots.txt允许的范围内爬取
- 设置合理的爬取间隔(>3秒/页)
- 不绕过付费墙获取数据
- 用户数据匿名化处理
6. 性能优化技巧
6.1 并行化处理
使用asyncio实现协程并发:
python复制async def run_spiders(keywords):
async with async_playwright() as p:
browser = await p.chromium.launch(headless=False)
tasks = [asyncio.create_task(
crawl_hotel(browser, proxy, kw)
) for kw in keywords]
await asyncio.gather(*tasks)
6.2 缓存复用
对静态资源启用磁盘缓存:
python复制context = await browser.new_context(
storage_state="auth.json",
record_video_dir="videos/"
)
6.3 智能节流算法
根据响应时间动态调整请求频率:
python复制def calculate_delay(last_response_time):
base = 1.5 # 基础间隔
factor = last_response_time * 0.8 # 响应时间系数
jitter = random.uniform(-0.5, 0.5) # 随机抖动
return max(0.5, base + factor + jitter)
7. 完整项目结构参考
code复制travel_price_spider/
├── config.py # 代理/账号配置
├── spiders/
│ ├── booking.py # 平台特定爬虫
│ ├── expedia.py
│ └── base.py # 基础爬虫类
├── utils/
│ ├── proxy.py # 代理管理
│ ├── captcha.py # 验证码处理
│ └── storage.py # 数据存储
├── requirements.txt
└── main.py # 任务调度入口
在main.py中实现任务调度:
python复制async def main():
platforms = [
("booking", ["北京", "上海", "广州"]),
("expedia", ["纽约", "东京", "巴黎"])
]
async with ProxyPool() as proxy:
for platform, cities in platforms:
spider = load_spider(platform)
await spider.run(proxy, cities)
asyncio.run(main())
这个架构下,新增平台只需继承BaseSpider实现特定解析逻辑,其他模块均可复用。我在实际项目中用这套代码稳定运行了6个月,平均每天采集3万条价格数据,代理成本控制在$20/月以内。
