← Alle Beiträge

Ausschreibungsmonitoring: Die Bekanntmachungen, die dein Scraper nie gesehen hat

Ausschreibungsmonitoring unterhalb der EU-Schwellenwerte bedeutet das Scraping vieler kleiner Portale. Der teure Fehler ist keine Blockade, sondern ein grüner Durchlauf ohne gespeicherte Daten.

216.000 €. Unterhalb dieser Grenze muss eine europäische Stadtverwaltung, die Dienstleistungen einkauft, ihre Ausschreibung nicht auf TED veröffentlichen, dem zentralen Register der EU.

Das bedeutet, dass die Aufträge, die ein kleiner Bieter realistisch gewinnen kann, woanders liegen: auf dem Portal der jeweiligen Kommune, Region, des Versorgers oder Ministeriums, das den Einkauf tätigt.

Die Herausforderung

Mehr als 250.000 öffentliche Auftraggeber in der EU geben laut Zählung der Europäischen Kommission jährlich rund 2,6 Billionen Euro für Bauleistungen, Lieferungen und Dienstleistungen aus. Die EU-Richtlinien regeln nur Verträge oberhalb der Schwellenwerte. Diese liegen ab dem 1. Januar 2026 bei 140.000 € für Liefer- und Dienstleistungen von obersten Bundesbehörden, 216.000 € für Kommunen und regionale Behörden sowie 5.404.000 € für Bauleistungen. Darunter gelten nationale Regeln und die Veröffentlichung auf TED ist freiwillig.

Deshalb ist Tender-Monitoring überhaupt eine Scraping-Aufgabe. Wenn du das für eine Ausschreibungsberatung oder ein Bauunternehmen baust, ist der Teil oberhalb des Schwellenwerts die leichte Aufgabe. Der Rest ist ein Long Tail an Portalen, die niemand dafür gebaut hat, von Maschinen gelesen zu werden.

BidPrime, ein Anbieter von Ausschreibungsdaten für den öffentlichen Sektor in Nordamerika, nannte die US-Version davon im Mai 2026 "Tausende von Workflows": Portale, die automatisierten Zugriff drosseln, Beschaffungssysteme, die einen Account oder eine Session erfordern, Anhänge, die getrennt von der Bekanntmachung gespeichert sind, URLs, die sich ohne Vorwarnung ändern. Nichts auf dieser Liste ist spezifisch für die USA.

Der Fehler, der beim Tender-Monitoring wirklich wehtut, ist aber nicht das Portal, das dir den Zugriff verweigert. Das taucht in deinen Logs auf. Es ist der Run, der grün zurückkommt, aber leer ist.

Ein kommunales Portal kann auf eine abgelaufene Session mit einer freundlichen Seite bei HTTP 200 antworten. Ein Wartungshinweis kann dort gerendert werden, wo die Ergebnistabelle sein sollte. Eine Suche, die du gestern abgeschickt hast, liefert dir wieder Seite eins, weil Seite zwei nur für das Cookie existierte, das sie angefordert hat. Dein Parser findet null Zeilen und schreibt "heute keine neuen Ausschreibungen". Und anders als ein verpasster Preis kommt eine verpasste Ausschreibung nicht wieder: Bis jemand die Lücke bemerkt, ist die Einreichungsfrist abgelaufen und die Bekanntmachung für einen Bieter wertlos.

Der Ansatz: Lass jeden Fetch beweisen, was er gesehen hat

Beginne mit dem, was du nicht scrapen musst. Die API von TED bietet anonymen Zugriff auf veröffentlichte Bekanntmachungen, ganz ohne API-Key. Rufe Daten oberhalb der Schwellenwerte also direkt dort ab. In Großbritannien ist Find a Tender seit dem 24. Februar 2025 die zentrale Plattform für alle Beschaffungsinformationen öffentlicher Auftraggeber, laut GOV.UK. Prüfe zuerst, was ein zentrales Register bereits abdeckt, bevor du einen Parser für ein gespiegeltes Portal schreibst.

Für alles andere sind vor allem drei Regeln entscheidend.

1. Definiere eine echte Antwort, einschließlich des Leerzustands. Eine Ergebnisseite enthält ihren Spalten-Header ("Schlusstermin", im genauen Wortlaut des Portals). Ein echter Tag ohne Einträge zeigt den spezifischen Leertext des Portals. Alles andere ist ein Fehler, keine Null. Trage beide Strings in die validate.data.accept-Liste des Requests ein. So schlägt der Request bei einer abgelaufenen Sitzung fehl, anstatt fälschlicherweise als leerer Tag gewertet zu werden. Unsere Engines werten erkannte Verifizierungsseiten bereits nicht als Erfolg, aber keine Engine kennt die genaue Timeout-Meldung jeder Behörde (diesen Teil musst du definieren). Der Abgleich erfolgt als exakter Substring mit Case-Sensitivity; kopiere den Text daher direkt von der Seite. Wir haben die Funktionsweise erklärt, als Validate-Regeln über den Erfolg entschieden haben, und das gleiche Problem aus Sicht von Zeitreihen in When a Block Looks Like a Data Point behandelt.

2. Prüfe die Trefferanzahl des Portals. Die meisten Ergebnislisten geben an, wie viele Bekanntmachungen zutreffen ("1-20 von 143"). Parse diese Zahl und vergleiche sie mit deinen gespeicherten Zeilen. Weichen sie ab, ist der Durchlauf unvollständig, unabhängig vom Statuscode.

3. Finde die Route einmal und spiele sie wieder ab. Paginierte und sessionbasierte Portale erwarten Folgeseiten meist von derselben IP-Adresse und demselben Browser. Mit einer Plattform wie FourA ermittelt Auto eine funktionierende Route für Seite eins und liefert die Session zurück: die Proxy-ID, den User-Agent und die Cookies des erfolgreichen Versuchs. Jede weitere Seite läuft über Single über diesen Exit, was dem produktiven Pfad entspricht. Auto dient nur zur Routenfindung.

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

Wenn das Portal deinen Suchstatus auf seinem Server speichert, sende dessen Session-Cookie in derselben headers-Liste. Single liefert jeden Set-Cookie-Header pro Redirect-Hop zurück. Ausschreibungsunterlagen liegen meist als PDFs hinter einem Download-Link. Die Redirect-Steuerung und der Raw-Buffer-Modus von Single liefern sie als Bytes statt als fehlerhaften Text zurück. Eine weitere Gewohnheit: Frage jedes Portal nach einem eigenen Zeitplan ab. Das sind kleine Server von kleinen Behörden, und zwanzig Requests auf einmal wirken dort deutlich verdächtiger als zwanzig über den Nachmittag verteilt.

Ergebnisse

  • 25% mehr erfasste Ausschreibungen. GroupBWT, ein Scraping-Dienstleister, berichtet dies für einen Kunden, der über 50 Vergabeportale im Vergleich zu einer manuellen Baseline überwacht, "nicht, weil mehr Ausschreibungen existierten, sondern weil sie keine mehr verpassten."
  • Kein Scraper für den Teil oberhalb der Schwellenwerte. TED-Bekanntmachungen stammen aus einer API ohne Key. Jede Stunde Parser-Arbeit fließt also in Portale ohne API.
  • Lücken fallen auf, solange die Bekanntmachung noch offen ist. Eine Seite, die die Accept-Regel nicht erfüllt, löst am selben Tag einen Fehler aus. Der Job wiederholt den Request, statt einen leeren Tag zu speichern.
  • Ein Routing-Discovery pro Portal, nicht pro Seite. Die Suche von Auto läuft einmal. Jede Folgeseite ist ein Single-Call auf dem gefundenen Exit.

Nur der erste Punkt ist ein gemessenes Ergebnis. Die anderen drei beschreiben, was das Setup verändert.

Fazit

Beim Ausschreibungs-Monitoring ist der teuerste Eintrag, den du speichern kannst: "Heute nichts Neues". Denn das ist der einzige, den du später nicht mehr korrigieren kannst. Preise sind morgen wieder da. Eine Ausschreibung, die endete, während dein Scraper eine Seite mit abgelaufener Session las, ist weg.

Lass den Job sich seine Nullen also verdienen. Eine Übersichtsseite, die dir weder die Tabelle noch den portalspezifischen Leerzustand oder einen passenden Zähler anzeigt, hat dir noch nichts geliefert. Das als echtes Ergebnis zu werten führt dazu, dass ein Bieter erst durch die Zuschlagsbekanntmachung von einem Auftrag erfährt.