Files
gesetzblatt/extract_bgbl.py
Michael Hermann 2bb70ae0ed Initiales gesetzblatt-Repo: Parser-Zweig ab 2023 von recht.bund.de.
Historie 1949–2022 bleibt in lawgit, bis der GPU-Lauf endet; Schnitt ohne Lücke.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-08-16 11:56:05 +02:00

115 lines
3.6 KiB
Python

#!/usr/bin/env python3
"""
Extrahiert Text aus BGBl-PDFs (Textschicht). Idempotent: vorhandene JSON überspringen.
python3 extract_bgbl.py --year 2025
python3 extract_bgbl.py --pdf bgbl/bgbl1/2025/bgbl1_2025_1.pdf
"""
from __future__ import annotations
import argparse
import json
import os
from concurrent.futures import ProcessPoolExecutor, as_completed
from pathlib import Path
from pypdf import PdfReader
from bgbl_util import BGBl_DIR, TEXT_DIR
MIN_CHARS_PER_PAGE = 80
def extract_pdf(pdf_path: Path) -> dict:
reader = PdfReader(str(pdf_path))
pages = []
for i, page in enumerate(reader.pages, start=1):
text = page.extract_text() or ""
pages.append({"page": i, "text": text, "chars": len(text.strip())})
total_chars = sum(p["chars"] for p in pages)
n = max(len(pages), 1)
quality = "ok" if total_chars / n >= MIN_CHARS_PER_PAGE else "needs_vision"
return {
"pdf": str(pdf_path),
"pages": len(pages),
"chars": total_chars,
"quality": quality,
"page_texts": [{"page": p["page"], "text": p["text"]} for p in pages],
"page_chars": [p["chars"] for p in pages],
}
def out_path_for(pdf_path: Path) -> Path:
try:
rel = pdf_path.resolve().relative_to(BGBl_DIR.resolve())
except ValueError:
rel = Path(pdf_path.name)
return TEXT_DIR / rel.with_suffix(".json")
def process(pdf_path: Path) -> tuple[str, str]:
dest = out_path_for(pdf_path)
try:
data = extract_pdf(pdf_path)
except Exception as exc:
return str(pdf_path), f"fehler: {type(exc).__name__}"
dest.parent.mkdir(parents=True, exist_ok=True)
dest.write_text(json.dumps(data, ensure_ascii=False), encoding="utf-8")
return str(pdf_path), data["quality"]
def main() -> None:
parser = argparse.ArgumentParser(description="Text aus BGBl-PDFs extrahieren")
parser.add_argument("--year", type=int, action="append")
parser.add_argument("--kind", default="bgbl1")
parser.add_argument("--pdf", type=Path)
parser.add_argument("--workers", type=int, default=max(os.cpu_count() or 4, 2))
parser.add_argument("--force", action="store_true")
args = parser.parse_args()
pdfs: list[Path] = []
if args.pdf:
pdfs = [args.pdf]
elif args.year:
for year in args.year:
pdfs.extend(sorted((BGBl_DIR / args.kind / str(year)).glob("*.pdf")))
else:
pdfs = sorted(BGBl_DIR.rglob("*.pdf"))
if not pdfs:
print("Keine PDFs. Zuerst: python3 download_rechtbund.py --live-test")
return
if not args.force:
offen = [p for p in pdfs if not out_path_for(p).exists()]
if len(offen) < len(pdfs):
print(f"{len(pdfs) - len(offen)} schon extrahiert, übersprungen")
pdfs = offen
if not pdfs:
print("Nichts zu tun.")
return
print(f"{len(pdfs)} PDFs, {args.workers} Prozesse")
zaehler = {"ok": 0, "needs_vision": 0}
fehler = []
with ProcessPoolExecutor(max_workers=args.workers) as pool:
futures = [pool.submit(process, p) for p in pdfs]
for i, fut in enumerate(as_completed(futures), 1):
pfad, status = fut.result()
if status.startswith("fehler"):
fehler.append((pfad, status))
else:
zaehler[status] = zaehler.get(status, 0) + 1
if i % 50 == 0 or i == len(pdfs):
print(f" {i}/{len(pdfs)}{zaehler}, {len(fehler)} Fehler", flush=True)
print(
f"Fertig: {zaehler['ok']} mit Textschicht, "
f"{zaehler['needs_vision']} brauchen OCR, {len(fehler)} Fehler"
)
if __name__ == "__main__":
main()