🎯 Trình duyệt đám mây tùy chỉnh, chống phát hiện được hỗ trợ bởi Chromium tự phát triển, thiết kế dành cho trình thu thập dữ liệu webtác nhân AI. 👉Dùng thử ngay
Quay lại blog

Xây dựng Trình theo dõi giá vé máy bay với API Scraping không cần scrapeless

Sophia Martinez
Sophia Martinez

Specialist in Anti-Bot Strategies

14-Jul-2026

TL;DR:

  • Một chuyến bay là một lộ trình và một ngày, không phải là một URL. Giá vé máy bay không có một trang chuẩn duy nhất như một SKU bán lẻ — cùng một chuyến đi là sự kết hợp của departure_id, arrival_id, outbound_date, và (đối với chuyến khứ hồi) return_date. Pipeline dưới đây truy vấn sự kết hợp đó trực tiếp thông qua Scrapeless Scraping API với diễn viên scraper.google.flights thay vì hiển thị và thu thập thông tin từ giao diện của Google Flights.
  • Loại chuyến đi là một tham số hạng nhất, không phải là một biến thể URL. data_type chuyển đổi cùng một yêu cầu giữa một chiều, chuyến khứ hồi, và nhiều thành phố, và nhiều thành phố sử dụng mảng multi_city_json riêng cho các chân bay. Một chiếc đồng hồ giá bán lẻ không bao giờ cần đến chiều kích này; một chiếc đồng hồ chuyến bay thì cần từ ngày đầu tiên.
  • Google Flights cung cấp tín hiệu biến động của chính mình. Đối tượng price_insights trong phản hồi mang theo lowest_price và phân loại price_level (low, typical, high) cùng với các đề nghị cá nhân trong best_flightsother_flights. Điều đó cho phép pipeline cảnh báo về "đây là một mức giá tốt ngay bây giờ," không chỉ là "đây rẻ hơn lần trước."
  • Lịch sử được khóa bằng lộ trình và ngày, được thêm vào một lần cho mỗi lần kiểm tra. Một nhật ký JSONL với một bản ghi cho mỗi sự kết hợp (departure_id, arrival_id, outbound_date, return_date) là đủ để tính toán một mức giá thấp liên tục và phát hiện sự giảm giá — không cần cơ sở dữ liệu cho đến khi danh sách theo dõi phát triển vượt quá một vài lộ trình.
  • Một sự giảm giá, hoặc một sự thay đổi price_level sang low, kích hoạt một webhook. Quyết định cảnh báo kết hợp so sánh số với tín hiệu định tính của Google, sau đó đăng một lần tới bất kỳ điểm cuối nào nhận nó.
  • Một trình lập lịch chạy vòng lặp; không có gì về nó cần phải chạy liên tục. Cron, Task Scheduler, hoặc một bộ hẹn giờ không máy chủ thực hiện kiểm tra theo nhịp điệu và thoát — pipeline không mang theo quy trình lâu dài.
  • Miễn phí để bắt đầu. Các tài khoản Scrapeless mới bao gồm khoản tín dụng miễn phí cho Scraping API — đăng ký tại app.scrapeless.com.

Giới thiệu: một mức giá là một mục tiêu di động với hai chiều, không phải một

Một mức giá bán lẻ là một số duy nhất gắn với một trang duy nhất. Một mức giá chuyến bay là một hàm của một lộ trình và một ngày, và cùng một lộ trình có thể có giá khác nhau cho từng ngày khởi hành trong cùng một tìm kiếm. Đó chính là lý do mà tính năng theo dõi giá vé máy bay của Google tồn tại: các hãng hàng không liên tục điều chỉnh giá vé dựa trên nhu cầu, và một người mua kiểm tra một lần và đặt vé đang đoán thời điểm. Trình theo dõi của Google gửi một bản tóm tắt khi lộ trình theo dõi di chuyển; nó không cung cấp cho người đọc dữ liệu thô, một webhook, hoặc một cách để kết hợp tín hiệu đó với bất cứ điều gì khác mà người đọc đang chạy.

Bài viết này xây dựng một pipeline nhỏ thực hiện công việc đó một cách tự động, dựa trên một lộ trình cụ thể và khoảng thời gian ngày thay vì giao diện của Google Flights. Scraping API của Scrapeless cung cấp một diễn viên được thiết kế riêng cho bề mặt này — scraper.google.flights — chấp nhận một sự kết hợp giữa lộ trình và ngày và trả về dữ liệu giá vé có cấu trúc: các đề nghị cá nhân, một hãng hàng không và thời gian cho mỗi đề nghị, và đánh giá mức giá của Google về vé. Pipeline truy vấn diễn viên đó, xuất tín hiệu giá, thêm chúng vào một nhật ký lịch sử được khóa theo lộ trình và ngày, quyết định xem việc đọc này có đáng để cảnh báo hay không, và kích hoạt một webhook khi cần. Một trình lập lịch thực hiện kiểm tra theo nhịp điệu.

Diễn viên trả về phản hồi disabled actor trực tiếp cho tài khoản được sử dụng để nghiên cứu bài viết này — một khoảng trống cấp kế hoạch, không phải là một pipeline bị lỗi — và khoảng trống đó được tài liệu hóa tại điểm mà nó quan trọng thay vì bị bỏ qua. Mỗi giai đoạn khác dưới đây đều được thực hiện đối với Python đã thực thi thực tế.

Những gì bạn có thể làm với nó

  • Theo dõi giá vé cá nhân. Theo dõi một lộ trình và khoảng thời gian ngày cụ thể và chỉ nhận thông báo khi giá thực sự di chuyển theo hướng hữu ích.
  • Mua sắm với ngày linh hoạt. Chạy cùng một lộ trình qua nhiều ngày ứng viên và so sánh price_insights.price_level qua các ngày đó để tìm khoảng thời gian rẻ nhất, không chỉ là ngày đơn rẻ nhất.
  • Giám sát chuyến đi doanh nghiệp. Một bàn du lịch có thể theo dõi các lộ trình đi làm hoặc lộ trình khách hàng thường xuyên và báo hiệu khi lộ trình đã đặt đã giảm giá, dẫn đến việc đặt lại.
  • Theo dõi hành trình đa thành phố. multi_city_json cho phép cùng một yêu cầu đại diện cho một chuyến đi đa chân, vì vậy một hành trình với ba hoặc bốn phân đoạn chỉ là một cuộc gọi thay vì ba hoặc bốn chiếc đồng hồ một chiều riêng biệt được kết hợp bằng tay.
  • Tập dữ liệu lịch sử giá. Nhật ký chỉ thêm vào cũng có thể được sử dụng như một chuỗi thời gian cho mỗi lộ trình, hữu ích cho phân tích xu hướng sau này hoặc để cung cấp cho một phép toán "đặt ngay hoặc chờ."

Tại sao chọn Scrapeless Scraping API cho điều này

Scrapeless Scraping API cung cấp các diễn viên cụ thể cho từng trang web — scraper.<site> — mà trả về JSON có cấu trúc cho một mục tiêu nhất định thay vì một trang đã được hiển thị mà người gọi phải phân tích. Đối với dữ liệu chuyến bay, điều đó có nghĩa:

  • Yêu cầu về tuyến đường và ngày tháng, không phải phiên trình duyệt. Lời gọi là một POST xác thực với một thân JSON; không có trang nào để hiển thị, không có bộ chọn nào để cố định, và không có phiên nào để giữ sống.
  • Kiểu chuyến đi được tích hợp vào hình dạng yêu cầu. data_type bao gồm một chiều (2), khứ hồi (1), và nhiều thành phố (3, với multi_city_json) như các tham số được tài liệu hóa, không phải là logic thu thập dữ liệu riêng biệt cho từng trường hợp.
  • Một phân loại giá mà trang mục tiêu tự tính toán. price_insights.price_level là phán đoán của Google về việc giá vé thấp, trung bình, hoặc cao cho tuyến đường - ống dẫn có thể dựa vào đó thay vì đoán một ngưỡng từ đầu.
  • Một yêu cầu thay vì một chồng phân phối. Dữ liệu giá vé hàng không thường đến được tay bên gọi thông qua một mẩu ghép của GDS và các nguồn cấp trực tiếp từ hãng hàng không - kiểu phân phối phân mảnh như tiêu chuẩn NDC mới của IATA tồn tại để chuẩn hóa ở phía ngành hàng không. Một cuộc gọi của tác nhân có cấu trúc đơn lẻ hoàn toàn tránh khỏi chồng đó cho một việc theo dõi giá chỉ đọc.

Nhận một khóa API miễn phí tại app.scrapeless.com. Trang trang sản phẩm Scraping API đề cập đến gia đình tác nhân mà ống dẫn này gọi, và hình dạng yêu cầu cho tác nhân cụ thể này được trình bày trong hướng dẫn yêu cầu Google Flights API.

Điều kiện tiên quyết

  • Python 3.10 hoặc mới hơn, cộng với gói requests (pip install requests).
  • Một tài khoản Scrapeless và khóa API, được đọc từ biến môi trường SCRAPELESS_API_KEY. Xác nhận rằng tác nhân scraper.google.flights đã được kích hoạt cho kế hoạch tài khoản trước khi chỉ định lưu lượng sản xuất vào đó - xem ghi chú dưới Fetch, bên dưới.
  • Một điểm cuối webhook để nhận thông báo (Slack, Discord hoặc bất kỳ relay nào chấp nhận JSON POST).
  • Một máy chủ có khả năng cron, Windows Task Scheduler, hoặc một bộ lập lịch không máy chủ để chạy kiểm tra theo chu kỳ.

Ống dẫn qua cái nhìn tổng quan

Ống dẫn có sáu giai đoạn được kết nối theo một đường thẳng: lấy → khám phá → trích xuất → chuyển đổi → lưu trữ → quyết định → thông báo, với một bộ lập lịch điều khiển toàn bộ theo chu kỳ.

  1. Lấy — POST một tuyến đường, một ngày (hoặc khoảng thời gian ngày), và một kiểu chuyến đi tới scraper.google.flights.
  2. Khám phá — đọc lại phong bì kiểu chuyến đi: phản hồi thực tế đã điền best_flights, other_flights, và price_insights.
  3. Trích xuất — lấy giá của đề nghị rẻ nhất và phân loại price_level của Google từ phong bì đó.
  4. Chuyển đổi — chuẩn hóa việc đọc thành một bản ghi chuẩn được lập chỉ mục theo tuyến đường và ngày tháng.
  5. Lưu trữ — thêm bản ghi vào nhật ký lịch sử.
  6. Quyết định và thông báo — so sánh với lịch sử, và gửi một webhook khi việc đọc đáng giá để công khai.

Lấy: Truy vấn một Tuyến đường và Một Cửa sổ Ngày

Yêu cầu là một POST xác thực duy nhất. Tên tác nhân đi vào actor; các tham số tuyến đường và ngày tháng đi vào input. data_type1 cho một chuyến khứ hồi và 2 cho một chiều; một chuyến khứ hồi cũng mang theo return_date.

python Copy
import os
import requests

API_URL = "https://api.scrapeless.com/api/v1/scraper/request"


def fetch_route(departure_id: str, arrival_id: str, outbound_date: str,
                 return_date: str | None = None, gl: str = "us", hl: str = "en") -> dict:
    payload = {
        "actor": "scraper.google.flights",
        "input": {
            "departure_id": departure_id,
            "arrival_id": arrival_id,
            "data_type": 1 if return_date else 2,   # 1 = khứ hồi, 2 = một chiều
            "outbound_date": outbound_date,
            "gl": gl,
            "hl": hl,
        },
    }
    if return_date:
        payload["input"]["return_date"] = return_date

    headers = {"Content-Type": "application/json", "x-api-token": os.environ["SCRAPELESS_API_KEY"]}
    response = requests.post(API_URL, headers=headers, json=payload, timeout=60)
    response.raise_for_status()
    return response.json()

outbound_datereturn_date theo dạng ngày lịch thông thường YYYY-MM-DD RFC 3339 xác định cho việc trao đổi thời gian ngày tháng trên internet — không có thành phần thời gian, vì một tìm kiếm giá vé được lập chỉ mục theo ngày khởi hành, không phải theo khoảnh khắc khởi hành. Một hành trình nhiều thành phố thay thế các trường đơn lẻ bằng data_type: 3 và một mảng multi_city_json, một đối tượng cho mỗi chặng, mỗi đối tượng mang theo departure_id, arrival_id, và date của riêng nó.

Khoảng cách tiên quyết: chạy cuộc gọi trên một tuyến đường thực tế đã trả về HTTP 400 trong thời gian thực cho tài khoản được sử dụng để xác minh pipeline này: {"code": 14002, "message": "diễn viên bị vô hiệu hóa: scraper.google.flights"}. Thông điệp đó khác với lỗi của một tên diễn viên không được công nhận ("diễn viên không hợp lệ: <name>" — đã được xác nhận bằng cách cố ý yêu cầu một vài biến thể giả mạo), điều đó có nghĩa là diễn viên đó thực sự tồn tại và được ghi nhận; nó bị hạn chế khỏi gói miễn phí được sử dụng ở đây chứ không phải là không có. Các cơ chế yêu cầu cơ bản của tài khoản không bị nghi vấn — diễn viên đồng cấp scraper.google.search đã trả về HTTP 200 thực sự với kết quả tự nhiên trên cùng một khóa và cùng một điểm cuối trong nghiên cứu này. Xác nhận rằng diễn viên đã được bật cho kế hoạch mục tiêu (GET /api/v1/me báo cáo kế hoạch và số dư tín dụng) trước khi xây dựng lưu lượng sản xuất trên nó, và coi tất cả từ Discover trở đi như đang chạy dựa trên hình thức phản hồi được tài liệu hóa của diễn viên thay vì một trang được ghi lại trực tiếp trong phiên này.

Discover: Đọc Bao Bì Trip-Type

Một tìm kiếm khứ hồi hoặc một chiều trả về hai mảng cung cấp — best_flightsother_flights — cộng với một đối tượng price_insights. Một tìm kiếm đa thành phố trả về cấu trúc tương đương trên mỗi hành trình thay vì trên mỗi chuyến bay đơn. Trước khi trích xuất bất cứ điều gì, pipeline chỉ cần biết trong số những khóa đó phản hồi thực sự đã được điền, vì một tuyến đường hẹp hoặc một ngày kỳ lạ có thể trả lại other_flights rỗng và chỉ best_flights mang lại các ưu đãi.

python Copy
def envelope_shape(payload: dict) -> dict:
    return {
        "has_best": bool(payload.get("best_flights")),
        "has_other": bool(payload.get("other_flights")),
        "has_insights": bool(payload.get("price_insights")),
    }


if __name__ == "__main__":
    sample = {"best_flights": [{"price": 312}], "other_flights": [], "price_insights": {}}
    print(envelope_shape(sample))

Chạy trên một phản hồi hẹp chỉ điền best_flights, envelope_shape trả về {'has_best': True, 'has_other': False, 'has_insights': False} — chính xác là trường hợp mà giai đoạn trích xuất bên dưới phải chịu đựng.

Đây là một kiểm tra đơn giản, nhưng điều đó quan trọng: trích xuất "ưu đãi rẻ nhất" từ một bao bì mà chưa bao giờ điền best_flights tạo ra một None thay vì một số sai, miễn là giai đoạn trích xuất kiểm tra cả hai mảng thay vì giả định rằng một trong số đó luôn có.

Extract: Lấy Các Tín Hiệu Giá từ Phản Hồi

Giai đoạn trích xuất lấy ưu đãi rẻ nhất từ cả hai mảng và các trường price_insights kèm theo. Cấu trúc dưới đây phản ánh hình thức phản hồi được tài liệu hóa cho gia đình diễn viên này — best_flightsother_flights là các mảng của các đối tượng {flights: [...], price, layovers}, và price_insights mang theo lowest_price, price_level, và một typical_price_range — vì cuộc gọi trực tiếp là khoảng cách đã nêu ở trên chứ không phải là một trang mà phiên này đã ghi lại.

python Copy
# Cấu trúc minh họa phù hợp với hình thức phản hồi được tài liệu hóa (xem
# ghi chú khoảng cách tiên quyết dưới Fetch) — không phải là một bản ghi trực tiếp.
FIXTURE_RESPONSE = {
    "best_flights": [
        {
            "flights": [{
                "departure_airport": {"id": "JFK", "time": "2026-08-10 08:00"},
                "arrival_airport": {"id": "LAX", "time": "2026-08-10 11:24"},
                "airline": "Example Air",
                "flight_number": "EX 101",
                "duration": 384,
                "travel_class": "Economy",
            }],
            "price": 312,
            "layovers": 0,
        }
    ],
    "other_flights": [
        {"flights": [{"airline": "Sample Airways", "flight_number": "SA 220"}], "price": 349, "layovers": 1}
    ],
    "price_insights": {
        "lowest_price": 289,
        "price_level": "low",
        "typical_price_range": [295, 430],
    },
}


def extract_fares(payload: dict) -> dict:
    best = payload.get("best_flights", [])
    other = payload.get("other_flights", [])
    insights = payload.get("price_insights", {})

    cheapest_offer = min([*best, *other], key=lambda offer: offer["price"], default=None)

    return {
        "cheapest_price": cheapest_offer["price"] if cheapest_offer else None,
        "cheapest_layovers": cheapest_offer["layovers"] if cheapest_offer else None,
        "offer_count": len(best) + len(other),
        "price_insights_lowest": insights.get("lowest_price"),
        "price_level": insights.get("price_level"),
        "typical_price_range": insights.get("typical_price_range"),
    }


if __name__ == "__main__":
    print(extract_fares(FIXTURE_RESPONSE))

Chạy trên cấu trúc, extract_fares trả về:

text Copy
{'cheapest_price': 312, 'cheapest_layovers': 0, 'offer_count': 2,
 'price_insights_lowest': 289, 'price_level': 'low', 'typical_price_range': [295, 430]}

Lưu ý khoảng cách giữa cheapest_price (giá đề nghị thấp nhất thực sự được trả về) và price_insights_lowest (sàn giá của Google cho tuyến đường). Hai giá trị này hiếm khi khớp chính xác — price_insights phản ánh một cái nhìn rộng hơn về tuyến đường so với các đề nghị cụ thể trong phản hồi này — vì vậy hãy giữ cả hai trường thay vì gộp chúng thành một số.

Biến đổi: Chuẩn hóa thành bản ghi tuyến đường-ngày

Danh tính của một chuyến bay theo dõi là bộ (departure_id, arrival_id, outbound_date, return_date), không phải là một URL. Giai đoạn biến đổi xây dựng khóa đó bên cạnh hình dạng bản ghi chính thống để lưu trữ và so sánh không bao giờ phải tái tạo nó.

python Copy
from datetime import datetime, timezone


def to_record(route: dict, fares: dict) -> dict:
    return {
        "departure_id": route["departure_id"],
        "arrival_id": route["arrival_id"],
        "outbound_date": route["outbound_date"],
        "return_date": route.get("return_date"),
        "trip_type": "round_trip" if route.get("return_date") else "one_way",
        "price": fares["cheapest_price"],
        "price_level": fares["price_level"],
        "currency": "USD",
        "checked_at": datetime.now(timezone.utc).strftime("%d-%b-%Y %H:%M UTC"),
    }


def route_key(record: dict) -> str:
    return f"{record['departure_id']}-{record['arrival_id']}:{record['outbound_date']}:{record.get('return_date')}"


if __name__ == "__main__":
    sample_route = {"departure_id": "JFK", "arrival_id": "LAX",
                     "outbound_date": "2026-08-10", "return_date": "2026-08-17"}
    sample_fares = {"cheapest_price": 312, "price_level": "low"}
    record = to_record(sample_route, sample_fares)
    print(record)
    print(route_key(record))

Khi chạy với tuyến đường mẫu (JFK đến LAX, ngày đi 10 tháng 8, ngày về 17 tháng 8) và các mức giá được trích xuất ở trên, to_record trả về một bản ghi với price: 312, price_level: 'low', trip_type: 'round_trip', và route_key trả về JFK-LAX:2026-08-10:2026-08-17. Một chuyến đi một chiều theo dõi trên cùng một tuyến đường và ngày đi tạo ra một khóa riêng ngay khi return_date bị thiếu, đó là điểm quan trọng: hai loại chuyến đi trên cùng một tuyến đường là hai theo dõi khác nhau, không phải một.

Lưu trữ: Một bản ghi chỉ thêm được khóa bằng Tuyến đường và Ngày

Lưu trữ là cùng một mẫu JSONL chỉ cho phép thêm mà một hệ thống theo dõi giá bán lẻ sử dụng, với một điểm khác biệt: tìm kiếm lọc theo khóa tuyến đường-ngày thay vì URL.

python Copy
import json

HISTORY_FILE = "flight_history.jsonl"


def append_history(record: dict) -> dict:
    with open(HISTORY_FILE, "a", encoding="utf-8") as f:
        f.write(json.dumps(record) + "\n")
    return record


def load_history(key: str) -> list[dict]:
    rows = []
    try:
        with open(HISTORY_FILE, encoding="utf-8") as f:
            for line in f:
                row = json.loads(line)
                if route_key(row) == key:
                    rows.append(row)
    except FileNotFoundError:
        pass
    return rows


if __name__ == "__main__":
    open(HISTORY_FILE, "w").close()  # bắt đầu bản demo này từ một nhật ký trống
    sample_record = {"departure_id": "JFK", "arrival_id": "LAX", "outbound_date": "2026-08-10",
                      "return_date": "2026-08-17", "trip_type": "round_trip",
                      "price": 312, "price_level": "low", "currency": "USD",
                      "checked_at": "13-Jul-2026 14:28 UTC"}
    append_history(sample_record)
    append_history(sample_record)
    with open(HISTORY_FILE, encoding="utf-8") as f:
        print(f"{len(f.readlines())} dòng trong {HISTORY_FILE} sau hai lần kiểm tra")

Việc thêm cùng một bản ghi hai lần và tải lại theo khóa trả về cả hai hàng theo thứ tự chèn, xác nhận rằng nhật ký tích lũy chứ không ghi đè — đảm bảo giống như một tệp chỉ cho phép thêm với một theo dõi theo khóa URL, chỉ được lọc theo một khóa khác. Qua một vài tuyến đường, hãy thay thế tệp bằng một bảng SQLite nhỏ với (departure_id, arrival_id, outbound_date, return_date) làm khóa tổng hợp; hình dạng đọc và ghi vẫn giữ nguyên.

Quyết định: So sánh với Lịch sử và Tín hiệu Giá của Google

Một hệ thống theo dõi giá bán lẻ có một quy tắc quyết định: liệu giá mới có thấp hơn giá thấp nhất đã thấy cho đến nay. Một hệ thống theo dõi giá vé có tín hiệu thứ hai — price_level — vì vậy giai đoạn quyết định kiểm tra cả hai: một giảm giá số chính thức thấp hơn giá thấp hiện hành, hoặc một lần đọc đầu tiên đã có giá price_level: "low".

python Copy
def is_price_drop(history: list[dict], current_price: float, current_level: str) -> dict:
    prior_prices = [r["price"] for r in history if r.get("price") is not None]
    previous_low = min(prior_prices) if prior_prices else None

    numeric_drop = (
        current_price is not None
        and previous_low is not None
        and current_price < previous_low
    )
    level_signal = current_level == "low"

    return {
        "dropped": numeric_drop,
        "worth_alerting": numeric_drop or (level_signal and previous_low is None),
        "current": current_price,
        "previous_low": previous_low,

"price_level": current_level,
}

if name == "main":
history = [{"price": 349}, {"price": 340}]
print(is_price_drop(history, current_price=312, current_level="thấp"))

Copy
So với hai mức giá trước là 349 và 340, một mức giá mới là 312 với `price_level: "thấp"` trả về `{'dropped': True, 'worth_alerting': True, 'current': 312, 'previous_low': 340, 'price_level': 'thấp'}`. Trường `worth_alerting` được thiết kế rộng hơn `dropped`: nó cũng kích hoạt cho lần đọc đầu tiên của một tuyến đường nếu phân loại của Google đã gọi là thấp, vì một so sánh về giá thấp chưa có gì để so sánh nhưng tín hiệu định tính thì có. Sự phân biệt đó phản ánh điều mà <a href="https://arxiv.org/abs/2411.08126" rel="nofollow"><strong>nghiên cứu về định giá động ngoại tuyến trong thị trường vé máy bay</strong></a> coi là một đặc điểm cấu trúc của danh mục: nguồn cung ghế máy bay được định giá lại dựa trên nhu cầu và công suất còn lại, không phải dựa trên lịch bán cố định, vì vậy một bức ảnh đơn lẻ đã có thể cung cấp thông tin mà không cần có lịch sử giá để so sánh.

## Cảnh báo: Kích hoạt Webhook khi có giảm giá

Cơ chế cảnh báo giống như một `requests.post` duy nhất mà một watch bán lẻ sử dụng — một cuộc gọi HTTP, không có hàng đợi, không có trung gian — với thông điệp được xây dựng từ danh tính tuyến đường và ngày tháng thay vì tên sản phẩm.

```python
def send_alert(route_label: str, decision: dict, webhook_url: str) -> int:
    message = (
        f"Giám sát giá: {route_label}\n"
        f"Bây giờ ${decision['current']} (đã từng ${decision['previous_low']}), "
        f"Google price_level={decision['price_level']}"
    )
    response = requests.post(webhook_url, json={"text": message}, timeout=15)
    response.raise_for_status()
    return response.status_code

raise_for_status() sẽ báo ngay khi điểm cuối webhook bị lỗi thay vì để một relay được cấu hình sai nuốt trọn mọi cảnh báo một cách im lặng. Nội dung {"text": message} phù hợp với hình thức webhook đầu vào phổ biến của Slack/Discord; điều chỉnh JSON theo những gì điểm cuối nhận được mong đợi.

Lịch trình: Đặt lịch theo chu kỳ bị giới hạn bởi chuyến đi

Kết nối việc lấy dữ liệu thông qua cảnh báo thành một hàm cho phép một lệnh gọi check_route() mà một trình lập lịch có thể thực hiện:

python Copy
def check_route(departure_id, arrival_id, outbound_date, return_date=None, webhook_url=None):
    payload = fetch_route(departure_id, arrival_id, outbound_date, return_date)
    fares = extract_fares(payload)
    route = {"departure_id": departure_id, "arrival_id": arrival_id,
             "outbound_date": outbound_date, "return_date": return_date}
    record = append_history(to_record(route, fares))
    decision = is_price_drop(load_history(route_key(record)), record["price"], record["price_level"])
    if decision["worth_alerting"] and webhook_url:
        send_alert(f"{departure_id}-{arrival_id} {outbound_date}", decision, webhook_url)
    return record, decision

Việc thực hiện hàng ngày là đủ cho hầu hết các giám sát cá nhân; một bàn điều hành du lịch doanh nghiệp theo dõi nhiều tuyến đường thường xuyên có thể thực hiện thường xuyên hơn. Khác với SKU bán lẻ, một bảng theo dõi chuyến bay cũng có một thời điểm kết thúc tự nhiên: khi ngày xuất phát đã qua, tuyến đường sẽ không còn là một tìm kiếm trực tiếp và lịch sử của nó đã hoàn tất. Một mục crontab là trình điều khiển đơn giản nhất trên Linux hoặc macOS, sử dụng cùng một năm trường cụm thời gian crontab POSIX mà bất kỳ công việc nào lên lịch trên nền tảng đều sử dụng:

bash Copy
# crontab -e — kiểm tra hai lần mỗi ngày, 07:00 và 19:00
0 7,19 * * * cd /opt/fare-watch && /usr/bin/python3 check.py >> watch.log 2>&1

Lịch Trình tác vụ Windows chạy cùng một lệnh trên một kích hoạt tương đương, và một bộ đếm không máy chủ cũng hoạt động — kịch bản giữ không có trạng thái nào ngoài фай lịch sử, vì vậy một cuộc gọi không trạng thái phù hợp với nó. Đối với một danh sách theo dõi của nhiều tuyến đường, lặp check_route() trên danh sách và giữ cho tính đồng thời ở mức vừa phải để tỷ lệ giới hạn tốc độ của khóa API vẫn thoải mái.

Những gì bạn nhận được

Mỗi lần kiểm tra sẽ thêm một bản ghi vào nhật ký lịch sử, được khóa theo tuyến đường và ngày tháng thay vì theo URL:

json Copy
// Lược đồ phản ánh chính xác những gì to_record()/append_history() ghi lại.
// Các giá trị trường chỉ là minh họa, khớp với thiết lập trên — không phải một lần đọc trực tiếp.
{
  "departure_id": "JFK",
  "arrival_id": "LAX",
  "outbound_date": "2026-08-10",
  "return_date": "2026-08-17",
  "trip_type": "round_trip",
  "price": 312,
  "price_level": "thấp",
  "currency": "USD",
  "checked_at": "13-Jul-2026 15:01 UTC"
}

Một vài điểm cần biết trước khi chạy điều này quy mô lớn:

  • priceprice_insights_lowest có thể khác nhau. Số đầu tiên là đề nghị rẻ nhất mà phản hồi cụ thể này đã trả về; cái sau là cái nhìn rộng hơn của Google về tuyến đường. Lưu cả hai nếu trường hợp sử dụng phía dưới quan tâm đến sự khác biệt.
  • Hình ảnh lưu trữ nên là toàn bộ giá vé, không phải là giá cơ bản. Các hãng hàng không Mỹ quảng cáo giá vé phải tuân theo quy định quảng cáo giá vé toàn phần của liên bang để báo giá toàn bộ giá mà hành khách phải trả, không phải là giá cơ bản bị lược bỏ — coi bất kỳ trường nào mà pipeline lưu trữ là price theo cách tương tự, vì vậy sự so sánh giữa các kiểm tra không bao giờ là giá cơ bản vào một ngày và tổng tất cả vào một ngày khác.
  • Nhiều thành phố thay đổi hình dạng, không phải pipeline. data_type: 3multi_city_json tạo ra phản hồi ở cấp độ hành trình thay vì chỉ là phản hồi cho một chặng bay; mở rộng to_record để gộp giá của mỗi chặng bay thành một tổng thay vì coi hành trình như một mức giá cố định.
  • glhl ảnh hưởng đến tiền tệ và ngôn ngữ, không chỉ là từ ngữ. Ghim cả hai theo dõi giống như cách một đồng hồ bán lẻ gắn proxy_country, để sự so sánh giữa các kiểm tra giữ trong cùng một thị trường.
  • Giá có thể null không phải là số không. Xử lý cheapest_price thiếu như None, chính xác như giai đoạn trích xuất đã làm — một phản hồi lạ thỉnh thoảng không bao giờ được phép đưa vào một số không giả tạo vào mức thấp liên tục.

Kết luận: một tuyến đường, một ngày, một quyết định

Pipeline giảm xuống hình dạng giống như bất kỳ đồng hồ giá nào khác — lấy dữ liệu, trích xuất, lưu trữ, quyết định, cảnh báo, lên lịch — với danh tính của một "đồng hồ" được định nghĩa lại cho danh mục: một cặp tuyến đường và ngày thay vì một URL, một tham số loại chuyến thay vì một hình dạng yêu cầu đơn lẻ, và một tín hiệu thứ hai, do mục tiêu cung cấp (price_level) có sẵn bên cạnh sự so sánh số nguyên thông thường. Mở rộng danh sách theo dõi đến nhiều tuyến đường, nhiều ngày, hoặc một hành trình nhiều thành phố sẽ tái sử dụng từng giai đoạn mà không thay đổi; chỉ các tham số được chuyển vào Fetch là thay đổi.

Sẵn sàng để Xây Dựng Pipeline Dữ Liệu AI của Bạn?

Tham gia cộng đồng để trao đổi ghi chú với những nhà phát triển xây dựng pipeline dữ liệu trên Scrapeless: Discord · Telegram.

Đăng ký tại app.scrapeless.com để nhận tín dụng API Scraping miễn phí, xác nhận rằng tác nhân scraper.google.flights được kích hoạt trong gói tài khoản, và điều chỉnh các giai đoạn trên phù hợp với các tuyến đường mà danh sách theo dõi cần. Xem giá để biết chi tiết về các gói, và tài liệu API Scraping để tham khảo tác nhân hiện tại.

Câu hỏi thường gặp

Q: scraper.google.flights có phải là một tác nhân thực, đã được tài liệu hóa không?
Có. Một tên tác nhân được đặt ra một cách cố ý sẽ trả về "invalid actor: <name>"; yêu cầu scraper.google.flights sẽ trả về "disabled actor: scraper.google.flights" thay vào đó — một lỗi khác biệt chỉ áp dụng cho một tác nhân đã được công nhận, niêm yết. Nó bị chặn trong một số gói thay vì không tồn tại; xác nhận rằng nó được kích hoạt cho gói của tài khoản mục tiêu trước khi tập trung lưu lượng sản xuất vào nó.

Q: Tại sao lại ghi lịch sử theo tuyến đường và ngày thay vì một URL, giống như cách đồng hồ bán lẻ làm?
Bởi vì giá vé máy bay không có một trang chính thống duy nhất như một SKU bán lẻ. Cùng một tuyến đường có giá khác nhau theo ngày bay đi, ngày về và loại chuyến đi, vì vậy danh tính của một "đồng hồ" phải là sự kết hợp đó, chứ không phải một URL.

Q: price_insights.price_level bổ sung gì so với một sự so sánh "rẻ hơn lần trước"?
Đó là phân loại của riêng Google về việc giá hiện tại có thấp, điển hình, hay cao cho tuyến đường, được tính toán từ dữ liệu vượt xa phản hồi đơn lẻ mà pipeline vừa nhận được. Kết hợp nó với sự so sánh mức thấp liên tục cho phép lần đọc đầu tiên của một tuyến đường có thể hành động, không chỉ là những lần đọc thứ hai và sau đó.

Q: Có thể xử lý một hành trình nhiều thành phố không?
Có, ở cấp yêu cầu — data_type: 3 với một mảng multi_city_json của các chặng bay là một hình dạng tham số được tài liệu hóa cho gia đình tác nhân này. Mở rộng to_record để gộp một hành trình nhiều chặng bay thành một tổng có thể so sánh là một mạch logic bổ sung duy nhất mà một đồng hồ nhiều thành phố cần bên cạnh những gì bài viết này xây dựng.

Q: Tần suất kiểm tra nên diễn ra như thế nào?
Một lần kiểm tra hàng ngày hoặc hai lần trong ngày phù hợp cho hầu hết các đồng hồ giá cá nhân. Một đồng hồ bay cũng có một thời điểm dừng tự nhiên: khi ngày đi qua, tuyến đường không còn là một tìm kiếm trực tiếp, không giống như một SKU bán lẻ có thể được theo dõi vô thời hạn.

Q: Có thể chạy điều này mà không cần tác nhân AI không?
Có. Mã Python trong Fetch qua Schedule chạy từ đầu đến cuối một mình — gọi, trích xuất, lưu trữ, quyết định, cảnh báo, và để một bộ lập lịch điều khiển nhịp điệu. Một tác nhân là một cách tiện lợi để điều khiển lựa chọn tuyến đường và ngày bằng ngôn ngữ tự nhiên, nhưng pipeline tự nó không cần gì hơn ngoài Python và một bộ lập lịch.

Tại Scrapless, chúng tôi chỉ truy cập dữ liệu có sẵn công khai trong khi tuân thủ nghiêm ngặt các luật, quy định và chính sách bảo mật trang web hiện hành. Nội dung trong blog này chỉ nhằm mục đích trình diễn và không liên quan đến bất kỳ hoạt động bất hợp pháp hoặc vi phạm nào. Chúng tôi không đảm bảo và từ chối mọi trách nhiệm đối với việc sử dụng thông tin từ blog này hoặc các liên kết của bên thứ ba. Trước khi tham gia vào bất kỳ hoạt động cạo nào, hãy tham khảo ý kiến ​​cố vấn pháp lý của bạn và xem xét các điều khoản dịch vụ của trang web mục tiêu hoặc có được các quyền cần thiết.

Bài viết phổ biến nhất

Danh mục