"""Nachschlagewerkzeug für den amtlichen Gesetzestext. Beim Schreiben der Erklärungen darf nichts aus dem Gedächtnis zitiert werden. Dieses Werkzeug macht das Nachschlagen so bequem, dass es keinen Grund gibt, es zu unterlassen. Grundlage ist `content/gesetze/index.json`, erzeugt aus dem amtlichen XML von gesetze-im-internet.de. Aufrufe ------- Eine Norm im Volltext:: python data-pipeline/gesetz.py WaffG "§ 12" python data-pipeline/gesetz.py AWaffV "§ 13" --absatz 2 python data-pipeline/gesetz.py StGB "§ 32" In allen Gesetzen nach einem Begriff suchen:: python data-pipeline/gesetz.py --suche "Anscheinswaffe" python data-pipeline/gesetz.py --suche "Schalldämpfer" --gesetz WaffG Innerhalb einer Anlage suchen (Anlagen sind lang):: python data-pipeline/gesetz.py WaffG "Anlage 1" --suche Kurzwaffen Die Gliederung einer Norm anzeigen – genau die Angaben, die eine Fundstelle enthalten darf:: python data-pipeline/gesetz.py WaffG "§ 12" --gliederung Übersicht aller Gesetze und ihres Standes:: python data-pipeline/gesetz.py --gesetze """ from __future__ import annotations import argparse import io import json import re import sys from pathlib import Path WURZEL = Path(__file__).resolve().parent.parent INDEX = WURZEL / 'content' / 'gesetze' / 'index.json' #: Zeichen Umfeld je Fundstelle bei der Volltextsuche. UMFELD = 320 def daten() -> dict: if not INDEX.exists(): raise SystemExit( f'Index fehlt: {INDEX}\nErst erzeugen mit: python data-pipeline/gesetze_index.py' ) return json.load(io.open(INDEX, encoding='utf-8'))['gesetze'] def gesetze_auflisten(gesetze: dict) -> int: for kuerzel, gesetz in gesetze.items(): anlagen = sum(1 for n in gesetz['normen'].values() if n['ist_anlage']) paragrafen = len(gesetz['normen']) - anlagen print(f'{kuerzel:12} {gesetz["langtitel"]}') print(f'{"":12} {paragrafen} Paragrafen, {anlagen} Anlagen') print(f'{"":12} Stand: {gesetz["stand"]}') print(f'{"":12} {gesetz["quelle"]}') print() return 0 def norm_zeigen(gesetz: dict, kuerzel: str, name: str, absatz: str | None) -> int: norm = gesetz['normen'].get(name) if norm is None: aehnlich = [b for b in gesetz['normen'] if b.replace(' ', '') == name.replace(' ', '')] hinweis = f'\nGemeint war vielleicht: {aehnlich[0]}' if aehnlich else '' print(f'{name} gibt es im {kuerzel} nicht.{hinweis}') return 1 print(f'=== {kuerzel} {name} – {norm["titel"]} ===') print(f'Stand: {gesetz["stand"]}\n') if absatz is not None: eintrag = norm['absaetze'].get(absatz) if eintrag is None: vorhanden = ', '.join(norm['absaetze']) or 'keine' print(f'Abs. {absatz} gibt es nicht. Vorhanden: {vorhanden}') return 1 print(f'({absatz}) {eintrag["text"]}') if eintrag['gliederung']: print(f'\nGliederung: {gliederung_text(eintrag["gliederung"])}') return 0 print(norm['text']) return 0 def gliederung_text(gliederung: dict[str, list[str]]) -> str: """Gliederung als Aufzaehlung. Die meisten Absaetze zaehlen mit Nummern auf, einige unmittelbar mit Buchstaben (etwa § 3a Abs. 2 SprengG). Die Bezeichnung richtet sich deshalb nach der Marke, nicht nach der Ebene. """ teile = [] for marke, unter in gliederung.items(): if not marke: # Eine leere Marke ist ein Fehler im Index, kein Grund # abzubrechen. Bis Fassung 0.24.1 stand hier `marke[0]`, und # `gesetz.py BeschG "§ 11" --gliederung` endete mitten in der # Ausgabe mit einem IndexError – ausgerechnet das Werkzeug, das # zeigen soll, welche Angaben eine Fundstelle tragen darf. teile.append('(unlesbare Marke im Index)') continue art = 'Nr.' if marke[0].isdigit() else 'Buchst.' teile.append(f'{art} {marke}' + (f' ({", ".join(unter)})' if unter else '')) return '; '.join(teile) def gliederung_zeigen(gesetz: dict, kuerzel: str, name: str) -> int: """Zeigt, welche Angaben eine Fundstelle auf diese Norm enthalten darf.""" norm = gesetz['normen'].get(name) if norm is None: print(f'{name} gibt es im {kuerzel} nicht.') return 1 print(f'=== {kuerzel} {name} – {norm["titel"]} ===') if norm['ist_anlage']: print('Anlage: keine Absatzgliederung. In einer Fundstelle „stelle" verwenden,') print('z. B. { "norm": "Anlage 1", "stelle": "Abschnitt 1 Unterabschnitt 1 Nr. 1.1" }') return 0 if not norm['absaetze']: print('Keine Absatzgliederung – die Norm wird als Ganzes zitiert.') return 0 for nummer, absatz in norm['absaetze'].items(): marke = gliederung_text(absatz['gliederung']) if absatz['gliederung'] else '(ohne Nummern)' print(f' Abs. {nummer}: {marke}') return 0 def suchen(gesetze: dict, begriff: str, nur: str | None, in_norm: str | None) -> int: muster = re.compile(re.escape(begriff), re.IGNORECASE) treffer = 0 for kuerzel, gesetz in gesetze.items(): if nur is not None and kuerzel != nur: continue for name, norm in gesetz['normen'].items(): if in_norm is not None and name != in_norm: continue for fund in muster.finditer(norm['text']): anfang = max(0, fund.start() - UMFELD) ende = min(len(norm['text']), fund.end() + UMFELD) auszug = norm['text'][anfang:ende].replace('\n', ' | ') print(f'--- {kuerzel} {name} ({norm["titel"][:60]}) ---') print(f'...{auszug}...\n') treffer += 1 if treffer >= 40: print('(weitere Treffer nicht angezeigt – Suchbegriff eingrenzen)') return 0 if treffer == 0: print(f'„{begriff}" kommt nicht vor' + (f' im {nur}' if nur else ' in keinem der Gesetze')) print('Das ist ein belastbares Ergebnis: Was hier fehlt, steht nicht im Gesetz.') return 0 def main() -> int: zerleger = argparse.ArgumentParser( description='Amtlichen Gesetzestext nachschlagen.', epilog='Ohne Nachschlagen keine Fundstelle.', ) zerleger.add_argument('gesetz', nargs='?', help='Kürzel, z. B. WaffG') zerleger.add_argument('norm', nargs='?', help='„§ 12" oder „Anlage 1"') zerleger.add_argument('--absatz', help='Nur diesen Absatz zeigen') zerleger.add_argument('--gliederung', action='store_true', help='Nur die Gliederung zeigen') zerleger.add_argument('--suche', help='Volltextsuche') zerleger.add_argument('--gesetze', action='store_true', help='Alle Gesetze auflisten') argumente = zerleger.parse_args() gesetze = daten() if argumente.gesetze: return gesetze_auflisten(gesetze) if argumente.suche is not None and argumente.gesetz is None: return suchen(gesetze, argumente.suche, None, None) if argumente.gesetz is None: zerleger.print_help() return 1 if argumente.gesetz not in gesetze: print(f'Unbekanntes Gesetz: {argumente.gesetz}') print(f'Bekannt sind: {", ".join(gesetze)}') return 1 gesetz = gesetze[argumente.gesetz] if argumente.suche is not None: return suchen(gesetze, argumente.suche, argumente.gesetz, argumente.norm) if argumente.norm is None: print(f'{argumente.gesetz} – {gesetz["langtitel"]}, Stand: {gesetz["stand"]}') print(f'\nNormen: {", ".join(gesetz["normen"])}') return 0 if argumente.gliederung: return gliederung_zeigen(gesetz, argumente.gesetz, argumente.norm) return norm_zeigen(gesetz, argumente.gesetz, argumente.norm, argumente.absatz) if __name__ == '__main__': sys.stdout.reconfigure(encoding='utf-8', errors='replace') # type: ignore[union-attr] raise SystemExit(main())