"""Saarland (SL) — Abstimmungsergebnisse-Parser (#106 / #161, ADR 0009). **Spezialfall:** Saarland publiziert keine Wortprotokolle, sondern eigene Abstimmungsergebnisse-HTML-Seiten pro Sitzung mit strukturiertem Vote-Block: ```

...Drucksache 17/2076... in Erster Lesung mit Stimmenmehrheit angenommen und an den Ausschuss [...] [SPD: dafür; CDU und AfD: dagegen]

``` Daher Input ist HTML, nicht PDF. ``parse_protocol(html_path)`` liest die HTML-Seite und extrahiert pro
  • einen Vote. URL-Pattern (nicht direkt vorhersagbar, daher Index-Scrape): ``https://www.landtag-saar.de/aktuelles/mitteilungen/abstimmungsergebnisse-der-{n}-landtagssitzung-vom-{datum}/`` Index-Seite: https://www.landtag-saar.de (Front-Listing der Mitteilungen). ## Vote-Block-Format Strukturierte Klammer-Notation pro Drucksache: - ``[SPD: dafür; CDU und AfD: dagegen]`` → JA=[SPD], NEIN=[CDU,AfD] - ``[SPD: dafür; CDU: dagegen; AfD: Enthaltung]`` → JA=[SPD], NEIN=[CDU], ENTH=[AfD] - ``[SPD und CDU: dafür; AfD: Enthaltung]`` → JA=[SPD,CDU], NEIN=[], ENTH=[AfD] ## Ergebnis-Mapping - ``angenommen`` (mit oder ohne ``mit Stimmenmehrheit|einstimmig``) → angenommen - ``abgelehnt`` → abgelehnt - ``zur Kenntnis genommen`` → uebersprungen (kein Vote) ## Fraktions-Mapping WP17 (ab 2022) WP17 Konstellation: SPD-Alleinregierung (43 Sitze), CDU + AfD Opposition. - ``SPD``, ``CDU``, ``AfD`` """ from __future__ import annotations import re from typing import Optional ALLE_FRAKTIONEN_SL = ["SPD", "CDU", "AfD"] #
  • ...
  • -Block per Sitzung; jeder Block enthaelt typischerweise # 1x Drucksache + 1x Status + 1x Vote-Klammer. LI_BLOCK_RE = re.compile( r"]*>(.*?)", re.DOTALL, ) DS_RE_SL = re.compile(r"Drucksache\s+(\d{1,2}/\d{2,5})") STATUS_RE = re.compile( r"(?:in\s+\w+\s+Lesung\s+)?" r"(?:mit\s+Stimmenmehrheit|einstimmig|mit\s+Mehrheit)?\s*" r"(?Pangenommen|abgelehnt|abgesetzt|zur\s+Kenntnis\s+genommen)", re.IGNORECASE, ) # Vote-Klammer: [SPD: dafür; CDU und AfD: dagegen] VOTE_BRACKET_RE = re.compile(r"\[(?P[^\[\]]+)\]") def _normalize_fraktionen_sl(phrase: str) -> list[str]: """SPD und CDU → ['CDU', 'SPD']; CDU → ['CDU'].""" found = set() for fr in ALLE_FRAKTIONEN_SL: if re.search(rf"\b{re.escape(fr)}\b", phrase, re.IGNORECASE): found.add(fr) return sorted(found) def _parse_vote_bracket(bracket_inner: str) -> dict: """Parst '[SPD: dafür; CDU und AfD: dagegen]' (innen ohne Klammern).""" votes = {"ja": [], "nein": [], "enthaltung": []} for segment in bracket_inner.split(";"): if ":" not in segment: continue fraktionen_phrase, _, status = segment.rpartition(":") status = status.strip().lower() fraktionen = _normalize_fraktionen_sl(fraktionen_phrase) if "dafür" in status or "ja" in status or "zustimm" in status: votes["ja"].extend(fraktionen) elif "dagegen" in status or "nein" in status or "ablehn" in status: votes["nein"].extend(fraktionen) elif "enthalt" in status: votes["enthaltung"].extend(fraktionen) for key in votes: votes[key] = sorted(set(votes[key])) return votes def _strip_html(text: str) -> str: text = re.sub(r"<[^>]+>", " ", text) text = text.replace("&", "&").replace(" ", " ") return re.sub(r"\s+", " ", text).strip() def parse_protocol(html_path: str) -> list[dict]: """Parst SL-Abstimmungsergebnisse-HTML, liefert Status + Votes.""" with open(html_path, "r", encoding="utf-8", errors="replace") as f: html = f.read() results = [] for m in LI_BLOCK_RE.finditer(html): block_html = m.group(1) block_text = _strip_html(block_html) ds_m = DS_RE_SL.search(block_text) if not ds_m: continue ds = ds_m.group(1) status_m = STATUS_RE.search(block_text) if not status_m: continue ergebnis = status_m.group("ergebnis").lower() if "kenntnis" in ergebnis: continue modus_match = re.search(r"einstimmig", block_text, re.IGNORECASE) einstimmig = bool(modus_match) vote_m = VOTE_BRACKET_RE.search(block_text) votes = {"ja": [], "nein": [], "enthaltung": []} if vote_m: votes = _parse_vote_bracket(vote_m.group("inner")) if einstimmig and not votes["ja"]: votes["ja"] = list(ALLE_FRAKTIONEN_SL) results.append({ "drucksache": ds, "ergebnis": ergebnis, "einstimmig": einstimmig, "kind": "direct", "votes": votes, "anchor_pos": m.start(), }) seen = set() deduped = [] for r in results: key = (r["drucksache"], r["anchor_pos"]) if key in seen: continue seen.add(key) deduped.append(r) return deduped