Dùng Python Scrape Google Flights: Từ Code đến Cảnh Báo Giá Vé

Cập nhật lần cuối vào April 16, 2026
Dùng Python Scrape Google Flights: Từ Code đến Cảnh Báo Giá Vé

Google đã khai tử API Flights từ năm 2018, nhưng giá vé máy bay thì vẫn lên xuống liên tục — có thể tới 17 lần trong 48 giờ chỉ với một tuyến bay nội địa. Nếu bạn muốn truy cập dữ liệu này bằng code, scraping gần như là con đường duy nhất.

Tôi đã dành khá nhiều thời gian để thử nghiệm các cách lấy dữ liệu chuyến bay từ Google, và bức tranh này đã thay đổi rất mạnh — đặc biệt sau khi Google triển khai SearchGuard vào tháng 1/2025. Trong hướng dẫn này, tôi sẽ cùng bạn xây dựng một Python scraper hoạt động được cho Google Flights bằng Playwright, chỉ cách xử lý các lớp chống bot khiến hầu hết người mới “kẹt cứng”, rồi mở rộng thành một hệ thống theo dõi giá tự động có cảnh báo. Nếu bạn không muốn đụng tới code, tôi cũng sẽ giới thiệu một cách làm không cần lập trình bằng Thunderbit để đạt kết quả tương tự chỉ trong khoảng hai phút.

Vì sao nên scrape Google Flights bằng Python?

Google Flights đang chiếm ưu thế lớn trong mảng tìm kiếm chuyến bay. Mức hiển thị trên di động tại Mỹ đã tăng vọt lên 47.6%, vượt qua mọi OTA lớn. Thị trường metasearch du lịch phía sau nó có giá trị tới $8.33 tỷ trong năm 2024, với tốc độ tăng trưởng kép 30.2%/năm. Tuy nhiên, kể từ khi QPX Express API bị ngừng hoàn toàn vào ngày 10/04/2018, hiện không còn cách chính thức nào để truy cập dữ liệu này bằng chương trình.

Trong khi đó, giá vé máy bay cùng một hành trình có thể dao động tới 50%, với chênh lệch trung bình khoảng 20 USD giữa mức thấp nhất và cao nhất. Các hãng như Delta sử dụng tới 77 “fare bucket” để định giá linh hoạt. Giá vé khứ hồi trung bình tại Mỹ vào đầu năm 2026 đang ở mức 408 USD, và giá vé máy bay cao hơn 14.9% so với cùng kỳ năm trước.

Nền tảng thống trị, không có API, giá biến động liên tục. Đó là lý do scrape Google Flights bằng Python đã trở thành một trong những dự án phổ biến nhất trên GitHub và trong các cộng đồng du lịch.

Đây là nhóm người được lợi nhiều nhất và lý do:

Loại người dùngTrường hợp sử dụngLợi ích chính
Cá nhân đi du lịchTheo dõi giá vé cho các tuyến cụ thể theo thời gianTiết kiệm trung bình $50 cho mỗi chuyến bay
Đại lý du lịchThông tin định giá cạnh tranhGiám sát chênh lệch giá theo thời gian thực
Đội ngũ travel của doanh nghiệpTối ưu chi phí giữa các tuyến bayTiết kiệm 10–30% chi phí công tác
Lập trình viênXây dựng ứng dụng so sánh giá véTruy cập dữ liệu giá bằng chương trình
Nhà nghiên cứuPhân tích biến động giá của hãng hàng khôngPhục vụ nghiên cứu học thuật và thị trường

Người dùng trên các diễn đàn nói rất thẳng thắn về lý do họ chuyển sang scraping: "Google Flights API was discontinued and I should use web scraping instead" là một quan điểm xuất hiện lặp đi lặp lại. Và hiệu quả đầu tư là có thật — Hopper cho biết độ chính xác dự đoán đạt 95% khi phân tích hơn 5 tỷ báo giá mỗi ngày, trong khi dữ liệu năm 2026 của Expedia cho thấy đặt vé trước 8–15 ngày có thể tiết kiệm khoảng $25 cho chuyến bay nội địa.

Bạn có thể scrape được những dữ liệu nào từ Google Flights?

Trang kết quả của Google Flights chứa một lượng dữ liệu khá phong phú. Thông thường bạn có thể lấy được:

  • Tên hãng bay (kèm logo)
  • Giờ khởi hành và mã sân bay
  • Giờ đến nơi và mã sân bay
  • Tổng thời gian bay
  • Số điểm dừng và thông tin quá cảnh (sân bay, thời lượng, có qua đêm hay không)
  • Giá vé (theo loại tiền tệ)
  • Phát thải CO2 (kg CO2e, kèm phần trăm chênh lệch so với chuyến bay điển hình)
  • Hạng vé, số hiệu chuyến bay, mẫu máy bay
  • Khoảng để chân
  • Tiện ích bổ sung (Wi‑Fi, ổ cắm điện, phát media)
  • Chỉ báo mức giá (thấp / bình thường / cao)
  • Cảnh báo trễ chuyến ("Thường trễ hơn 30 phút")

Việc có đầy đủ dữ liệu hay không còn tùy theo tuyến bay, ngày bay và loại vé (một chiều hay khứ hồi). Dưới đây là ví dụ một bản ghi chuyến bay sau khi scrape ở dạng 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"
    }
  ]
}

Thiết lập môi trường Python

Trước khi viết bất kỳ dòng scraping nào, bạn cần chuẩn bị vài thứ.

Yêu cầu ban đầu:

  • Mức độ khó: Trung bình
  • Thời gian cần thiết: Khoảng 1–2 giờ cho toàn bộ hướng dẫn
  • Bạn cần có: Python 3.7+, kiến thức Python cơ bản, một trình duyệt nền Chromium

Cài các thư viện cần thiết

Chúng ta sẽ dùng Playwright để tự động hóa trình duyệt (Google Flights được render 100% bằng JavaScript — request HTTP tĩnh sẽ không trả về gì hữu ích), cùng một vài công cụ hỗ trợ:

pip install playwright playwright-stealth pandas
playwright install chromium
  • Playwright — tự động hóa trình duyệt headless, xử lý render JavaScript, có cơ chế chờ tích hợp
  • playwright-stealth — giảm các tín hiệu dễ bị phát hiện là bot
  • pandas — dùng cho phân tích dữ liệu và xuất CSV sau này

Vì sao chọn Playwright thay vì Selenium hoặc requests

Google Flights sẽ không hoạt động chỉ với requests + BeautifulSoup vì nội dung trang được dựng hoàn toàn bằng JavaScript. Bạn cần một trình duyệt thực.

Tính năngPlaywrightSeleniumrequests + BS4
Render JSHỗ trợ đầy đủHỗ trợ đầy đủKhông có
Tốc độNhanh hơn 42% tổng thểMức cơ bảnKhông áp dụng cho trường hợp này
Hỗ trợ asyncNativeChỉ tuần tựKhông áp dụng
Mức dùng bộ nhớÍt hơn 30%Cao hơnTối thiểu
Vượt phát hiện botTốt (khi kết hợp stealth)Dễ bị phát hiện hơnKhông áp dụng

Playwright nhanh hơn, hiện đại hơn và hỗ trợ async tốt hơn. Riêng với Google Flights, đây là lựa chọn rõ ràng nhất.

Từng bước: Cách scrape Google Flights bằng Python

Đây là phần cốt lõi của hướng dẫn. Chúng ta sẽ xây scraper từng bước một.

google-flights-scraping-workflow.webp

Bước 1: Định nghĩa các data class

Hãy bắt đầu bằng cách cấu trúc tham số tìm kiếm và dữ liệu chuyến bay bằng Python dataclass. Cách này giúp code gọn gàng và dễ mở rộng về sau.

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 = ""

Mỗi trường đều tương ứng trực tiếp với dữ liệu mà ta sẽ trích xuất từ trang. Có sẵn cấu trúc này từ đầu sẽ giúp bạn không phải truyền quanh những dictionary lộn xộn về sau.

Bước 2: Hiểu cấu trúc URL của Google Flights

Google Flights mã hóa tham số tìm kiếm bằng Protobuf được mã hóa Base64 trong tham số URL tfs. Bạn có thể tự giải mã cách này, hoặc chọn hướng đơn giản hơn: tạo URL truy vấn theo ngôn ngữ tự nhiên.

Cách đơn giản nhất là dùng format query:

https://www.google.com/travel/flights?q=flights+from+SFO+to+JFK+on+2026-05-15&curr=USD

Nếu muốn kiểm soát tốt hơn, bạn có thể tạo URL bằng code:

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"

Cách khác — reverse-engineering mã hóa Protobuf — cho bạn khả năng kiểm soát chính xác hơn, nhưng lại dễ hỏng mỗi khi Google thay đổi định dạng nội bộ. Những thư viện như fast-flights trên GitHub dùng giải mã Protobuf để bỏ qua luôn bước parse HTML, nhưng đây là hướng nâng cao hơn.

Bước 3: Khởi động trình duyệt và truy cập Google Flights

Đây là phần thiết lập Playwright. Chúng ta dùng playwright-stealth để giảm rủi ro bị phát hiện ngay từ đầu.

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()

Ta chạy headless cho môi trường production (chuyển sang headless=False khi debug), đặt viewport và user agent giống người dùng thật, đồng thời gán sẵn cookie SOCS để bỏ qua popup xin chấp thuận — phần này sẽ nói kỹ hơn ở mục chống bot.

Bước 4: Điều hướng đến trang kết quả tìm kiếm

Tải URL đã tạo và chờ kết quả chuyến bay xuất hiện:

        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
        )

Nếu gặp timeout ở đây, nguyên nhân thường là popup đồng ý điều khoản đã chặn trang (xem cách sửa bằng cookie ở Bước 3) hoặc Google đang trả CAPTCHA. Chúng ta sẽ xử lý cả hai trường hợp trong phần chống bot.

Bước 5: Tải toàn bộ kết quả chuyến bay

Google Flights ẩn thêm kết quả phía sau nút "Show more flights". Bạn cần bấm nút này lặp lại cho đến khi tất cả chuyến bay đều hiện ra:

        # 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

Vòng lặp này sẽ bấm nút, chờ 2 giây để kết quả mới render, rồi dừng lại khi nút không còn hiển thị. Trong quá trình test của tôi, đa số tuyến bay sẽ có 1–3 trang kết quả.

Bước 6: Trích xuất dữ liệu chuyến bay bằng CSS selector

Bây giờ ta sẽ parse dữ liệu thực tế từ trang đã tải. Dưới đây là các selector (đã xác minh tính đến tháng 4/2026 — xem phần bảo trì phía dưới để hiểu vì sao mốc thời gian này quan trọng):

        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

Lưu ý: các class như pIav2d, sSHqwe, FpEdX được Google Closure Compiler tạo ra và có thể thay đổi ở bất kỳ lần build nào. Selector dựa trên aria-label thường ổn định hơn nhiều. Tôi sẽ trình bày chiến lược bảo trì đầy đủ ở phần sau.

Bước 7: Lưu kết quả ra JSON hoặc CSV

Cuối cùng, lưu dữ liệu scrape kèm dấu thời gian (rất quan trọng nếu sau này bạn làm theo dõi giá):

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())

Chạy xong, bạn sẽ thấy file flights.jsonflights.csv chứa kết quả. Trong các lần test của tôi, truy vấn SFO-JFK thường trả về 30–80 lựa chọn chuyến bay và mất khoảng 15–20 giây để hoàn tất.

Hướng dẫn sinh tồn chống bot khi scrape Google Flights

Phần lớn bài hướng dẫn dừng ở đây. Phần lớn scraper thất bại cũng ở đây. Google đã triển khai SearchGuard vào tháng 1/2025, và điều này khiến gần như mọi SERP scraper ngừng hoạt động chỉ sau một đêm. Google mô tả nó là "sản phẩm của hàng chục nghìn giờ công và hàng triệu đô la đầu tư." Google Flights được đánh giá 4/5 về độ khó khi scraping.

Rất ít bài viết của đối thủ đi sâu vào phần này, nhưng đây lại là nguyên nhân số 1 khiến scraper của mọi người chết máy. Sau đây là những gì bạn phải đối mặt và cách xử lý.

anti-bot-survival-guide.webp

Thêm độ trễ ngẫu nhiên giữa các request

Đây là lớp phòng thủ đơn giản nhất để tránh bị giới hạn tốc độ. Chỉ hai dòng code nhưng hiệu quả ở mức trung bình:

import time
import random

time.sleep(random.uniform(3, 7))

Hãy chèn nó giữa các lần điều hướng trang. Khoảng thời gian cố định (ví dụ đúng 5 giây mỗi lần) là tín hiệu rất dễ bị phát hiện — hãy làm ngẫu nhiên.

Xoay vòng User-Agent

Gửi cùng một chuỗi user-agent cho mọi request là một dấu hiệu rất dễ lộ. Hãy xoay từ một danh sách:

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)

Vượt qua phát hiện headless

Google kiểm tra cờ navigator.webdriver và các tín hiệu tự động hóa khác. Thư viện playwright-stealth xử lý phần lớn những tín hiệu này, nhưng bạn cũng nên đặt các launch arguments như ở Bước 3. Những flag quan trọng là:

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

Cách này đủ để vượt qua các lớp phát hiện cơ bản. SearchGuard còn đi sâu hơn — theo dõi tốc độ di chuột, thời gian gõ phím và mô hình cuộn trang — nhưng với khối lượng scraping vừa phải, stealth mode cộng với độ trễ hợp lý thường là đủ.

Xoay proxy: datacenter vs residential

Nếu bạn làm hơn vài lượt tìm kiếm, bạn sẽ cần proxy. Sự khác biệt là rất đáng kể:

Tính năngProxy DatacenterProxy Residential
Tốc độ100–1,000 Mbps10–100 Mbps
Tỷ lệ thành công (Google)20–40%85–95%
Chi phí$0.10–$0.50/IP/tháng$5–$15/GB
Rủi ro bị phát hiệnCaoRất thấp

Proxy residential rẻ hơn khoảng 180 lần trên mỗi request thành công khi scrape các website được bảo vệ. Giá năm 2026 của một số nhà cung cấp: Smartproxy từ $7/GB, Bright Data $8.40/GB, Oxylabs $8/GB.

Thêm proxy vào Playwright như sau:

browser = await pw.chromium.launch(
    proxy={"server": "http://proxy-host:port",
           "username": "user", "password": "pass"}
)

Xử lý popup đồng ý cookie

Người dùng liên tục phản ánh popup "I agree to terms" là vật cản: "first google will show you the 'I agree to terms and conditions' popup." Cách gọn nhất là gán sẵn cookie SOCS (đã nêu ở Bước 3). Nếu vẫn không hiệu quả, hãy bấm thủ công qua popup:

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

Lưu ý: text của nút thay đổi theo ngôn ngữ — "Alle akzeptieren" trong tiếng Đức, "Tout accepter" trong tiếng Pháp.

Bảng tham chiếu nhanh về chống bot

Kỹ thuậtĐộ khóHiệu quảCó cần code không?
Độ trễ ngẫu nhiên (2–7 giây)ThấpTrung bình2 dòng
Xoay vòng user-agentThấpTrung bình5 dòng
Vượt phát hiện headlessTrung bìnhCaoPlaywright launch args
plugin playwright-stealthTrung bình60–80% trên các site cơ bảnpip install
Xoay proxy (datacenter)Trung bìnhTrung bìnhCấu hình
Xoay proxy (residential)Trung bìnhThành công 85–95%Cấu hình
Gán trước cookie consent (SOCS)ThấpBắt buộc1 dòng

Về tốc độ an toàn được khuyến nghị: giữ độ trễ 10–20 giây giữa các request khi xoay IP. Ngưỡng của Google vào khoảng 100 request/phút cho mỗi IP trước khi bạn nhận lỗi 429, và nếu duy trì trên 1.000 request/ngày/IP có thể bị khóa tạm thời.

Vì sao CSS selector của Google Flights cứ bị hỏng, và cách khắc phục

Đây là điểm gây đau đầu số 1, cách biệt khá xa. Các thread trên diễn đàn đầy những biến thể của câu "tất cả những gì tôi nhận lại chỉ là 14 danh sách rỗng." Hầu như bài hướng dẫn nào cũng đưa selector, nhưng không ai giải thích vì sao chúng hỏng.

Vì sao selector của Google Flights thay đổi

Có 3 nguyên nhân chính:

  1. Obfuscation từ Closure Compiler. Google dùng Closure Stylesheets để tạo class như BVAVmfYMlIz thông qua goog.setCssNameMapping(). Những class này thay đổi ở mỗi lần build — đôi khi mỗi tuần.

  2. A/B testing. Các nhóm người dùng khác nhau sẽ thấy cấu trúc HTML khác nhau cùng một lúc. Scraper của bạn có thể chạy tốt trên máy bạn nhưng lại lỗi với người ở vùng khác.

  3. Khác biệt theo ngôn ngữ/khu vực. Người dùng EU sẽ thấy thuật ngữ, bố cục và thậm chí cả trường dữ liệu khác với người dùng Mỹ.

Viết selector có độ bền cao hơn

Hãy ưu tiên selector dựa trên ý nghĩa thay vì hình thức bên ngoài:

# 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")')

Thứ tự độ ổn định của selector (ổn định nhất đến kém ổn định nhất):

  1. Thuộc tính aria-label — gắn với accessibility, hiếm khi thay đổi
  2. Thuộc tính data-* — được thêm rõ ràng cho chức năng
  3. Thuộc tính role — vai trò ARIA mang tính ngữ nghĩa
  4. Selector dựa trên text — khớp nội dung hiển thị
  5. Khớp một phần class — ví dụ [class*="price"]
  6. Tên class obfuscate đầy đủ — nên tránh nếu có thể

Thêm hàm kiểm tra dữ liệu

Đừng để selector bị hỏng mà âm thầm tạo ra dữ liệu rỗng. Hãy phát hiện sớm:

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

Hãy chạy hàm này trên từng chuyến bay đã scrape. Nếu bắt đầu thấy cảnh báo, đã đến lúc mở trang kiểm tra và cập nhật selector.

Chiến lược bảo trì selector

  • Kiểm tra selector hàng tháng, hoặc ngay khi chất lượng đầu ra giảm
  • Tách selector ra thành một dictionary cấu hình riêng để dễ sửa
  • Các selector trong bài viết này được xác minh lần cuối: tháng 4/2026
  • Có thể cân nhắc thư viện fast-flights như một lựa chọn thay thế — nó dùng giải mã Protobuf thay vì CSS selector, nên tránh được vấn đề này hoàn toàn (dù vẫn có rủi ro riêng nếu Google thay đổi định dạng dữ liệu nội bộ)

Từ một lần scrape đơn lẻ đến bộ theo dõi giá Google Flights tự động

Phần lớn bài hướng dẫn kết thúc ở đoạn "lưu ra JSON." Nhưng tiêu đề của bài này có chữ "Price Alerts." Đến lúc hoàn thiện phần đó.

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

Lên lịch để scraper chạy tự động

Cách 1: thư viện Python schedule (đơn giản nhất, đa nền tảng):

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)

Cách 2: cron job (Linux/Mac):

# Run at 6 AM and 6 PM daily
0 6,18 * * * cd /path/to/scraper && python scraper.py

Cách 3: Windows Task Scheduler — tạo một tác vụ cơ bản chạy python scraper.py theo lịch bạn muốn.

Điểm đánh đổi là: tất cả cách này đều cần một máy luôn bật. Nếu bạn chạy trên laptop có chế độ ngủ, bạn sẽ bỏ lỡ các lần scrape.

Lưu trữ lịch sử giá vé

Hãy chuyển từ việc ghi đè file JSON sang append vào cơ sở dữ liệu 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()

Sau một tuần scrape hai lần mỗi ngày, bạn sẽ có đủ dữ liệu để bắt đầu nhìn ra xu hướng.

Phân tích xu hướng giá và đặt cảnh báo

Tìm lựa chọn rẻ nhất từ dữ liệu lịch sử của bạn:

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']})"
)

Kích hoạt email cảnh báo khi giá giảm xuống dưới ngưỡng bạn đặt:

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")

Tần suất scrape được khuyến nghị: hai lần mỗi ngày là đủ cho nhu cầu theo dõi cá nhân (thời điểm ngẫu nhiên giúp giảm rủi ro bị phát hiện). Nếu theo dõi cho mục đích kinh doanh, có thể 4–6 giờ một lần. Chỉ scrape theo giờ khi đang trong giai đoạn sale vé ngắn hạn, và chỉ nên làm tạm thời.

Con đường dễ hơn: Scheduled Scraper của Thunderbit

Nếu việc quản lý cron job, server chạy liên tục và cấu hình proxy nghe giống một hệ thống quá nặng để duy trì, thì Scheduled Scraper của Thunderbit có thể xử lý cùng nhu cầu đó mà không cần các lớp hạ tầng phức tạp. Bạn chỉ cần mô tả khoảng thời gian scraping bằng ngôn ngữ tự nhiên, nhập URL Google Flights, rồi scraper sẽ tự chạy trên hạ tầng cloud của Thunderbit — có sẵn xử lý chống bot và xuất kết quả trực tiếp sang Google Sheets, Excel hoặc Airtable. Nó không thay thế hoàn toàn cho cách làm bằng Python (vì bạn sẽ mất độ tùy biến), nhưng với nhu cầu rất cụ thể là "tôi muốn một bảng theo dõi giá vé", đây là con đường nhanh nhất. Bạn có thể thử trên gói miễn phí.

Khi Python là quá mức cần thiết: cách không cần code để scrape Google Flights

Sau khi xây xong toàn bộ phần trên, tôi phải nói thật: có khá nhiều thứ phải vận hành. Không phải ai cũng cần mức kiểm soát cao như vậy. Selector có thể hỏng, proxy cần xoay, cron job phải giám sát. Nếu mục tiêu của bạn chỉ là "định kỳ đưa giá vé vào bảng tính", có những lựa chọn nhanh hơn nhiều.

So sánh: Python tự xây vs. dịch vụ API vs. Thunderbit

Cách tiếp cậnThời gian thiết lậpCần viết code khôngXử lý chống botLên lịchChi phí
Playwright tự làm (hướng dẫn này)1–2 giờPython (trung cấp)Tự cấu hìnhThủ công (cron)Miễn phí + phí proxy
SerpApi Google Flights endpoint15 phútChỉ gọi APIĐã xử lýQua APIKhoảng $50+/tháng
Thunderbit Chrome Extension2 phútKhôngScrape trên cloudCó scheduler tích hợpCó gói miễn phí

Một lưu ý về SerpApi: Google đã nộp đơn kiện DMCA chống lại SerpApi vào tháng 12/2025, cáo buộc số lượng request của họ tăng 25,000% trong hai năm. Nếu bạn đang đánh giá các nhà cung cấp API, rủi ro pháp lý này là điều đáng cân nhắc.

Thunderbit scrape Google Flights như thế nào

Mở kết quả Google Flights trong Chrome, bấm nút "AI Suggest Fields" của Thunderbit — AI sẽ đọc trang và gợi ý các cột như hãng bay, giá, giờ khởi hành, số điểm dừng — xem lại các trường được đề xuất, rồi bấm "Scrape." Kết quả sẽ xuất hiện trong một bảng và bạn có thể xuất sang Excel, Google Sheets, Airtable hoặc Notion — tất cả đều dùng được trên gói miễn phí.

Riêng với bài toán theo dõi giá, Scheduled Scraper và Cloud Scraping của Thunderbit (có thể xử lý đồng thời 50 trang) thay thế toàn bộ hạ tầng cron + proxy + server.

Python cho bạn toàn quyền kiểm soát và khả năng tùy biến không giới hạn. Thunderbit cho bạn tốc độ và không phải bảo trì. Hãy chọn theo mục tiêu thực tế của bạn. Nếu bạn muốn tìm hiểu thêm về các hướng scraping không cần code, hãy xem bài hướng dẫn của chúng tôi về top no-code web scrapers.

flight-data-options-comparison.webp

Scrape Google Flights có hợp pháp không? Những điều bạn cần biết

Người dùng trên diễn đàn thường đặt câu hỏi này: "scraping Google Flights directly violates the TOS that Google has." Đây là mối lo có cơ sở — đặc biệt khi API đã bị ngừng và không có lựa chọn chính thức thay thế.

Vi phạm điều khoản sử dụng vs. trách nhiệm pháp lý

Điều khoản sử dụng của Google (cập nhật ngày 22/05/2024) nêu rõ người dùng không được "truy cập hoặc sử dụng Dịch vụ hay bất kỳ nội dung nào thông qua phương tiện tự động (như robot, spider hoặc scraper)." Vi phạm ToS là vi phạm hợp đồng (thuộc dân sự) — không đồng nghĩa với việc phạm pháp hình sự.

Tiền lệ pháp lý quan trọng là: hiQ v. LinkedIn (Tòa Phúc thẩm Khu vực 9, 2022) xác lập rằng việc scrape dữ liệu công khai không vi phạm Computer Fraud and Abuse Act (CFAA). Tuy nhiên, vụ việc cuối cùng kết thúc bằng thỏa thuận dàn xếp, và vụ kiện của Google vào tháng 12/2025 chống SerpApi lại dựa trên một hướng pháp lý khác — DMCA Mục 1201 (lách các biện pháp bảo vệ công nghệ) — vốn có thể nghiêm trọng hơn.

Thực hành tốt để scrape có trách nhiệm

  • Giới hạn tốc độ request — chèn độ trễ 10–20 giây và xoay IP
  • Đừng scrape dữ liệu cá nhân — giá vé là dữ liệu tổng hợp công khai
  • Đừng tự động vượt CAPTCHA (đây là vùng rủi ro theo DMCA)
  • Chỉ dùng dữ liệu cho nghiên cứu cá nhân, không xây sản phẩm thương mại cạnh tranh nếu không có giấy phép phù hợp
  • Xem xét API chính thức nếu có sẵn

Các nguồn dữ liệu thay thế

Nếu bạn thấy scraping quá rủi ro cho trường hợp của mình, vẫn có những lựa chọn API hợp pháp:

Nhà cung cấpChi phíGói miễn phíGhi chú
SerpApi$75–$3,750+/tháng250 lượt tìm kiếm/thángJSON Google Flights trực tiếp (đang chịu giám sát pháp lý)
Kiwi TequilaMiễn phí (mô hình affiliate)Không giới hạnTốt nhất cho startup và thử nghiệm
AmadeusTrả theo mức sử dụng2,000 request/thángHơn 400 hãng bay, có khả năng đặt vé
SkyscannerTùy chỉnhCần được phê duyệt52 thị trường, 30 ngôn ngữ

Chúng tôi đã viết một phân tích sâu hơn về hệ quả pháp lý của web scraping nếu bạn muốn xem bức tranh đầy đủ.

Kết luận và các điểm rút ra chính

Bài này khá dài. Đây là những điều quan trọng nhất:

  • Python + Playwright là cách linh hoạt nhất để scrape Google Flights, nhưng sẽ cần bảo trì định kỳ
  • Các biện pháp chống bot (độ trễ, xoay user-agent, proxy residential) là bắt buộc chứ không phải tùy chọn — đặc biệt sau SearchGuard
  • Selector rất dễ hỏng — hãy ưu tiên aria-label và selector dựa trên text, kiểm tra dữ liệu đầu ra, và duy trì lịch bảo trì
  • Tự động hóa bằng schedule hoặc cron để biến một lần scrape thành hệ thống theo dõi giá thực sự, có dữ liệu lịch sử và cảnh báo email
  • Thunderbit cung cấp giải pháp không cần code với scheduling sẵn có, cloud scraping và xử lý chống bot — rất hợp nếu mục tiêu của bạn là một bảng theo dõi giá vé, chứ không phải một dự án lập trình
  • Tuân thủ giới hạn pháp lý — giới hạn tốc độ, chỉ scrape dữ liệu công khai, và cân nhắc các API thay thế cho mục đích thương mại

Bạn có thể lấy code từ hướng dẫn này, hoặc cài Thunderbit Chrome extension để đi nhanh hơn. Dù chọn cách nào, bạn cũng sẽ theo dõi giá vé thay vì phải refresh Google Flights bằng tay.

Nếu muốn học thêm kỹ thuật scraping bằng Python, hãy xem các hướng dẫn của chúng tôi về cách web scrape bằng Pythoncác công cụ web scraping Python tốt nhất.

FAQs

1. Tôi có thể scrape Google Flights mà không cần Python không?

Có. Các dịch vụ API như SerpApi và Kiwi Tequila cung cấp dữ liệu chuyến bay có cấu trúc qua API call (không cần tự động hóa trình duyệt). Nếu muốn làm hoàn toàn không cần code, Thunderbit Chrome extension có thể scrape trực tiếp kết quả Google Flights ngay trong trình duyệt, với trường do AI gợi ý và xuất dữ liệu chỉ bằng một cú nhấp.

2. Google có chặn scraping Google Flights không?

Google có dùng phát hiện bot (SearchGuard), CAPTCHA và rate limiting. Với các biện pháp chống bot phù hợp — độ trễ ngẫu nhiên, xoay user-agent, proxy residential và cấu hình trình duyệt stealth — bạn vẫn có thể scrape ổn định ở mức lưu lượng vừa phải. Xem phần chống bot ở trên để biết kỹ thuật và ngưỡng cụ thể.

3. Tôi nên scrape Google Flights bao lâu một lần để theo dõi giá?

Hai lần mỗi ngày (vào thời điểm ngẫu nhiên) là đủ cho nhu cầu theo dõi cá nhân và giữ rủi ro bị phát hiện ở mức thấp. Với theo dõi cho doanh nghiệp, mỗi 4–6 giờ cùng với xoay proxy. Tránh scrape theo giờ, trừ khi đang trong đợt giảm giá vé ngắn hạn — vì như vậy nguy cơ bị chặn sẽ tăng đáng kể.

4. Có Google Flights API miễn phí không?

API chính thức Google QPX Express đã bị ngừng vào tháng 4/2018. Hiện không có bản thay thế chính thức miễn phí. Lựa chọn miễn phí gần nhất là Kiwi Tequila API (mô hình affiliate, tìm kiếm không giới hạn). SerpApi có 250 lượt tìm kiếm miễn phí mỗi tháng. Với đa số người dùng, scraping hoặc dùng công cụ không cần code như Thunderbit là con đường thực tế nhất.

5. Vì sao CSS selector của Google Flights liên tục trả về dữ liệu rỗng?

Google dùng Closure Compiler để tạo class obfuscate, và các class này thay đổi ở mỗi lần build. A/B testing và khác biệt ngôn ngữ/khu vực cũng làm cấu trúc HTML thay đổi giữa người dùng. Cách khắc phục: dùng thuộc tính aria-label và selector dựa trên text thay vì class, thêm hàm validation để phát hiện hỏng sớm, và kiểm tra selector hàng tháng. Xem phần chiến lược bảo trì selector để biết chi tiết.

Tìm hiểu thêm

Ke
Ke
CTO tại Thunderbit | Chuyên gia Khoa học Dữ liệu cấp cao & ML Với gần một thập kỷ kinh nghiệm trong học máy và khoa học dữ liệu, Ke Shen là cựu sinh viên Đại học Columbia và từng là Chuyên gia Khoa học Dữ liệu cấp cao tại Walmart Labs. Sở hữu chuyên môn sâu về Python, R, Java và Thống kê, được đồng nghiệp công nhận, anh chia sẻ những góc nhìn thực chiến về cách đưa các thuật toán AI phức tạp từ lý thuyết vào kiến trúc sẵn sàng cho môi trường sản xuất.
Mục lục

Cào một trang web chỉ bằng cách hỏi

Nói bạn cần gì bằng tiếng Anh đơn giản. Hoặc tốt hơn, không cần nói gì cả.

Dùng Thunderbit ngay miễn phí
Trích xuất dữ liệu bằng AI
Dễ dàng chuyển dữ liệu sang Google Sheets, Airtable hoặc Notion
Chrome Store Rating
PRODUCT HUNT#1 Product of the Week