"""Bremen (HB) — Beschlussprotokoll-Parser (#106 / #153, ADR 0009). **Limitierung:** Bremen publiziert wie Hessen nur kompakte Beschlussprotokolle (TOPs + Status-Saetze), KEINE Wortprotokolle mit Vote-Block. Daher liefert dieser Parser pro Drucksache: - Ergebnis-Status (angenommen/abgelehnt/info) - KEINE Fraktions-Vote-Detail (Vote-Listen bleiben leer) URL-Pattern (verifiziert WP21 Sitzungen 33+): ``https://www.bremische-buergerschaft.de/dokumente/wp{wp:02}/{land|stadt}/protokoll/b{wp:02}{l|s}{n:04}.pdf`` WP21 Land-Sitzungen: ``b21l0001.pdf``, ``b21l0002.pdf``, ... WP21 Stadt-Sitzungen: ``b21s0001.pdf``, ``b21s0002.pdf``, ... Auto-Ingest-Cron: pure URL-Probing per Sitzungs-Index funktioniert. ## Status-Saetze (verifiziert WP21 Sitzung 33) ``` Die Bürgerschaft (Landtag) lehnt den Antrag ab. Die Bürgerschaft (Landtag) lehnt den Änderungsantrag (21/1688) ab. Die Bürgerschaft (Landtag) stimmt dem Änderungsantrag (21/1764) zu. Die Bürgerschaft (Landtag) beschließt das Gesetz in erster Lesung. Die Bürgerschaft (Landtag) nimmt von der Mitteilung Kenntnis. ← skip (kein Vote) ``` ## Status-Mapping - ``lehnt ... ab`` → ergebnis="abgelehnt" - ``stimmt ... zu`` → ergebnis="angenommen" - ``beschließt ...`` → ergebnis="angenommen" - ``verabschiedet`` → ergebnis="angenommen" - ``nimmt ... Kenntnis`` → ergebnis="kenntnis" (skip) ## Drucksachen-Lookup - Inline-Form: ``(21/1234)`` direkt im Status-Satz - Block-Form: ``(Drucksache 21/N)`` rueckwaerts vom Anchor (TOP-Block) """ from __future__ import annotations import re from typing import Optional try: import fitz except ImportError: fitz = None ALLE_FRAKTIONEN_HB = ["SPD", "GRÜNE", "CDU", "DIE LINKE", "FDP", "BIW"] # Anchor: "Die Bürgerschaft (Landtag|Stadtbürgerschaft) ... " ANCHOR_RE = re.compile( r"Die\s+Bürgerschaft\s+\((?:Landtag|Stadtbürgerschaft)\)\s+" r"(?Plehnt|stimmt|beschließt|nimmt|verabschiedet|verweist|" r"überweist|leitet)\s+" r"(?P[^.]{0,500}?)" r"(?P\sab\.|\szu\.|\sKenntnis\.|\.)", re.DOTALL, ) DS_INLINE_RE = re.compile(r"\((\d{1,2}/\d{2,5})\)") DS_BLOCK_RE = re.compile(r"Drucksache\s+(\d{1,2}/\d{2,5})") def _classify_status(verb: str, rest: str, terminator: str) -> str: """Map HB-Beschluss-Vokabular auf einheitliche ergebnis-Codes.""" verb_l = verb.lower() rest_l = rest.lower() term_l = terminator.lower().strip() if "kenntnis" in rest_l or "kenntnis" in term_l: return "kenntnis" if verb_l == "lehnt" and "ab" in term_l: return "abgelehnt" if verb_l == "stimmt" and "zu" in term_l: return "angenommen" if verb_l in ("beschließt", "verabschiedet"): return "angenommen" if verb_l in ("verweist", "überweist", "leitet"): return "überwiesen" return "unbekannt" def _resolve_drucksache_hb(text: str, anchor_start: int, rest: str) -> Optional[str]: """Drucksachen-Aufloesung: erst Inline-Form aus rest, dann Block-Form.""" inline = DS_INLINE_RE.search(rest) if inline: return inline.group(1) window_start = max(0, anchor_start - 1500) window = text[window_start:anchor_start] matches = list(DS_BLOCK_RE.finditer(window)) if matches: return matches[-1].group(1) return None def _normalize_text(text: str) -> str: text = re.sub(r"(?<=[a-zäöüß])-\s+(?=[a-zäöüß])", "", text) return re.sub(r"\s+", " ", text) def parse_protocol(pdf_path: str) -> list[dict]: """Parst HB-Beschlussprotokoll, liefert Status pro Drucksache. Vote-Listen bleiben leer (HB publiziert nur Status, keine Fraktions-Detail). """ if fitz is None: raise ImportError("PyMuPDF (fitz) ist erforderlich fuer den HB-Parser") doc = fitz.open(pdf_path) full = "".join(p.get_text() for p in doc) doc.close() full = _normalize_text(full) results = [] for m in ANCHOR_RE.finditer(full): verb = m.group("verb") rest = m.group("rest") terminator = m.group("terminator") ergebnis = _classify_status(verb, rest, terminator) if ergebnis in ("kenntnis", "unbekannt"): continue ds = _resolve_drucksache_hb(full, m.start(), rest) if not ds: continue results.append({ "drucksache": ds, "ergebnis": ergebnis, "einstimmig": False, # Beschlussprotokoll → keine Vote-Detail "kind": "direct", "votes": {"ja": [], "nein": [], "enthaltung": []}, "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