Files
2026-06-22 11:44:10 +02:00

9.6 KiB

Amateurfunk — BNetzA Question Catalog → Anki Decks

A small two-stage Python pipeline that downloads the German amateur-radio exam question catalog ("Fragenkatalog") published by the Bundesnetzagentur (BNetzA) and turns it into Anki decks.

The full source-discovery notes, JSON schema, exam-structure details, and per-stage design decisions live in DESIGN.md. This file is a short orientation for anyone (human or agent) opening the project.

What the catalog is

  • Official German amateur-radio exam questions for classes N, E, A (German license tiers).
  • Published by the Bundesnetzagentur under the DL-DE→BY-2.0 open data license (free reuse, attribution required).
  • Distributed as a single ZIP containing one JSON file with the full question tree, plus a svgs/ folder with figures referenced by individual questions.
  • Current edition at time of writing: 3. Auflage, März 2024 (issued 2024-03-20, valid from 2024-06-24, ~1750 questions).

Canonical source

  • ZIP (machine-readable): https://www.bundesnetzagentur.de/SharedDocs/Downloads/DE/Sachgebiete/Telekommunikation/Unternehmen_Institutionen/Frequenzen/Amateurfunk/Fragenkatalog/PruefungsfragenZIP.zip?__blob=publicationFile
  • Landing page (short link): https://www.bnetza.de/amateurfunk-fragenkatalog
  • PDF (human-readable, not used by this pipeline): same path with Pruefungsfragen.pdf instead of PruefungsfragenZIP.zip.

The ZIP URL is stable across editions — BNetzA replaces the file in-place. The Last-Modified HTTP header is reliable for change detection. The filename inside the ZIP (fragenkatalog3b.json) encodes the edition (3b = 3rd edition, revision b) and will change on new editions, so we discover it from the archive rather than hard-coding.

Pipeline overview

BNetzA ZIP ──[Stage 1: amateurfunk_fetch.py]──► data/<slug>/
                                                  ├── fragenkatalog*.json
                                                  ├── svgs/
                                                  ├── README.txt
                                                  └── manifest.json

data/ ──[Stage 2: amateurfunk_anki.py]──► anki/
                                            ├── amateurfunk-technische-kenntnisse-n.apkg
                                            ├── amateurfunk-technische-kenntnisse-e.apkg  (one file, 11 topic sub-decks)
                                            ├── amateurfunk-technische-kenntnisse-a.apkg  (one file, 11 topic sub-decks)
                                            ├── amateurfunk-betriebliche-kenntnisse.apkg
                                            └── amateurfunk-kenntnisse-von-vorschriften.apkg

shorthand.json ──[Stage 2b: amateurfunk_shorthand.py]──► anki/
                                            └── amateurfunk-abkuerzungen-q-gruppen.apkg

technical.json ──[Stage 2c: amateurfunk_technical.py]──► anki/
                                            └── amateurfunk-technische-abkuerzungen.apkg

Stage 1 — amateurfunk_fetch.py

  1. Download the ZIP from the canonical URL.
  2. Verify it is a valid ZIP and contains the expected JSON + SVG files.
  3. Extract to a target directory (default: ./data/<edition>/).
  4. Emit a small manifest.json next to the data: source URL, fetched-at timestamp, Last-Modified from the server, JSON edition metadata, sha256 of the ZIP.
  5. Be idempotent — re-running without an upstream change is a no-op. The skip key is the HTTP Last-Modified header recorded on the previous manifest; the ZIP is deleted by default after extraction, so the recorded sha256 is provenance, not a re-verification target. See DESIGN.md §4 for the full idempotency contract.

Stage 2 — amateurfunk_anki.py

  1. Read the latest edition from data/ (following manifest-latest.json to a per-edition directory).
  2. Split the catalog into five categories. Betriebliche and Vorschriften get one deck each (shared across every candidate). Technische is additionally fanned out per license class using a strict equality split on the question's class field: one .apkg each for N, E, and A. The E (463) and A (716) packages — the large pools — are each built as a deck tree: one sub-deck per first-level catalog topic (the 11 subsections) under an anchoring Technische Kenntnisse::E / ::A parent, so each imports as a single file but studies topic by topic. N stays a single flat deck. The set of split classes is TOPIC_SPLIT_CLASSES. The klasse-N|E|A tag is still emitted on every note for inside-Anki filtering.
  3. Render every question as an Anki note: shuffled A/B/C/D choices on the front, the displayed position of the correct answer on the back. Inline $...$ LaTeX is converted to MathJax \(...\) delimiters; the catalog's safe inline markup (<u>...</u>) is preserved. If the question's number has an entry in explanations.json (see EXPLANATIONS.md), an English explanation block is appended to the back; a "low confidence" badge shows for entries with confidence < 7.
  4. Hand-roll the v11 Anki collection (SQLite + JSON config) and package it as a .apkg ZIP with deterministic timestamps. By default each build mints a fresh shuffle seed, so answers are reshuffled every run. Pass --seed (and --epoch) for the reproducible-build contract: same catalog + same seed + same timestamp → byte-identical output across runs.

The Anki design decisions (shuffle seeding, deterministic build epoch, SVG dark-mode handling, schema choices) live in DESIGN.md §7.

Stage 2b — amateurfunk_shorthand.py

A sibling builder for a standalone reference deck of Q-groups and operating abbreviations — the ones in the exam plus the most common on-air shorthand the exam never covers (real operating knowledge, not just the test). Content lives in the hand-curated shorthand.json (editorial, tracked in git, like explanations.json); the references/Q-Codes.md reference is where the exam-present codes were catalogued.

Each code is one Anki note with two card templates — forward (code → meaning) and reverse (meaning → code) — so a single record drives both directions. A Q-group means one thing as a statement (QSO) and another as a question (QSO?), so each Q-group yields two notes; plain abbreviations yield one. All IDs/GUIDs are hashed from the displayed code form (stable re-import). The deck is catalog-independent and fully deterministic; it only consults data/ to borrow the manifest build epoch when present. Low-level apkg/SQLite machinery is imported from amateurfunk_anki.py so the two stay in lockstep. The glossary machinery shared with Stage 2c (two-template note type, two-cards-per-note writer, packager, build-epoch resolver, entry validator) also lives here.

Stage 2c — amateurfunk_technical.py

A third glossary deck, same card mechanics as Stage 2b (one note, forward+reverse templates), for the technical vocabulary rather than operating shorthand: modulation/modes (SSB, FM, CW), signal domains (NF, HF, ZF), building blocks (VFO, PLL, AGC), components, measurements (dB, SWR, PEP), propagation, digital modes, and the organisations/regulations (ITU, CEPT, EMV) — exam terms plus common HAM abbreviations beyond the exam. Content lives in the curated technical.json; each entry carries a German category (Betriebsart, Bauteil, …) shown on the card and used as a kategorie-* tag. The shared glossary machinery is imported from amateurfunk_shorthand.py; this script only adds the data shape, the deck/model names, and the tag scheme. IDs live in their own technical namespace so the two glossary decks never collide on import.

Repo conventions

  • Python 3.11+, standard library only. No third-party dependencies in either stage.
  • Single-file scripts: amateurfunk_fetch.py, amateurfunk_anki.py. No frameworks, no CLI library beyond argparse.
  • Style: section banners, commented constants, docstrings on every function, inline comments at decision points. The two scripts intentionally read the same way.
  • Outputs are build artifacts: kept under data/ and anki/, both gitignored.
  • License attribution string (required by DL-DE→BY-2.0) is preserved verbatim from the upstream README.txt whenever we redistribute the data.

Working on this repo

  • EXPLANATIONS.md — the editorial contract for agents asked to add or improve per-question explanations. The schema, the workflows ("explain everything unexplained", "improve everything below confidence 7"), the source/confidence guidance, and the MathJax typesetting rules (§4a) with a verification sweep live there. The $...$ typesetting is the most error-prone part of the file — read §4a before touching any formula, and fix every occurrence in both the explanation body and the Hilfsmittel: note.
  • Start from DESIGN.md — it has the JSON schema, the question/answer conventions (answer A is always correct upstream → consumers shuffle before display), the LaTeX-in-questions caveat, the exam-structure rationale for the Anki package layout (Betriebliche, Vorschriften, and Technische split per license class), and per-stage design notes.
  • Do not invent new download URLs; the ones in DESIGN.md were verified against the live BNetzA site.
  • When BNetzA publishes a new edition, expect a new fragenkatalog<N><rev>.json filename inside the ZIP. The fetcher must not hard-code the current name.
  • Both stages have a fixture-driven test suite. Run with python3 -m unittest test_amateurfunk_fetch test_amateurfunk_anki. Network access is only needed for the manual smoke test of Stage 1.