查看: 982|回复: 0

Python爬虫实战:使用Playwright抓取Google Flig

[复制链接]
发表于 6 小时前 | 显示全部楼层 |阅读模式
对于需要获取航班价格、航线时刻或碳排放数据的开发者来说,Google Flights 是一个信息量很大的数据源。但它的页面由大量动态脚本驱动,传统 requests 方案很难直接提取内容。本文给出一个基于 Python + Playwright 的抓取实现,覆盖环境搭建、数据类定义、搜索表单填充、航班结果提取、重试机制与 JSON 保存,最后讨论 IP 封锁和 CAPTCHA 的常见应对思路。

环境准备
建议先用虚拟环境隔离依赖,避免污染全局 Python。终端下执行:
  1. # 创建虚拟环境
  2. python -m venv flights-env
  3. # 激活虚拟环境(Windows)
  4. flights-env\Scripts\activate
  5. # 激活虚拟环境(macOS/Linux)
  6. source flights-env/bin/activate
复制代码

接着安装 Playwright、Tenacity 和 asyncio 相关包,并下载 Chromium 浏览器内核:
  1. pip install playwright tenacity asyncio
  2. playwright install chromium
复制代码

Tenacity 用于实现自动重试,Playwright 负责模拟用户操作 Chromium,asyncio 支撑异步流程。

数据结构设计
为了保持代码清晰,先定义两个数据类:一个描述搜索条件,一个描述单条航班结果。
  1. from dataclasses import dataclass
  2. from typing import Optional
  3. @dataclass
  4. class SearchParameters:
  5.     departure: str
  6.     destination: str
  7.     departure_date: str
  8.     return_date: Optional[str] = None
  9.     ticket_type: str = "One way"
  10. @dataclass
  11. class FlightData:
  12.     airline: str
  13.     departure_time: str
  14.     arrival_time: str
  15.     duration: str
  16.     stops: str
  17.     price: str
  18.     co2_emissions: str
  19.     emissions_variation: str
复制代码

SearchParameters 中的 ticket_type 支持 "One way"、"Round trip" 等选项;FlightData 与后续 CSS 选择器提取的字段一一对应。

FlightScraper 类与 CSS 选择器
将抓取逻辑封装在 FlightScraper 中,用类属性集中管理 CSS 选择器。这些选择器会随 Google Flights 页面结构调整而变化,抓取前建议先通过 DevTools 确认。
  1. class FlightScraper:
  2.     SELECTORS = {
  3.         "airline": "div.sSHqwe.tPgKwe.ogfYpf",
  4.         "departure_time": 'span[aria-label^="Departure time"]',
  5.         "arrival_time": 'span[aria-label^="Arrival time"]',
  6.         "duration": 'div[aria-label^="Total duration"]',
  7.         "stops": "div.hF6lYb span.rGRiKd",
  8.         "price": "div.FpEdX span",
  9.         "co2_emissions": "div.O7CXue",
  10.         "emissions_variation": "div.N6PNV",
  11.     }
复制代码

模拟填写搜索表单
用户手动搜索时,会先选择机票类型、输入出发地、目的地和日期。Playwright 的异步 API 可以模拟这些操作:
  1. async def _fill_search_form(self, page, params: SearchParameters) -> None:
  2.     ticket_type_div = page.locator("div.VfPpkd-TkwUic[jsname='oYxtQd']").first
  3.     await ticket_type_div.click()
  4.     await page.locator("li").filter(has_text=params.ticket_type).nth(0).click()
  5.     from_input = page.locator("input[aria-label='Where from?']")
  6.     await from_input.fill(params.departure)
  7.     to_input = page.locator("input[aria-label='Where to?']")
  8.     await to_input.fill(params.destination)
  9.     date_input = page.locator("input[aria-label='Departure date']")
  10.     await date_input.fill(params.departure_date)
复制代码

这里使用 aria-label 定位输入框,比依赖 class 更稳定。日期格式需要与页面要求一致,例如 "2024-12-01"。

加载所有可用航班
Google Flights 默认只展示一部分结果,需要点击 "more flights" 按钮展开完整列表。通过循环不断点击,直到按钮消失:
  1. async def _load_all_flights(self, page) -> None:
  2.     while True:
  3.         try:
  4.             more_button = await page.wait_for_selector(
  5.                 'button[aria-label*="more flights"]', timeout=5000
  6.             )
  7.             if more_button:
  8.                 await more_button.click()
  9.                 await page.wait_for_timeout(2000)
  10.             else:
  11.                 break
  12.         except Exception:
  13.             break
复制代码

提取航班数据
当所有结果都展开后,使用 li.pIav2d 定位每条航班记录,再根据 SELECTORS 提取对应字段。
  1. async def _extract_flight_data(self, page) -> list[FlightData]:
  2.     await page.wait_for_selector("li.pIav2d", timeout=30000)
  3.     flights = await page.query_selector_all("li.pIav2d")
  4.     flights_data = []
  5.     for flight in flights:
  6.         flight_info = {}
  7.         for key, selector in self.SELECTORS.items():
  8.             element = await flight.query_selector(selector)
  9.             if element:
  10.                 flight_info[key] = await element.inner_text()
  11.             else:
  12.                 flight_info[key] = ""
  13.         flights_data.append(FlightData(**flight_info))
  14.     return flights_data
复制代码

如果某个元素不存在,这里置为空字符串,避免构造 FlightData 时因缺字段报错。

添加重试逻辑
抓取过程中可能遇到网络抖动或页面暂时拒绝响应,可以用 tenacity 装饰器设置最多重试 3 次、每次等待 5 秒。完整的抓取流程如下:
  1. from tenacity import retry, stop_after_attempt, wait_fixed
  2. from playwright.async_api import async_playwright
  3. @retry(stop=stop_after_attempt(3), wait=wait_fixed(5))
  4. async def search_flights(self, params: SearchParameters) -> list[FlightData]:
  5.     async with async_playwright() as p:
  6.         browser = await p.chromium.launch(headless=True)
  7.         context = await browser.new_context()
  8.         page = await context.new_page()
  9.         await page.goto("https://www.google.com/flights")
  10.         await self._fill_search_form(page, params)
  11.         await self._load_all_flights(page)
  12.         flights = await self._extract_flight_data(page)
  13.         await browser.close()
  14.         return flights
复制代码

这里将 _load_all_flights 也纳入调用链,确保结果完整。

将结果保存为 JSON
抓取到的数据需要落地保存,便于后续分析。用时间戳生成唯一文件名,并同时记录搜索参数和航班列表:
  1. import json
  2. from datetime import datetime
  3. def save_results(self, flights: list[FlightData], params: SearchParameters) -> str:
  4.     timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
  5.     filename = f"flight_results_{params.departure}_{params.destination}_{timestamp}.json"
  6.     output_data = {
  7.         "search_parameters": vars(params),
  8.         "flights": [vars(flight) for flight in flights],
  9.     }
  10.     with open(filename, "w", encoding="utf-8") as f:
  11.         json.dump(output_data, f, indent=2, ensure_ascii=False)
  12.     return filename
复制代码

运行爬虫
下面是一段可执行的启动示例,搜索从 LAX 到 JFK 的单程航班:
  1. import asyncio
  2. async def main():
  3.     scraper = FlightScraper()
  4.     params = SearchParameters(
  5.         departure="LAX",
  6.         destination="JFK",
  7.         departure_date="2024-12-01",
  8.         ticket_type="One way"
  9.     )
  10.     try:
  11.         flights = await scraper.search_flights(params)
  12.         filename = scraper.save_results(flights, params)
  13.         print("Flights scraped successfully.")
  14.         print(f"Saved to {filename}")
  15.     except Exception as e:
  16.         print(f"Error during flight search: {str(e)}")
  17. if __name__ == "__main__":
  18.     asyncio.run(main())
复制代码

注意:FlightScraper 需要完整定义类的继承关系,实际运行时建议将上述方法都放在同一个类作用域中。

应对 IP 封锁和 CAPTCHA
Google Flights 对自动化流量检测很严格,常见的问题有两个:

一是 IP 被封禁。高频请求会被限制访问,解决办法是使用轮换代理,让每次请求的出口 IP 不同。

二是 CAPTCHA 验证。当被判断为机器人时,页面会弹出人机验证。这类问题很难只靠 Playwright 绕过,通常需要接入第三方打码服务或网络解锁器,例如 Bright Data 的 Web Unlocker 就能自动处理 CAPTCHA 挑战。

从工程角度,抓取频率应尽可能放缓,并实时监控页面是否出现异常元素,以便及时切换 IP 或验证码策略。

本教程给出的是一个可扩展的抓取框架,你可以根据实际业务需求调整选择器、增加代理池或改造数据存储层。Google Flights 页面版本更新频繁,选择器可能需要同步修改,但整体流程保持不变。
回复

使用道具 举报

您需要登录后才可以回帖 登录 | 注册

本版积分规则

指导单位

江苏省公安厅

江苏省通信管理局

浙江省台州刑侦支队

DEFCON GROUP 86025

Hacking Group 021A

旗下站点

态势感知中心

应急响应中心

红盟安全

联系我们

官方QQ群:112851260

官方邮箱:security#ihonker.org(#改成@)

官方核心成员

关注微信公众号

Archiver|手机版|小黑屋| ( 沪ICP备2021026908号 )

GMT+8, 2026-9-1 17:03 , Processed in 0.029462 second(s), 17 queries , Gzip On, Redis On.

Powered by ihonker.com

Copyright © 2015-现在.

  • 返回顶部