"""Sjekker at hvert sitat i en kravmatrise står ordrett på siden det er oppgitt å stå på. python sjekk_sitater.py kravmatrise.csv konkurransegrunnlag.html Matrisen må ha kolonnene «nr», «krav», «side», «sitat» og «status», med semikolon mellom. Dokumentet er eksempelet på gribben.no, der hver side er en
. Celler i en tabellrad slås sammen med « | », slik kopiknappen i eksempelet gjør. Hvorfor dette skriptet finnes: én feil en språkmodell kan gjøre i en kravmatrise, er å skrive et krav som høres riktig ut, med et sitat som ikke står der, eller på feil side. Et ordrett sitat med sidetall kan sjekkes. En omskrivning kan ikke. Hvor ofte det skjer sammenlignet med at et krav blir utelatt, vet vi ikke, og utelatte krav finner ikke skriptet. Sitater under 20 tegn telles for seg. Et sitat på ett ord står nesten alltid et sted i dokumentet, og et avkuttet sitat kan skjule en betingelse. De er ikke feil, men de er ikke sjekket heller. Det skriptet ikke gjør: det sjekker ikke at kravet er forstått riktig, og det finner ikke krav matrisen mangler. Det er fortsatt din stikkprøve. """ import csv import re import sys from html.parser import HTMLParser BLOKK = {"p", "h1", "h2", "h3", "h4", "li", "div"} KORTESTE_SITAT = 20 class Sider(HTMLParser): def __init__(self): super().__init__(convert_charrefs=True) self.sider = {} # sidetall -> liste med linjer self.side = None self.dybde = 0 # hvor dypt vi er inne i gjeldende
self.linje = [] self.celler = None self.celle = [] def handle_starttag(self, tag, attrs): a = dict(attrs) if tag == "section" and "data-side" in a: self.side = a["data-side"] self.sider[self.side] = [] self.dybde = 0 if self.side is None: return if tag == "section": self.dybde += 1 if tag == "tr": self.celler = [] if tag in ("td", "th"): self.celle = [] def handle_endtag(self, tag): if self.side is None: return if tag in ("td", "th") and self.celler is not None: self.celler.append(" ".join("".join(self.celle).split())) self.celle = [] elif tag == "tr" and self.celler is not None: self.sider[self.side].append(" | ".join(self.celler)) self.celler = None elif tag in BLOKK: self.tom_linje() if tag == "section": self.dybde -= 1 if self.dybde == 0: self.tom_linje() self.side = None def handle_data(self, data): if self.side is None: return if self.celler is not None: self.celle.append(data) else: self.linje.append(data) def tom_linje(self): t = " ".join("".join(self.linje).split()) if t: self.sider[self.side].append(t) self.linje = [] def normaliser(t): # Samme mellomrom og samme anførselstegn, ellers ingenting. Ordrett betyr ordrett. t = t.replace(" ", " ").replace("«", '"').replace("»", '"') return re.sub(r"\s+", " ", t).strip() def main(): if len(sys.argv) != 3: sys.exit(__doc__) matrise, dokument = sys.argv[1], sys.argv[2] p = Sider() with open(dokument, encoding="utf-8") as f: p.feed(f.read()) tekst = {s: normaliser("\n".join(linjer)) for s, linjer in p.sider.items()} with open(matrise, encoding="utf-8-sig", newline="") as f: rader = list(csv.DictReader(f, delimiter=";")) funnet, feil, uten, korte = 0, [], [], [] for r in rader: sitat = normaliser(r["sitat"]) if not sitat: uten.append(r) continue if len(sitat) < KORTESTE_SITAT: korte.append(r) continue side = r["side"].strip() if side in tekst and sitat in tekst[side]: funnet += 1 continue andre = [s for s, t in tekst.items() if sitat in t] feil.append((r, andre)) print(f"Dokumentet har {len(tekst)} sider. Matrisen har {len(rader)} rader.") print(f"Sitat funnet ordrett på oppgitt side: {funnet}") print(f"Rader uten sitat (fant ikke): {len(uten)}") for r in uten: print(f" rad {r['nr']}: {r['krav']} ({r['status']})") print(f"Sitat for korte til å sjekke (under {KORTESTE_SITAT} tegn): {len(korte)}") for r in korte: print(f" rad {r['nr']}: {r['krav']} (sitat: «{normaliser(r['sitat'])}»)") print(f"Sitat som IKKE står på oppgitt side: {len(feil)}") for r, andre in feil: hvor = f"står på side {', '.join(andre)}" if andre else "finnes ikke i dokumentet" print(f" rad {r['nr']}: {r['krav']} (oppgitt side {r['side'] or '?'}, {hvor})") sys.exit(1 if feil or korte else 0) if __name__ == "__main__": main()