2026-04-28 22:23:51 +02:00
#!/bin/bash
# BL-uebergreifender Auto-Ingest fuer Plenarprotokolle (#106 / #126 Phase 3).
#
# Pro registriertem BL: liest letztes ingestetes Protokoll, probiert das
# naechste, ingestet bei 200, wiederholt bis 404 (mit GAP_TOLERANCE).
# Idempotent (Compound-PK in plenum_vote_results), kein State ausser DB.
#
# Wird via Cron taeglich morgens aufgerufen. Ausgabe nach
# /var/log/gwoe-ingest-protocols.log.
#
# Usage:
# auto-ingest-protocols.sh [CONTAINER]
set -euo pipefail
CONTAINER = " ${ 1 :- gwoe -antragspruefer } "
GAP_TOLERANCE = 3 # 3 aufeinanderfolgende 404 → fertig fuer dieses BL
# Pro BL: URL-Pattern + Wahlperiode-Auflistung.
# WP-Liste ergibt sich aus aktiven Wahlperioden in BUNDESLAENDER. Hier
# aktuell + Vorgaenger-WP, weil Plenum noch in der laufenden WP arbeitet
# und alte Sitzungen gelegentlich nachtraeglich digitalisiert werden.
#
2026-04-28 23:21:39 +02:00
# Format: BL_CODE|WAHLPERIODE|PROTOKOLL_ID_PREFIX|URL_PATTERN
# URL-Pattern unterstuetzt zwei Platzhalter:
# {n} — Sitzungs-Nr unkpaddet (z.B. NRW: MMP18-1.pdf)
# {n3} — Sitzungs-Nr 3-stellig zero-gepadded (z.B. BUND: 20001.xml)
2026-04-28 22:23:51 +02:00
PROTO_TARGETS = (
2026-04-28 23:21:39 +02:00
"NRW|18|MMP18-|https://www.landtag.nrw.de/portal/WWW/dokumentenarchiv/Dokument/MMP18-{n}.pdf"
"NRW|17|MMP17-|https://www.landtag.nrw.de/portal/WWW/dokumentenarchiv/Dokument/MMP17-{n}.pdf"
"BUND|20|BTP20-|https://dserver.bundestag.de/btp/20/20{n3}.xml"
2026-04-29 01:09:07 +02:00
"BUND|19|BTP19-|https://dserver.bundestag.de/btp/19/19{n3}.xml"
feat(#150): BE-Parser produktiv — Berliner Abgeordnetenhaus-Plenarprotokolle
Dritter vollwertiger Plenarprotokoll-Parser nach NRW + BUND.
URL-Pattern verifiziert (WP19 Sitzungen 1, 10, 50, 80, 100):
https://www.parlament-berlin.de/ados/{wp}/IIIPlen/protokoll/plen{wp}-{n:03}-pp.pdf
Anchor-Sprache (NRW-aehnlich, mit Berliner-Eigenheit 'pro forma'):
Wer den Antrag auf Drucksache 19/X annehmen moechte, ... – Das sind
die Fraktionen Buendnis 90/Die Gruenen und Die Linke.
Wer stimmt dagegen? – Das sind die Fraktionen der CDU, SPD und AfD.
Wer enthaelt sich, pro forma? – Das ist niemand.
Damit ist der Antrag abgelehnt.
Pattern:
- Result-Anchor: Damit ist [Antrag/Aenderungsantrag/Gesetzentwurf/...]
(angenommen|abgelehnt)
- Vote-Block: 3 Q+A-Paare im Reden-Stil (annehmen moechte / dagegen /
enthaelt sich)
- Drucksachen-Lookup: 'Drucksache 19/N(-suffix)' rueckwaerts (1500-char Fenster)
Fraktions-Mapping WP19:
- Buendnis 90/Die Gruenen → GRÜNE
- Die Linke → LINKE
- CDU, SPD, AfD, FDP
21 Tests in test_protokoll_parsers_be.py.
Cron-PROTO_TARGETS erweitert um BE WP19 (~80 Sitzungen).
Stub-Test angepasst.
905 Tests gruen (889 → 905, +16 fuer BE).
2026-04-29 00:37:47 +02:00
"BE|19|PlPr19-|https://www.parlament-berlin.de/ados/19/IIIPlen/protokoll/plen19-{n3}-pp.pdf"
2026-04-29 01:09:07 +02:00
"BE|18|PlPr18-|https://www.parlament-berlin.de/ados/18/IIIPlen/protokoll/plen18-{n3}-pp.pdf"
2026-04-28 22:23:51 +02:00
)
echo " === auto-ingest-protocols $( date -Iseconds) === "
2026-04-29 01:01:52 +02:00
# ─── HH: Index-Page-Scrape statt URL-Pattern ──────────────────────────
# Hamburg hat keine vorhersagbare URL-Pattern (Blob-IDs + Hashes).
# Stattdessen: Index-Seite scrapen, jedes gefundene PDF einzeln ingesten.
echo "--- HH WP23 (Index-Scrape) ---"
docker exec " $CONTAINER " python <<EOF
import re, sys
import urllib.request
import sqlite3
import asyncio
# Index-Seite scrapen
req = urllib.request.Request(
"https://www.hamburgische-buergerschaft.de/recherche-info/protokolle" ,
headers = { "User-Agent" : "Mozilla/5.0 GWOeAntragspruefer" } ,
)
try:
html = urllib.request.urlopen( req, timeout = 20) .read( ) .decode( "utf-8" , errors = "replace" )
except Exception as e:
print( f" Index-Scrape fehlgeschlagen: {e}" )
sys.exit( 0)
# PDFs extrahieren: /resource/blob/{ID}/{HASH}/{N}-vorl-beschlussprotokoll-DD-MM-YYYY-data.pdf
pdf_re = re.compile(
r'href="(/resource/blob/(\d+)/([a-f0-9]+)/(\d+)-vorl-beschlussprotokoll-(\d{2}-\d{2}-\d{4})[^"]*\.pdf)"'
)
matches = list( pdf_re.finditer( html) )
print( f" {len(matches)} HH-Protokolle in Index gefunden" )
# Bereits ingestete Protokolle holen
db = sqlite3.connect( "/app/data/gwoe-antraege.db" )
existing = { row[ 0] for row in db.execute(
"SELECT quelle_protokoll FROM plenum_vote_results WHERE bundesland='HH'"
) }
from app.ingest_votes import ingest_pdf
from pathlib import Path
import tempfile
new_count = 0
for m in matches:
href, blob_id, h, sitzung, datum = m.groups( )
pid = f"PlPr23-{sitzung}"
if pid in existing:
continue
url = "https://www.hamburgische-buergerschaft.de" + href
print( f" → neu: {pid} ({datum})" )
with tempfile.NamedTemporaryFile( suffix = ".pdf" , delete = False) as tmp:
tmp_path = Path( tmp.name)
try:
urllib.request.urlretrieve( url, tmp_path)
stats = asyncio.run( ingest_pdf(
tmp_path, bundesland = "HH" , protokoll_id = pid, quelle_url = url,
) )
print( f" parsed: {stats['parsed']}, written: {stats['written']}" )
new_count += 1
except Exception as e:
print( f" Fehler: {e}" )
finally:
tmp_path.unlink( missing_ok = True)
print( f" HH: {new_count} neue Protokolle ingestet" )
EOF
2026-04-28 22:23:51 +02:00
for entry in " ${ PROTO_TARGETS [@] } " ; do
2026-04-28 23:21:39 +02:00
IFS = '|' read -r bl wp prefix pattern <<< " $entry "
echo " --- ${ bl } WP ${ wp } (prefix= ${ prefix } ) --- "
2026-04-28 22:23:51 +02:00
2026-04-28 23:21:39 +02:00
# Hoechste bisher ingestete Sitzungs-Nr fuer diesen BL/Prefix
2026-04-28 22:29:36 +02:00
last_n = $( docker exec " $CONTAINER " python -c "
import sqlite3
c = sqlite3.connect( '/app/data/gwoe-antraege.db' ) .cursor( )
c.execute( \" SELECT COALESCE( MAX( CAST( SUBSTR( quelle_protokoll, ${# prefix } +1) AS INTEGER) ) , 0) FROM plenum_vote_results WHERE bundesland = ? AND quelle_protokoll LIKE ?\" , ( '${bl}' , '${prefix}%' ) )
print( c.fetchone( ) [ 0] )
" 2>/dev/null || echo " 0" )
# Sanity: numeric check
if ! [ [ " $last_n " = ~ ^[ 0-9] +$ ] ] ; then last_n = 0; fi
2026-04-28 22:23:51 +02:00
start_n = $(( last_n + 1 ))
echo " Letztes ingestes ${ prefix } : ${ last_n } , probiere ab ${ start_n } "
consecutive_404 = 0
for n in $( seq $start_n $(( last_n + 50 )) ) ; do
2026-04-28 23:21:39 +02:00
n3 = $( printf "%03d" " $n " )
url = " ${ pattern // \{ n3 \} / $n3 } "
url = " ${ url // \{ n \} / $n } "
2026-04-28 22:23:51 +02:00
http = $( curl -sS -o /dev/null -w "%{http_code}" --max-time 15 " $url " || echo "000" )
if [ " $http " = "200" ] ; then
consecutive_404 = 0
pid = " ${ prefix } ${ n } "
echo " → ingest ${ pid } "
docker exec " $CONTAINER " python -m app.ingest_votes \
--url " $url " --bundesland " $bl " --protokoll-id " $pid " 2>& 1 \
| tail -3 | sed 's/^/ /' || echo " !! ingest fehlgeschlagen"
elif [ " $http " = "404" ] ; then
consecutive_404 = $(( consecutive_404 + 1 ))
if [ $consecutive_404 -ge $GAP_TOLERANCE ] ; then
break
fi
fi
done
done
echo " === auto-ingest done $( date -Iseconds) === "