feat(#154): HE-Parser produktiv — Hessen Beschlussprotokoll (Status-Only)
Hessen publiziert nur Beschlussprotokolle (Tagesordnung + Status), KEINE Wortprotokolle mit Vote-Block. Daher minimaler Parser: - Drucksache + Status (angenommen/abgelehnt/ueberwiesen) - Vote-Listen bleiben leer (HE hat keine Fraktions-Detail) URL-Pattern (verifiziert WP21 Sitzungen 61-63): http://starweb.hessen.de/cache/hessen/landtag/Plenum/{wp}/Beschlussprotokoll_PL_{n}_{datum}.pdf Datum-Teil DD-MM-YYYY → URL-Vorhersage unmoeglich, Auto-Ingest braucht Index-Scrape via starweb.hessen.de/starweb/LIS/Pd_Eingang.htm (analog HH). Status-Mapping: - "angenommen" → ergebnis="angenommen" - "Abgelehnt" → ergebnis="abgelehnt" - "Nach (Aussprache|Lesung) an [Ausschuss]" → ergebnis="ueberwiesen" - "Entgegengenommen", "Abgehalten", "Zur Kenntnis genommen" → uebersprungen Tests: PROTOKOLL_PARSERS-Set jetzt {NRW, BUND, BE, HH, TH, HE}. STUB_BL_CODES auf 11 BL reduziert (BB, BW, BY, HB, LSA, MV, NI, RP, SH, SL, SN bleiben). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
399dbc2639
commit
8125dbb731
@ -33,6 +33,7 @@ from .bund import parse_protocol as _parse_bund
|
|||||||
from .be import parse_protocol as _parse_be
|
from .be import parse_protocol as _parse_be
|
||||||
from .hh import parse_protocol as _parse_hh
|
from .hh import parse_protocol as _parse_hh
|
||||||
from .th import parse_protocol as _parse_th
|
from .th import parse_protocol as _parse_th
|
||||||
|
from .he import parse_protocol as _parse_he
|
||||||
|
|
||||||
# Typ-Alias fuer Lesbarkeit; Parser-Signatur ist bewusst minimal.
|
# Typ-Alias fuer Lesbarkeit; Parser-Signatur ist bewusst minimal.
|
||||||
ProtokollParser = Callable[[str], list[dict]]
|
ProtokollParser = Callable[[str], list[dict]]
|
||||||
@ -43,6 +44,7 @@ PROTOKOLL_PARSERS: dict[str, ProtokollParser] = {
|
|||||||
"BE": _parse_be,
|
"BE": _parse_be,
|
||||||
"HH": _parse_hh,
|
"HH": _parse_hh,
|
||||||
"TH": _parse_th,
|
"TH": _parse_th,
|
||||||
|
"HE": _parse_he,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@ -1,47 +1,128 @@
|
|||||||
"""Hessen (HE) — Plenarprotokoll-Parser STUB (#106 Folge, ADR 0009).
|
"""Hessen (HE) — Beschlussprotokoll-Parser (#106 / #154, ADR 0009).
|
||||||
|
|
||||||
**Status: noch nicht implementiert.** Dieser Modul-Stub enthaelt
|
**Limitierung:** Hessen publiziert nur kompakte Beschlussprotokolle
|
||||||
Recherche-Findings vom 2026-04-28, sodass die Implementer-Session
|
(Tagesordnung + Status), KEINE Wortprotokolle mit Vote-Block. Daher
|
||||||
direkt produktiv loslegen kann. Der Stub wird **nicht** in
|
liefert dieser Parser pro Drucksache:
|
||||||
``app.protokoll_parsers.PROTOKOLL_PARSERS`` registriert — der
|
- Ergebnis-Status (angenommen/abgelehnt/ueberwiesen/...)
|
||||||
Auto-Ingest-Cron ueberspringt HE solange.
|
- KEINE Fraktions-Vote-Detail (Vote-Listen bleiben leer)
|
||||||
|
|
||||||
## Recherche
|
URL-Pattern (verifiziert WP21 Sitzungen 61, 62, 63):
|
||||||
|
``http://starweb.hessen.de/cache/hessen/landtag/Plenum/{wp}/Beschlussprotokoll_PL_{n}_{datum}.pdf``
|
||||||
|
|
||||||
| Feld | Wert |
|
Datum-Teil ``DD-MM-YYYY`` macht direkte URL-Vorhersage unmoeglich. Der
|
||||||
|---|---|
|
Auto-Ingest-Cron muss die Index-Seite scrapen:
|
||||||
| **Doku-System** | portala |
|
``https://starweb.hessen.de/starweb/LIS/Pd_Eingang.htm`` (analog HH).
|
||||||
| **Base-URL** | https://starweb.hessen.de/portal |
|
|
||||||
| **Familie** | BB/RP-Familie |
|
|
||||||
| **Format** | HTML bevorzugt; ggf. PDF als Fallback |
|
|
||||||
|
|
||||||
## URL-Discovery
|
## Format-Beispiel (verifiziert WP21 Sitzung 62)
|
||||||
|
|
||||||
Plenum-Protokolle wahrscheinlich als HTML mit semantischen Tags pro Beschluss — wenn HTML zugaenglich, EINFACHER als PDF-Parser
|
```
|
||||||
|
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
|
||||||
|
```
|
||||||
|
|
||||||
## Bezug
|
Pattern:
|
||||||
|
- ``Drucks. {wp}/N`` (mit Punkt nach Drucks)
|
||||||
|
- Naechstes Status-Wort: ``angenommen``, ``Abgelehnt``, ``Beschlussempfehlung angenommen``
|
||||||
|
- Vorangehende Lesungs-Phrase: ``In erster/zweiter/dritter Lesung``
|
||||||
|
|
||||||
- Architektur: ADR 0009 (Plenarprotokoll-Parser-Registry)
|
## Ergebnis-Mapping
|
||||||
- Roadmap: ``docs/protokoll-parser-roadmap.md``
|
|
||||||
- Referenz-Implementation: ``app/protokoll_parsers/nrw.py``
|
|
||||||
(38 Tests, 19/19-Fixture-Garantie)
|
|
||||||
- Folge-Issue: https://repo.toppyr.de/tobias/gwoe-antragspruefer/issues/154 (Titel: "protokoll-parser: HE (Hessen)")
|
|
||||||
|
|
||||||
## Aufwand
|
- ``angenommen`` → ergebnis="angenommen"
|
||||||
|
- ``Abgelehnt`` → ergebnis="abgelehnt"
|
||||||
Geschaetzt 1-3 Tage konzentrierte Arbeit:
|
- ``Nach (Aussprache|erster Lesung|zweiter Lesung) an [Ausschuss]`` → ergebnis="überwiesen"
|
||||||
- 2-4h URL-Discovery + Format-Inspektion (Sample-Protokoll inhaltlich anschauen)
|
- ``Entgegengenommen``, ``Abgehalten`` → kind="info" (kein Vote-Beschluss)
|
||||||
- 4-8h Anchor-Phrasen-Reverse-Engineering + Parser-Implementierung
|
|
||||||
- 4h Tests mit Fixture-Pinning
|
|
||||||
- 1h Eintrag in PROTOKOLL_PARSERS + auto-ingest-protocols.sh
|
|
||||||
"""
|
"""
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import re
|
||||||
|
from typing import Optional
|
||||||
|
|
||||||
def parse_protocol(path: str) -> list[dict]:
|
try:
|
||||||
"""STUB — siehe Modul-Docstring."""
|
import fitz
|
||||||
raise NotImplementedError(
|
except ImportError:
|
||||||
"HE-Plenarprotokoll-Parser ist noch nicht implementiert. "
|
fitz = None
|
||||||
"Siehe app/protokoll_parsers/he.py-Docstring fuer Recherche-Findings "
|
|
||||||
"und docs/protokoll-parser-roadmap.md."
|
|
||||||
|
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
|
||||||
|
|||||||
@ -20,8 +20,8 @@ import pytest
|
|||||||
from app.protokoll_parsers import PROTOKOLL_PARSERS, supported_bundeslaender
|
from app.protokoll_parsers import PROTOKOLL_PARSERS, supported_bundeslaender
|
||||||
|
|
||||||
STUB_BL_CODES = [
|
STUB_BL_CODES = [
|
||||||
# BUND/BE/HH/TH raus, weil seit 2026-04-28/29 produktive Parser
|
# BUND/BE/HH/TH/HE raus, weil seit 2026-04-28/29 produktive Parser
|
||||||
"BB", "BW", "BY", "HB", "HE",
|
"BB", "BW", "BY", "HB",
|
||||||
"LSA", "MV", "NI", "RP", "SH", "SL", "SN",
|
"LSA", "MV", "NI", "RP", "SH", "SL", "SN",
|
||||||
]
|
]
|
||||||
|
|
||||||
@ -76,8 +76,8 @@ class TestRegistryDiscipline:
|
|||||||
|
|
||||||
def test_stubs_not_in_registry(self):
|
def test_stubs_not_in_registry(self):
|
||||||
registered = set(supported_bundeslaender())
|
registered = set(supported_bundeslaender())
|
||||||
# Aktuell: NRW + BUND + BE + HH + TH produktiv
|
# Aktuell: NRW + BUND + BE + HH + TH + HE produktiv
|
||||||
assert registered == {"NRW", "BUND", "BE", "HH", "TH"}, (
|
assert registered == {"NRW", "BUND", "BE", "HH", "TH", "HE"}, (
|
||||||
"Unerwartete Registry-Eintraege. Wenn neue BL implementiert sind, "
|
"Unerwartete Registry-Eintraege. Wenn neue BL implementiert sind, "
|
||||||
"diesen Test anpassen UND den Stub durch echten Parser ersetzen."
|
"diesen Test anpassen UND den Stub durch echten Parser ersetzen."
|
||||||
)
|
)
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user