Fuenfter produktiver Parser nach NRW + BUND + BE + HH. URL-Pattern verifiziert (WP8 Sitzungen 1, 10, 20, 30, 40, 42): https://www.thueringer-landtag.de/uploads/tx_tltcalendar/protocols/Arbeitsfassung{n}.pdf Anchor-Sprache (BE-aehnlich): Wer dem zustimmt, ... Das sind die Stimmen aus den Fraktionen der CDU, BSW, SPD und Die Linke. Wer stimmt gegen ...? Das sind die Stimmen aus der Fraktion der AfD. Damit ist [...] mehrheitlich angenommen. Pattern: - Result-Anchor: Damit ist [Subjekt] (mehrheitlich|einstimmig)? (angenommen|abgelehnt) - Vote-Block: Wer dem zustimmt / Wer stimmt gegen / Wer enthaelt sich - Drucksachen-Lookup: 'Drucksache 8/N' rueckwaerts Fraktions-Mapping WP8 (ab Mai 2024): CDU, AfD, BSW, Linke, SPD (WP7-Faktionen GRUENE/FDP fuer Backfill ebenfalls im Mapping). Cron-PROTO_TARGETS um TH-WP8 erweitert. Stub-Test angepasst.
141 lines
5.4 KiB
Bash
Executable File
141 lines
5.4 KiB
Bash
Executable File
#!/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.
|
|
#
|
|
# 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)
|
|
PROTO_TARGETS=(
|
|
"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"
|
|
"BUND|19|BTP19-|https://dserver.bundestag.de/btp/19/19{n3}.xml"
|
|
"BE|19|PlPr19-|https://www.parlament-berlin.de/ados/19/IIIPlen/protokoll/plen19-{n3}-pp.pdf"
|
|
"BE|18|PlPr18-|https://www.parlament-berlin.de/ados/18/IIIPlen/protokoll/plen18-{n3}-pp.pdf"
|
|
"TH|8|PlPr8-|https://www.thueringer-landtag.de/uploads/tx_tltcalendar/protocols/Arbeitsfassung{n}.pdf"
|
|
)
|
|
|
|
echo "=== auto-ingest-protocols $(date -Iseconds) ==="
|
|
|
|
# ─── 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
|
|
|
|
for entry in "${PROTO_TARGETS[@]}"; do
|
|
IFS='|' read -r bl wp prefix pattern <<< "$entry"
|
|
echo "--- ${bl} WP${wp} (prefix=${prefix}) ---"
|
|
|
|
# Hoechste bisher ingestete Sitzungs-Nr fuer diesen BL/Prefix
|
|
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
|
|
|
|
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
|
|
n3=$(printf "%03d" "$n")
|
|
url="${pattern//\{n3\}/$n3}"
|
|
url="${url//\{n\}/$n}"
|
|
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) ==="
|