[EN]
Region: europe-west4 [v2.1.0] | status: online!
// projects / competitive_intel.tf

Competitive-intelligence platform

type: data platform · status: production · year: 2025
TL;DR
Twee scrapers met bewust tegengestelde filosofieën — een wegwerp-snapshotcrawler en een append-only PDP-archief — delen één library en één deterministisch ID-schema. Wekelijkse runs dedupliceren en archiveren prijs- en vertrekdata uit de reisbranche in BigQuery, en dbt transformeert die naar geteste dashboardmodellen. Geen console-klik in productie: elke GCP-resource is Terraform.
Infrastructure
TerraformCloud Run JobsCloud WorkflowsSecret ManagerGitHub Actions
Data
BigQuerydbt
Language
Python

Wekelijkse, idempotente dataverzameling in de reisbranche — volledig op GCP, alles Infrastructure-as-Code.

Context & probleem

Concurrentiedata in de reisbranche is vluchtig: prijzen en vertrekdata verschuiven dagelijks, pagina's verschijnen en verdwijnen. Een enkele scrape geeft een momentopname; waarde zit in de tijdreeks — prijsbewegingen, nieuwe vertrekken, verdwenen reizen. Tegelijk wil je niet elke run een volledige crawl van zware product-detailpagina's (PDP's) doen: dat is traag, duur op proxy-verkeer, en fragiel.

De kern van het ontwerp is die spanning expliciet maken in twee gescheiden componenten in plaats van één scraper te overladen met conditionele logica.

Architectuur: twee scrapers, twee filosofieën

De belangrijkste ontwerpbeslissing: niet één configureerbare scraper, maar twee componenten die elk één ding goed doen en bewust verschillende robuustheidsniveaus hebben.

Snapshot-crawlerArchief-scraper (PDP)
RolGoedkope sitemap-crawl, bepaalt wat er te scrapen valtDiepe PDP-extractie, bouwt het historisch archief
Levensduur dataWegwerp — elke run overschrijftPermanent — append-only tijdreeks
HTTPsession.get() direct, geen retryfetch_with_retry() met backoff + proxy
RobuustheidMinimaal, mag falenVolledig: dedup, fingerprinting, anomaly detection
BQ-schrijfmodusoverschrijven (snapshot)WRITE_APPEND
Voedtde archief-scraperde dbt-laag

Waarom de asymmetrie bewust is. De snapshot-crawler is een voedingskanaal: hij hoeft alleen de actuele set URL's en hun paden te leveren. Faalt een run, dan draai je opnieuw — er gaat niets verloren omdat de output toch wegwerp is. Hem volplempen met retry-logica en deduplicatie zou complexiteit toevoegen aan iets wat juist simpel en vervangbaar moet zijn.

De archief-scraper is het tegenovergestelde: elke succesvolle fetch is een onvervangbaar punt in een tijdreeks. Daar zit alle robuustheid — retries, idempotente IDs, contentfingerprinting, anomaliedetectie op rij-aantallen. De scheiding houdt beide componenten leesbaar: geen if is_snapshot:-takken die twee tegengestelde eisen in één codepad proberen te verzoenen.

Gedeelde library

Beide scrapers — en elke toekomstige scraper — bouwen op één gedeelde library. Geen class-hiërarchie, gewoon directe functies per verantwoordelijkheid.

ModuleVerantwoordelijkheid
ids.pyDeterministische ID-generatie (make_scrape_id, make_scrape_url_id, make_scrape_url_departure_id)
http.pyfetch_with_retry(session, url, timeout=15, is_api=False)Response | None
bq.pypush_rows, check_exists, get_scraped_paths, get_sitemap_urls
config.pyCentrale config: project/dataset, proxy-toggle, delays, max retries
log.pyGestructureerde logging: log(severity, message, **extra)

De library is de afspraak: een nieuwe scraper hoeft niet na te denken over hoe een ID eruitziet of hoe je naar BigQuery schrijft. Dat is één keer goed opgelost en wordt overal hergebruikt.

Determinisme & idempotentie

De ruggengraat van het hele platform. IDs zijn deterministisch, nooit random. Elke rij krijgt een ID via een base64-encoded MD5-hash over een vaste sleutelcompositie:

make_scrape_url_id()  →  b64_md5("<bron>|{vertrek_datum}|{page_path}")

Dit levert direct een aantal eigenschappen op:

Bovenop de IDs:

HTTP-laag & robuustheid

Alle PDP-verkeer loopt via fetch_with_retry(). De enige uitzondering is de sitemap-crawl, die bewust kaal session.get() gebruikt (wegwerp, mag falen).

BigQuery-datamodel

Twee schrijfregimes, passend bij de twee scrapers:

Idempotentie (zie boven) maakt append veilig: dubbele runs leiden niet tot dubbele rijen omdat de deterministische IDs collisies vangen.

dbt-transformatielaag

Ruwe append-only data is geen dashboarddata. De dbt-laag transformeert in duidelijke lagen, elk model getest en gedocumenteerd:

staging  →  stg_*         (1:1 opschoning van bron)
marts    →  fct_* / dim_* (business logic, feiten & dimensies)
dashboard→  dash_*        (presentatie-views voor de frontend)

BigQuery-craft die ertoe doet in dit domein:

Infrastructuur & operations

Het platform is serverless en volledig declaratief.

Engineeringbeslissingen die ertoe doen

Wat dit project boven "een scriptje dat scrapet" tilt:

  1. Twee filosofieën i.p.v. één configureerbare moloch. Robuustheid waar het telt (archief), simpliciteit waar het mag (snapshot). Complexiteit wordt geplaatst, niet vermeden.
  2. Determinisme als fundament. Deterministische IDs maken idempotentie, veilige append, en stabiele joins in één klap mogelijk. Eén ontwerpkeuze, drie problemen opgelost.
  3. Append-only met vangrails. Historie is heilig; truncate bestaat niet. Anomaliedetectie voorkomt dat een gebroken run het archief stil vervuilt.
  4. De shared library als contract. Nieuwe scrapers erven robuustheid in plaats van die opnieuw te schrijven.
  5. Alles IaC, least-privilege, secrets uit de code. Productie is reproduceerbaar en auditeerbaar; de infra is zelf het bewijs van discipline.

Status

Het platform draait in productie met de wekelijkse cadans. De dbt-modellen voeden geteste dashboard-views; de tijdreeks groeit append-only verder. Volgende stappen liggen in de presentatielaag — live dashboards en exposures bovenop fct_competitor__departures.