Google Flights, это отправная точка для большинства путешественников, сравнивающих тарифы перед бронированием. Сервис показывает конкурирующие авиакомпании в одном списке с ценами, временем вылета и прилёта, длительностью рейса и количеством пересадок, что делает публичные результаты полезным источником данных для тех, кто отслеживает тарифы, исследует маршруты или наблюдает за движением цен между перевозчиками. Те же цифры, которые путешественник видит на странице, нужны системе мониторинга тарифов в структурированной таблице.
Это руководство показывает, как парсить Google Flights с помощью Python надёжным способом. Вы создадите небольшой работоспособный скрапер, который получает отрисованную страницу результатов Google Flights через Crawling API, парсит каждый рейс с помощью BeautifulSoup и экспортирует записи в JSON и CSV для мониторинга тарифов. Всё руководство ограничено публичными данными о рейсах, которые любой может увидеть без учётной записи, а раздел о правовых вопросах ближе к концу, не просто формальность, поэтому прочитайте его, прежде чем направить скрапер на реальные объёмы данных.
Что вы создадите
Скрипт на Python, который принимает публичный URL поиска Google Flights, получает отрисованный HTML через Crawling API и извлекает структурированную запись для каждого рейса на странице. В качестве рабочего примера мы будем использовать образцовый маршрут и извлекать следующие поля из каждого объявления:
- Авиакомпания, оперирующий перевозчик (или перевозчики), указанный в объявлении.
- Цена, отображаемый тариф для данного маршрута.
- Время вылета, местное время вылета рейса.
- Время прилёта, местное время прилёта, включая маркеры +1 / +2 суток.
- Длительность, общее время в пути для данного маршрута.
- Пересадки, является ли рейс прямым или сколько пересадок предусмотрено.
Почему обычный запрос не работает на Google Flights
Если отправить обычный HTTP-запрос к URL Google Flights из скрипта, вы не получите страницу, которую видите в браузере. Google Flights формирует список результатов на стороне клиента: исходный HTML представляет собой почти пустую оболочку, а карточки рейсов заполняются JavaScript после загрузки страницы. Обычный requests.get никогда не выполняет этот JavaScript, поэтому возвращает разметку без рейсов, и каждый написанный вами селектор возвращает пустой результат.
Кроме того, Google следит за автоматическим трафиком. Запросы, не похожие на запросы реального браузера, получают проверочную страницу или блокируются ещё до того, как достигают объявлений. Поэтому рабочий скрапер Google Flights требует двух вещей в одном запросе: браузера, который реально отрисовывает страницу, и IP-адреса, который Google воспринимает как обычного посетителя. Можно собрать это самостоятельно с помощью безголового браузера плюс пула ротирующихся резидентных прокси, но поддержание этой инфраструктуры в рабочем состоянии, это большая часть всей работы. Crawling API объединяет обе вещи в одном вызове: вы отправляете URL, API отрисовывает страницу в реальном браузере с доверенного резидентного IP и возвращает готовый HTML для парсинга.
В отличие от классической страницы результатов с серверным рендерингом, в исходном HTML Google Flights почти нет ничего полезного. Карточки рейсов появляются только после выполнения JavaScript. Именно поэтому в этом руководстве токен для JavaScript-рендеринга используется с самого начала, а не как запасной вариант. Если при запросе возвращается страница с нулевым количеством рейсов, причина почти всегда в отсутствии отрисовки.
Предварительные требования
Перед написанием кода нужно подготовить несколько вещей. Ни одна из них не займёт много времени.
Базовые знания Python. Вы должны уверенно писать и запускать Python-скрипты и устанавливать пакеты через pip. Если BeautifulSoup вам незнаком, наше руководство по использованию BeautifulSoup в Python охватывает основы парсинга, которые предполагает этот учебник.
Python 3.8 или новее. Проверьте версию командой python --version. Если Python не установлен, скачайте его с python.org или через дистрибутив вроде Anaconda.
Аккаунт Crawlbase и токен. Зарегистрируйтесь, откройте панель управления и скопируйте токены со страницы документации аккаунта. Google Flights требует отрисовки, поэтому вы будете использовать JavaScript-токен, а не обычный. Первые 1 000 запросов бесплатны, а добавление платёжных данных до их исчерпания открывает ещё 9 000 бесплатных запросов. Храните токен как пароль: он аутентифицирует ваши запросы, поэтому не добавляйте его в систему контроля версий.
Настройка проекта
Создайте виртуальное окружение, чтобы зависимости проекта оставались изолированными, затем установите библиотеки, необходимые скраперу.
python --version python -m venv google_flights_env source google_flights_env/bin/activate pip install crawlbase beautifulsoup4
В Windows активируйте окружение командой google_flights_env\Scripts\activate вместо строки с source. Два пакета выполняют основную работу: crawlbase, официальная библиотека Python, которая обращается к Crawling API и управляет отрисовкой, а beautifulsoup4 парсит возвращаемый HTML, чтобы вы могли извлекать отдельные поля по CSS-селектору.
Шаг 1: Получение отрисованных результатов
Начните с получения готовой страницы. Импортируйте класс CrawlingAPI, инициализируйте его с JavaScript-токеном и запросите URL поиска с включённой отрисовкой. Проверка кода статуса перед парсингом позволяет сразу замечать ошибки, а не замалчивать их, что особенно важно для цели, которая применяет троттлинг.
from crawlbase import CrawlingAPI api = CrawlingAPI({"token": "YOUR_CRAWLBASE_JS_TOKEN"}) def crawl(page_url): options = {"ajax_wait": "true", "page_wait": 5000} response = api.get(page_url, options) status = response["headers"].get("pc_status") if status == "200": return response["body"].decode("utf-8") print(f"Request failed: {status}") return None if __name__ == "__main__": page_url = "https://www.google.com/travel/flights/search?tfs=CBwQAhopEgoyMDI0LTA3LTE0ag0IAxIJL20vMDFmMDhycgwIAxIIL20vMDZ5NTcaKRIKMjAyNC0wNy0yMGoMCAMSCC9tLzA2eTU3cg0IAxIJL20vMDFmMDhy&hl=en-US&curr=EUR" html = crawl(page_url) print(html[:500] if html else "No HTML returned")
Два параметра ожидания важны для цели с клиентским рендерингом. ajax_wait указывает API ожидать завершения загрузки асинхронного контента, а page_wait выдерживает фиксированное количество миллисекунд после загрузки, чтобы карточки рейсов с поздним рендерингом появились до захвата страницы. Пять секунд, разумная отправная точка; увеличьте значение, если список рейсов возвращается неполным или пустым. Обратите внимание на заголовок pc_status: это собственный статус Crawlbase для базового запроса, и именно на него следует ориентироваться при определении успешности запроса, а не на внешний HTTP-код. Запустите скрипт, вы должны увидеть реальную разметку объявлений, а не пустую оболочку, которую возвращает обычный запрос.
Google Flights требует отрисованной страницы через доверенный IP-адрес в одном вызове. Именно это обеспечивает JavaScript-токен из фрагмента кода выше: Crawling API запускает страницу в реальном браузере, ждёт, пока карточки рейсов заполнятся, ротирует резидентные IP на стороне сервера и возвращает готовый HTML, избавляя вас от необходимости самостоятельно управлять безголовым парком и пулом прокси. Начните с публичного поиска на бесплатном тарифе.
Шаг 2: Парсинг каждого рейса с помощью BeautifulSoup
Получив отрисованный HTML, загрузите его в BeautifulSoup и извлеките каждый рейс из его элемента объявления. Google оборачивает каждый рейс в элемент списка, поэтому вы находите все элементы, а затем извлекаете из каждого авиакомпанию, длительность, цену, время вылета и прилёта, а также информацию о пересадках. Приведённые ниже селекторы взяты непосредственно из действующей вёрстки; проверьте текущие имена классов в инструментах разработчика браузера (правый клик, затем «Просмотр кода») перед длительным запуском.
from bs4 import BeautifulSoup def text_or_none(listing, selector): el = listing.select_one(selector) return el.get_text(strip=True) if el else None def scrape_dates(listing): dep = listing.select_one( 'span.mv1WYe span:first-child [jscontroller="cNtv4b"] span') arr = listing.select_one( 'span.mv1WYe span:last-child [jscontroller="cNtv4b"] span') departure = dep.get_text(strip=True) if dep else None arrival = arr.get_text(strip=True) if arr else None return departure, arrival def scrape_flights(html): soup = BeautifulSoup(html, "html.parser") flights = [] for listing in soup.select("li.pIav2d"): departure, arrival = scrape_dates(listing) flights.append({ "airline": text_or_none(listing, "div.Ir0Voe div.sSHqwe"), "price": text_or_none(listing, "div.U3gSDe div.FpEdX span"), "departure": departure, "arrival": arrival, "duration": text_or_none(listing, "div.AdWm1c.gvkrdb"), "stops": text_or_none(listing, "div.EfT7Ae span.ogfYpf"), }) return flights
Селектор-обёртка li.pIav2d соответствует каждому объявлению о рейсе на странице, а каждое поле считывается из селектора внутри этого объявления. div.Ir0Voe div.sSHqwe содержит название авиакомпании, div.U3gSDe div.FpEdX span, цену, div.AdWm1c.gvkrdb, длительность, а div.EfT7Ae span.ogfYpf, текст о пересадках (например, "Nonstop" или "1 stop"). Время вылета и прилёта находятся в общем блоке span.mv1WYe, где первый дочерний элемент соответствует вылету, а последний, прилёту, каждый обёрнут в span с [jscontroller="cNtv4b"]. Небольшой помощник text_or_none означает, что отсутствующее поле возвращает None, а не прерывает цикл, что удобно для страниц, где не каждая карточка содержит все поля.
Имена классов Google, такие как pIav2d и sSHqwe, обфусцированы и ротируются при обновлении фронтенда. Рассматривайте приведённые выше селекторы как начальный шаблон, а не контракт. Когда поле возвращается пустым для каждого рейса, проверьте живую страницу в инструментах разработчика браузера и обновите селектор. Периодическое обслуживание селекторов, норма для любого производственного скрапера, а не признак поломки.
Шаг 3: Объединение и экспорт
Теперь свяжите получение и парсинг в один работоспособный скрипт, а затем запишите структурированный вывод в JSON и CSV. JSON удобен для дальнейшей обработки; CSV сразу открывается в таблице или в листе мониторинга тарифов, который вы сравниваете день за днём.
import csv import json from crawlbase import CrawlingAPI from bs4 import BeautifulSoup api = CrawlingAPI({"token": "YOUR_CRAWLBASE_JS_TOKEN"}) def crawl(page_url): options = {"ajax_wait": "true", "page_wait": 5000} response = api.get(page_url, options) if response["headers"].get("pc_status") == "200": return response["body"].decode("utf-8") return None def text_or_none(listing, selector): el = listing.select_one(selector) return el.get_text(strip=True) if el else None def scrape_dates(listing): dep = listing.select_one( 'span.mv1WYe span:first-child [jscontroller="cNtv4b"] span') arr = listing.select_one( 'span.mv1WYe span:last-child [jscontroller="cNtv4b"] span') departure = dep.get_text(strip=True) if dep else None arrival = arr.get_text(strip=True) if arr else None return departure, arrival def scrape_flights(html): soup = BeautifulSoup(html, "html.parser") flights = [] for listing in soup.select("li.pIav2d"): departure, arrival = scrape_dates(listing) flights.append({ "airline": text_or_none(listing, "div.Ir0Voe div.sSHqwe"), "price": text_or_none(listing, "div.U3gSDe div.FpEdX span"), "departure": departure, "arrival": arrival, "duration": text_or_none(listing, "div.AdWm1c.gvkrdb"), "stops": text_or_none(listing, "div.EfT7Ae span.ogfYpf"), }) return flights def save_json(flights, path="google_flights.json"): with open(path, "w", encoding="utf-8") as f: json.dump(flights, f, ensure_ascii=False, indent=2) def save_csv(flights, path="google_flights.csv"): fields = ["airline", "price", "departure", "arrival", "duration", "stops"] with open(path, "w", newline="", encoding="utf-8") as f: writer = csv.DictWriter(f, fieldnames=fields) writer.writeheader() writer.writerows(flights) def main(): page_url = "https://www.google.com/travel/flights/search?tfs=CBwQAhopEgoyMDI0LTA3LTE0ag0IAxIJL20vMDFmMDhycgwIAxIIL20vMDZ5NTcaKRIKMjAyNC0wNy0yMGoMCAMSCC9tLzA2eTU3cg0IAxIJL20vMDFmMDhy&hl=en-US&curr=EUR" html = crawl(page_url) if not html: print("No HTML returned") return flights = scrape_flights(html) save_json(flights) save_csv(flights) print(f"Saved {len(flights)} flights") if __name__ == "__main__": main()
Запустите полный скрипт командой python main.py. Он получает отрисованную страницу результатов для образцового маршрута, извлекает запись для каждого рейса и записывает google_flights.json и google_flights.csv. Для мониторинга тарифа запускайте скрипт по расписанию и добавляйте CSV каждого запуска в файл истории: столбец цены со временем становится кривой тарифа. Чтобы сканировать другой маршрут или дату, замените значение tfs и даты в URL; парсер обработает всё, что вернётся.
Как выглядит вывод
Вы получаете чистый список записей о рейсах, по одному объекту на объявление, готовых для записи в JSON, CSV или базу данных. Вот сокращённый пример JSON для дальнемагистрального маршрута:
[ { "airline": "Cebu Pacific", "price": "€924", "departure": "10:10 PM", "arrival": "9:45 AM+2", "duration": "29 hr 35 min", "stops": "1 stop" }, { "airline": "Etihad", "price": "€2,038", "departure": "10:25 PM", "arrival": "6:10 PM+1", "duration": "13 hr 45 min", "stops": "Nonstop" }, { "airline": "Emirates", "price": "€2,215", "departure": "9:30 PM", "arrival": "5:20 PM+1", "duration": "13 hr 50 min", "stops": "Nonstop" } ]
Те же записи попадают в google_flights.csv с одной строкой заголовка (airline,price,departure,arrival,duration,stops) и одной строкой на рейс, что идеально подходит для мониторинга тарифов: добавляйте данные каждого дня, и столбец цены превратится во временной ряд, который можно визуализировать или использовать для оповещений.
Масштабирование на несколько маршрутов и дат
Один маршрут на один день, это демонстрация; система мониторинга тарифов запускает тот же парсинг по нескольким маршрутам и диапазону дат вылета. Форма остаётся той же: сформируйте каждый URL поиска, получите его через Crawling API с включённой отрисовкой и спарсите с той же функцией scrape_flights. Привычка, которая поддерживает длительный запуск в рабочем состоянии, это пауза между запросами, а не отправка их в плотном цикле.
import time route_urls = [ "https://www.google.com/travel/flights/search?tfs=...&curr=EUR", "https://www.google.com/travel/flights/search?tfs=...&curr=USD", ] all_flights = [] for url in route_urls: html = crawl(url) if html: all_flights.extend(scrape_flights(html)) time.sleep(3) print(f"Collected {len(all_flights)} flights across {len(route_urls)} routes")
Любой ответ 5XX от API бесплатен, поэтому повтор заблокированного или превысившего время ожидания URL ничего не стоит. Поскольку каждый запрос Google Flights требует отрисовки, каждый запрос использует тариф JavaScript-токена; проверьте страницу с ценами, чтобы узнать, как подсчитываются отрисованные запросы, прежде чем масштабировать монитор. Если вы предпочитаете маршрутизировать собственный трафик через ротирующийся пул, а не использовать управляемый API, Smart AI Proxy (также называемый AI Proxy) предоставляет ту же ротацию резидентных IP в виде прокси-эндпоинта.
Поддержание разблокированного состояния
Даже при обработанной отрисовке и доверенном IP Google отслеживает трафик с признаками скрапинга интенсивнее, чем большинство сайтов. Несколько привычек помогут поддерживать запуск в рабочем состоянии.
- Дозируйте запросы. Атака поисковых страниц в плотном цикле, самый быстрый способ получить проверку. Распределяйте запросы и варьируйте маршруты, вместо того чтобы листать один поиск на полной скорости.
- Опирайтесь на ротацию. Пул резидентных IP распределяет запросы по множеству адресов реальных пользователей, чтобы ни один не превысил лимит. Crawling API делает это за вас; если вы создаёте собственный стек, именно эту часть нужно реализовать правильно.
-
Давайте странице время на отрисовку. Если рейсы возвращаются пустыми, увеличьте
page_wait, прежде чем предполагать, что сломались селекторы. Карточкам нужно время для заполнения. - Проверяйте страницу при пустых полях. Google ротирует обфусцированные имена классов. Если результаты перестали парситься, откройте живую страницу в инструментах разработчика и обновите селекторы.
Общее руководство по тактике см. в статье как парсить сайты без блокировок. Поскольку Google Flights полностью рендерится на клиенте, наше руководство по парсингу JavaScript-сайтов объясняет, почему отрисовка важна и как её включить, а если вы также отслеживаете цены на жильё, статья парсинг Google Hotels с Python использует тот же паттерн с отрисованным запросом.
Законен ли скрапинг Google Flights?
Допустимость скрапинга Google Flights зависит от условий использования Google, вашей юрисдикции и того, как вы используете данные. Условия Google ограничивают автоматический доступ, поэтому скрапинг может противоречить этим условиям вне зависимости от тщательности вашего инструментария. Ни один из приведённых здесь кодов не меняет этого; он лишь решает техническую часть задачи. Прочитайте условия Google и его robots.txt и рассматривайте оба документа как границу того, что вы собираете.
Несколько правил, которых стоит придерживаться. Собирайте только публичные данные о результатах рейсов: авиакомпании, цены, время, длительность и информацию о пересадках, которые любой может видеть на странице результатов без учётной записи. Поддерживайте достаточно низкий объём запросов, чтобы не нагружать серверы Google, и дозируйте сканирование, а не запускайте его на полную мощность. Это руководство намеренно ограничено публичными объявлениями, поскольку именно это позволяет сохранить правомерность работы. Оно не охватывает ничего за логином, данных аккаунта или личных данных, платёжных или booking-потоков, а также контента о тарифах, который вы будете распространять как собственный.
Также стоит знать, что базовые тарифы поступают от авиакомпаний и глобальных дистрибьюторских систем, и многие авиакомпании публикуют официальные API тарифов или партнёрские программы именно для такого рода доступа. Если ваш проект требует гарантированных данных о тарифах в больших объёмах с правом распространения, официальное соглашение о данных, правильный путь, а не более хитрый скрапер. Используйте скрапинг для мониторинга и исследований публичных объявлений, а к официальному API обращайтесь, когда переросли этот масштаб.
Ключевые выводы
- Google Flights полностью рендерится на клиенте. Обычный запрос возвращает пустую оболочку, поэтому отрисовка обязательна, а не опциональна, чтобы вообще увидеть карточки рейсов.
- Вам нужны отрисовка и доверенный IP вместе. Crawling API принимает JavaScript-токен, запускает страницу в реальном браузере, ротирует резидентные IP на стороне сервера и возвращает готовый HTML.
-
BeautifulSoup выполняет извлечение. Выберите каждое объявление
li.pIav2d, затем считайте из него авиакомпанию, цену, время вылета, время прилёта, длительность и пересадки, и ожидайте, что обфусцированные имена классов будут меняться. - Экспортируйте в JSON и CSV. CSV с одной строкой на рейс подходит для мониторинга тарифов: добавляйте данные каждого запуска, и столбец цены становится временным рядом.
- Оставайтесь в рамках публичных данных. Соблюдайте условия использования Google и robots.txt, поддерживайте низкий объём и предпочитайте официальный API авиакомпаний или GDS для высокообъёмного доступа с правом распространения.
Часто задаваемые вопросы
Почему обычный запрос не возвращает рейсы в Google Flights?
Google Flights формирует карточки результатов на стороне клиента с помощью JavaScript, поэтому исходный HTML, который скачивает обычный requests.get, в основном представляет собой пустую оболочку без рейсов. Чтобы увидеть объявления, нужно отрисовать страницу в реальном браузере. Получение данных через Crawling API с JavaScript-токеном выполняет эту отрисовку и возвращает готовый HTML, поэтому все селекторы в этом руководстве предполагают отрисованную страницу.
Можно ли парсить Google Flights с помощью Python?
Да. С помощью библиотеки crawlbase и BeautifulSoup вы можете получить отрисованную страницу результатов и извлечь авиакомпанию, цену, время вылета и прилёта, длительность и пересадки. Crawling API служит мостом, который отрисовывает страницу и доставляет ваш запрос в Google с доверенного IP, так что запросы обрабатываются без блокировок. Для более широкого введения в Python см. наше руководство по парсингу сайтов с Python.
Какие поля можно извлечь из объявления Google Flights?
В этом руководстве извлекается шесть полей из каждого рейса: авиакомпания, цена, время вылета, время прилёта (включая маркеры +1 / +2 суток), общая длительность и текст о пересадках, например "Nonstop" или "1 stop". Его можно расширить, добавив селекторы для других видимых полей, таких как аэропорты пересадок или оценки выбросов углерода. Оставайтесь в рамках публичных данных о рейсах и избегайте всего, что требует входа в аккаунт или booking-потока.
Нужен ли JavaScript-рендеринг для парсинга Google Flights?
Да, и это ключевое отличие от многих других целей. Google Flights практически ничего не содержит в исходном HTML; карточки рейсов появляются только после выполнения JavaScript. Crawling API предлагает JavaScript-токен рендеринга плюс параметры ajax_wait и page_wait, которые загружают страницу так, как это делает реальный браузер. Наше руководство по парсингу JavaScript-сайтов объясняет, когда отрисовка необходима и как она работает.
Как отслеживать тариф во времени?
Запускайте скрапер по расписанию для фиксированного маршрута и даты и добавляйте строку CSV каждого запуска (с временной меткой) в файл истории. Столбец цены по всем запускам, это ваша кривая тарифа, которую можно визуализировать или настроить оповещение при падении ниже порога. Поддерживайте умеренную частоту: несколько проверок в день на маршрут вполне достаточно, чтобы оставаться в рамках рекомендаций по дозированию.
Мои селекторы ничего не возвращают. Что изменилось?
Почти наверняка, разметка Google. Имена классов, такие как pIav2d и sSHqwe, обфусцированы и ротируются при обновлении фронтенда, поэтому селекторы, работавшие в прошлом месяце, могут сломаться. Сначала убедитесь, что страница действительно отрисовалась, увеличив page_wait; если да, а поля по-прежнему пустые, проверьте живую страницу результатов в инструментах разработчика браузера и обновите селекторы. Периодическое обслуживание селекторов, норма для любого производственного скрапера.
Обходите любой сайт в масштабе, без борьбы с инфраструктурой.
Crawlbase берёт на себя прокси, отпечатки и CAPTCHA, чтобы ваша команда выпускала конвейеры данных вместо поддержки обвязки краулинга. 1 000 запросов бесплатно, без карты.
