Google은 2018년에 Flights API를 닫았어요. 그런데 항공권 값은 지금도 쉴 새 없이 출렁이죠. 국내선 한 편만 봐도 48시간 사이에 17번까지 바뀌기도 해요(출처). 이런 가격을 코드로 다뤄 보려 하면, 현실적으로 웹 스크래핑 말고는 길이 거의 없어요.
저는 Google에서 항공편 데이터를 빼내는 방법을 꽤 오래 굴려 봤어요. 2025년 1월 Google이 SearchGuard를 푼 뒤로는 분위기가 확 달라졌고요. 이 글에서는 Playwright로 Google Flights 스크래퍼를 Python으로 직접 짜는 법, 다들 여기서 막히는 안티봇 구간을 넘기는 법, 그리고 이걸 자동 가격 추적기와 알림으로 키우는 법을 차례대로 풀어 볼게요. 코드가 부담스러운 분께는 Thunderbit로 2분 만에 같은 결과를 뽑는 노코드 방식도 같이 보여 드려요.
왜 Python으로 Google Flights를 스크래핑해야 할까?
항공권 검색 시장은 사실상 Google Flights 판이에요. 미국 모바일 검색 점유율이 47.6%까지 올라와 주요 OTA를 전부 제쳤어요. 이걸 떠받치는 여행 메타검색 시장은 2024년 83.3억 달러 규모이고, 매년 30.2%씩 커지는 중이에요. 문제는 QPX Express API가 2018년 4월 10일 자로 완전히 닫혔다는 점이에요. 공식적으로 이 데이터를 코드로 받을 길이 사라진 거죠.
게다가 항공권은 같은 일정이라도 최대 50%까지 흔들려요. 최저가와 최고가 차이는 평균 20달러쯤 되고요. Delta는 동적 가격을 위해 요금 구간을 77개나 운영해요. 2026년 초 미국 왕복 평균가는 408달러 수준이고, 항공료는 1년 새 14.9% 올랐어요.
강한 플랫폼, 사라진 공식 API, 출렁이는 요금. 이 셋이 겹친 덕에 Python으로 Google Flights를 긁는 프로젝트가 GitHub와 여행 커뮤니티에서 식지 않고 인기예요.
누구에게 뭐가 도움이 되는지부터 짚어 볼게요:
| 사용자 유형 | 활용 사례 | 핵심 이점 |
|---|---|---|
| 개인 여행자 | 특정 노선의 가격을 장기 추적 | 항공편당 평균 $50 절약 |
| 여행사 | 경쟁사 요금 인텔리전스 확보 | 실시간 운임 패리티 모니터링 |
| 기업 출장팀 | 노선별 비용 최적화 | 기업 출장비 10~30% 절감 |
| 개발자 | 항공권 비교 서비스 구축 | 가격 데이터의 프로그램 접근 |
| 연구자 | 항공사 가격 변동성 분석 | 학술 및 시장 조사 |
포럼에서도 왜 스크래핑으로 돌아섰는지 거침없이 말해요. *"Google Flights API가 중단돼서 웹 스크래핑을 써야 한다"*는 얘기가 계속 올라오죠. ROI도 또렷해요. Hopper는 하루 50억 건 넘는 가격 견적을 돌려 95% 예측 정확도를 내세우고, Expedia의 2026년 데이터를 보면 출발 8~15일 전에 예약하면 국내선에서 $25 정도 아낄 수 있어요.
Google Flights에서 어떤 데이터를 수집할 수 있을까?
Google Flights 결과 페이지에는 생각보다 알찬 데이터가 깔려 있어요. 보통 이 정도를 가져올 수 있어요:
- 항공사명(및 로고)
- 출발 시간과 공항 코드
- 도착 시간과 공항 코드
- 총 비행 시간
- 경유 횟수 및 경유지 정보(공항, 체류 시간, 야간 경유 여부)
- 항공권 가격(통화별)
- CO2 배출량(kg CO2e, 일반 항공편 대비 백분율 차이)
- 여행 클래스, 항공편 번호, 기종
- 좌석 공간 정보
- 편의 기능(와이파이, 전원 콘센트, 미디어 스트리밍)
- 가격 수준 표시(낮음/보통/높음)
- 지연 경고("30분 이상 지연되는 경우가 많음")
뭘 가져올 수 있는지는 노선, 날짜, 편도냐 왕복이냐에 따라 달라져요. 항공편 한 건을 JSON으로 정리하면 이런 모양이에요:
{
"search_date": "2026-04-16",
"route": "SFO-JFK",
"departure_date": "2026-05-15",
"flights": [
{
"airline": "United Airlines",
"flight_number": "UA123",
"departure_time": "08:00",
"departure_airport": "SFO",
"arrival_time": "16:35",
"arrival_airport": "JFK",
"duration_minutes": 335,
"stops": 0,
"price_usd": 287,
"price_level": "low",
"co2_kg": 156,
"co2_vs_typical": "-12%",
"travel_class": "Economy"
}
]
}
Python 환경 설정하기
코드를 짜기 전에 몇 가지 준비물부터 챙겨요.
사전 준비:
- 난이도: 중급
- 소요 시간: 전체 튜토리얼 기준 약 1~2시간
- 준비물: Python 3.7 이상, 기본적인 Python 지식, Chrome 기반 브라우저
필요한 라이브러리 설치
브라우저 자동화는 Playwright로 해요. Google Flights는 화면을 100% JavaScript로 그리기 때문에, 단순 HTTP 요청으로는 알맹이가 안 나와요. 여기에 보조 도구 몇 개를 얹어요:
pip install playwright playwright-stealth pandas
playwright install chromium
- Playwright — 헤드리스 브라우저 자동화, JavaScript 렌더링 처리, 내장 대기 기능 제공
- playwright-stealth — 흔한 봇 탐지 신호를 보정
- pandas — 이후 데이터 분석과 CSV 내보내기용
Selenium이나 requests 대신 Playwright를 쓰는 이유
Google Flights는 requests + BeautifulSoup 조합으로는 안 돌아가요. 콘텐츠가 전부 JavaScript로 그려지거든요. 진짜 브라우저가 있어야 해요.
| 기능 | Playwright | Selenium | requests + BS4 |
|---|---|---|---|
| JS 렌더링 | 완전 지원 | 완전 지원 | 없음 |
| 속도 | 전체적으로 42% 더 빠름 | 기준점 | 이 용도에서는 해당 없음 |
| 비동기 지원 | 기본 지원 | 순차 처리만 | 해당 없음 |
| 메모리 사용량 | 30% 더 적음 | 더 높음 | 최소 |
| 봇 탐지 회피 | 양호(stealth 사용 시) | 탐지되기 쉬움 | 해당 없음 |
Playwright는 더 빠르고, 더 요즘 도구답고, 비동기 처리도 강해요. Google Flights에는 가장 잘 맞는 선택이에요.
단계별 가이드: Python으로 Google Flights 스크래핑하기
이제 본론이에요. 스크래퍼를 한 조각씩 붙여 완성해 봐요.

1단계: 데이터 클래스 정의하기
먼저 Python dataclass로 검색 조건과 항공편 데이터를 틀에 맞춰요. 이렇게 잡아 두면 코드도 깔끔하고, 나중에 늘리기도 편해요.
from dataclasses import dataclass, field
from typing import Optional, List
@dataclass
class SearchParams:
origin: str # e.g., "SFO"
destination: str # e.g., "JFK"
departure_date: str # e.g., "2026-05-15"
return_date: Optional[str] = None
trip_type: str = "one-way" # "one-way" or "round-trip"
travel_class: str = "economy"
@dataclass
class FlightData:
airline: str = ""
departure_time: str = ""
arrival_time: str = ""
duration: str = ""
stops: str = ""
price: str = ""
co2_emissions: str = ""
필드 하나하나가 페이지에서 뽑을 값과 바로 이어져요. 이런 틀을 먼저 잡으면 지저분한 딕셔너리를 여기저기 넘길 일이 없어요.
2단계: Google Flights URL 구조 이해하기
Google Flights는 tfs URL 파라미터에 Base64로 인코딩한 Protobuf를 담아 검색 조건을 저장해요. 이 인코딩을 역공학할 수도 있지만, 자연어 쿼리 URL을 쓰는 쪽이 훨씬 간단해요.
가장 쉬운 길은 검색 쿼리 형식이에요:
https://www.google.com/travel/flights?q=flights+from+SFO+to+JFK+on+2026-05-15&curr=USD
더 촘촘하게 제어하고 싶으면 URL을 코드로 만들어요:
def build_flights_url(origin: str, destination: str, date: str) -> str:
base = "https://www.google.com/travel/flights"
query = f"flights from {origin} to {destination} on {date}"
return f"{base}?q={query.replace(' ', '+')}&curr=USD"
Protobuf 인코딩을 역공학하는 길도 있긴 해요. 다만 Google이 내부 형식을 손대면 금방 깨져요. GitHub의 fast-flights 같은 라이브러리는 Protobuf 디코딩으로 HTML 파싱을 통째로 건너뛰는데, 이것도 Google이 내부 형식을 바꾸면 영향을 받는 좀 더 고급 방식이에요.
3단계: 브라우저 실행 후 Google Flights로 이동하기
아래는 Playwright 설정 예시예요. 출발선부터 탐지 위험을 낮추려고 playwright-stealth를 써요.
import asyncio
from playwright.async_api import async_playwright
from playwright_stealth import Stealth
async def scrape_flights(params: SearchParams) -> List[FlightData]:
async with Stealth().use_async(async_playwright()) as pw:
browser = await pw.chromium.launch(
headless=True,
args=[
"--disable-blink-features=AutomationControlled",
"--disable-dev-shm-usage",
"--no-first-run",
]
)
context = await browser.new_context(
viewport={"width": 1920, "height": 1080},
user_agent=(
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) "
"AppleWebKit/537.36 (KHTML, like Gecko) "
"Chrome/125.0.0.0 Safari/537.36"
),
locale="en-US",
timezone_id="America/New_York",
)
# Pre-set cookie consent to skip the popup
await context.add_cookies([{
"name": "SOCS",
"value": "CAESHwgBEhJnd3NfMjAyNTAyMjctMF9SQzIaBXpoLUNOIAEaBgiAy6O-Bg",
"domain": ".google.com",
"path": "/"
}])
page = await context.new_page()
실서비스에서는 헤드리스로 돌리고요(디버깅 땐 headless=False로). 현실적인 viewport와 user-agent를 넣고, SOCS 쿠키를 미리 심어 동의 팝업을 건너뛰어요. 이 부분은 뒤 안티봇 섹션에서 더 파고들어요.
4단계: 검색 결과 페이지로 이동하기
만든 URL을 열고 항공편 결과가 뜰 때까지 기다려요:
url = build_flights_url(
params.origin, params.destination, params.departure_date
)
await page.goto(url, wait_until="networkidle")
# Wait for flight results to load
await page.wait_for_selector(
"li.pIav2d", timeout=15000
)
여기서 타임아웃이 나면 보통 둘 중 하나예요. 동의 팝업이 페이지를 가렸거나(3단계 쿠키 설정 참고), Google이 CAPTCHA를 띄운 경우죠. 둘 다 뒤 안티봇 섹션에서 다뤄요.
5단계: 모든 항공편 결과 불러오기
Google Flights는 나머지 결과를 "Show more flights" 버튼 뒤에 숨겨 둬요. 모든 항공편이 나올 때까지 이 버튼을 계속 눌러야 해요:
# Click "Show more flights" until all results are loaded
while True:
try:
more_button = page.locator(
'button:has-text("Show more flights")'
)
if await more_button.is_visible(timeout=3000):
await more_button.click()
await page.wait_for_timeout(2000)
else:
break
except Exception:
break
이 루프는 버튼을 누른 뒤 2초간 새 결과를 기다리고, 버튼이 더 안 보이면 멈춰요. 제 테스트로는 대부분 노선이 1~3페이지쯤 결과가 나와요.
6단계: CSS 선택자로 항공편 데이터 추출하기
이제 로드된 페이지에서 항공편 데이터를 실제로 파싱해요. 아래 선택자는 2026년 4월 기준으로 확인한 값이에요(왜 이 날짜가 중요한지는 뒤 유지보수 섹션에서 설명해요).
flights = []
cards = await page.query_selector_all("li.pIav2d")
for card in cards:
flight = FlightData()
# Airline name
airline_el = await card.query_selector(
"div.sSHqwe span:not([class])"
)
if airline_el:
flight.airline = (await airline_el.inner_text()).strip()
# Departure time
dep_el = await card.query_selector(
'span[aria-label*="Departure time"]'
)
if dep_el:
flight.departure_time = (await dep_el.inner_text()).strip()
# Arrival time
arr_el = await card.query_selector(
'span[aria-label*="Arrival time"]'
)
if arr_el:
flight.arrival_time = (await arr_el.inner_text()).strip()
# Duration
dur_el = await card.query_selector("div.gvkrdb")
if dur_el:
flight.duration = (await dur_el.inner_text()).strip()
# Stops
stops_el = await card.query_selector("div.EfT7Ae span")
if stops_el:
flight.stops = (await stops_el.inner_text()).strip()
# Price
price_el = await card.query_selector(
"div.FpEdX span"
)
if price_el:
flight.price = (await price_el.inner_text()).strip()
# CO2 emissions
co2_el = await card.query_selector("div.O7CXue")
if co2_el:
flight.co2_emissions = (
await co2_el.get_attribute("aria-label") or ""
).strip()
flights.append(flight)
await browser.close()
return flights
한 가지 조심할 게 있어요. pIav2d, sSHqwe, FpEdX 같은 클래스명은 Google의 Closure Compiler가 만든 값이라, 빌드가 바뀔 때마다 달라져요. 반면 aria-label 기반 선택자는 훨씬 잘 버텨요. 전체 유지보수 전략은 아래에서 다뤄요.
7단계: 결과를 JSON 또는 CSV로 저장하기
마지막으로 긁은 데이터를 타임스탬프와 함께 저장해요(가격 추적할 때 이게 결정적이에요):
import json
from datetime import datetime
from dataclasses import asdict
async def main():
params = SearchParams(
origin="SFO",
destination="JFK",
departure_date="2026-05-15"
)
flights = await scrape_flights(params)
output = {
"search_date": datetime.now().isoformat(),
"params": asdict(params),
"flights": [asdict(f) for f in flights],
}
with open("flights.json", "w") as f:
json.dump(output, f, indent=2)
# Also save as CSV
import pandas as pd
df = pd.DataFrame([asdict(f) for f in flights])
df["search_date"] = datetime.now().isoformat()
df["route"] = f"{params.origin}-{params.destination}"
df.to_csv("flights.csv", index=False)
print(f"Scraped {len(flights)} flights")
asyncio.run(main())
이 코드를 돌리면 flights.json과 flights.csv에 결과가 떨어져요. 제 테스트에서는 SFO-JFK 검색에 보통 3080개 항공편이 잡혔고, 전체 실행은 1520초쯤 걸렸어요.
Google Flights 스크래핑을 위한 안티봇 생존 가이드
대부분 튜토리얼은 여기서 끝나요. 그리고 대부분 스크래퍼도 바로 여기서 무너져요. Google은 2025년 1월 SearchGuard를 풀면서 거의 모든 SERP 스크래퍼를 하룻밤에 무력화했어요. Google 표현으로는 "수만 시간 인력과 수백만 달러가 들어간 결과물"이래요. Google Flights는 스크래핑 난이도가 5점 만점에 4점으로 매겨져요.
경쟁 글들은 이 대목을 깊이 안 다루지만, 실제로 스크래퍼가 멈추는 가장 큰 이유가 바로 여기예요. 뭘 상대하는지, 어떻게 받아칠지 살펴봐요.

요청 사이에 무작위 지연 넣기
속도 제한을 피하는 가장 기본기예요. 코드는 두 줄이면 되고, 효과는 중간쯤이에요:
import time
import random
time.sleep(random.uniform(3, 7))
페이지 이동 사이사이에 넣어요. 매번 정확히 5초 같은 고정 간격은 오히려 의심을 사요. 꼭 랜덤하게 둬요.
User-Agent 로테이션
매 요청마다 같은 user-agent를 보내면 티가 너무 나요. 여러 문자열을 번갈아 써요:
import random
USER_AGENTS = [
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 Chrome/125.0.0.0 Safari/537.36",
"Mozilla/5.0 (Macintosh; Intel Mac OS X 14_5) AppleWebKit/605.1.15 Safari/605.1.15",
"Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 Chrome/124.0.0.0 Safari/537.36",
"Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:126.0) Gecko/20100101 Firefox/126.0",
]
user_agent = random.choice(USER_AGENTS)
헤드리스 탐지 우회
Google은 navigator.webdriver 플래그와 다른 자동화 신호를 봐요. playwright-stealth가 대부분 처리해 주지만, 3단계에서 보여 준 실행 인자도 같이 넣어야 해요. 핵심 플래그는 이거예요:
args=[
"--disable-blink-features=AutomationControlled",
"--disable-dev-shm-usage",
"--no-first-run",
]
이 정도면 기본 탐지는 어느 정도 넘겨요. SearchGuard는 마우스 속도, 키 입력 타이밍, 스크롤 패턴까지 더 깊게 들여다봐요. 그래도 중간 정도 트래픽이라면 stealth 모드와 현실적인 지연만으로 충분한 경우가 많아요.
프록시 로테이션: 데이터센터 vs. 주거용
검색 몇 번 수준을 넘어서면 프록시가 필요해요. 차이가 꽤 커요:
| 기능 | 데이터센터 프록시 | 주거용 프록시 |
|---|---|---|
| 속도 | 100~1,000 Mbps | 10~100 Mbps |
| 성공률(Google) | 20~40% | 85~95% |
| 비용 | IP당 월 $0.10~$0.50 | GB당 $5~$15 |
| 탐지 위험 | 높음 | 매우 낮음 |
방어가 센 사이트를 긁을 때는 주거용 프록시가 성공 요청 1건당 비용 기준으로 약 180배 더 쌀 수 있어요. 2026년 기준 제공업체 가격은 Smartproxy $7/GB, Bright Data $8.40/GB, Oxylabs $8/GB 수준이에요.
Playwright에 프록시를 붙이려면 이렇게 해요:
browser = await pw.chromium.launch(
proxy={"server": "http://proxy-host:port",
"username": "user", "password": "pass"}
)
쿠키 동의 팝업 처리하기
사용자들은 "I agree to terms" 팝업이 제일 거슬린다고 자주 말해요. "처음에 Google이 'I agree to terms and conditions' 팝업을 띄울 것이다" 하는 식이죠. 제일 깔끔한 해법은 3단계처럼 SOCS 쿠키를 미리 심는 거예요. 그래도 안 되면 직접 눌러 넘겨요:
try:
accept_btn = page.locator('button:has-text("Accept all")')
if await accept_btn.is_visible(timeout=3000):
await accept_btn.click()
await page.wait_for_timeout(1000)
except Exception:
pass # No popup present
참고로 버튼 문구는 지역마다 달라요. 독일어는 "Alle akzeptieren", 프랑스어는 "Tout accepter"예요.
안티봇 빠른 참고표
| 기술 | 난이도 | 효과 | 코드 필요 여부 |
|---|---|---|---|
| 무작위 지연(2~7초) | 낮음 | 중간 | 2줄 |
| User-agent 로테이션 | 낮음 | 중간 | 5줄 |
| 헤드리스 탐지 우회 | 중간 | 높음 | Playwright 실행 인자 |
| playwright-stealth 플러그인 | 중간 | 기본 사이트에서 60~80% | pip 설치 |
| 프록시 로테이션(데이터센터) | 중간 | 중간 | 설정 필요 |
| 프록시 로테이션(주거용) | 중간 | 85~95% 성공 | 설정 필요 |
| 쿠키 동의 사전 설정(SOCS) | 낮음 | 필수 | 1줄 |
권장 안전 속도는 IP 로테이션을 쓰면서 요청 간격을 10~20초 두는 거예요. Google 임계치는 IP당 분당 100회쯤이고, 하루 1,000회 넘게 이어지면 일시 차단이 걸릴 수 있어요.
Google Flights 선택자가 계속 깨지는 이유와 해결 방법
이게 단연 가장 큰 골칫거리예요. 포럼에는 "결과가 전부 빈 리스트 14개뿐이다" 같은 글이 넘쳐나요. 대부분 튜토리얼은 선택자만 알려 줄 뿐, 왜 깨지는지는 말 안 해요.
왜 Google Flights 선택자가 바뀌는가
이유는 크게 셋이에요.
-
Closure Compiler 난독화. Google은 Closure Stylesheets로
goog.setCssNameMapping()을 돌려BVAVmf,YMlIz같은 클래스명을 찍어 내요. 이 값은 빌드마다 달라지고, 때론 매주 바뀌어요. -
A/B 테스트. 사용자마다 다른 HTML 구조가 동시에 굴러가요. 내 환경에선 잘 되는데 다른 지역에선 막힐 수 있어요.
-
로케일 차이. EU 사용자는 미국 사용자와 다른 용어, 레이아웃, 데이터 필드를 봐요.
더 튼튼한 선택자 작성하기
겉모습보다 의미에 묶인 선택자를 먼저 골라요:
# Fragile — breaks on every build
price_el = await card.query_selector("div.BVAVmf > div.YMlIz")
# More resilient — tied to accessibility labels
dep_el = await card.query_selector('span[aria-label*="Departure time"]')
# Also resilient — text-based matching
more_btn = page.locator('button:has-text("Show more flights")')
선택자 안정성 순서(가장 안정적 → 가장 불안정):
aria-label속성 — 접근성 목적이라 잘 바뀌지 않음data-*속성 — 기능 목적의 명시적 속성role속성 — ARIA 역할은 의미 기반- 텍스트 기반 선택자 — 화면에 보이는 문구와 일치
- 클래스명 부분 일치 — 예:
[class*="price"] - 난독화된 전체 클래스명 — 가능하면 피하세요
검증 함수 추가하기
선택자가 깨져서 빈 데이터가 조용히 쌓이는 일은 막아야 해요. 초반에 잡아내요:
import logging
logger = logging.getLogger(__name__)
def validate_flight(data: FlightData) -> bool:
required = ["airline", "price", "departure_time",
"arrival_time", "duration"]
valid = True
for field_name in required:
if not getattr(data, field_name, ""):
logger.warning(
f"Missing '{field_name}' — selectors may need updating"
)
valid = False
return valid
긁은 항공편마다 이 검증을 돌려요. 경고가 뜨기 시작하면 페이지 구조를 다시 보고 선택자를 손볼 때예요.
선택자 유지보수 전략
- 매달 한 번, 아니면 결과 품질이 떨어지면 즉시 선택자 점검
- 선택자는 별도 설정 딕셔너리로 빼 두면 수정이 쉬움
- 이 글의 선택자는 2026년 4월 기준으로 마지막 검증됨
- 대안으로 fast-flights 라이브러리도 고려해 볼 만함. CSS 선택자 대신 Protobuf 디코딩을 써서 이 문제를 비켜 가지만, Google이 내부 형식을 바꾸면 역시 영향을 받음
1회성 스크래핑에서 자동 Google Flights 가격 추적기로 확장하기
대부분 튜토리얼은 "JSON으로 저장하기"에서 멈춰요. 하지만 이 글 제목엔 "Price Alerts"가 있죠. 이제 그 부분을 채워 봐요.

스크래퍼를 자동 실행되도록 예약하기
옵션 1: Python schedule 라이브러리(가장 간단하고 플랫폼을 안 가려요):
import schedule
import time
def run_scraper():
asyncio.run(main())
schedule.every().day.at("06:00").do(run_scraper)
schedule.every().day.at("18:00").do(run_scraper)
while True:
schedule.run_pending()
time.sleep(60)
옵션 2: cron 작업(Linux/Mac):
# Run at 6 AM and 6 PM daily
0 6,18 * * * cd /path/to/scraper && python scraper.py
옵션 3: Windows 작업 스케줄러 — 원하는 일정으로 python scraper.py를 돌리는 기본 작업을 만들어요.
이 방식들엔 공통 전제가 하나 있어요. 늘 켜진 머신이 있어야 한다는 점이에요. 노트북이 잠들면 스크래핑도 같이 놓쳐요.
과거 가격 데이터 저장하기
JSON 파일을 덮어쓰는 대신, SQLite에 쌓아 두도록 바꿔요:
import sqlite3
from datetime import datetime
def init_db():
conn = sqlite3.connect("flights.db")
conn.execute("""
CREATE TABLE IF NOT EXISTS flights (
id INTEGER PRIMARY KEY AUTOINCREMENT,
scrape_date TEXT NOT NULL,
route TEXT NOT NULL,
airline TEXT,
departure_time TEXT,
arrival_time TEXT,
duration TEXT,
stops TEXT,
price_usd REAL,
co2_emissions TEXT
)
""")
conn.execute(
"CREATE INDEX IF NOT EXISTS idx_route_date "
"ON flights(route, scrape_date)"
)
conn.commit()
return conn
def save_flight(conn, route: str, flight: FlightData):
price_num = float(
flight.price.replace("$", "").replace(",", "")
) if flight.price else None
conn.execute(
"INSERT INTO flights "
"(scrape_date, route, airline, departure_time, "
"arrival_time, duration, stops, price_usd, co2_emissions) "
"VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)",
(datetime.now().isoformat(), route, flight.airline,
flight.departure_time, flight.arrival_time,
flight.duration, flight.stops, price_num,
flight.co2_emissions)
)
conn.commit()
일주일 동안 하루 두 번씩 긁으면, 이제 가격 흐름이 눈에 들어올 만큼 데이터가 쌓여요.
가격 추세 분석하고 알림 설정하기
쌓인 데이터에서 가장 싼 항공권을 찾아봐요:
import pandas as pd
import sqlite3
conn = sqlite3.connect("flights.db")
df = pd.read_sql_query(
"SELECT * FROM flights WHERE route = 'SFO-JFK'", conn
)
summary = df.groupby("scrape_date")["price_usd"].agg(
["min", "max", "mean"]
)
cheapest = df.loc[df["price_usd"].idxmin()]
print(
f"Cheapest: ${cheapest['price_usd']:.0f} on "
f"{cheapest['scrape_date']} ({cheapest['airline']})"
)
가격이 설정한 기준 아래로 떨어지면 이메일 알림을 쏴요:
import smtplib
from email.mime.text import MIMEText
def send_price_alert(route, price, threshold, recipient):
msg = MIMEText(
f"Price drop alert! {route}: ${price:.0f} "
f"(below your ${threshold:.0f} threshold)"
)
msg["Subject"] = f"Flight Deal: {route} at ${price:.0f}"
msg["From"] = "alerts@example.com"
msg["To"] = recipient
with smtplib.SMTP_SSL("smtp.gmail.com", 465) as server:
server.login("alerts@example.com", "your_app_password")
server.send_message(msg)
# After each scrape, check for deals
min_price = df["price_usd"].min()
threshold = 250
if min_price < threshold:
send_price_alert("SFO-JFK", min_price, threshold,
"you@email.com")
권장 스크래핑 빈도는 이래요. 개인 가격 추적이면 하루 두 번이면 충분하고, 탐지 위험도 낮아요(시간은 랜덤으로 두는 게 좋아요). 비즈니스 모니터링이면 4~6시간마다가 적당해요. 시간당 스크래핑은 단기 세일 기간에만, 그것도 잠깐만 쓰는 게 좋아요.
쉬운 방법: Thunderbit의 Scheduled Scraper
cron 작업, 늘 켜진 서버, 프록시 설정을 직접 챙기는 게 너무 번거롭다면, Thunderbit의 Scheduled Scraper가 같은 일을 훨씬 가볍게 해 줘요. 스크래핑 간격을 평범한 말로 적고, Google Flights URL만 넣으면 돼요. 그러면 Thunderbit 클라우드 인프라에서 알아서 돌고, 안티봇 대응도 내장돼 있으며, 결과는 Google Sheets, Excel, Airtable로 바로 빠져나가요. 물론 Python 방식을 100% 대신하진 못해요(커스터마이징은 줄어요). 그래도 목표가 "가격 추적용 스프레드시트"라면 가장 빠른 길이에요. 무료 요금제로도 돌려 볼 수 있어요.
Python이 과할 때: 노코드로 Google Flights 스크래핑하는 방법
여기까지 다 구현하고 나니 솔직히 말씀드리면, 들어가는 부품이 꽤 많아요. 누구나 이만큼 제어가 필요한 건 아니에요. 선택자는 깨지고, 프록시는 돌려야 하고, cron은 점검해야 하죠. 목표가 그냥 "항공권 값을 정기적으로 스프레드시트에 넣는 것"이라면 더 빠른 길이 있어요.
비교: 직접 만드는 Python 방식 vs. API 서비스 vs. Thunderbit
| 접근 방식 | 설정 시간 | 코딩 필요 여부 | 안티봇 대응 | 스케줄링 | 비용 |
|---|---|---|---|---|---|
| DIY Playwright(이 튜토리얼) | 1~2시간 | Python(중급) | 수동 설정 | 수동(cron) | 무료 + 프록시 비용 |
| SerpApi Google Flights 엔드포인트 | 15분 | API 호출만 | 처리됨 | API 통해 가능 | 월 약 $50+ |
| Thunderbit Chrome 확장 프로그램 | 2분 | 없음 | 클라우드 스크래핑 | 내장 스케줄러 | 무료 요금제 제공 |
SerpApi에 한마디 덧붙이면, Google은 2025년 12월 SerpApi를 상대로 DMCA 소송을 냈어요. 2년 동안 요청량이 25,000% 늘었다고 주장했고요. API 제공업체를 따져 보는 중이라면 이런 법적 불확실성도 챙겨 볼 가치가 있어요.
Thunderbit로 Google Flights를 스크래핑하는 방법
Chrome에서 Google Flights 검색 결과를 연 뒤 Thunderbit의 "AI 필드 추천" 버튼을 누르면, AI가 페이지를 읽고 항공사, 가격, 출발 시간, 경유 횟수 같은 열을 제안해요. 제안된 필드를 확인하고 "스크래핑"을 누르면 끝이에요. 결과는 표로 뜨고, Excel, Google Sheets, Airtable, Notion으로 바로 빠져요. 무료 요금제에서도 가능해요.
특히 가격 추적이라면 Thunderbit의 Scheduled Scraper와 Cloud Scraping이(한 번에 50페이지를 처리해요) cron + 프록시 + 서버 인프라 전체를 갈음해요.
Python은 완전한 제어와 끝없는 커스터마이징을 줘요. Thunderbit은 빠르고 유지보수가 거의 없다는 게 강점이고요. 진짜 목적에 맞춰 고르면 돼요. 노코드 스크래핑을 더 보고 싶으면 최고의 노코드 웹 스크래퍼 가이드를 참고하세요.

Google Flights 스크래핑은 합법일까? 알아야 할 것들
포럼에 이런 질문이 자주 올라와요. "Google Flights를 직접 스크래핑하면 Google 이용약관을 어기는 거 아닌가요?" API가 닫혔고 공식 대안도 없으니, 충분히 걱정될 만해요.
약관 위반과 법적 책임은 같은가?
Google 서비스 약관(2024년 5월 22일 업데이트)에는 사용자가 "자동화된 수단(예: 로봇, 스파이더, 스크래퍼)을 사용해 서비스나 콘텐츠에 접근하거나 사용해서는 안 된다"고 적혀 있어요. 약관 위반은 계약 위반(민사 문제)일 뿐, 곧장 불법과 같지는 않아요.
중요한 판례는 hiQ v. LinkedIn(제9순회항소법원, 2022년)이에요. 공개 데이터 스크래핑이 Computer Fraud and Abuse Act(CFAA)를 어기지 않는다고 봤죠. 다만 이 사건은 합의로 끝났고, Google이 2025년 12월 SerpApi에 건 소송은 DMCA 1201조(기술적 보호조치 우회)라는 다른 법적 이론을 들고 있어 더 무겁게 볼 여지가 있어요.
책임 있는 스크래핑을 위한 모범 사례
- 요청 속도를 제한하세요 — IP 로테이션과 함께 10~20초 지연
- 개인 정보를 수집하지 마세요 — 항공권 가격은 공개된 집계 데이터예요
- CAPTCHA를 프로그램으로 우회하지 마세요(DMCA 리스크 영역이에요)
- 데이터는 개인 연구용으로만 사용하고, 적절한 라이선스 없이 경쟁 상용 제품에 쓰지 마세요
- 가능하면 공식 API를 고려하세요
대체 데이터 소스
스크래핑이 너무 부담스럽다면, 합법적인 API 선택지도 있어요:
| 제공업체 | 비용 | 무료 요금제 | 참고사항 |
|---|---|---|---|
| SerpApi | 월 $75~$3,750+ | 월 250회 검색 | Google Flights JSON 직접 제공(법적 검토 중) |
| Kiwi Tequila | 무료(제휴 모델) | 무제한 | 스타트업과 테스트에 적합 |
| Amadeus | 사용량 기반 | 월 2,000회 요청 | 400개 이상의 항공사, 예약 기능 포함 |
| Skyscanner | 맞춤형 | 승인 필요 | 52개 시장, 30개 언어 |
더 깊이 알고 싶으면 웹 스크래핑의 법적 이슈 분석 글도 참고하세요.
결론 및 핵심 요약
내용이 많았어요. 핵심만 추리면 이래요:
- Python + Playwright는 Google Flights 스크래핑에 가장 유연하지만, 꾸준한 유지보수가 따라와요
- 안티봇 대응(지연, user-agent 로테이션, 주거용 프록시)은 선택이 아니라 필수예요. SearchGuard 이후엔 더 그래요
- 선택자는 자주 깨져요 — 가능하면
aria-label과 텍스트 기반 선택자를 쓰고, 출력을 검증하고, 유지보수 일정을 잡아 둬요 schedule이나 cron으로 자동화하면 1회성 스크래핑을 진짜 가격 추적기와 이메일 알림으로 바꿀 수 있어요- Thunderbit는 노코드 대안으로, 내장 스케줄링·클라우드 스크래핑·안티봇 대응을 갖췄어요. 코딩 프로젝트보다 가격 추적용 스프레드시트가 목표라면 특히 잘 맞아요
- 법적 경계를 존중하세요 — 요청 속도를 제한하고, 공개 데이터만 모으고, 상업적 사용엔 API 대안을 검토해요
이 튜토리얼 코드를 그대로 가져다 써도 되고, 빠르게 가고 싶으면 Thunderbit Chrome 확장 프로그램을 깔아도 돼요. 어느 쪽이든 이제 새로고침만 반복하지 않고 항공권 값을 직접 추적할 수 있어요.
Python 스크래핑 기법을 더 배우고 싶으면 Python으로 웹 스크래핑하는 방법과 최고의 Python 웹 스크래핑 도구 가이드를 확인해 보세요.
자주 묻는 질문
1. Python 없이 Google Flights를 스크래핑할 수 있나요?
네. SerpApi와 Kiwi Tequila 같은 API 서비스는 브라우저 자동화 없이 API 호출만으로 구조화된 항공편 데이터를 줘요. 완전한 노코드라면 Thunderbit Chrome 확장 프로그램으로 브라우저 안에서 Google Flights 결과를 바로 긁고, AI가 제안한 필드로 한 번에 내보낼 수 있어요.
2. Google은 항공권 스크래핑을 차단하나요?
Google은 봇 탐지(SearchGuard), CAPTCHA, 속도 제한을 써요. 그래도 무작위 지연, user-agent 로테이션, 주거용 프록시, stealth 브라우저 설정 같은 안티봇 대응을 제대로 얹으면 중간 규모에선 비교적 안정적으로 긁을 수 있어요. 구체적인 기법과 기준은 위 안티봇 섹션을 참고하세요.
3. 가격 추적용으로 Google Flights를 얼마나 자주 스크래핑해야 하나요?
개인 가격 확인이면 하루 두 번, 시간은 랜덤으로 두면 충분하고 탐지 위험도 낮아요. 비즈니스 모니터링이면 4~6시간마다, 프록시 로테이션과 함께 돌리세요. 단기 특가 기간이 아니라면 시간당 스크래핑은 피하는 게 좋아요. 차단 위험이 확 올라가요.
4. 무료 Google Flights API가 있나요?
공식 Google QPX Express API는 2018년 4월 종료됐고, 무료 공식 대체 서비스는 없어요. 가장 가까운 무료 옵션은 Kiwi Tequila API로, 제휴 모델이며 검색은 무제한이에요. SerpApi는 월 250회 무료 검색을 줘요. 대부분 사용자에겐 스크래핑이나 Thunderbit 같은 노코드 도구가 현실적인 선택이에요.
5. Google Flights CSS 선택자가 왜 계속 빈 데이터만 반환하나요?
Google은 Closure Compiler로 난독화된 클래스명을 찍어 내고, 이 값은 빌드마다 바뀌어요. A/B 테스트와 로케일 차이도 사용자마다 HTML 구조를 다르게 만들죠. 해법은 클래스명 대신 aria-label 속성과 텍스트 기반 선택자를 쓰고, 검증 함수를 넣어 오류를 일찍 잡고, 매달 선택자를 점검하는 거예요. 자세한 전략은 선택자 유지보수 섹션을 참고하세요.
더 알아보기


