"""Gibt Fragen des amtlichen Katalogs lesbar aus – mit Lösung. Gedacht für die Arbeit an den Erklärungstexten: Man braucht die Frage, die markierten richtigen Antworten und – bei offenen Fragen – die Musterantwort des Bundesverwaltungsamtes an einer Stelle. Aufrufe ------- Ein Abschnitt, ausschnittweise:: python data-pipeline/fragen.py --abschnitt I.1 --von 0 --anzahl 10 Ein ganzes Kapitel ohne Abschnittsgliederung:: python data-pipeline/fragen.py --kapitel II --von 0 --anzahl 10 Einzelne Fragen:: python data-pipeline/fragen.py --id I.1-04 I.2-01 Nur die Kennungen (etwa zum Aufteilen in Arbeitspakete):: python data-pipeline/fragen.py --abschnitt I.1 --nur-ids Wortformen im Katalog zählen – für die Arbeit am Glossar:: python data-pipeline/fragen.py --wortform führt geführt Führens Zeigt zusätzlich an, ob zu einer Frage bereits eine Erklärung vorliegt. """ from __future__ import annotations import argparse import io import json import re import sys from pathlib import Path WURZEL = Path(__file__).resolve().parent.parent KATALOG = WURZEL / 'content' / 'katalog' / 'katalog.json' ERKLAERUNGEN = WURZEL / 'content' / 'erklaerungen.json' ALTTEXTE = WURZEL / 'content' / 'alttexte.json' def vorhandene_erklaerungen() -> set[str]: if not ERKLAERUNGEN.exists(): return set() daten = json.load(io.open(ERKLAERUNGEN, encoding='utf-8')) return set(daten.get('zuFrage') or {}) def alttexte() -> dict[str, str]: """Bild-ID -> Beschreibung. Die Datei bildet Bild-ID auf `{alt, beschreibung, hinweis}` ab. Fuer die Arbeit an den Erklaerungen ist alles drei nuetzlich: `alt` ist der ausgelieferte Alternativtext, `beschreibung` die erklaerende Bedeutung, die die Anwendung erst nach dem Beantworten nachreicht, `hinweis` die interne Notiz zur Herkunft der Formulierung. Bei den Fragen, in denen die Zeichen selbst die Antwortoptionen sind, verschweigt `alt` die Bedeutung bewusst - dann tragen sie die beiden anderen Felder. """ if not ALTTEXTE.exists(): return {} daten = json.load(io.open(ALTTEXTE, encoding='utf-8')) ergebnis: dict[str, str] = {} for schluessel, wert in daten.items(): if schluessel.startswith('_'): continue # Kopfzeilen der Datei, kein Bild if isinstance(wert, str): ergebnis[schluessel] = wert else: teile = [wert.get('alt', '')] if wert.get('beschreibung'): teile.append(f'Bedeutung: {wert["beschreibung"]}') if wert.get('hinweis'): teile.append(f'intern: {wert["hinweis"]}') ergebnis[schluessel] = ' | '.join(t for t in teile if t) return ergebnis #: Zeichen, die in einem deutschen Wort vorkommen – wie in shared/glossar.ts. #: `` taugt nicht: Es kennt nur ASCII und saehe in „Schiessstaette" an jedem #: Umlaut eine Wortgrenze. WORTZEICHEN = 'A-Za-zÄÖÜäöüßẞ0-9' def katalogkorpus(katalog: dict) -> str: """Fragen, Antwortmoeglichkeiten und Musterantworten als ein Text.""" teile: list[str] = [] for frage in katalog['fragen']: teile.append(frage['frage']['text']) muster = frage.get('musterantwort') if muster: teile.append(muster['text']) for option in frage.get('optionen') or []: teile.append(option['inhalt']['text']) return ' '.join(teile) def erklaerungskorpus() -> str: """Die Erklaerungstexte als ein Text. Abkuerzungen wie WBK, NWR oder VDMA kommen im amtlichen Katalog gar nicht vor - sehr wohl aber in den Erklaerungen, die die Anwendung anzeigt. Fuer WCAG 3.1.4 zaehlt, was der Nutzer liest, nicht nur der Fragenwortlaut. """ if not ERKLAERUNGEN.exists(): return '' daten = json.load(io.open(ERKLAERUNGEN, encoding='utf-8')) teile: list[str] = [] for eintrag in (daten.get('zuFrage') or {}).values(): teile.append(eintrag.get('kurz', '')) teile.append(eintrag.get('text', '')) teile.append(eintrag.get('merksatz', '') or '') return ' '.join(teile) def wortform_suchen(formen: list[str], katalog: dict) -> int: """Zaehlt, wie oft Wortformen als eigenes Wort vorkommen. Durchsucht werden Katalog UND Erklaerungen. Gesucht wird mit derselben Wortgrenze wie in der Anwendung. """ korpus = katalogkorpus(katalog) + ' ' + erklaerungskorpus() for form in formen: ausdruck = rf'(? None: marke = ' [Erklärung liegt vor]' if frage['id'] in schon_da else '' ort = frage['kapitel'] + (f' / {frage["abschnitt"]}' if frage.get('abschnitt') else '') print(f'### {frage["id"]} (amtliche Nr. {frage["amtliche_nummer"]}, {ort}, ' f'Typ {frage["typ"]}, Katalogseite {frage["seite"]}){marke}') print(f'FRAGE: {frage["frage"]["text"]}') for bild_id in frage.get('bilder') or []: beschreibung = bilder.get(bild_id, '(kein Alternativtext hinterlegt)') print(f' [Abbildung zur Frage – {bild_id}: {beschreibung}]') if frage['typ'] == 'freitext': muster = (frage.get('musterantwort') or {}).get('text', '') print(f'MUSTERANTWORT (amtlich): {muster}') else: for option in frage.get('optionen') or []: marke_o = 'RICHTIG' if option['korrekt'] else 'falsch ' print(f' [{marke_o}] {option["label"]}) {option["inhalt"]["text"]}') for bild_id in option.get('bilder') or []: beschreibung = bilder.get(bild_id, '(kein Alternativtext hinterlegt)') print(f' [Abbildung – {bild_id}: {beschreibung}]') print() def main() -> int: zerleger = argparse.ArgumentParser(description='Fragen des amtlichen Katalogs ausgeben.') zerleger.add_argument('--kapitel') zerleger.add_argument('--abschnitt') zerleger.add_argument('--id', nargs='+') zerleger.add_argument('--von', type=int, default=0) zerleger.add_argument('--anzahl', type=int) zerleger.add_argument('--nur-ids', action='store_true') zerleger.add_argument('--ohne-erklaerung', action='store_true', help='Nur Fragen, zu denen noch keine Erklärung vorliegt') zerleger.add_argument('--wortform', nargs='+', help='Zählt Vorkommen dieser Wortformen im Katalog (für das Glossar)') argumente = zerleger.parse_args() katalog = json.load(io.open(KATALOG, encoding='utf-8')) if argumente.wortform: return wortform_suchen(argumente.wortform, katalog) schon_da = vorhandene_erklaerungen() bilder = alttexte() fragen = katalog['fragen'] if argumente.id: gesucht = set(argumente.id) fragen = [f for f in fragen if f['id'] in gesucht] else: if argumente.kapitel: fragen = [f for f in fragen if f['kapitel'] == argumente.kapitel] if argumente.abschnitt: fragen = [f for f in fragen if f.get('abschnitt') == argumente.abschnitt] if argumente.ohne_erklaerung: fragen = [f for f in fragen if f['id'] not in schon_da] fragen = fragen[argumente.von :] if argumente.anzahl is not None: fragen = fragen[: argumente.anzahl] if argumente.nur_ids: for frage in fragen: print(frage['id']) return 0 print(f'# {len(fragen)} Fragen\n') for frage in fragen: frage_zeigen(frage, bilder, schon_da) return 0 if __name__ == '__main__': sys.stdout.reconfigure(encoding='utf-8', errors='replace') # type: ignore[union-attr] raise SystemExit(main())