URL enthaelt Datum (DD-MM-YYYY), keine Vorhersage moeglich. Daher analog HH: starweb-Index scrapen, neue PDFs einzeln ingesten. Index-URL: https://starweb.hessen.de/starweb/LIS/Pd_Eingang.htm PDF-Pattern: cache/hessen/landtag/Plenum/{wp}/Beschlussprotokoll_PL_{n}_{datum}.pdf Protokoll-ID: PlPr{wp}-{n} (z.B. PlPr21-62) Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
202 lines
7.5 KiB
Bash
Executable File
202 lines
7.5 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
|
|
|
|
# ─── HE: Index-Page-Scrape (URL enthaelt Datum, nicht vorhersagbar) ───
|
|
# Hessen-Beschlussprotokoll-URL-Pattern hat Datum-Anteil DD-MM-YYYY,
|
|
# daher Pattern via starweb-Index extrahieren statt vorzuhersagen.
|
|
echo "--- HE WP21 (Index-Scrape) ---"
|
|
docker exec "$CONTAINER" python <<'EOF'
|
|
import re, sys
|
|
import urllib.request
|
|
import sqlite3
|
|
import asyncio
|
|
|
|
req = urllib.request.Request(
|
|
"https://starweb.hessen.de/starweb/LIS/Pd_Eingang.htm",
|
|
headers={"User-Agent": "Mozilla/5.0 GWOeAntragspruefer"},
|
|
)
|
|
try:
|
|
# 302 → portal/browse.tt.html, urllib folgt automatisch
|
|
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)
|
|
|
|
# href="http://starweb.hessen.de/cache/hessen/landtag/Plenum/{wp}/Beschlussprotokoll_PL_{n}_{datum}.pdf"
|
|
pdf_re = re.compile(
|
|
r'href="(https?://starweb\.hessen\.de/cache/hessen/landtag/Plenum/(\d+)/'
|
|
r'Beschlussprotokoll_PL_(\d+)_(\d{2}-\d{2}-\d{4})\.pdf)"'
|
|
)
|
|
matches = list(pdf_re.finditer(html))
|
|
print(f" {len(matches)} HE-Beschlussprotokolle in Index gefunden")
|
|
|
|
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='HE'"
|
|
)}
|
|
|
|
from app.ingest_votes import ingest_pdf
|
|
from pathlib import Path
|
|
import tempfile
|
|
|
|
new_count = 0
|
|
for m in matches:
|
|
url, wp, sitzung, datum = m.groups()
|
|
pid = f"PlPr{wp}-{sitzung}"
|
|
if pid in existing:
|
|
continue
|
|
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="HE", 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" HE: {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) ==="
|