waffensachkunde
Waffensachkunde – Lernsoftware für die Sachkundeprüfung nach § 7 WaffG. Barrierefrei, offline, EUPL-1.2.
/ data-pipeline explore_details.py
| 1 | # -*- coding: utf-8 -*- |
| 2 | """Klärt die verbleibenden Layout-Fragen vor dem Bau des Parsers. |
| 3 | |
| 4 | Prüft: exakte Spaltengrenzen, Kreuz-in-Checkbox-Erkennung, Unterstreichungen |
| 5 | (Kernelemente der Musterantworten), Kapitelkopfzeilen und seitenübergreifende |
| 6 | Fragen. |
| 7 | """ |
| 8 | import io |
| 9 | import re |
| 10 | import sys |
| 11 | from collections import Counter, defaultdict |
| 12 | |
| 13 | import fitz |
| 14 | |
| 15 | sys.stdout = io.TextIOWrapper(sys.stdout.buffer, encoding="utf-8") |
| 16 | PDF = r"C:\Antigravity\Waffensachkundeprüfung\Fragenkatalog_sachkunde_mitAntworten.pdf" |
| 17 | FIRST_PAGE = 5 # 0-basiert: PDF-Seite 6 |
| 18 | |
| 19 | doc = fitz.open(PDF) |
| 20 | |
| 21 | # ---------------------------------------------------------------- Spalten |
| 22 | vlines = Counter() |
| 23 | hlines_full = Counter() |
| 24 | box_x = Counter() |
| 25 | for pno in range(FIRST_PAGE, doc.page_count): |
| 26 | for d in doc[pno].get_drawings(): |
| 27 | for item in d["items"]: |
| 28 | if item[0] == "l": |
| 29 | p1, p2 = item[1], item[2] |
| 30 | if abs(p1.x - p2.x) < 0.7 and abs(p1.y - p2.y) > 3: |
| 31 | vlines[round(p1.x)] += 1 |
| 32 | elif item[0] == "re": |
| 33 | r = item[1] |
| 34 | if r.width < 1.6 and r.height > 3: |
| 35 | vlines[round(r.x0)] += 1 |
| 36 | if 6 < r.width < 20 and abs(r.width - r.height) < 4: |
| 37 | box_x[round(r.x0)] += 1 |
| 38 | |
| 39 | print("Vertikale Tabellenlinien (x -> Häufigkeit), Top 12:") |
| 40 | for x, n in vlines.most_common(12): |
| 41 | print(f" x={x:4d} {n}") |
| 42 | print(f"\nCheckbox-x-Positionen: {box_x.most_common(6)}") |
| 43 | |
| 44 | # ------------------------------------------------- Kreuz-in-Checkbox-Test |
| 45 | page = doc[FIRST_PAGE] |
| 46 | boxes, diagonals = [], [] |
| 47 | for d in page.get_drawings(): |
| 48 | for item in d["items"]: |
| 49 | if item[0] == "re": |
| 50 | r = item[1] |
| 51 | if 6 < r.width < 20 and abs(r.width - r.height) < 4 and r.x0 > 480: |
| 52 | boxes.append(r) |
| 53 | elif item[0] == "l": |
| 54 | p1, p2 = item[1], item[2] |
| 55 | if abs(p1.x - p2.x) > 2 and abs(p1.y - p2.y) > 2 and p1.x > 480: |
| 56 | diagonals.append((p1, p2)) |
| 57 | |
| 58 | print(f"\nSeite 6: {len(boxes)} Checkboxen, {len(diagonals)} Diagonalsegmente") |
| 59 | for b in sorted(boxes, key=lambda r: r.y0): |
| 60 | inside = sum(1 for p1, p2 in diagonals |
| 61 | if b.x0 - 2 <= p1.x <= b.x1 + 2 and b.y0 - 2 <= p1.y <= b.y1 + 2) |
| 62 | words = page.get_text("words", clip=fitz.Rect(290, b.y0 - 4, 500, b.y0 + 14)) |
| 63 | label = " ".join(w[4] for w in words)[:46] |
| 64 | print(f" y={b.y0:6.1f} Diagonalen={inside} -> {'ANGEKREUZT' if inside >= 2 else 'leer '} {label!r}") |
| 65 | |
| 66 | # --------------------------------------------------------- Unterstreichung |
| 67 | print("\n--- Unterstreichungs-Kandidaten (dünne Füllrechtecke) ---") |
| 68 | thin = Counter() |
| 69 | for pno in range(FIRST_PAGE, doc.page_count): |
| 70 | for d in doc[pno].get_drawings(): |
| 71 | if d.get("fill") is None: |
| 72 | continue |
| 73 | for item in d["items"]: |
| 74 | if item[0] == "re": |
| 75 | r = item[1] |
| 76 | if 0.3 < r.height < 1.6 and r.width > 4: |
| 77 | thin[round(r.height, 2)] += 1 |
| 78 | print(f"Höhenverteilung dünner Rechtecke: {sorted(thin.items())}") |
| 79 | |
| 80 | # Beispielseite mit Musterantwort-Hervorhebung (Frage 1.01 auf Seite 6) |
| 81 | p6 = doc[FIRST_PAGE] |
| 82 | for d in p6.get_drawings(): |
| 83 | for item in d["items"]: |
| 84 | if item[0] == "re": |
| 85 | r = item[1] |
| 86 | if 0.6 < r.height < 1.6 and r.width > 4 and r.x0 > 280: |
| 87 | above = p6.get_text("words", clip=fitz.Rect(r.x0 - 2, r.y0 - 12, r.x1 + 2, r.y0)) |
| 88 | print(f" Unterstrichen y={r.y0:.1f} x={r.x0:.0f}-{r.x1:.0f}: {' '.join(w[4] for w in above)!r}") |
| 89 | |
| 90 | # ------------------------------------------------------------ Kapitelköpfe |
| 91 | print("\n--- Kapitelkopfzeilen (aus dem Seitenkopf) ---") |
| 92 | heads = defaultdict(list) |
| 93 | for pno in range(FIRST_PAGE, doc.page_count): |
| 94 | head = doc[pno].get_text("text", clip=fitz.Rect(60, 30, 540, 95)).strip() |
| 95 | lines = [l.strip() for l in head.splitlines() if l.strip()] |
| 96 | heads[tuple(lines[:3])].append(pno + 1) |
| 97 | for key, pages in heads.items(): |
| 98 | print(f" Seiten {pages[0]}-{pages[-1]} ({len(pages)}): {key}") |
| 99 | |
| 100 | # ------------------------------------------- Fragennummern und Seitenlage |
| 101 | print("\n--- Fragennummern je Seite (Spalte 1) ---") |
| 102 | num_re = re.compile(r"^\d{1,3}(?:\.\d{2,3})?$") |
| 103 | total = 0 |
| 104 | per_page_first = {} |
| 105 | for pno in range(FIRST_PAGE, doc.page_count): |
| 106 | words = doc[pno].get_text("words", clip=fitz.Rect(60, 92, 103, 800)) |
| 107 | nums = [w[4] for w in sorted(words, key=lambda w: w[1]) if num_re.match(w[4])] |
| 108 | total += len(nums) |
| 109 | if nums: |
| 110 | per_page_first[pno + 1] = (nums[0], nums[-1], len(nums)) |
| 111 | print(f"Gefundene Fragennummern gesamt: {total}") |
| 112 | sample = list(per_page_first.items()) |
| 113 | for pg, v in sample[:5] + sample[-5:]: |
| 114 | print(f" Seite {pg}: {v[0]} … {v[1]} ({v[2]} Fragen)") |