gwoe-antragspruefer/app/protokoll_parsers/bb.py
Dotty Dotter 33bb564ed1 feat(#149): BB-Parser produktiv — Brandenburger Plenarprotokolle (Status-Only)
URL-Pattern verifiziert WP8 Sitzung 22:
https://www.parlamentsdokumentation.brandenburg.de/starweb/LBB/ELVIS/parladoku/w8/plpr/{n}.pdf

**Wichtig:** parladoku-PDF-URL liefert 403 ohne Cookie-Session. Erst
GET auf portal/browse.tt.html?wp=8 zur Cookie-Akquise, dann mit
gesetztem Cookie die PDF-URL aufrufen. Ingest-Cron implementiert
diesen Flow per http.cookiejar.CookieJar in Python.

Anchor-Pattern (NRW-aehnlich):
- "Damit ist [Subj] (mehrheitlich|einstimmig)? (angenommen|abgelehnt|ueberwiesen)"
- Drucksachen-Lookup: Drucksache 8/N rueckwaerts vom Anchor

Vote-Style: Handzeichen-only (kein Fraktionen-Listing). Daher
Vote-Listen leer; einstimmig=True setzt JA=alle WP8-Fraktionen
(SPD, AfD, CDU, BSW, GRÜNE).

Tests: 14 BB-Tests, Verifikation S22 → 26 Vote-Anchors extrahiert.
Stand: 10 produktive Parser
(NRW, BUND, BE, HH, TH, HE, SH, HB, SL, BB).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-29 02:04:21 +02:00

119 lines
3.7 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"""Brandenburg (BB) — Plenarprotokoll-Parser (#106 / #149, ADR 0009).
URL-Pattern (verifiziert WP8 Sitzung 22):
``https://www.parlamentsdokumentation.brandenburg.de/starweb/LBB/ELVIS/parladoku/w8/plpr/{n}.pdf``
**Wichtig:** parladoku-PDF-URL braucht Cookie-Session vom Portal. Erst
GET auf ``portal/browse.tt.html?wp=8`` zur Cookie-Akquise, dann mit
gesetztem Cookie die PDF-URL aufrufen. Im Auto-Ingest-Cron deshalb
ein eigener Block, der den BB-Cookie-Flow durchlaeuft.
## Anchor-Sprache (verifiziert WP8 Sitzung 22)
```
Wer dem zustimmt, den bitte ich um das Handzeichen. Ich bitte um die
Gegenprobe. Stimmenthaltungen? Damit ist der Antrag mehrheitlich
abgelehnt; es gab keine Enthaltungen.
```
Pattern (NRW-aehnlich):
- **Result-Anchor:** ``Damit ist [Subjekt] (mehrheitlich|einstimmig)? (angenommen|abgelehnt|überwiesen)``
- **Vote-Block:** Q+A im Reden-Stil (Handzeichen-only, ohne Fraktionen-Listing)
- "Wer dem zustimmt, ... Handzeichen"
- "Gegenprobe"
- "Enthaltungen?"
- **Drucksachen-Lookup:** ``Drucksache 8/N`` rueckwaerts vom Anchor
**Limitierung:** BB-Plenarprotokolle nennen die Fraktionen nicht
explizit — Vote-Listen bleiben leer. ``einstimmig=True`` setzt
JA=alle WP8-Fraktionen als Approximation.
## Fraktions-Mapping WP8 (ab 2024)
WP8 Konstellation (2024-Wahl): SPD + BSW (Koalition), AfD + CDU + GRÜNE
(Opposition).
- ``SPD``, ``AfD``, ``CDU``, ``BSW``, ``GRÜNE``
"""
from __future__ import annotations
import re
from typing import Optional
try:
import fitz
except ImportError:
fitz = None
ALLE_FRAKTIONEN_BB = ["SPD", "AfD", "CDU", "BSW", "GRÜNE"]
# Result-Anchor: "Damit ist/sind [Subjekt] (modus)? (ergebnis)"
RESULT_ANCHOR_RE = re.compile(
r"Damit\s+(?:ist|sind)\s+(?:der|die|das|dieser?|dieses|beide|alle|auch)?\s*"
r"(?P<subject>Antrag|Alternativantrag|Änderungsantrag|Gesetzentwurf|"
r"Entschließungsantrag|Beschlussempfehlung|Tagesordnungspunkt|Anträge)?"
r"[^.]{0,200}?(?P<modus>einstimmig|mehrheitlich|mit\s+(?:großer\s+)?Mehrheit)?\s*"
r"(?P<ergebnis>angenommen|abgelehnt|überwiesen)",
re.DOTALL,
)
DS_RE_BB = re.compile(r"Drucksache\s+8/(\d{1,5})")
def _resolve_drucksache_bb(text: str, anchor_start: int) -> Optional[str]:
window_start = max(0, anchor_start - 1500)
window = text[window_start:anchor_start]
matches = list(DS_RE_BB.finditer(window))
if matches:
return f"8/{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]:
if fitz is None:
raise ImportError("PyMuPDF (fitz) ist erforderlich fuer den BB-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 RESULT_ANCHOR_RE.finditer(full):
modus = (m.group("modus") or "").lower()
ergebnis = m.group("ergebnis")
ds = _resolve_drucksache_bb(full, m.start())
if not ds:
continue
einstimmig = "einstimmig" in modus
votes = {"ja": [], "nein": [], "enthaltung": []}
if einstimmig:
votes["ja"] = list(ALLE_FRAKTIONEN_BB)
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