gwoe-antragspruefer/app/protokoll_parsers/he.py

129 lines
4.2 KiB
Python
Raw Normal View History

"""Hessen (HE) — Beschlussprotokoll-Parser (#106 / #154, ADR 0009).
**Limitierung:** Hessen publiziert nur kompakte Beschlussprotokolle
(Tagesordnung + Status), KEINE Wortprotokolle mit Vote-Block. Daher
liefert dieser Parser pro Drucksache:
- Ergebnis-Status (angenommen/abgelehnt/ueberwiesen/...)
- KEINE Fraktions-Vote-Detail (Vote-Listen bleiben leer)
URL-Pattern (verifiziert WP21 Sitzungen 61, 62, 63):
``http://starweb.hessen.de/cache/hessen/landtag/Plenum/{wp}/Beschlussprotokoll_PL_{n}_{datum}.pdf``
Datum-Teil ``DD-MM-YYYY`` macht direkte URL-Vorhersage unmoeglich. Der
Auto-Ingest-Cron muss die Index-Seite scrapen:
``https://starweb.hessen.de/starweb/LIS/Pd_Eingang.htm`` (analog HH).
## Format-Beispiel (verifiziert WP21 Sitzung 62)
```
13. Dritte Lesung Gesetzentwurf Landesregierung
Gesetz ueber die Feststellung des Haushaltsplans...
Drucks. 21/4047 zu Drucks. 21/3503 zu Drucks. 21/2971
In dritter Lesung angenommen: Gesetz beschlossen
```
Pattern:
- ``Drucks. {wp}/N`` (mit Punkt nach Drucks)
- Naechstes Status-Wort: ``angenommen``, ``Abgelehnt``, ``Beschlussempfehlung angenommen``
- Vorangehende Lesungs-Phrase: ``In erster/zweiter/dritter Lesung``
## Ergebnis-Mapping
- ``angenommen`` ergebnis="angenommen"
- ``Abgelehnt`` ergebnis="abgelehnt"
- ``Nach (Aussprache|erster Lesung|zweiter Lesung) an [Ausschuss]`` ergebnis="überwiesen"
- ``Entgegengenommen``, ``Abgehalten`` kind="info" (kein Vote-Beschluss)
"""
from __future__ import annotations
import re
from typing import Optional
try:
import fitz
except ImportError:
fitz = None
ALLE_FRAKTIONEN_HE = ["CDU", "SPD", "GRÜNE", "AfD", "FDP", "LINKE"]
# Anchor-Pattern: Drucks. {wp}/N + Status (mit beliebigem Text dazwischen)
ENTRY_RE = re.compile(
r"Drucks\.\s+(?P<ds>\d{1,2}/\d{2,5})"
r"(?P<between>[^]{0,800}?)"
r"\s*(?P<status>"
r"(?:In (?:erster|zweiter|dritter) Lesung\s+)?angenommen"
r"|(?:In (?:erster|zweiter|dritter) Lesung\s+)?Abgelehnt"
r"|Beschlussempfehlung angenommen"
r"|Beschlussempfehlung abgelehnt"
r"|Nach (?:Aussprache|erster Lesung|zweiter Lesung|dritter Lesung) an [A-Z]+(?:\s*,\s*[A-Z]+)*"
r"|Entgegengenommen(?:\s+und\s+besprochen)?"
r"|Abgehalten"
r"|Zur Kenntnis genommen"
r")",
re.DOTALL | re.IGNORECASE,
)
def _classify_status(status: str) -> str:
"""Map HE-Status auf einheitliche ergebnis-Codes."""
s = status.lower()
if "abgelehnt" in s:
return "abgelehnt"
if "nach" in s and "an " in s:
return "überwiesen"
if "angenommen" in s:
return "angenommen"
if "abgehalten" in s or "entgegengenommen" in s or "zur kenntnis genommen" in s:
return "kenntnis" # eigene Kategorie ohne Vote-Bedeutung
return "unbekannt"
def _normalize_text(text: str) -> str:
return re.sub(r"\s+", " ", text)
def parse_protocol(pdf_path: str) -> list[dict]:
"""Parst HE-Beschlussprotokoll und liefert Status pro Drucksache.
Vote-Listen bleiben leer (HE publiziert nur Status, kein Vote-Detail).
"""
if fitz is None:
raise ImportError("PyMuPDF (fitz) ist erforderlich fuer den HE-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 ENTRY_RE.finditer(full):
ds = m.group("ds")
status = m.group("status").strip()
ergebnis = _classify_status(status)
if ergebnis in ("kenntnis", "unbekannt"):
# Kein Vote-Beschluss — uebersprungen, sonst muellt es die DB voll
continue
results.append({
"drucksache": ds,
"ergebnis": ergebnis,
"einstimmig": False, # Beschlussprotokoll hat keine Vote-Detail
"kind": "direct",
"votes": {"ja": [], "nein": [], "enthaltung": []},
"anchor_pos": m.start(),
"_he_raw_status": status, # debug-info, nicht persistiert
})
# Dedup ueber (drucksache, anchor_pos)
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