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

Giám sát thông báo đấu thầu: Những thông báo scraper của bạn chưa từng thấy

Giám sát đấu thầu dưới ngưỡng EU đồng nghĩa với việc cào dữ liệu từ hàng loạt cổng thông tin nhỏ. Lỗi gây thiệt hại lớn không phải là bị từ chối truy cập, mà là lượt chạy báo xanh nhưng không lưu lại được gì.

216.000 €. Dưới ngưỡng đó, một hội đồng thành phố ở Châu Âu khi mua sắm dịch vụ không bắt buộc phải công bố gói thầu trên TED, hệ thống đăng ký trung tâm của EU.

Điều này có nghĩa là các hợp đồng mà một nhà thầu nhỏ có khả năng trúng thầu thực tế lại nằm ở nơi khác: trên cổng thông tin của bất kỳ hội đồng, khu vực, đơn vị tiện ích hoặc bộ ngành nào đang thực hiện việc mua sắm.

Thách thức

Hơn 250.000 cơ quan công quyền tại EU chi khoảng 2,6 nghìn tỷ euro mỗi năm cho các công trình, vật tư và dịch vụ, theo thống kê của Ủy ban Châu Âu. Các chỉ thị của EU chỉ quản lý những hợp đồng vượt ngưỡng, các mức này từ ngày 1 tháng 1 năm 2026 được ấn định ở mức 140.000 € đối với vật tư và dịch vụ của chính phủ trung ương, 216.000 € đối với chính quyền địa phương và khu vực, và 5.404.000 € đối với các công trình xây dựng. Dưới các mức này, các quy định quốc gia sẽ được áp dụng và việc đăng tải lên TED là tự nguyện.

Đó là lý do tại sao việc theo dõi thầu về bản chất là một công việc scraping. Nếu bạn đang xây dựng hệ thống này cho một công ty tư vấn đấu thầu hoặc một công ty xây dựng, phần dữ liệu trên ngưỡng là phần dễ dàng. Phần còn lại là một danh sách dài các cổng thông tin vốn không được thiết kế để máy móc có thể đọc hiểu.

BidPrime, đơn vị bán dữ liệu đấu thầu khu vực công tại Bắc Mỹ, đã gọi phiên bản tại Mỹ của vấn đề này là "hàng nghìn quy trình làm việc" vào tháng 5 năm 2026: các cổng thông tin bóp nghẽn truy cập tự động, các hệ thống mua sắm yêu cầu tài khoản hoặc session, các tệp đính kèm được lưu tách biệt khỏi thông báo, các URL thay đổi mà không báo trước. Không có điều nào trong danh sách đó là chỉ xảy ra riêng ở Mỹ.

Nhưng lỗi gây thiệt hại nặng nề nhất trong việc theo dõi thầu không phải là cổng thông tin từ chối bạn. Trường hợp đó sẽ hiển thị trong log của bạn. Vấn đề nằm ở lần chạy trả về trạng thái xanh nhưng bên trong không có dữ liệu gì.

Một cổng thông tin của hội đồng có thể phản hồi một session đã hết hạn bằng một trang thông báo thông thường với mã HTTP 200. Một thông báo bảo trì có thể hiển thị ngay tại vị trí đáng lẽ là bảng kết quả. Một tìm kiếm bạn đã gửi hôm qua có thể trả lại cho bạn trang một lần nữa, bởi vì trang hai chỉ tồn tại đối với cookie đã yêu cầu nó. Parser của bạn tìm thấy không có dòng nào và ghi nhận "hôm nay không có gói thầu mới". Và không giống như một mức giá bị bỏ lỡ, một gói thầu bị bỏ lỡ sẽ không quay trở lại: vào thời điểm ai đó phát hiện ra khoảng trống dữ liệu, hạn nộp hồ sơ đã qua và thông báo đó không còn giá trị gì đối với nhà thầu.

Cách tiếp cận: Bắt buộc mọi lần fetch phải chứng minh những gì nó ghi nhận

Bắt đầu với những gì không cần phải crawl. API của TED cung cấp quyền truy cập ẩn danh vào các thông báo đã xuất bản, không yêu cầu key, vì vậy hãy lấy phần dữ liệu vượt ngưỡng từ đó. Tại Vương quốc Anh, Find a Tender là nền tảng trung tâm nơi tất cả các bên mời thầu công bố thông tin mua sắm công từ ngày 24 tháng 2 năm 2025, theo GOV.UK. Hãy kiểm tra những gì sổ đăng ký trung tâm đã bao gồm trước khi bạn viết parser cho một cổng thông tin mà nó phản chiếu.

Đối với mọi thứ khác, ba quy tắc sau nắm giữ phần lớn vai trò.

1. Xác định một câu trả lời thực sự, bao gồm cả câu trả lời rỗng. Trang kết quả mang tiêu đề cột của nó ("Closing date", theo nguyên văn của cổng thông tin). Một ngày rỗng trung thực mang dòng trạng thái trống riêng của cổng thông tin. Bất cứ điều gì khác là một khoảng trống, không phải số 0. Đưa cả hai chuỗi vào danh sách validate.data.accept của request, và trang hết hạn phiên sẽ làm request thất bại thay vì được coi là một ngày không có hoạt động. Engine của chúng tôi đã từ chối tính một trang xác minh mà nó nhận dạng được là thành công, nhưng không có engine nào biết cách mọi hội đồng diễn đạt trang hết thời gian chờ của họ (phần đó phải do bạn cung cấp). Việc khớp chuỗi là một chuỗi con đơn thuần, có phân biệt chữ hoa chữ thường, vì vậy hãy sao chép văn bản từ trang thay vì gõ lại. Chúng tôi đã đề cập đến cơ chế này khi các quy tắc validate bắt đầu quyết định thành công, và vấn đề tương tự từ góc độ chuỗi thời gian trong When a Block Looks Like a Data Point.

2. Kiểm tra số lượng mà cổng thông tin hiển thị. Hầu hết các danh sách kết quả đều cho biết có bao nhiêu thông báo khớp ("1-20 of 143"). Hãy parse số đó và so sánh với các hàng bạn đã lưu trữ. Nếu chúng khác nhau, lượt chạy vẫn chưa kết thúc, bất kể status code báo gì.

3. Tìm tuyến đường một lần, sau đó phát lại. Các cổng thông tin có phân trang và gắn liền với session thường mong đợi các trang tiếp theo đến từ cùng một địa chỉ và cùng một trình duyệt. Với một nền tảng như FourA, Auto sẽ tìm một tuyến đường hoạt động hiệu quả cho trang đầu tiên và trả về session của nó: ID proxy, user agent và cookie từ lần thử thành công. Mọi trang sau đó đều đi qua Single trên cổng ra đó, đây chính là đường dẫn production. Auto chỉ đóng vai trò tìm đường.

import math, re, requests

API = "https://api.foura.ai/api"
H = {"X-API-Key": "pk_live_..."}
LIST = "https://tenders.example-council.gov/open?page={}"
PER_PAGE = 20

# A real answer is the results table or the portal's own empty-state line.
# An expired session or a maintenance notice served at 200 is neither.
CHECK = {"data": {"accept": ["Closing date", "No open tenders"]}}

def fetch(endpoint, body):
    r = requests.post(f"{API}/{endpoint}", headers=H, json=body).json()
    if r.get("error"):
        raise RuntimeError(r["error"])  # a gap to refill, not a quiet day
    return r

# Auto finds a route that works and hands back its session...
first = fetch("auto", {"url": LIST.format(1), "validate": CHECK})
proxy, ua = first["session"]["proxy"], first["session"]["userAgent"]
pages = [first["data"]]

# ...and Single replays it on the same exit for every other page.
m = re.search(r"\d+-\d+ of (\d+)", pages[0])
total = int(m.group(1)) if m else 0
for n in range(2, math.ceil(total / PER_PAGE) + 1):
    pages.append(fetch("single", {
        "method": "GET", "url": LIST.format(n), "proxy": proxy,
        "headers": [["User-Agent", ua]], "validate": CHECK,
    })["data"])

Nếu cổng thông tin lưu trạng thái tìm kiếm của bạn trên máy chủ của họ, hãy gửi cookie phiên của nó trong cùng danh sách headers; Single trả về mọi header Set-Cookie theo từng bước chuyển hướng (redirect hop). Tài liệu mời thầu thường nằm sau liên kết tải xuống dưới dạng PDF, và tính năng kiểm soát chuyển hướng cùng chế độ raw buffer của Single sẽ lấy chúng về dưới dạng byte thay vì văn bản bị lỗi. Thêm một thói quen nữa: hãy crawl từng cổng thông tin theo lịch riêng của nó. Đây là những máy chủ nhỏ do các hội đồng địa phương nhỏ vận hành, và hai mươi request dồn vào một đợt trông tệ hơn nhiều đối với họ so với hai mươi request rải rác trong một buổi chiều.

Results

  • Thu thập được nhiều hơn 25% gói thầu. GroupBWT, một công ty dịch vụ scraping, báo cáo con số này cho một khách hàng theo dõi hơn 50 cổng thông tin mua sắm công so với mốc đối chứng thủ công, "không phải vì có nhiều gói thầu hơn xuất hiện, mà vì họ không còn bỏ sót chúng nữa."
  • Không cần scraper cho phần vượt ngưỡng. Các thông báo của TED đến từ một API không yêu cầu khóa (key), vì vậy từng giờ công viết parser đều được dành cho các cổng thông tin không có API.
  • Phát hiện lỗ hổng dữ liệu khi thông báo vẫn đang mở. Một trang không vượt qua quy tắc chấp nhận (accept rule) sẽ báo lỗi ngay trong ngày xảy ra sự cố, và tác vụ sẽ thử lại thay vì lưu lại một ngày trống.
  • Mỗi cổng thông tin chỉ cần tìm tuyến một lần, không phải trên từng trang. Quá trình tìm kiếm của Auto chỉ diễn ra một lần; mọi trang tiếp theo đều là lệnh gọi Single trên cổng ra (exit) mà nó đã tìm thấy.

Chỉ có mục đầu tiên là kết quả đo lường được; ba mục còn lại mô tả những thay đổi mà thiết lập này mang lại.

Key Takeaway

Trong việc theo dõi đấu thầu, dòng đắt đỏ nhất bạn có thể lưu vào cơ sở dữ liệu là "hôm nay không có gì mới", bởi vì đó là dòng duy nhất bạn không thể sửa chữa sau này. Giá cả có thể cập nhật lại vào ngày mai. Nhưng một gói thầu đã đóng trong khi scraper của bạn đang đọc trang hết hạn phiên thì không thể.

Vì vậy, hãy buộc tác vụ phải kiểm chứng kỹ các kết quả rỗng. Một trang danh sách không thể hiển thị bảng dữ liệu, không có dòng trạng thái trống từ chính cổng thông tin hoặc không có số lượng khớp thì vẫn chưa cung cấp thông tin gì cho bạn, và coi đó là câu trả lời chính là lý do khiến nhà thầu chỉ biết về một hợp đồng khi có thông báo trao thầu.