Python으로 Google Flights 데이터 수집하기: 코드 작성부터 가격 알림까지

최종 업데이트: June 29, 2026
Python으로 Google Flights 데이터 수집하기: 코드 작성부터 가격 알림까지

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로 그려지거든요. 진짜 브라우저가 있어야 해요.

기능PlaywrightSeleniumrequests + BS4
JS 렌더링완전 지원완전 지원없음
속도전체적으로 42% 더 빠름기준점이 용도에서는 해당 없음
비동기 지원기본 지원순차 처리만해당 없음
메모리 사용량30% 더 적음더 높음최소
봇 탐지 회피양호(stealth 사용 시)탐지되기 쉬움해당 없음

Playwright는 더 빠르고, 더 요즘 도구답고, 비동기 처리도 강해요. Google Flights에는 가장 잘 맞는 선택이에요.

단계별 가이드: Python으로 Google Flights 스크래핑하기

이제 본론이에요. 스크래퍼를 한 조각씩 붙여 완성해 봐요.

google-flights-scraping-workflow.webp

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.jsonflights.csv에 결과가 떨어져요. 제 테스트에서는 SFO-JFK 검색에 보통 3080개 항공편이 잡혔고, 전체 실행은 1520초쯤 걸렸어요.

Google Flights 스크래핑을 위한 안티봇 생존 가이드

대부분 튜토리얼은 여기서 끝나요. 그리고 대부분 스크래퍼도 바로 여기서 무너져요. Google은 2025년 1월 SearchGuard를 풀면서 거의 모든 SERP 스크래퍼를 하룻밤에 무력화했어요. Google 표현으로는 "수만 시간 인력과 수백만 달러가 들어간 결과물"이래요. Google Flights는 스크래핑 난이도가 5점 만점에 4점으로 매겨져요.

경쟁 글들은 이 대목을 깊이 안 다루지만, 실제로 스크래퍼가 멈추는 가장 큰 이유가 바로 여기예요. 뭘 상대하는지, 어떻게 받아칠지 살펴봐요.

anti-bot-survival-guide.webp

요청 사이에 무작위 지연 넣기

속도 제한을 피하는 가장 기본기예요. 코드는 두 줄이면 되고, 효과는 중간쯤이에요:

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 Mbps10~100 Mbps
성공률(Google)20~40%85~95%
비용IP당 월 $0.10~$0.50GB당 $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 선택자가 바뀌는가

이유는 크게 셋이에요.

  1. Closure Compiler 난독화. Google은 Closure Stylesheetsgoog.setCssNameMapping()을 돌려 BVAVmf, YMlIz 같은 클래스명을 찍어 내요. 이 값은 빌드마다 달라지고, 때론 매주 바뀌어요.

  2. A/B 테스트. 사용자마다 다른 HTML 구조가 동시에 굴러가요. 내 환경에선 잘 되는데 다른 지역에선 막힐 수 있어요.

  3. 로케일 차이. 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")')

선택자 안정성 순서(가장 안정적 → 가장 불안정):

  1. aria-label 속성 — 접근성 목적이라 잘 바뀌지 않음
  2. data-* 속성 — 기능 목적의 명시적 속성
  3. role 속성 — ARIA 역할은 의미 기반
  4. 텍스트 기반 선택자 — 화면에 보이는 문구와 일치
  5. 클래스명 부분 일치 — 예: [class*="price"]
  6. 난독화된 전체 클래스명 — 가능하면 피하세요

검증 함수 추가하기

선택자가 깨져서 빈 데이터가 조용히 쌓이는 일은 막아야 해요. 초반에 잡아내요:

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"가 있죠. 이제 그 부분을 채워 봐요.

scraper-to-price-tracker-sfo-jfk.webp

스크래퍼를 자동 실행되도록 예약하기

옵션 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은 빠르고 유지보수가 거의 없다는 게 강점이고요. 진짜 목적에 맞춰 고르면 돼요. 노코드 스크래핑을 더 보고 싶으면 최고의 노코드 웹 스크래퍼 가이드를 참고하세요.

flight-data-options-comparison.webp

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 속성과 텍스트 기반 선택자를 쓰고, 검증 함수를 넣어 오류를 일찍 잡고, 매달 선택자를 점검하는 거예요. 자세한 전략은 선택자 유지보수 섹션을 참고하세요.

더 알아보기

Ke
Ke
Thunderbit CTO | 시니어 데이터 사이언티스트 & ML 전문가 머신러닝과 데이터 과학 분야에서 약 10년에 가까운 경험을 쌓아온 Ke Shen은 컬럼비아 대학교 출신이며, 전 Walmart Labs의 시니어 데이터 사이언티스트였습니다. Python, R, Java, 통계 분야에서 동료들에게도 인정받는 깊은 전문성을 바탕으로, 복잡한 AI 알고리즘을 이론에서 실제 운영 수준의 아키텍처로 전환하는 데 필요한 실전 인사이트를 공유합니다.
목차

말 한마디로 웹페이지를 스크래핑하세요

원하는 걸 평범한 영어로 말하면 됩니다. 아니면 아무 말도 안 해도 괜찮아요.

Thunderbit 사용해보기 무료
AI로 데이터 추출하기
Google Sheets, Airtable, Notion으로 데이터를 손쉽게 منتقل하세요
Chrome Store Rating
PRODUCT HUNT#1 Product of the Week