Парсинг Google Flights на Python: от кода к оповещениям о ценах

Последнее обновление: April 16, 2026
Парсинг Google Flights на Python: от кода к оповещениям о ценах

Google закрыла свой Flights API ещё в 2018 году, а цены на авиабилеты по-прежнему меняются — до 17 раз за 48 часов по одному внутреннему маршруту. Если тебе нужен программный доступ к этим данным, парсинг по сути остаётся единственным реальным вариантом.

Я потратил немало времени на тестирование разных способов получения данных о перелётах из Google, и ситуация сильно изменилась — особенно после того, как Google внедрила SearchGuard в январе 2025 года. В этом руководстве я покажу, как собрать рабочий Python-скрипт для Google Flights с помощью Playwright, как обойти антибот-защиту, которая ломает большинство решений, а затем как превратить всё это в автоматический трекер цен с уведомлениями. Если ты не хочешь писать код, я также покажу вариант без кода с Thunderbit, который даёт тот же результат примерно за две минуты.

Зачем парсить Google Flights на Python?

Google Flights — один из лидеров в поиске авиабилетов. На мобильных устройствах в США его видимость взлетела до 47,6%, обойдя все крупные OTA-сервисы. Рынок туристического метапоиска, на котором он работает, оценивается в $8,33 млрд в 2024 году и растёт на 30,2% CAGR. Но после того как QPX Express API навсегда отключили 10 апреля 2018 года, официального способа получать эти данные программно больше нет.

При этом цены на авиабилеты могут колебаться до 50% даже на одном и том же маршруте, а средний разброс между минимальной и максимальной ценой составляет около $20. Такие авиакомпании, как Delta, используют 77 тарифных корзин для динамического ценообразования. Средняя цена перелёта туда-обратно по США в начале 2026 года составляет $408, а стоимость билетов на 14,9% выше, чем годом ранее.

Доминирующая платформа, отсутствие API, нестабильные цены — именно поэтому парсинг Google Flights на Python стал одним из самых популярных проектов на GitHub и в туристических сообществах.

Вот кому это полезно и зачем:

Тип пользователяСценарий использованияГлавная выгода
Индивидуальные путешественникиОтслеживать цены по конкретным маршрутамЭкономия в среднем $50 на перелёт
Туристические агентстваАнализ конкурентных ценМониторинг паритета тарифов в реальном времени
Корпоративные travel-командыОптимизация расходов по маршрутамЭкономия 10–30% на деловых поездках
РазработчикиСоздавать приложения для сравнения тарифовПрограммный доступ к ценовым данным
ИсследователиАнализ волатильности цен авиакомпанийАкадемические и рыночные исследования

Пользователи форумов говорят об этом прямо: «Google Flights API был отключён, значит, нужно использовать web scraping» — такая мысль встречается снова и снова. И отдача от этого реальная — Hopper утверждает, что точность прогнозов достигает 95% при анализе более 5 млрд ценовых предложений в день, а данные Expedia за 2026 год показывают, что бронирование за 8–15 дней до вылета экономит около $25 на внутренних рейсах.

Какие данные можно получить из Google Flights?

Страница результатов Google Flights содержит на удивление богатый набор полей. Обычно доступны такие данные:

  • Название авиакомпании (и логотип)
  • Время вылета и код аэропорта
  • Время прибытия и код аэропорта
  • Общая длительность перелёта
  • Количество пересадок и сведения о стыковках (аэропорт, длительность, ночная ли)
  • Цена билета (в зависимости от валюты)
  • Выбросы CO2 (кг CO2e, с процентной разницей относительно типичных рейсов)
  • Класс обслуживания, номер рейса, модель самолёта
  • Параметры пространства для ног
  • Дополнительные удобства (Wi‑Fi, розетки, потоковое видео)
  • Индикатор ценового уровня (низкий/обычный/высокий)
  • Предупреждения о задержках (например, «часто задерживается более чем на 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 — автоматизация headless-браузера, обработка JavaScript и встроенные механизмы ожидания
  • playwright-stealth — маскирует типичные сигналы бота
  • pandas — для анализа данных и последующего экспорта в CSV

Почему Playwright лучше Selenium или requests

Google Flights не работает только с requests + BeautifulSoup — весь контент страницы рендерится JavaScript’ом. Нужен настоящий браузер.

ФункцияPlaywrightSeleniumrequests + BS4
Отрисовка JSПолная поддержкаПолная поддержкаНет
СкоростьНа 42% быстрее в среднемБазовый уровеньНеприменимо
Поддержка asyncНативнаяТолько последовательный режимНеприменимо
Потребление памятиНа 30% меньшеВышеМинимальное
Обход антибот-защитыХорошо (со stealth)Легче обнаружитьНеприменимо

Playwright быстрее, современнее и лучше работает с async. Для Google Flights это очевидный фаворит.

Пошагово: как парсить Google Flights на Python

Это основная часть руководства. Мы соберём парсер поэтапно.

google-flights-scraping-workflow.webp

Шаг 1: Определите классы данных

Начни со структуры параметров поиска и данных о рейсе через dataclass’ы Python. Это сделает код чище и упростит дальнейшее расширение.

from dataclasses import dataclass, field
from typing import Optional, List

@dataclass
class SearchParams:
    origin: str          # например, "SFO"
    destination: str     # например, "JFK"
    departure_date: str  # например, "2026-05-15"
    return_date: Optional[str] = None
    trip_type: str = "one-way"  # "one-way" или "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: Разберись со структурой URL Google Flights

Google Flights кодирует параметры поиска с помощью Base64-encoded Protobuf в URL-параметре tfs. Можно попробовать reverse engineering этого формата, а можно пойти более простым путём и сформировать 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 меняет внутренний формат. Такие библиотеки, как fast-flights на GitHub, используют декодирование Protobuf и обходят HTML-парсинг полностью, но это уже более продвинутый подход.

Шаг 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",
        )
        # Заранее задаём cookie согласия, чтобы пропустить всплывающее окно
        await context.add_cookies([{
            "name": "SOCS",
            "value": "CAESHwgBEhJnd3NfMjAyNTAyMjctMF9SQzIaBXpoLUNOIAEaBgiAy6O-Bg",
            "domain": ".google.com",
            "path": "/"
        }])
        page = await context.new_page()

Мы запускаем headless-режим для продакшена (для отладки переключи на headless=False), задаём реалистичные viewport и user-agent, а также заранее ставим cookie SOCS, чтобы пропустить окно согласия — об этом подробнее в разделе про антибот-защиту.

Шаг 4: Перейди к результатам поиска

Загрузи сформированный URL и дождись появления результатов:

        url = build_flights_url(
            params.origin, params.destination, params.departure_date
        )
        await page.goto(url, wait_until="networkidle")

        # Ждём, пока загрузятся результаты рейсов
        await page.wait_for_selector(
            "li.pIav2d", timeout=15000
        )

Если здесь возникает таймаут, обычно причина одна из двух: либо всплывающее окно согласия блокирует страницу (см. исправление через cookie в шаге 3), либо Google выдал CAPTCHA. Оба случая мы разберём ниже.

Шаг 5: Подгрузи все результаты

Google Flights скрывает дополнительные варианты за кнопкой «Show more flights». Нужно нажимать её, пока не станут видны все рейсы:

        # Нажимаем "Show more flights", пока не загрузятся все результаты
        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 года — ниже в разделе поддержки мы ещё вернёмся к тому, почему дата проверки так важна:

        flights = []
        cards = await page.query_selector_all("li.pIav2d")

        for card in cards:
            flight = FlightData()

            # Название авиакомпании
            airline_el = await card.query_selector(
                "div.sSHqwe span:not([class])"
            )
            if airline_el:
                flight.airline = (await airline_el.inner_text()).strip()

            # Время вылета
            dep_el = await card.query_selector(
                'span[aria-label*="Departure time"]'
            )
            if dep_el:
                flight.departure_time = (await dep_el.inner_text()).strip()

            # Время прибытия
            arr_el = await card.query_selector(
                'span[aria-label*="Arrival time"]'
            )
            if arr_el:
                flight.arrival_time = (await arr_el.inner_text()).strip()

            # Длительность
            dur_el = await card.query_selector("div.gvkrdb")
            if dur_el:
                flight.duration = (await dur_el.inner_text()).strip()

            # Пересадки
            stops_el = await card.query_selector("div.EfT7Ae span")
            if stops_el:
                flight.stops = (await stops_el.inner_text()).strip()

            # Цена
            price_el = await card.query_selector(
                "div.FpEdX span"
            )
            if price_el:
                flight.price = (await price_el.inner_text()).strip()

            # Выбросы CO2
            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 генерируются Closure Compiler от Google и могут измениться в любой момент. Селекторы на основе 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)

    # Также сохраняем в 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 обычно возвращает 30–80 вариантов и занимает около 15–20 секунд.

Антибот-гайд для парсинга Google Flights

Большинство туториалов заканчиваются здесь. Большинство парсеров — тоже. В январе 2025 года Google запустила SearchGuard, и это за одну ночь сломало почти все SERP-парсеры. Google описывает это как «результат десятков тысяч человеко-часов и миллионов долларов инвестиций». По оценке scraperly.com, сложность парсинга Google Flights — 4 из 5.

Ни один материал конкурентов не разбирает это глубоко, хотя именно из-за этого у людей чаще всего перестают работать парсеры. Вот с чем ты столкнёшься и как это обходить.

anti-bot-survival-guide.webp

Случайные задержки между запросами

Самая простая защита от rate limiting. Две строки кода, средняя эффективность:

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)

Обход headless-детекта

Google проверяет флаг navigator.webdriver и другие признаки автоматизации. Библиотека playwright-stealth закрывает большую часть таких сигналов, но также стоит задать launch arguments, показанные в шаге 3. Ключевые флаги:

args=[
    "--disable-blink-features=AutomationControlled",
    "--disable-dev-shm-usage",
    "--no-first-run",
]

Это помогает пройти базовую проверку. SearchGuard идёт глубже — анализирует скорость движения мыши, задержки ввода с клавиатуры и паттерны прокрутки, — но при умеренных объёмах парсинга stealth-режим плюс реалистичные задержки обычно хватает.

Ротация прокси: дата-центры vs. residential

Если ты делаешь больше нескольких запросов, понадобятся прокси. Разница существенная:

ХарактеристикаДата-центровые проксиResidential-прокси
Скорость100–1,000 Mbps10–100 Mbps
Процент успешных запросов (Google)20–40%85–95%
Стоимость$0.10–$0.50/IP в месяц$5–$15/GB
Риск обнаруженияВысокийОчень низкий

Residential-прокси примерно в 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"}
)

Обработка всплывающего окна согласия с cookie

Пользователи постоянно жалуются на окно «I agree to terms» как на блокирующий фактор: «сначала Google покажет popup с согласием на условия». Самый чистый способ — заранее установить cookie SOCS (как показано в шаге 3). Если это не сработает, просто нажми кнопку вручную:

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  # Всплывающего окна нет

Примечание: текст кнопки зависит от локали — в немецкой версии это «Alle akzeptieren», во французской — «Tout accepter».

Краткая шпаргалка по антибот-методам

МетодСложностьЭффективностьНужен код?
Случайные задержки (2–7 сек)НизкаяСредняя2 строки
Ротация user-agentНизкаяСредняя5 строк
Обход headless-детектаСредняяВысокаяlaunch args Playwright
Плагин playwright-stealthСредняя60–80% на базовых сайтахpip install
Ротация прокси (дата-центр)СредняяСредняяКонфиг
Ротация прокси (residential)Средняя85–95% успехаКонфиг
Предустановка cookie согласия (SOCS)НизкаяОбязательно1 строка

Рекомендованная безопасная частота: держи паузу 10–20 секунд между запросами при ротации IP. Порог Google примерно 100 запросов в минуту на IP до получения 429, а при устойчивой нагрузке свыше 1,000 запросов в день на IP могут включиться временные блокировки.

Почему ваши селекторы Google Flights постоянно ломаются и как это исправить

Это, безусловно, главная боль. На форумах полно сообщений в духе «в ответ я получаю только 14 пустых списков». Каждый туториал даёт селекторы. Но почти никто не объясняет, почему они ломаются.

Почему селекторы Google Flights меняются

Причин три:

  1. Обфускация через Closure Compiler. Google использует Closure Stylesheets для генерации классов вроде BVAVmf и YMlIz через goog.setCssNameMapping(). Они меняются при каждом билде — иногда еженедельно.

  2. A/B-тестирование. Разным пользователям одновременно показываются разные HTML-структуры. Твой парсер может работать у тебя и падать у человека в другом регионе.

  3. Различия локалей. Пользователи из ЕС видят другие термины, другой layout и даже другие поля данных, чем пользователи из США.

Пиши устойчивые селекторы

Предпочитай селекторы, привязанные к смыслу, а не к внешнему виду:

# Хрупкий вариант — ломается при каждом билде
price_el = await card.query_selector("div.BVAVmf > div.YMlIz")

# Более устойчивый — завязан на accessibility labels
dep_el = await card.query_selector('span[aria-label*="Departure time"]')

# Тоже устойчивый — поиск по тексту
more_btn = page.locator('button:has-text("Show more flights")')

Иерархия устойчивости селекторов (от самых надёжных к самым слабым):

  1. Атрибуты aria-label — связаны с доступностью, меняются редко
  2. Атрибуты data-* — добавляются специально для функциональности
  3. Атрибуты role — семантические роли ARIA
  4. Текстовые селекторы — по видимому контенту
  5. Поиск по части class — например, [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"Не найдено значение '{field_name}' — возможно, селекторы нужно обновить"
            )
            valid = False
    return valid

Проверяй этим каждый результат. Если начали появляться предупреждения, пора открыть страницу и обновить селекторы.

Стратегия поддержки селекторов

  • Проверяй селекторы ежемесячно или сразу, если качество данных падает
  • Храни селекторы отдельно, в конфиге, чтобы их было легко менять
  • Селекторы в этой статье последний раз проверялись: апрель 2026
  • В качестве альтернативы рассмотрите библиотеку fast-flights — она использует декодирование Protobuf вместо CSS-селекторов, полностью обходя эту проблему (хотя у неё тоже есть уязвимость, если Google изменит внутренний формат данных)

От одноразового парсинга к автоматическому трекеру цен Google Flights

Большинство туториалов заканчиваются на «сохранить в JSON». Но в заголовке этой статьи обещаны «уведомления о ценах». Пора выполнить обещание.

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 job (Linux/Mac):

# Запуск ежедневно в 6:00 и 18:00
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['price_usd']:.0f} на "
    f"{cheapest['scrape_date']} ({cheapest['airline']})"
)

Отправляй email-уведомление, когда цена опускается ниже заданного порога:

import smtplib
from email.mime.text import MIMEText

def send_price_alert(route, price, threshold, recipient):
    msg = MIMEText(
        f"Цена упала! {route}: ${price:.0f} "
        f"(ниже вашего порога ${threshold:.0f})"
    )
    msg["Subject"] = f"Предложение на рейс: {route} за ${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)

# После каждого парсинга проверяем наличие выгодных цен
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 часов с ротацией прокси. Каждый час — только в период краткосрочных распродаж и лишь на короткое время.

Простой путь: Scheduled Scraper от Thunderbit

Если управлять cron-задачами, постоянно включённым сервером и прокси-конфигурациями кажется тебе слишком большой инфраструктурой, Scheduled Scraper в Thunderbit решает ту же задачу без всей этой нагрузки. Ты описываешь интервал парсинга простыми словами, указываешь URL Google Flights, и скриптер запускается автоматически на облачной инфраструктуре Thunderbit — с уже встроенной антибот-обработкой и экспортом результатов прямо в Google Sheets, Excel или Airtable. Это не замена полноценному Python-подходу (ты теряешь гибкость), но для задачи «нужна таблица для отслеживания цен» это самый быстрый путь. Попробовать можно на бесплатном тарифе.

Когда Python — это избыточно: no-code способы парсить Google Flights

После того как ты всё это соберёшь, скажу честно: компонентов много. Не всем нужен такой уровень контроля. Селекторы ломаются, прокси нужно ротировать, cron-задачи надо мониторить. Если твоя цель — просто регулярно получать цены на билеты в таблицу, есть более быстрые варианты.

Сравнение: Python своими руками vs. API-сервисы vs. Thunderbit

ПодходВремя настройкиНужен кодОбработка антибот-защитыРасписаниеСтоимость
DIY Playwright (этот туториал)1–2 часаPython (средний уровень)ВручнуюВручную (cron)Бесплатно + стоимость прокси
Серверный endpoint SerpApi для Google Flights15 минТолько API-запросыУже решеноЧерез APIОт ~$50/мес
Thunderbit Chrome Extension2 минНе нуженОблачный парсингВстроенный планировщикЕсть бесплатный тариф

Небольшое замечание про SerpApi: в декабре 2025 года Google подал против SerpApi иск по DMCA, утверждая, что объём их запросов вырос на 25 000% за два года. Если ты выбираешь API-провайдера, эту правовую неопределённость стоит учитывать.

Как Thunderbit парсит Google Flights

Открой результаты поиска Google Flights в Chrome, нажми кнопку Thunderbit «AI Suggest Fields» — ИИ прочитает страницу и предложит колонки вроде авиакомпании, цены, времени вылета и пересадок — проверь предложенные поля и нажми «Scrape». Результаты появятся в таблице, которую можно экспортировать в Excel, Google Sheets, Airtable или Notion — всё это доступно на бесплатном тарифе.

Для задачи отслеживания цен особенно полезны Scheduled Scraper и Cloud Scraping от Thunderbit, который умеет обрабатывать до 50 страниц одновременно. Они полностью заменяют всю инфраструктуру из cron, прокси и серверов.

Python даёт полный контроль и практически безграничную кастомизацию. Thunderbit даёт скорость и нулевое обслуживание. Выбирай исходя из своей реальной задачи. Если хочешь узнать больше о no-code-подходах, посмотри наш гид по лучшим no-code web scraper’ам.

flight-data-options-comparison.webp

Законно ли парсить Google Flights? Что важно знать

Пользователи форумов часто задают этот вопрос: «прямой парсинг Google Flights нарушает условия использования Google». Опасение понятное — особенно с учётом того, что API больше нет и легальной альтернативы от Google тоже нет.

Нарушение ToS vs. юридическая ответственность

В условиях использования Google (обновлены 22 мая 2024 года) сказано, что пользователи не должны «получать доступ или использовать сервисы или любой контент с помощью автоматизированных средств (таких как роботы, пауки или скрейперы)». Нарушение ToS — это нарушение договора, то есть гражданско-правовой вопрос; это не то же самое, что нарушение закона.

Ключевой прецедент: hiQ v. LinkedIn (9-й окружной суд, 2022) установил, что парсинг общедоступных данных не нарушает Computer Fraud and Abuse Act (CFAA). Однако дело завершилось мировым соглашением, а декабрьский иск Google 2025 года против SerpApi опирается на другую правовую теорию — DMCA Section 1201 (обход технических средств защиты), — что может быть серьёзнее.

Лучшие практики ответственного парсинга

  • Ограничивай частоту запросов — задержки 10–20 секунд с ротацией IP
  • Не парси персональные данные — цены на билеты являются публично отображаемыми агрегированными данными
  • Не обходи CAPTCHA программно (именно здесь возникает риск по DMCA)
  • Используй данные для личного анализа, а не для создания конкурирующего коммерческого продукта без соответствующей лицензии
  • Рассмотри официальные API, если они доступны

Альтернативные источники данных

Если для твоего сценария парсинг кажется слишком рискованным, есть легальные API-варианты:

ПоставщикСтоимостьБесплатный тарифПримечания
SerpApi$75–$3,750+/мес250 поисков/месJSON по Google Flights напрямую (под юридическим вниманием)
Kiwi TequilaБесплатно (affiliate-модель)БезлимитноЛучший вариант для стартапов и тестов
AmadeusОплата по факту2,000 запросов/месБолее 400 авиакомпаний, есть бронирование
SkyscannerИндивидуальноТребуется одобрение52 рынка, 30 языков

Если хочешь полный контекст, у нас есть более подробный разбор правовых аспектов web scraping.

Итоги и главные выводы

Информации было много. Вот что действительно важно:

  • Python + Playwright — самый гибкий способ парсить Google Flights, но он требует постоянной поддержки
  • Антибот-меры — задержки, ротация user-agent, residential-прокси — не опция, а необходимость, особенно после SearchGuard
  • Селекторы часто ломаются — по возможности используй aria-label и текстовые селекторы, проверяй результат и планируй регулярное обслуживание
  • Автоматизируй через schedule или cron, чтобы превратить одноразовый парсинг в полноценный трекер цен с историей и email-уведомлениями
  • Thunderbit предлагает no-code-альтернативу со встроенным планировщиком, облачным парсингом и обработкой антибот-защиты — идеально, если тебе нужна таблица для отслеживания цен, а не полноценный кодовый проект
  • Соблюдай правовые границы — ограничивай частоту запросов, собирай только публичные данные и рассматривай API-альтернативы для коммерческого использования

Берите код из этого руководства или установи расширение Thunderbit для Chrome, если хочешь пройти путь быстрее. В любом случае ты начнёшь отслеживать цены на авиабилеты вместо того, чтобы вручную обновлять Google Flights.

Больше техник Python-парсинга ты найдёшь в наших гайдах о том, как парсить сайты на Python и о лучших инструментах для web scraping на Python.

FAQ

1. Можно ли парсить Google Flights без Python?

Да. API-сервисы вроде SerpApi и Kiwi Tequila предоставляют структурированные данные о перелётах через API-запросы — без автоматизации браузера. А если нужен полностью no-code-вариант, расширение Thunderbit для Chrome может напрямую парсить результаты Google Flights из браузера с полями, предложенными ИИ, и экспортом в один клик.

2. Блокирует ли Google парсинг авиарейсов?

Google использует детектирование ботов (SearchGuard), CAPTCHA и rate limiting. При правильных антибот-меры — случайных задержках, ротации user-agent, residential-прокси и stealth-настройках браузера — можно стабильно работать на умеренных объёмах. Конкретные техники и пороги смотри в разделе про антибот-защиту выше.

3. Как часто нужно парсить Google Flights для отслеживания цен?

Двух запусков в день в случайное время достаточно для личного мониторинга цен, и это снижает риск детекта. Для бизнес-мониторинга — каждые 4–6 часов с ротацией прокси. Избегай почасового парсинга, кроме краткосрочных распродаж, — это заметно повышает риск блокировки.

4. Есть ли бесплатный Google Flights API?

Официальный Google QPX Express API был отключён в апреле 2018 года. Бесплатной официальной замены нет. Ближе всего к бесплатному варианту — Kiwi Tequila API (affiliate-модель, безлимитные запросы). SerpApi даёт 250 бесплатных поисков в месяц. Для большинства пользователей практичнее либо парсинг, либо no-code-инструмент вроде Thunderbit.

5. Почему мои CSS-селекторы Google Flights постоянно возвращают пустые данные?

Google использует Closure Compiler для генерации обфусцированных имён классов, которые меняются при каждом билде. A/B-тесты и различия локалей тоже приводят к тому, что HTML-структура отличается у разных пользователей. Решение: используй aria-label и текстовые селекторы вместо имён классов, добавь функцию валидации, чтобы быстро ловить поломки, и проверяй селекторы каждый месяц. Подробную стратегию см. в разделе про поддержку селекторов.

Подробнее

Ke
Ke
Технический директор в Thunderbit | Senior Data Scientist и эксперт по ML Имея почти десятилетний опыт в машинном обучении и data science, Кэ Шэнь — выпускник Колумбийского университета и бывший Senior Data Scientist в Walmart Labs. Обладая глубокой, признанной коллегами экспертизой в Python, R, Java и статистике, он делится проверенными на практике наблюдениями о том, как переводить сложные AI-алгоритмы из теории в production-grade архитектуру.
Содержание

Собирай страницу, просто задав вопрос

Скажи, что тебе нужно, простыми словами. А ещё лучше — ничего не говори.

Попробовать Thunderbit бесплатно
Извлекай данные с помощью ИИ
Легко передавай данные в Google Sheets, Airtable или Notion
Chrome Store Rating
PRODUCT HUNT#1 Product of the Week