← Tất cả bài viết

Scraping dữ liệu tồn kho đại lý vượt qua các trang tổng hợp

Scraping dữ liệu tồn kho đại lý thường gặp sự cố ở chính các trang web đại lý, không phải các bên tổng hợp. Giá hiển thị phía client, một vài mẫu nền tảng và các khối chặn trông giống như dữ liệu thật.

Mọi người đều bắt đầu bằng việc xây dựng scraper cho các trang tổng hợp trước. Cars.com, CarGurus, AutoTrader: ba trang web, mỗi trang một parser, và đến cuối tuần bạn đã có một luồng dữ liệu trông như toàn bộ thị trường xe đã qua sử dụng.

Sau đó, ai đó sẽ hỏi phần còn lại ở đâu.

Thách thức

NADA thống kê có 16.972 đại lý xe thương mại hạng nhẹ được nhượng quyền tại Hoa Kỳ tính đến báo cáo giữa năm 2025, và con số đó chưa bao gồm các bãi xe độc lập vốn không được ai thống kê nhất quán. Các bài viết thứ cấp trích dẫn cùng số liệu NADA dao động từ 15.720 đến 16.990, điều này cho thấy thị trường này được đo lường ở mức độ nào.

Mỗi đại lý riêng lẻ đó đều vận hành một trang web riêng. Lượng hàng tồn kho trên đó là kho xe thực tế của đại lý, được định giá theo ngày hôm nay, nhiều ngày trước khi bất kỳ thông tin nào xuất hiện trên một trang niêm yết của bên thứ ba. Nếu bạn đang định giá xe đã qua sử dụng, dự báo giá trị còn lại, hoặc bán một công cụ cạnh tranh lại cho các đại lý, dữ liệu từ trang web của từng đại lý chính là thứ bạn cần. Các trang tổng hợp chỉ là một bản sao trễ hạn và đã bị lọc bớt.

Vì vậy, các đội ngũ kỹ thuật bắt đầu nhắm vào các trang web của từng đại lý, và họ lần lượt phát hiện ra ba điều.

Chúng không hề độc nhất. Hầu như tất cả đều chạy trên một nhóm nhỏ các nền tảng trang web đại lý: Dealer.com, DealerOn, Dealer Inspire, CDK, Reynolds, Sincro, Lotlinx (danh sách chính xác phụ thuộc vào nguồn thống kê). Bất kỳ ai bán dữ liệu này đều xây dựng một parser cho mỗi nền tảng, chứ không phải cho từng đại lý. dealer website inventory scraper của Apify đã chỉ rõ điều này: nhận diện nền tảng trước, sau đó mới trích xuất dữ liệu. Một số ít template có thể bao phủ hàng chục nghìn đại lý. Đó là tin tốt trong câu chuyện này.

Giá thường không nằm trong HTML bạn vừa tải về. Các nền tảng này render khối thông tin giá ở phía client, các ước tính khoản thanh toán và chương trình khuyến mãi thường được trả về trong một lệnh gọi thứ hai sau đó. Một lượt fetch HTTP thông thường chỉ mang về năm sản xuất, hãng xe, mẫu xe, số dặm đã đi và số VIN. Trường giá trả về sẽ bị trống.

Và phần âm thầm làm hỏng các bộ dữ liệu: trên trang web đại lý, một lỗi trông giống hệt như một dữ liệu thực tế. Một dịch vụ phát hiện bot phản hồi một request đáng ngờ bằng HTTP 200 kèm một trang chuyển tiếp trung gian. Một khối thông tin giá không bao giờ được render sẽ để lại chuỗi "Call for Price" trong DOM, đây cũng là điều mà các đại lý chủ động ghi vào đó. Cả hai dòng dữ liệu này khi vào kho dữ liệu của bạn đều trông sạch sẽ như nhau.

Chúng tôi đã đề cập đến mô hình đó trong một lĩnh vực khác, nơi một lần bị chặn trông giống hệt một điểm dữ liệu. Ngành ô tô là phiên bản phức tạp hơn, vì việc "không có giá" là một trạng thái kinh doanh hợp lệ chứ không phải là một bất thường rõ ràng.

Điều gì thay đổi khi bạn phân loại theo chi phí

Các pipeline vận hành ổn định không tự tổ chức theo từng trang web. Chúng được tổ chức theo chi phí của một request.

Request đắt đỏ nhất là request đầu tiên gửi đến một đại lý: request phải chạy một trình duyệt, vượt qua bất kỳ rào cản nào mà nền tảng của đại lý đặt ra phía trước, và lấy về một session. Mọi thứ sau đó chỉ là một lệnh gọi HTTP chi phí thấp tái sử dụng những gì request đầu tiên đã thu được.

import requests

FOURA = "https://api.foura.ai/api"
AUTH = {"Authorization": "Bearer pk_live_..."}

# First page of a rooftop: render it, clear the defense, keep the session.
first = requests.post(f"{FOURA}/browser", headers=AUTH, json={
    "url": "https://example-motors.com/used-inventory/index.htm",
    "unblocker": True,
    "timeout_ms": 45000,
}).json()

listings = first["body"]
jar      = "; ".join(f'{c["name"]}={c["value"]}' for c in first["cookies"])
agent    = first["userAgent"]
exit_id  = first["proxy"]     # opaque proxy ID, send it back to stay on the same exit

Sau đó duyệt toàn bộ phần còn lại trong danh mục của đại lý đó mà không cần trả thêm chi phí cho trình duyệt:

page = requests.post(f"{FOURA}/single", headers=AUTH, json={
    "method": "GET",
    "url": "https://example-motors.com/used-inventory/index.htm?start=20",
    "proxy": exit_id,
    "headers": [["Cookie", jar], ["User-Agent", agent]],
    "validate": {
        "status": {"accept": [200]},
        "data": {
            "accept": ["vehicle-card"],
            "fail": ["Just a moment", "Access Denied"]
        }
    }
}).json()

Hai chi tiết ở đó quan trọng hơn vẻ ngoài của chúng.

Session di chuyển dưới dạng một đơn vị duy nhất. Một clearance cookie được liên kết với exit node đã tạo ra nó và với User-Agent được dùng khi tạo. Gửi lại nó từ một nơi khác, hoặc dưới một User-Agent khác, trang web sẽ buộc bạn quay lại từ bước challenge. Đó là lý do tại sao cookie jar, chuỗi User-Agent và proxy ID luôn đi cùng nhau. Đây là chi tiết chúng tôi thấy nhiều người làm sai nhất: họ giữ cookie, bỏ exit node, rồi tự hỏi tại sao lộ trình giá rẻ không còn rẻ nữa.

Khối validate chính là thứ loại bỏ vấn đề lỗi ngầm (silent failure). Đó là cách request xác định một trang thực tế trông như thế nào: chấp nhận một marker chỉ xuất hiện khi lưới danh sách đã render, thất bại nếu gặp các chuỗi chuyển tiếp (interstitial). Một response không thỏa mãn các quy tắc đó không phải là một dòng dữ liệu có giá trị price bị null. Nó là một lỗi, được phân loại chính xác, và không được tính là thành công. Trong ngành ô tô, hãy khai báo cả marker hợp lệ lẫn danh sách chuỗi lỗi, vì chuỗi "Call for Price" thực sự không rõ ràng nhưng chuỗi "thẻ xe chưa bao giờ render" thì luôn rõ ràng.

Khi bạn chưa biết một nền tảng nhất định cần lộ trình nào, Auto sẽ xác định điều đó chỉ trong một lệnh gọi duy nhất và trả về session đã hoạt động. Hãy coi đó là bước thăm dò, không phải lộ trình cho môi trường production. Khi bạn đã biết một nền tảng cần trình duyệt còn các nền tảng lân cận thì không, hãy ghim từng nền tảng vào direct engine và ngừng trả tiền cho orchestrator để tìm lại cùng một câu trả lời mỗi đêm.

Kết quả

Tính toán nhanh với một tác vụ quy mô trung bình (kịch bản minh họa dựa trên benchmark ngành, không phải một khách hàng cụ thể): 4.000 đại lý, mỗi nơi có khoảng 180 xe đã qua sử dụng, được làm mới hàng đêm.

  • 4.000 trang được render thay vì 720.000. Một lần render cho mỗi đại lý để mở session; 716.000 trang còn lại đi qua lộ trình giá rẻ trên cùng session đó. Tỷ lệ này, chứ không phải parser, quyết định việc thu thập dữ liệu hàng đêm có khả thi về chi phí hay không.
  • Hai loại giá bị thiếu, nằm trong hai bảng khác nhau. Với các quy tắc validate được thiết lập, trường hợp "đại lý không công bố giá" và "chúng ta chưa từng lấy được trang" không còn dùng chung một cấu trúc dòng dữ liệu. Model của bạn sẽ chỉ nhìn thấy trường hợp đầu tiên.
  • Một parser cho mỗi nền tảng, không phải cho từng đại lý. Nhận diện nền tảng từ response, chuyển HTML cho parser phụ trách nền tảng đó. Một đại lý mới trên một nền tảng đã hỗ trợ sẽ không tốn thêm chi phí tích hợp.
  • Một đại lý đổi nền tảng sẽ báo lỗi rõ ràng. Việc nhận diện nền tảng không khớp, dòng dữ liệu sẽ không được ghi, và ai đó sẽ nhận được ticket thay vì sáu tuần nhận dữ liệu giá sai trong âm thầm.

Điểm phức tạp phát sinh ở chỗ: các tập đoàn đại lý lớn ngày càng xây dựng nhiều trang web tùy biến ngoài các nền tảng chuẩn, và những trang đó vẫn cần các parser tự viết thủ công cùng toàn bộ chi phí bảo trì đi kèm. Độ mới của dữ liệu giữa các đại lý cũng không đồng đều. Một số nền tảng cache các trang danh mục rất lâu, vì vậy "giá hôm nay" có thể đã cũ một ngày bất kể bạn thu thập thường xuyên đến mức nào. Nếu model của bạn coi mọi timestamp của đại lý đều có tính realtime như nhau, đó là sai sót mà không hạ tầng thu thập nào có thể khắc phục được.

Điểm mấu chốt

Phần khó nhất của dữ liệu ngành ô tô chưa bao giờ nằm ở ba trang tổng hợp lớn mà ai cũng dùng làm thước đo chuẩn. Thách thức nằm ở mười bảy nghìn trang web nhỏ lẻ, những trang vốn không đáng để xây dựng một scraper riêng lẻ nhưng lại mang giá trị rất lớn khi gom chung lại.

Mô hình này cũng xuất hiện ở nhiều lĩnh vực khác ngoài ô tô. Hiệu thuốc, đại lý thiết bị, chuỗi tạp hóa khu vực, hay bất kỳ mô hình nhượng quyền nào: phần đuôi dài (long tail) chỉ trông có vẻ tốn kém khi bạn coi mỗi trang là một hệ thống riêng biệt. Thực tế thường không phải vậy. Xử lý chuẩn xác request đầu tiên, tái sử dụng session hiệu quả, và phần việc còn lại sẽ không còn là vấn đề thu thập dữ liệu nữa mà chuyển thành bài toán parsing dữ liệu, vốn tiết kiệm chi phí hơn rất nhiều.