"""Teller kategorier i en merket fritekstfil, finner mulige personopplysninger og trekker en stikkprøve. python tell.py (bruker kommentarer.csv) python tell.py minfil.csv Fila må ha kolonnene «id», «kommentar» og «kategori», med semikolon mellom. Lagrer du den fra norsk Excel, er «CSV UTF-8» tryggest. Vanlig «CSV (semikolondelt)» fra norsk Excel lagres i Windows-1252, og den leser skriptet også, men det sier fra når det skjer. Har den også «fasit», som den konstruerte fila har, sammenligner skriptet stikkprøven med fasiten. Det kan du aldri gjøre med ekte data, men det viser hva en stikkprøve faktisk fanger. Hvorfor telle her og ikke i chatten: en språkmodell lager tekst, den teller ikke. Den kan gi et tall som ser presist ut uten å ha gått gjennom hver rad. Et skript eller et regneark går gjennom alle, hver gang, og gir samme svar neste gang. Kjører chatverktøyet kode på fila og viser deg koden, er det koden som teller, og da gjelder det samme som her: les den. Mønstrene for personopplysninger finner telefonnummer skrevet 000 00 000 eller 00000000, e-post og ordrenummer. Andre skrivemåter slipper gjennom. Navn er vanskeligere. Skriptet ser etter ord med stor forbokstav midt i en setning, og det bommer på navn som står først. Derfor er det en hjelp før du limer inn, ikke en garanti. """ import csv import random import re import sys from collections import Counter FRO = 2026 STIKKPROVE = 30 MONSTRE = { "telefon": re.compile(r"\b\d{3} ?\d{2} ?\d{3}\b"), "e-post": re.compile(r"[\w.+-]+@[\w-]+\.[\w.]+"), "ordrenummer": re.compile(r"\bordre\w*\.? *\d{5,}", re.IGNORECASE), } # Stor forbokstav etter en liten bokstav eller et komma, altså ikke først i # en setning. Grovt med vilje: det skal heller vise for mange enn for få. MULIG_NAVN = re.compile(r"(?<=[a-zæøå,] )[A-ZÆØÅ][a-zæøå]+") def prosent(n, av): return f"{100 * n / av:.1f}".replace(".", ",") + " %" def les(fil): # Norsk Excel lagrer «CSV (semikolondelt)» i Windows-1252, ikke UTF-8. # Da krasjer en ren UTF-8-lesing på første æ, ø eller å. Vi prøver UTF-8 # først, fordi feil tegnkoding ellers kan gi stille rusk i stedet for en feil. for koding in ("utf-8-sig", "cp1252"): try: with open(fil, encoding=koding, newline="") as f: return list(csv.DictReader(f, delimiter=";")), koding except UnicodeDecodeError: continue sys.exit(f"Klarte ikke å lese {fil} som UTF-8 eller Windows-1252.") def main(): fil = sys.argv[1] if len(sys.argv) > 1 else "kommentarer.csv" rader, koding = les(fil) mangler = {"id", "kommentar", "kategori"} - set(rader[0] if rader else ()) if mangler: sys.exit(f"Fant ikke kolonnene {', '.join(sorted(mangler))} i {fil}. " "Skilletegnet må være semikolon, og overskriftene må stå på første linje.") n = len(rader) uten = [r["id"] for r in rader if not r["kategori"].strip()] ider = [r["id"] for r in rader] print(f"Leste {n} rader fra {fil}") if koding == "cp1252": print(" Fila var lagret i Windows-1252, slik norsk Excel gjør. Sjekk at æ, ø og å ser riktige ut.") print(f"Rader uten kategori: {len(uten)}") print(f"Doble id-er: {n - len(set(ider))}") # Telling telling = Counter(r["kategori"] for r in rader) print() print(f"{'Kategori':<12}{'Antall':>8}{'Andel':>10}") for kat, antall in telling.most_common(): print(f"{kat:<12}{antall:>8}{prosent(antall, n):>10}") print(f"{'Sum':<12}{n:>8}{prosent(n, n):>10}") # Personopplysninger treff = [] for r in rader: typer = [navn for navn, m in MONSTRE.items() if m.search(r["kommentar"])] if typer: treff.append((r["id"], ", ".join(typer))) print() print(f"Mulige personopplysninger (telefon, e-post, ordrenummer): {len(treff)} rader") for id_, typer in treff: print(f" id {id_}: {typer}") navn = [(r["id"], m) for r in rader for m in MULIG_NAVN.findall(r["kommentar"])] print() print(f"Mulige navn (stor forbokstav midt i setningen): {len(navn)} treff") for id_, ord_ in navn: print(f" id {id_}: {ord_}") print(" Et navn først i en setning blir ikke funnet. Les selv før du limer inn.") # Stikkprøve storrelse = min(STIKKPROVE, n) utvalg = random.Random(FRO).sample(rader, storrelse) print() print(f"Stikkprøve på {storrelse} rader (frø {FRO}), les disse selv:") print(" id " + ", ".join(sorted((r["id"] for r in utvalg), key=int))) if "fasit" in rader[0]: feil_utvalg = sorted((r for r in utvalg if r["kategori"] != r["fasit"]), key=lambda r: int(r["id"])) feil_alle = sum(r["kategori"] != r["fasit"] for r in rader) print() print("Bare fordi dataene er konstruerte, sammenlignet med fasit:") print(f" Stikkprøven: {len(feil_utvalg)} av {storrelse} merket feil") for r in feil_utvalg: print(f" id {r['id']}: merket {r['kategori']}, fasit {r['fasit']}") print(f" Hele fila: {feil_alle} av {n} merket feil ({prosent(feil_alle, n)})") fasit = Counter(r["fasit"] for r in rader) print() print(f"{'Kategori':<12}{'Merket':>8}{'Fasit':>8}") for kat, antall in telling.most_common(): print(f"{kat:<12}{antall:>8}{fasit[kat]:>8}") if __name__ == "__main__": main()