Scraper SL: Saarland (Eigensystem, Wahl 2027-04-18) #19

Closed
opened 2026-04-08 22:22:08 +02:00 by tobias · 2 comments
Owner

Wahltermin

2027-04-18 — Saarland (SL), aktuell 17. Wahlperiode.

Backend

Feld Wert
Doku-System Eigensystem
Base-URL https://www.landtag-saar.de
dokukratie-Scraper sl
Drucksachen-Format 17/1234

Adapter-Strategie

Eigensystem — kein etablierter Adapter wiederverwendbar. Nächster Schritt: HTML-Form bzw. JSON-API der Drucksachen-Suche auf landtag-saar.de reverse-engineeren. Achtung: dokukratie-Scraper sl als Referenz, aber er nutzt evtl. Memorious-spezifische Pfade.

Was zu tun ist

  1. Live-Backend anschauen — falls SPA, HAR-Trace einer realen Suche aus DevTools ziehen (siehe Vorgehen in #12 für ParlDok bzw. #13 für eUI).
  2. Adapter in webapp/app/parlamente.py implementieren — entweder als neue Subklasse von ParlamentAdapter oder als zweiter Registry-Eintrag eines existierenden parametrisierbaren Adapters.
  3. Eintrag in der ADAPTERS-Registry am Ende der Datei.
  4. Smoke-Test lokal: ADAPTERS["SL"].search("Schule", limit=10) liefert echte Anträge mit Datum + Fraktionen, sortiert newest-first.
  5. Aktivierung via Folge-Issue (siehe Hängt mit … zusammen unten) — dieses Issue ist nur der Adapter selbst, nicht das Indexieren der Wahlprogramme oder das Frontend-aktiv-Setzen.

Akzeptanzkriterien

  • parlamente.py::ADAPTERS["SL"] existiert und ist instanziierbar
  • search(query="Schule", limit=10) liefert ≥3 echte Drucksachen mit korrektem Datum, Fraktionen, PDF-Link
  • get_document(drucksache) für eine reale Drucksache der laufenden WP liefert das Dokument zurück
  • download_text(drucksache) extrahiert Text aus dem PDF
  • Folge-Issue für Frontend-Aktivierung verlinkt
## Wahltermin **2027-04-18** — Saarland (SL), aktuell 17. Wahlperiode. ## Backend | Feld | Wert | |---|---| | Doku-System | `Eigensystem` | | Base-URL | https://www.landtag-saar.de | | dokukratie-Scraper | `sl` | | Drucksachen-Format | `17/1234` | ## Adapter-Strategie Eigensystem — kein etablierter Adapter wiederverwendbar. Nächster Schritt: HTML-Form bzw. JSON-API der Drucksachen-Suche auf landtag-saar.de reverse-engineeren. Achtung: dokukratie-Scraper `sl` als Referenz, aber er nutzt evtl. Memorious-spezifische Pfade. ## Was zu tun ist 1. Live-Backend anschauen — falls SPA, HAR-Trace einer realen Suche aus DevTools ziehen (siehe Vorgehen in #12 für ParlDok bzw. #13 für eUI). 2. Adapter in `webapp/app/parlamente.py` implementieren — entweder als neue Subklasse von `ParlamentAdapter` oder als zweiter Registry-Eintrag eines existierenden parametrisierbaren Adapters. 3. Eintrag in der `ADAPTERS`-Registry am Ende der Datei. 4. Smoke-Test lokal: `ADAPTERS["SL"].search("Schule", limit=10)` liefert echte Anträge mit Datum + Fraktionen, sortiert newest-first. 5. Aktivierung via Folge-Issue (siehe `Hängt mit … zusammen` unten) — dieses Issue ist nur der Adapter selbst, nicht das Indexieren der Wahlprogramme oder das Frontend-aktiv-Setzen. ## Akzeptanzkriterien - [ ] `parlamente.py::ADAPTERS["SL"]` existiert und ist instanziierbar - [ ] `search(query="Schule", limit=10)` liefert ≥3 echte Drucksachen mit korrektem Datum, Fraktionen, PDF-Link - [ ] `get_document(drucksache)` für eine reale Drucksache der laufenden WP liefert das Dokument zurück - [ ] `download_text(drucksache)` extrahiert Text aus dem PDF - [ ] Folge-Issue für Frontend-Aktivierung verlinkt
Author
Owner

Hinweise aus dokukratie/sl.yml

Backend ist NICHT eUI/StarWeb/ParlDok, sondern ein Umbraco-CMS mit JSON-API:

  • POST-Endpoint: https://www.landtag-saar.de/umbraco/aawSearchSurfaceController/SearchSurface/GetSearchResults/
  • Detail-Pfad: /vorgaenge/{operation_id}
  • POST-Body (JSON):
    {
      "CurrentSearchTab": 1,
      "Filter.Periods": [14, 15, 16],
      "KendoFilter": {"...": "Document type contains 'Anfrage'"},
      "OnlyTitle": false,
      "Pagination": {"Skip": 0, "Take": 10},
      "Sections.Print": true,
      "Sort": {"SortType": 0, "SortValue": 0},
      "Value": ""
    }
    
  • Response: JSON mit Vorgängen, Felder via JQ-ähnliche Pfade extrahiert (title, published_at, reference, legislative_term, originators_raw, answerers, document_type, download_url, operation_id)
  • Download: (.//div[@class="operation-file-download"])[last()]

Adapter-Empfehlung: komplett eigene Implementierung, nicht mit anderen teilbar. Ähnlichste Vorlage ist der ParLDokAdapter (#4) wegen der zweistufigen JSON-API. Pagination via Pagination.Skip/Pagination.Take.

## Hinweise aus dokukratie/sl.yml **Backend ist NICHT eUI/StarWeb/ParlDok**, sondern ein Umbraco-CMS mit JSON-API: - POST-Endpoint: `https://www.landtag-saar.de/umbraco/aawSearchSurfaceController/SearchSurface/GetSearchResults/` - Detail-Pfad: `/vorgaenge/{operation_id}` - POST-Body (JSON): ```json { "CurrentSearchTab": 1, "Filter.Periods": [14, 15, 16], "KendoFilter": {"...": "Document type contains 'Anfrage'"}, "OnlyTitle": false, "Pagination": {"Skip": 0, "Take": 10}, "Sections.Print": true, "Sort": {"SortType": 0, "SortValue": 0}, "Value": "" } ``` - Response: JSON mit Vorgängen, Felder via JQ-ähnliche Pfade extrahiert (title, published_at, reference, legislative_term, originators_raw, answerers, document_type, download_url, operation_id) - Download: `(.//div[@class="operation-file-download"])[last()]` **Adapter-Empfehlung:** komplett eigene Implementierung, nicht mit anderen teilbar. Ähnlichste Vorlage ist der `ParLDokAdapter` (#4) wegen der zweistufigen JSON-API. Pagination via `Pagination.Skip`/`Pagination.Take`.
tobias added the
phase-3
scraper
labels 2026-04-08 23:16:35 +02:00
Author
Owner

Resolved (2026-04-10) — Adapter live, Sub-D-Smoke-Test grün

Implementation in 6dfcd69. Schema-Discovery dank User-HAR-Capture (TEMP/www.landtag-saar.de.har).

Endpoint

POST /umbraco/aawSearchSurfaceController/SearchSurface/GetSearchResults/

Content-Type: application/x-www-form-urlencoded; charset=UTF-8 mit rohem JSON im Body (Kendo-Konvention von $.ajax ohne expliziten contentType — der Server akzeptiert es trotzdem).

Body-Schema (PFLICHT minimal)

Gotcha: Sections={} muss leer sein. Sobald ich Sections.Print=true etc. gesetzt habe (wie es die JS-App im Frontend macht!), antwortet der Server mit HTTP 500 und einer generischen "An error occurred while processing your request."-Meldung. Erst der HAR-Capture hat gezeigt, dass die JS-App in der echten Session ebenfalls Sections:{} schickt — die Section-Konfiguration läuft client-seitig über Kendo-Filter.

Response-Mapping

  • DocumentNumber → drucksache (z.B. "17/11")
  • Title → title
  • DocumentType → typ; client-side gefiltert auf "Antrag" weil die Print-Section auch Anfragen/Gesetzentwürfe enthält (~30-50% Antrag-Anteil)
  • Publisher (kollektive Anträge: "CDU", "SPD") + DocumentAuthor (individuelle MdL: "Schmitt-Lang, Jutta (CDU);…") durch parteien.extract_fraktionen normalisiert
  • PublicDate (2022-05-12T00:00:00) → datum (auf 10 Zeichen ISO)
  • FilePath (/file.ashx?FileId=…&FileName=…) → ⚠️ HTML-Iframe-Wrapper, NICHT das PDF! Echter Binär-Endpoint ist /Downloadfile.ashx (Großbuchstabe!) mit denselben Query-Parametern. Der Wrapper hat beim ersten Smoke-Test PyMuPDF mit "no objects found" angeschmissen.

Akzeptanzkriterien

  • parlamente.py::ADAPTERS["SL"] existiert und ist instanziierbar
  • search("Schule", limit=5) liefert 2 echte Drucksachen mit Datum, Fraktionen, PDF-Link:
    • 17/11 [CDU] "Schule als Lern- und Bildungsort weiter stärken und Multiprofessionalität …"
    • 17/419 [AfD] "Eine gute Bildungspolitik als wesentlicher Bestandteil …"
  • get_document("17/11") matcht via Volltext-Suche mit Value="17/11"
  • download_text("17/11") extrahiert 3520 Zeichen Volltext (Header, Fraktion, Resolutionstext)
  • Folge-Issue für Frontend-Aktivierung: #31

Tests + Deploy

  • 185/185 lokal grün
  • Live deployed (6dfcd69)
  • Adapter ist im Container instanziiert und antwortet, UI-Aktivierung läuft separat über #31

Closing.

## Resolved (2026-04-10) — Adapter live, Sub-D-Smoke-Test grün Implementation in 6dfcd69. Schema-Discovery dank User-HAR-Capture (TEMP/www.landtag-saar.de.har). ### Endpoint POST /umbraco/aawSearchSurfaceController/SearchSurface/GetSearchResults/ Content-Type: application/x-www-form-urlencoded; charset=UTF-8 mit rohem JSON im Body (Kendo-Konvention von $.ajax ohne expliziten contentType — der Server akzeptiert es trotzdem). ### Body-Schema (PFLICHT minimal) **Gotcha:** Sections={} muss leer sein. Sobald ich Sections.Print=true etc. gesetzt habe (wie es die JS-App im Frontend macht!), antwortet der Server mit HTTP 500 und einer generischen "An error occurred while processing your request."-Meldung. Erst der HAR-Capture hat gezeigt, dass die JS-App in der echten Session ebenfalls Sections:{} schickt — die Section-Konfiguration läuft client-seitig über Kendo-Filter. ### Response-Mapping - DocumentNumber → drucksache (z.B. "17/11") - Title → title - DocumentType → typ; **client-side gefiltert auf "Antrag"** weil die Print-Section auch Anfragen/Gesetzentwürfe enthält (~30-50% Antrag-Anteil) - Publisher (kollektive Anträge: "CDU", "SPD") **+** DocumentAuthor (individuelle MdL: "Schmitt-Lang, Jutta (CDU);…") durch parteien.extract_fraktionen normalisiert - PublicDate (2022-05-12T00:00:00) → datum (auf 10 Zeichen ISO) - FilePath (/file.ashx?FileId=…&FileName=…) → ⚠️ **HTML-Iframe-Wrapper, NICHT das PDF!** Echter Binär-Endpoint ist /Downloadfile.ashx (Großbuchstabe!) mit denselben Query-Parametern. Der Wrapper hat beim ersten Smoke-Test PyMuPDF mit "no objects found" angeschmissen. ### Akzeptanzkriterien - [x] parlamente.py::ADAPTERS["SL"] existiert und ist instanziierbar - [x] search("Schule", limit=5) liefert 2 echte Drucksachen mit Datum, Fraktionen, PDF-Link: - 17/11 [CDU] "Schule als Lern- und Bildungsort weiter stärken und Multiprofessionalität …" - 17/419 [AfD] "Eine gute Bildungspolitik als wesentlicher Bestandteil …" - [x] get_document("17/11") matcht via Volltext-Suche mit Value="17/11" - [x] download_text("17/11") extrahiert 3520 Zeichen Volltext (Header, Fraktion, Resolutionstext) - [x] Folge-Issue für Frontend-Aktivierung: #31 ### Tests + Deploy - 185/185 lokal grün - Live deployed (6dfcd69) - Adapter ist im Container instanziiert und antwortet, UI-Aktivierung läuft separat über #31 Closing.
Sign in to join this conversation.
No description provided.