waffensachkunde

Waffensachkunde – Lernsoftware für die Sachkundeprüfung nach § 7 WaffG. Barrierefrei, offline, EUPL-1.2.

/ data-pipeline README.md

17,1 KB Rohdatei

Datenpipeline: BVA-Fragenkatalog → JSON

Wandelt den amtlichen „Fragenkatalog für die Sachkundeprüfung (gemäß § 7 WaffG)" des Bundesverwaltungsamtes (Stand 16.12.2024) in eine strukturierte, barrierefrei nutzbare Datenbasis um. Der Fragenwortlaut bleibt dabei unverändert.

Ausführen

python data-pipeline/parse_catalog.py
python data-pipeline/validate_catalog.py
python data-pipeline/diagnose_kreuze.py

Der Parser liest Fragenkatalog_sachkunde_mitAntworten.pdf aus dem Projektwurzelverzeichnis und schreibt nach content/katalog/:

DateiInhalt
katalog.jsonAlle 575 Fragen mit Optionen, Lösungen, Musterantworten
assets/*.png17 Prüf- und Zulassungszeichen
trennstriche.txtZeilenend-Trennungen, bei denen ein Bindestrich erhalten blieb (QS-Liste)

validate_catalog.py prüft das Ergebnis gegen die am Original ermittelten Sollwerte und bricht bei Abweichungen mit Exit-Code 1 ab. Er ist damit als CI-Schritt geeignet.

diagnose_kreuze.py prüft die Kreuzerkennung am Bild statt am Vektorpfad und ist damit die einzige Stelle, die sie unabhängig von der Logik des Parsers belegt (siehe unten, Punkt 2). Auch er antwortet mit Exit-Code 0 oder 1; ein voller Lauf über alle 124 Seiten dauert rund zweieinhalb Sekunden.

python data-pipeline/diagnose_kreuze.py --seiten 6-20   # Ausschnitt
python data-pipeline/diagnose_kreuze.py --selbsttest    # die Probe prüfen

Voraussetzung: pip install pymupdf

Sollwerte (alle am Original verifiziert)

KennzahlWert
Fragen gesamt575
davon Multiple Choice471
davon offene Fragen104 (103 Freitext + 1 Lückentext)
Antwortoptionen1430
angekreuzte Optionen629
Fragen mit mehreren richtigen Antworten114
Fragen mit allen Optionen richtig28
Fragen ohne jede richtige Option0
Fragen mit Abbildung23
Bild-Assets17

Verteilung je Abschnitt: I.1 = 90, I.2 = 154, I.3 = 35, I.4 = 23, I.5 = 43, II = 92, III = 49, IV = 89.

Die letzten beiden Zeilen sind die Randfälle der Kreuzerkennung und zugleich die beiden Fehlerbilder eines Einlesefehlers: alles angekreuzt (Layout nicht erkannt) und nichts angekreuzt (Kreuze verloren). validate_catalog.py hält deshalb nicht nur die Anzahl 28 fest, sondern die Menge der 28 Fragen samt amtlicher Nummer und PDF-Seite — eine Zählung bliebe grün, wenn eine Frage die Kreuze verlöre und eine andere welche bekäme. Dass die 28 echt sind, ist am Original belegt (siehe unten, Punkt 2).

Wie die Vorlage gelesen wird

Das PDF ist born-digital mit fehlerfreiem Textlayer – kein OCR nötig. Die Fragen stehen in einer unsichtbaren vierspaltigen Tabelle:

x  71–103   amtliche Fragennummer      (Anker für den Fragenbeginn)
x 104–292   Fragetext
x 292–540   Antwortoptionen bzw. Musterantwort
x 503–529   Ankreuzkästchen

Beim Parsen sind fünf Eigenheiten der Vorlage zu beachten; jede war Ursache eines konkreten Fehlers und ist im Code kommentiert:

  1. Fragetext und erste Antwortoption stehen oft in derselben Textzeile. Getrennt wird deshalb nach x-Koordinate, nicht nach Zeilen.
  2. Lösungen stehen nicht im Text, sondern als Vektorgrafik: ein Kästchen ist angekreuzt, wenn zwei Diagonalsegmente vollständig darin liegen.

Diese Erkennung steht vor einer Lücke, nicht vor einem Schwellwert: Nachgemessen über alle 1430 Kästchen liegen in genau 629 jeweils zwei Diagonalsegmente, in den übrigen 801 keines — kein einziges hat eines, drei oder vier. Unabhängig gegengeprüft durch Rendern derselben Kästchen bei 8-facher Vergrößerung: Tintendichte im Inneren 0,0000 bei allen 801, 0,1653 bis 0,1978 bei allen 629, null Widersprüche.

Diese Gegenprobe ist als diagnose_kreuze.py wiederholbar und gehört deshalb nicht zu den Erkundungsskripten unten. Sie ist die einzige Prüfung des Projekts, die nicht wieder über Vektorpfade urteilt: Ein Denkfehler in der Geometrie stünde sonst in Umsetzung und Prüfung gleichermaßen. Beide Verfahren teilen genau eine Annahme — wo ein Kästchen liegt — und keine darüber, was ein Kreuz ist. --selbsttest erzeugt jedes der vier Fehlerbilder absichtlich (Vektor sieht ein Kreuz zu viel, eines zu wenig; Tinte im leeren Kästchen; angekreuztes Kästchen fast leer) und prüft, dass die Probe anschlägt.

Anlass der Nachmessung waren die 28 Fragen, bei denen alle Optionen angekreuzt sind — der Verdacht, ein Layout sei nicht erkannt und vorsichtshalber alles genommen worden. Alle 28 Tabellenzeilen wurden zusätzlich als Bild betrachtet: Die Kreuze stehen so in der Vorlage. Es sind durchweg Aufzählungsfragen, bei denen jede Nennung richtig ist („Welche Munitionsarten sind vom Waffengesetz erfasst?" — Patronenmunition, hülsenlose Munition, pyrotechnische Munition).

  1. Kästchen sitzen vertikal mittig zur Option, bei mehrzeiligen Optionen also unterhalb ihrer ersten Zeile. Die Optionsgrenzen werden daher aus den Labels („a)", „b)", …) am linken Spaltenrand abgeleitet; Kästchen werden anschließend der Reihenfolge nach zugeordnet.
  2. Kernelemente der Musterantworten sind unterstrichen – Füllrechtecke der Höhe ≈ 0,84 pt, unterscheidbar von Tabellenlinien (≈ 0,48 pt). Sie werden als hervorgehoben-Segmente übernommen und tragen später das Freitext-Training.
  3. Ein Prüfzeichen beginnt oberhalb seines eigenen Labels. Besteht eine Antwortmöglichkeit nur aus einem Zeichen und trägt keinen Text, setzt der Satz das Zeichen senkrecht mittig in seine Tabellenzeile – das Label steht dann innerhalb des Bildrechtecks. Liegt genau ein Label im Bild, gilt es; sonst entscheidet die Bildmitte. Nachgemessen: 43 Bilder stehen in Antwortspalten, bei 38 liegt kein Label im Bild.

Bis Fassung 0.19.2 stand hier ein Zerschneider, der ein Bild an der leersten Bildzeile in zwei Hälften teilte, sobald eine Option leer blieb. Er sprang im ganzen Katalog genau einmal an – bei Frage 3.05 – und lag dort falsch: Er trennte das Doppelzeichen BKA-Raute über PTB-Trapez, das in Frage 2.70 ungeteilt eine einzige Antwortmöglichkeit bildet („Reizstoff-Sprühdosen mit dem Zeichen"), und hängte die obere Hälfte an die falsche Antwort. Bleibt heute eine Antwortmöglichkeit ohne Text und ohne Bild, rät nichts mehr – der Katalog vermerkt es als Warnung.

Zeilenend-Trennungen werden zusammengeführt: vor Bindewörtern bleibt der Ergänzungsstrich („Kinder- und Jugendarbeit"), vor Großschreibung der echte Bindestrich („Physikalisch-Technische"), sonst wird das Wort geschlossen („Signalge-" + „bung" → „Signalgebung").

Datenformat

{
  "meta":   { "titel", "herausgeber", "stand", "quellenangabe",
              "quelldatei_sha256", "fragen_gesamt" },
  "kapitel": [ { "id": "I", "titel", "abschnitte": [ { "id": "I.1", "titel" } ] } ],
  "bilder":  [ { "id", "datei", "breite", "hoehe", "alt": null,
                 "beschreibung": null } ],
  "fragen":  [ {
    "id": "I.1-01",            // stabile ID: Kapitelpräfix + laufende Nummer
    "amtliche_nummer": "1.01", // Nummer wie im Katalog, bleibt sichtbar
    "kapitel": "I", "abschnitt": "I.1",
    "typ": "mc" | "freitext" | "lueckentext",
    "seite": 6,
    "frage":   { "text": "…", "segmente": [ { "t": "…", "h": true } ] },
    "bilder":  [ "zeichen-…" ],
    // bei typ == "mc":
    "optionen": [ { "label": "a", "inhalt": { "text", "segmente" },
                    "korrekt": true, "bilder": [] } ],
    // sonst:
    "musterantwort": { "text": "…", "segmente": [ … ] }
  } ]
}

segmente trägt die Auszeichnung mit: {"t": "…"} ist normaler Text, {"t": "…", "h": true} ein hervorgehobenes Kernelement. text ist dieselbe Zeichenfolge ohne Auszeichnung und dient Suche und Vorlesefunktion.

Offene Punkte

  • Bildtexte: liegen für alle 17 Bilder in content/alttexte.json und werden beim Erzeugen in katalog.json eingesetzt. Sie sind eigener, nicht-amtlicher Inhalt. Je Zeichen zwei Stufen (Prüfplan, Szenario S4): alt ist der Alternativtext des Bildes, beschreibung die erklärende Bedeutung, die die Anwendung erst nach dem Beantworten nachreicht und im Prüfungslauf gar nicht zeigt. Achtung bei den Fragen 3.05, 3.24 und IV-39: Dort sind die Zeichen selbst die Antwortoptionen – der Alt-Text beschreibt deshalb rein die Form („Bundesadler über dem Buchstaben N") und verrät weder Bedeutung noch Lösung; genau dafür gibt es die zweite Stufe. Die Formulierungen der beschreibung stammen aus den Erklärungstexten in content/erklaerungen.json, nicht aus einer eigenen Recherche.
  • Lückentext 5.01: Die Lücken sind im Fragetext derzeit nur Leerstellen. Für ein echtes Lückentext-Training müssen sie noch markiert werden (Einzelfall).
  • Manuelle Sichtprüfung aller 575 Fragen gegen das PDF steht aus; die automatischen Prüfungen decken Struktur und Vollständigkeit ab, nicht jeden Buchstaben.

Erklärungstexte und Rechtsfundstellen

Der amtliche Katalog nennt die richtige Antwort, aber nie den Grund. Die Erklärungen in content/erklaerungen.json liefern ihn nach – eigener redaktioneller Inhalt, streng getrennt vom amtlichen Werk.

Bei Waffenrecht ist eine erfundene Fundstelle kein Schönheitsfehler: Wer sich darauf verlässt, lernt geltendes Recht falsch. Deshalb steht vor dem Inhalt die Prüfgrundlage.

Ablauf

# 1. Amtliche Gesetzestexte holen (einmalig bzw. bei Gesetzesänderung)
cd content/gesetze
for g in waffg_2002 awaffv beschg beschussv stgb sprengg_1976 sprengv_1; do
  curl -sSO "https://www.gesetze-im-internet.de/$g/xml.zip" && unzip -oq xml.zip && rm xml.zip
done
cd ../..

# 2. Prüfbaren Index bauen
python data-pipeline/gesetze_index.py

# 3. Beim Schreiben nachschlagen
python data-pipeline/gesetz.py WaffG "§ 12" --gliederung
python data-pipeline/gesetz.py --suche "Anscheinswaffe"
python data-pipeline/fragen.py --abschnitt I.1 --von 0 --anzahl 10

# 4. Teilstücke zusammenführen
python data-pipeline/erklaerungen_zusammenfuehren.py <verzeichnis>

# 5. Prüfen – ohne bestandene Prüfung wird nicht ausgeliefert
python data-pipeline/pruefe_erklaerungen.py
python data-pipeline/pruefe_erklaerungen.py --selbsttest

# 6. Die zitierten Normtexte für die Auslieferung ausschneiden
python data-pipeline/normtexte_bauen.py
python data-pipeline/normtexte_bauen.py --selbsttest

Warum normtexte.json neben dem Index steht

Der Index enthält die sieben Gesetze vollständig – 914 Normen, darunter katalogfremde wie § 173 StGB. Ausgeliefert wird nur, was in Erklärungen oder Glossar wirklich zitiert wird: 142 Paragrafen und 6 Anlagen, rund 500 KiB. Ohne diese Datei führte „Im Gesetz nachlesen“ ins Leere – eine ausdrücklich vollständig offline arbeitende Software forderte zu etwas auf, das offline gerade nicht ging.

Anlagen kennen keine Absatzgliederung und sind seitenlang. Sie werden in ihre eigenen Gliederungsblöcke zerlegt, und zwar verlustfrei: Die Blöcke ergeben aneinandergehängt wieder Zeichen für Zeichen den Ausgangstext. Das Skript rechnet es für jede Anlage nach und bricht ab, statt etwas zu schreiben. Ein Block ist ein Sprungziel, kein Ausschnitt – ein falsch gesetzter Schnitt wäre ein verfälschtes Gesetzeszitat in einer Lernsoftware für eine Rechtsprüfung.

Die Themengliederung

python data-pipeline/themen_bauen.py
python data-pipeline/themen_bauen.py --selbsttest

Der amtliche Katalog gliedert nur Kapitel I in Abschnitte. Für die 230 Fragen der Kapitel II bis IV schreibt themen_bauen.py eine redaktionelle Feingliederung nach content/themen.json – 29 Gruppen, von Hand geschrieben.

Das Skript schreibt nur, wenn jede Frage dieser Kapitel in genau einer Gruppe liegt. Eine lückenhafte Gliederung wäre schlimmer als keine: „Thema üben“ überginge dann Fragen, ohne es zu sagen. Nach jeder Katalogänderung deshalb erneut laufen lassen – neue Fragen fallen sonst durch das Raster, und zwar unbemerkt.

Nach jeder Gesetzesänderung: erst Schritt 2, dann Schritt 5, dann Schritt 6. Wer Schritt 6 vergisst, liefert die Normtexte des alten Standes aus, während die Fundstellen schon gegen den neuen geprüft wurden.

Was die Prüfung leistet

pruefe_erklaerungen.py schlägt jede Fundstelle im Index nach: Gesetz, Paragraf, Absatz, Nummer, Buchstabe. Was dort nicht steht, existiert nicht. Bei Normen ohne Absatzgliederung (etwa § 37a WaffG) wird gegen die Aufzählungsmarken im Normtext geprüft, Satzangaben gegen die mögliche Satzanzahl, Anlagen-Stellen per Textsuche. Die Ausgabe unterscheidet „vollständig geprüft“ (jede Angabe nachgewiesen) von „nur Existenz geprüft“. Der Selbsttest erzeugt jede der 22 Fehlerarten absichtlich – eine Prüfung, die nichts beanstandet, ist von einer funktionierenden nicht zu unterscheiden.

Was sie nicht leisten kann

Ob die zitierte Norm die Aussage inhaltlich trägt. Eine Erklärung kann auf einen existierenden Paragrafen verweisen und trotzdem falsch sein – genau das ist beim Erproben passiert: Eine Erklärung zum Begriff „Umgang“ zitierte § 2 Abs. 2 WaffG, der die Erlaubnispflicht regelt und nicht den Begriff. Richtig ist § 1 Abs. 3 WaffG. Die Fundstelle kam durch die Prüfung.

Dagegen hilft nur, dass jede Erklärung von einer zweiten, unabhängigen Stelle gegen den Normtext gelesen wird. Fehlt eine gesetzliche Grundlage – bei Sicherheitsregeln und Waffentechnik der Normalfall –, verlangt das Feld ohneFundstelleGrund, das auszusprechen, statt eine Fundstelle zu erfinden.

Gesetzesstand

content/erklaerungen.json vermerkt im Kopf den Stand jedes zitierten Gesetzes, wörtlich aus dem amtlichen XML. Die Prüfung vergleicht ihn mit dem Index und schlägt an, wenn beides auseinanderläuft. Ändert sich ein Gesetz, ist damit erkennbar, worauf sich die Erklärungen bezogen haben.

Die XML-Rohdateien sind nicht versioniert (siehe .gitignore); versioniert ist nur der daraus erzeugte Index.

Glossar der Fachbegriffe

Das Waffenrecht benutzt Alltagswörter mit eigener Bedeutung. „Führen" heißt nicht lenken, „Erwerben" hat mit Kaufen nichts zu tun. Wer das nicht weiß, versteht die Frage nicht – unabhängig davon, wie gut er den Stoff kann. Genau darauf zielen WCAG 3.1.3 (Ungewöhnliche Wörter) und 3.1.4 (Abkürzungen); beide stehen im Prüfplan dieses Projekts als Zusage.

Ablauf

# Wortformen gegen Katalog und Erklärungen prüfen
python data-pipeline/fragen.py --wortform führt geführt Führens

# Teilstücke zusammenführen
python data-pipeline/glossar_zusammenfuehren.py <verzeichnis>

# Prüfen – ohne bestandene Prüfung wird nicht ausgeliefert
python data-pipeline/pruefe_glossar.py
python data-pipeline/pruefe_glossar.py --selbsttest

Wortformen

Deutsch flektiert. Ohne die Wortformen bliebe ein Begriff in genau den Fragen unerkannt, in denen er gebraucht wird. Gesucht wird mit derselben Wortgrenze wie in der Anwendung –  taugt dafür nicht, weil es nur ASCII kennt und in „Schießstätte" an jedem ß eine Grenze sähe.

Ein Eintrag darf ohne Wortform bleiben. Er erscheint dann nur in der Glossaransicht und an keiner Frage. Das ist bei Abkürzungen normal, die in den Erklärungen vorkommen, aber nicht im amtlichen Fragenwortlaut.

Woher die Bestimmungen stammen

Wo das Gesetz einen Begriff selbst bestimmt, gibt der Eintrag diese Bestimmung wieder – nicht eine eigene Umschreibung. Die waffenrechtlichen Begriffe stehen in Anlage 1 Abschnitt 2 WaffG, die technischen in Abschnitt 1. Jede Fundstelle wird mit derselben Prüfung wie bei den Erklärungen nachgewiesen; pruefe_glossar.py erbt sie wörtlich, damit für beide derselbe Maßstab gilt.

Selbsttest

--selbsttest verletzt jede Regel einmal absichtlich – Eindeutigkeit, Wortformen, Querverweise, Form, redliche Lücken, Gesetzesstand – und besteht nur, wenn jede Verletzung auch beanstandet wird. Dazu drei Gegenproben: Ein gültiger Eintrag darf weder Befund noch Hinweis auslösen, die Wortsuche muss eine echte Form im Katalog wirklich finden (sonst wäre „ohne Vorkommen" die Standardantwort und keiner der Wortform-Fälle sagte etwas aus), und der ausgelieferte Bestand muss unbeanstandet durchlaufen. Zusammen 34 Fälle.

Bis Fassung 0.21.0 war die Fahne wirkungslos: Sie stand im Modulkopf, aber main() las sys.argv nicht. Der Aufruf mit --selbsttest fuhr stumm dieselbe Bestandsprüfung und meldete Exit-Code 0 – in einer Prüfkette lief damit dieselbe Prüfung zweimal, während die glossareigenen Regeln von keinem Selbsttest gedeckt waren. Ein vertippter Schalter fällt heute mit Exit-Code 2 auf, statt ersatzweise die Bestandsprüfung zu fahren.

Rechtlicher Hinweis

Der Katalog ist ein amtliches Werk; sein Wortlaut wird unverändert übernommen und mit Quellenangabe geführt (meta.quellenangabe). Eigene didaktische Inhalte (Erklärungen, Eselsbrücken) sind davon getrennt zu halten und als nicht-amtlich zu kennzeichnen. Details siehe PLAN.md, Abschnitt 2.5.

Erkundungsskripte

explore_layout.py, explore_details.py und diagnose_columns.py haben die oben genannten Layout-Konstanten am Original vermessen. Sie gehören nicht zum Produktivlauf, dokumentieren aber, woher die Zahlen stammen, und sind bei einer neuen Katalogfassung der erste Anlaufpunkt.