"""Baut aus den amtlichen Gesetzes-XML einen prüfbaren Fundstellenindex. Warum das sein muss ------------------- Die Erklärungstexte zu den 575 Prüfungsfragen nennen Fundstellen wie „§ 12 Abs. 1 Nr. 3 Buchst. b WaffG". Eine erfundene oder verschobene Fundstelle ist in diesem Fach kein Schönheitsfehler: Wer sich darauf verlässt, lernt geltendes Recht falsch. Verlassen wird sich deshalb nicht auf Sorgfalt, sondern auf eine maschinelle Prüfung – dieser Index ist ihre Grundlage. Quelle ------ Ausschließlich das amtliche XML von gesetze-im-internet.de, herausgegeben vom Bundesministerium der Justiz gemeinsam mit der juris GmbH. Die Dateien liegen unverändert unter `content/gesetze/`; dieses Skript liest sie und schreibt `content/gesetze/index.json`. Gesetzestexte sind nach § 5 Abs. 1 UrhG gemeinfrei. Sie werden nicht mit der Anwendung ausgeliefert – der Index dient allein der Prüfung im Entwicklungsbaum. Gliederung ---------- Das Gesetzes-XML gliedert Aufzählungen als `
`-Blöcke ihren Absatznummern zu. Ein Absatz kann sich über mehrere `
` erstrecken. Ein Block ohne eigene Nummer gehört deshalb zum zuletzt begonnenen Absatz. """ absaetze: dict[str, Absatz] = {} laufend: str | None = None for text, gliederung in bloecke: treffer = ABSATZ_MUSTER.match(text) if treffer: laufend = treffer.group(1) absaetze[laufend] = Absatz( nummer=laufend, text=text[treffer.end() :], gliederung=dict(gliederung), ) elif laufend is not None: absaetze[laufend].text += '\n' + text for marke, unter in gliederung.items(): absaetze[laufend].gliederung.setdefault(marke, unter) return absaetze def norm_lesen(norm: ET.Element) -> Norm | None: md = norm.find('metadaten') if md is None: return None bezeichnung = (md.findtext('enbez') or '').strip() if not bezeichnung: return None inhalt = norm.find('textdaten/text/Content') bloecke: list[tuple[str, dict[str, list[str]]]] = [] if inhalt is not None: for block in inhalt: text = text_von(block) if text: bloecke.append((text, gliederung_von(block, bezeichnung))) ist_anlage = bezeichnung.lower().startswith('anlage') return Norm( bezeichnung=bezeichnung, titel=(md.findtext('titel') or '').strip(), text='\n'.join(t for t, _ in bloecke), absaetze={} if ist_anlage else absaetze_lesen(bloecke), ist_anlage=ist_anlage, ) def gesetz_lesen(pfad: Path) -> tuple[dict[str, str], dict[str, Norm]]: wurzel = ET.parse(pfad).getroot() kopf: dict[str, str] = {} normen: dict[str, Norm] = {} for norm_element in wurzel.findall('norm'): md = norm_element.find('metadaten') if md is None: continue # Der erste Datensatz trägt die Angaben zum Gesetz als Ganzes. if not kopf: stand = md.find('standangabe/standkommentar') kopf = { 'jurabk': (md.findtext('jurabk') or '').strip(), 'amtabk': (md.findtext('amtabk') or '').strip(), 'langtitel': (md.findtext('langue') or '').strip(), 'ausfertigung': (md.findtext('ausfertigung-datum') or '').strip(), 'stand': (stand.text or '').strip() if stand is not None else '', } norm = norm_lesen(norm_element) if norm is not None and (norm.bezeichnung.startswith('§') or norm.ist_anlage): normen[norm.bezeichnung] = norm return kopf, normen def main() -> int: gesetze: dict[str, object] = {} for kuerzel, (datei, bezeichnung, kennung) in QUELLEN.items(): pfad = GESETZE / datei if not pfad.exists(): print(f'FEHLT: {kuerzel} ({datei})') return 1 kopf, normen = gesetz_lesen(pfad) paragrafen = {b: n for b, n in normen.items() if not n.ist_anlage} anlagen = {b: n for b, n in normen.items() if n.ist_anlage} gesetze[kuerzel] = { 'bezeichnung': bezeichnung, 'langtitel': kopf.get('langtitel', ''), 'stand': kopf.get('stand', ''), 'ausfertigung': kopf.get('ausfertigung', ''), 'quelle': f'https://www.gesetze-im-internet.de/{kennung}/', 'normen': { b: { 'titel': n.titel, 'ist_anlage': n.ist_anlage, 'absaetze': { a.nummer: {'gliederung': a.gliederung, 'text': a.text} for a in n.absaetze.values() }, 'text': n.text, } for b, n in normen.items() }, } gegliedert = sum( 1 for n in paragrafen.values() for a in n.absaetze.values() if a.gliederung ) print( f'{kuerzel:10} {len(paragrafen):4} Paragrafen, {len(anlagen)} Anlagen, ' f'{gegliedert:4} gegliederte Absaetze' ) ergebnis = { 'hinweis': ( 'Maschinell erzeugt aus dem amtlichen XML von gesetze-im-internet.de ' '(Bundesministerium der Justiz / juris GmbH). Nicht von Hand aendern - ' 'neu erzeugen mit: python data-pipeline/gesetze_index.py' ), 'gesetze': gesetze, } with io.open(ZIEL, 'w', encoding='utf-8', newline='\n') as datei_aus: json.dump(ergebnis, datei_aus, ensure_ascii=False, indent=1) print(f'\ngeschrieben: {ZIEL.name} ({ZIEL.stat().st_size / 1_048_576:.1f} MB)') # Was beim Lesen nicht aufging, gehört ausgesprochen. Ein Absatz mit # unlesbarer Gliederung fällt sonst niemandem auf – und an ihm scheitern # anschliessend richtige Zitate. if MARKENBEFUNDE: print(f'\nUnsaubere Gliederungsmarken im amtlichen XML ({len(MARKENBEFUNDE)}):') for befund in MARKENBEFUNDE: print(f' {befund}') return 0 if __name__ == '__main__': raise SystemExit(main())