← Wszystkie wpisy

Monitoring przetargów: ogłoszenia, których Twój scraper nigdy nie wykrył

Monitoring przetargów poniżej progu UE wymaga scrapowania długiego ogona małych portali. Kosztowna awaria to nie odmowa dostępu, ale pomyślny przebieg, który nie zapisał niczego.

216 000 EUR. Poniżej tego progu europejski urząd miasta zamawiający usługi nie musi publikować przetargu w TED, centralnym rejestrze UE.

Oznacza to, że kontrakty, które mały oferent może realistycznie wygrać, znajdują się gdzie indziej: na portalu konkretnej gminy, regionu, przedsiębiorstwa komunalnego lub ministerstwa realizującego zamówienie.

Wyzwanie

Ponad 250 000 organów publicznych w UE wydaje około 2,6 biliona EUR rocznie na roboty budowlane, dostawy i usługi, według szacunków Komisji Europejskiej. Dyrektywy UE regulują jedynie umowy powyżej progów, które od 1 stycznia 2026 r. wynoszą 140 000 EUR dla dostaw i usług administracji centralnej, 216 000 EUR dla władz lokalnych i regionalnych oraz 5 404 000 EUR dla robót budowlanych. Poniżej tych wartości stosuje się przepisy krajowe, a publikacja w TED jest dobrowolna.

Dlatego właśnie monitoring przetargów w ogóle wymaga scrapingu. Jeśli budujesz takie rozwiązanie dla firmy doradczej lub budowlanej, część powyżej progów jest tą prostą. Reszta to długi ogon portali, których nikt nie zaprojektował z myślą o odczycie maszynowym.

BidPrime, sprzedający dane o przetargach publicznych w Ameryce Północnej, w maju 2026 r. nazwał amerykańską wersję tego problemu "tysiącami odrębnych procesów": portale ograniczające automatyczny dostęp, systemy zamówień wymagające konta lub sesji, załączniki przechowywane oddzielnie od ogłoszenia, adresy URL zmieniające się bez ostrzeżenia. Żaden punkt z tej listy nie dotyczy wyłącznie USA.

Jednak błędem, który najbardziej uderza w monitoringu przetargów, nie jest portal odrzucający żądanie. Taki błąd widać w logach. Prawdziwym problemem jest przebieg zakończony statusem sukcesu, który nie zawiera żadnych danych.

Portal urzędu może odpowiedzieć na wygasłą sesję zwykłą stroną ze statusem HTTP 200. Komunikat o pracach technicznych może wyrenderować się tam, gdzie powinna być tabela wyników. Wyszukiwanie wysłane wczoraj może ponownie zwrócić stronę pierwszą, ponieważ strona druga istniała tylko dla pliku cookie, który o nią poprosił. Twój parser znajduje zero wierszy i zapisuje "brak nowych przetargów dzisiaj". W przeciwieństwie do pominiętej zmiany ceny, pominięty przetarg nie wraca: zanim ktokolwiek zauważy lukę, termin składania ofert mija, a ogłoszenie staje się bezwartościowe dla oferenta.

Podejście: Każde pobranie musi udowodnić, co faktycznie odebrało

Zacznij od tego, co nie wymaga scrapowania. API TED zapewnia anonimowy dostęp do opublikowanych ogłoszeń, bez wymaganego klucza, więc dane powyżej progów pobieraj stamtąd. W Wielkiej Brytanii Find a Tender jest centralną platformą, na której wszystkie instytucje zamawiające publikują informacje o zamówieniach od 24 lutego 2025 r., według GOV.UK. Sprawdź, co obejmuje już centralny rejestr, zanim napiszesz parser dla portalu, który go powiela.

W przypadku całej reszty kluczowe znaczenie mają trzy zasady.

1. Zdefiniuj poprawną odpowiedź, w tym odpowiedź pustą. Strona wyników zawiera nagłówek kolumny („Data zamknięcia”, według nazewnictwa portalu). Prawdziwy dzień bez wyników zawiera natywny komunikat portalu o stanie pustym. Wszystko inne to błąd, a nie zero. Umieść oba ciągi znaków na liście validate.data.accept requesta, a strona wygasłej sesji spowoduje błąd requesta, zamiast udawać brak ogłoszeń. Nasze silniki odrzucają rozpoznaną stronę weryfikacji jako sukces, ale żaden silnik nie wie, jak każdy urząd formułuje swój komunikat o upłynięciu limitu czasu (tę część musisz wskazać samodzielnie). Dopasowanie jest zwykłym podciągiem z uwzględnieniem wielkości liter, więc skopiuj tekst ze strony, zamiast wpisywać go ręcznie. Mechanizm ten opisaliśmy, gdy reguły walidacji zaczęły decydować o sukcesie, a ten sam problem od strony szeregów czasowych w artykule Gdy blokada wygląda jak punkt danych.

2. Sprawdź liczbę wyświetlaną przez portal. Większość list wyników podaje liczbę pasujących ogłoszeń („1-20 ze 143”). Sparsuj tę liczbę i porównaj ją z zapisanymi wierszami. Jeśli się różnią, przebieg nie jest ukończony, niezależnie od kodów statusu.

3. Znajdź trasę raz, a potem ją odtwarzaj. Portale ze stronicowaniem i sesjami zazwyczaj oczekują kolejnych stron z tego samego adresu i tej samej przeglądarki. Na platformie takiej jak FourA tryb Auto znajduje trasę działającą dla pierwszej strony i zwraca jej sesję: ID proxy, user agent oraz pliki cookie z udanej próby. Każda kolejna strona przechodzi przez Single na tym węźle wyjściowym, co stanowi ścieżkę produkcyjną. Auto służy wyłącznie do wyznaczania ścieżki.

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

Jeśli portal przechowuje stan wyszukiwania na swoim serwerze, wyślij jego session cookie na tej samej liście headers; Single zwraca każdy nagłówek Set-Cookie dla każdego przeskoku przekierowania. Dokumenty przetargowe zwykle znajdują się pod linkiem do pobrania jako pliki PDF, a tryb kontroli przekierowań i surowego bufora w Single zwraca je jako bajty zamiast uszkodzonego tekstu. Jeszcze jeden nawyk: odpytuj każdy portal według jego własnego harmonogramu. To małe serwery zarządzane przez małe jednostki samorządowe, a dwadzieścia requestów w jednej serii wygląda dla nich znacznie gorzej niż dwadzieścia rozłożonych na całe popołudnie.

Wyniki

  • O 25% więcej przechwyconych przetargów. GroupBWT, firma świadcząca usługi scrapingu, raportuje taki wynik dla klienta monitorującego ponad 50 portali zamówień publicznych w porównaniu z weryfikacją ręczną: "nie dlatego, że pojawiło się więcej przetargów, ale dlatego, że przestali je przegapiać".
  • Brak scrapera dla części powyżej progów unijnych. Ogłoszenia TED pochodzą z API, które nie wymaga klucza, więc każda godzina pracy nad parserem trafia do portali, które nie mają API.
  • Luki wychodzą na jaw, gdy ogłoszenie jest wciąż aktywne. Strona, która nie spełnia reguły accept, zgłasza błąd tego samego dnia, a zadanie ponawia próbę zamiast zapisywać pusty dzień.
  • Jedno wykrycie ścieżki na portal, a nie na stronę. Wyszukiwanie Auto odbywa się raz; każda kolejna strona to wywołanie Single na znalezionym wyjściu.

Tylko pierwszy punkt to zmierzony wynik; pozostałe trzy opisują zmiany wprowadzone przez tę konfigurację.

Kluczowy wniosek

W monitoringu przetargów najdroższy wiersz, jaki możesz zapisać, to "dzisiaj nic nowego", ponieważ jako jedynego nie da się go później skorygować. Ceny wrócą jutro. Przetarg, który został zamknięty, podczas gdy twój scraper czytał stronę wygasłej sesji, już nie.

Spraw, aby zadanie zapracowało na swoje zera. Strona z listą, która nie potrafi pokazać tabeli, natywnego komunikatu portalu o braku wyników ani pasującej liczby elementów, nie przekazała ci jeszcze żadnej informacji, a traktowanie jej jako odpowiedzi prowadzi do sytuacji, w której oferent dowiaduje się o kontrakcie dopiero z ogłoszenia o jego udzieleniu.