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>
119 lines
3.7 KiB
Python
119 lines
3.7 KiB
Python
"""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
|