Hoppa yfir í efnið

Textamynstur og vandaðar vinnubætur við gagnahreinsun

Endur og eins berast okkur viðfangsefni úr flokknum Ýmislegt sem hýsa hvorki glæsilegar myndskrár né dularfull hljóðmerki. Þess í stað gætum við staðið frammi fyrir 50.000 textalínum og einni hnitmiðaðri spurningu:

Hvert skyldi það auðkenni vera sem tíðast birtist í námunda við orðið OPEN?

Vissulega væri okkur fært að stauta okkur í gegnum línurnar handvirkt. Giftudrjúgara telst þó að smíða hnitmiðað vinnsluferli:

lesa → sía → draga út → samræma → raða → telja → staðfesta

Í þessum kafla notum við grep, sort, uniq, cut, awk, jq og Python til þess að ráða fram úr háværu suði gagnanna og leiða svarið í ljós. Sérhvert sýnidæmi hvílir á tilbúnum gögnum úr gagnaglímum.

Vandaðri gagnahreinsun fylgir ábyrgð

Sérhver ákvörðun um að eyða línum, hnika til hástöfum eða vísa ógildum færslum á bug, felur í sér ákveðna túlkun rannsakandans. Okkur ber því að varðveita hina upprunalegu skrá, færa allar reglur til bókar og leggja mat á hve umfangsmikill hluti gagnanna vék úr vegi.

Hefjum leikinn á rannsóknarspurningu og sýnishorni

Leikum okkur með þá tilhugsun að skráin atburdir.txt hýsi eftirfarandi:

2026-07-10T10:00:01Z INFO user=anna action=OPEN item=blue
2026-07-10T10:00:02Z DEBUG cache=hit
2026-07-10T10:00:03Z INFO user=bjarni action=CLOSE item=red
BROKEN LINE
2026-07-10T10:00:04Z INFO user=anna action=OPEN item=green
2026-07-10T10:00:05Z INFO user=katla action=OPEN item=blue

Löngu áður en lagt er í ritun flókinna skipana, skulum við huga að eftirfarandi:

wc -l atburdir.txt
head -n 5 atburdir.txt
tail -n 5 atburdir.txt
file atburdir.txt

Við skulum leggja eftirfarandi spurningar fyrir okkur:

  • Skyldi sérhver lína hýsa eina staka færslu?
  • Er þar hauslínum eða auðum línum fyrir að fara?
  • Telst afmarkarinn (e. delimiter) ætíð vera sá sami?
  • Hvíla öll gildi innan sama gagnasniðsins?
  • Hvaða lína það er sem fer á svig við mynstrið?
  • Eru okkur allar línurnar nauðsynlegar eigi að svara spurningunni?

Rýnið í takmarkað sýnishorn, en færið sönnur á heildina

Upphafshluti textans kann að virðast harla reglulegur, enda þótt raunin sé önnur þegar neðar dregur, svo sem við tuttugustu þúsundustu línuna. Nýtið ykkur því sýnishornið við mótun aðferðarinnar, en gætið þess að beita talningu og villuprófunum á gögnin í heild sinni í kjölfarið.

Pípur sem hnitmiðuð vinnsluferli

Við höfum áður lært um pípur og staðalstrauma. Píputáknið | hlutast til um að miðla frálagi einnar skipunar áfram til þeirrar næstu:

grep 'action=OPEN' atburdir.txt | head

Viðameira vinnsluferli:

grep 'action=OPEN' atburdir.txt \
  | grep -oE 'user=[^ ]+' \
  | cut -d= -f2 \
  | sort \
  | uniq -c \
  | sort -nr

Frálag sýnidæmisins hljóðar svo:

      2 anna
      1 katla

Vinnuskrefin teljast vera þessi:

  1. að einskorða úrvinnsluna við OPEN-línur,
  2. að duga út strenginn user= og samsvarandi gildi fram að næsta millibili,
  3. að þurrka út forskeytið user=,
  4. að raða notandanöfnum í rétta röð,
  5. að telja samfylgjandi og einsleitar línur,
  6. að raða niðurstöðunum þannig að hinar algengustu beri hæst.

Okkur er fært að láta reyna á sérhvert vinnuskref út af fyrir sig. Öllu auðveldara reynist að koma auga á hnökra í sex hnitmiðuðum skrefum en innan ramma einnar viðamikillar og óljósrar skipunar.

grep: leit að línum og hnitmiðuðum textabrotum

Hversdagsleg leit:

grep 'OPEN' atburdir.txt

Án tillits til há- eða lágstafa:

grep -i 'open' atburdir.txt

Að viðbættum línunúmerum:

grep -n 'BROKEN' atburdir.txt

Andhverf leit, það er að segja þær línur sem falla síst að mynstrinu:

grep -vE '^2026-[0-9]{2}-[0-9]{2}T' atburdir.txt

Stillingin -E virkjar útvíkkaðar reglulegar segðir (e. extended regular expressions). Stillingin -o hlutast til um að prenta einvörðungu það textabrot sem féll að mynstrinu:

grep -oE 'gg\{[^}]+\}' bland.txt

Mynstrið ber eftirfarandi merkingu:

gg\{      hinn bókstaflegi texti gg{
[^}]+     einn eða fleiri stafir sem teljast síst vera }
\}        hinn bókstaflegi }

Óseðjandi mynstur (e. greedy patterns)

Mynstrið:

gg\{.*\}

kann að hrifsa til sín of viðamikinn hluta línunnar:

gg{fyrri} rusl gg{seinni}

Mynstrið .* telst vera óseðjandi og getur því spannað allt rýmið frá fyrsta slaufusviga til hins síðasta. [^}]+ stöðvar við fyrsta lokaslaufusviga og er skýrara fyrir einfalt fánasnið.

Æfing: útdráttur gildra frambjóðenda

Æfing 1

bland.txt inniheldur:

ekkert hér
svar=gg{fyrsti}
bilað gg{vantar_lok
tveir: gg{annar} og gg{thridi}
tomt=gg{}

Hvaða niðurstöðu færir eftirfarandi skipun okkur?

grep -oE 'gg\{[^}]+\}' bland.txt
Lausn við æfingu 1

Niðurstaðan hljóðar svo:

gg{fyrsti}
gg{annar}
gg{thridi}

gg{vantar_lok skartar síst lokasviga og fellur því ekki að mynstrinu. gg{} hýsir engan staf á milli sviganna, en + táknið gerir kröfu um að þar sé að minnsta kosti einn stafur fyrir að fara. Skipunin grep -o er þess umkomin að skila mörgum samsvörunum úr einni og sömu línunni.

sort og uniq: röðun gagna, hópun og talning

Aðferðin uniq hlutast einvörðungu til um að sameina þær einsleitu línur sem liggja hlið við hlið. Af þeim sökum er okkur að jafnaði nauðsynlegt að beita röðun í upphafi:

sort notendur.txt | uniq -c

Dæmigert vinnsluferli við tíðnigreiningu:

sort notendur.txt | uniq -c | sort -nr
  • uniq -c kemur talningunni fyrir í upphafi línunnar,
  • sort -n raðar tölulega,
  • sort -r snýr röðinni við með öllu.

Sért stillingunni -n sleppt, verður röðunin af textalegum toga:

10
2
9

Beiting sort -n færir okkur hins vegar tölulega röð:

2
9
10

Staðfærsla og endurtekningarferli

Varast ber að röðun gagna ræðst iðulega af tungumála- og staðfærslustillingum (e. locale). Krefjist úrlausn gagnaglímu nákvæmrar bætaröðunar, er okkur farsælast að styðjast við:

LC_ALL=C sort gildi.txt

Gætið þess að færa þessa stillingu til bókar í úrlausn ykkar. C-röðun er ekki „rétt íslensk röðun“, heldur fyrirsjáanleg röðun eftir bætagildum fyrir mörg einföld ASCII-verkefni.

Æfing: af hverju þarf sort?

Æfing 2

Skráin er:

anna
katla
anna
anna
katla

Hver skyldi munurinn vera á eftirfarandi:

uniq -c notendur.txt

og:

sort notendur.txt | uniq -c
Lausn við æfingu 2

Hin fyrri skipun hlutast til um að telja einvörðungu þær runur sem liggja hlið við hlið:

      1 anna
      1 katla
      2 anna
      1 katla

Hin síðari skipun gætir þess hins vegar að safna öllum einsleitum nöfnum saman á einn stað:

      3 anna
      2 katla

Búi hin upprunalega röð yfir tiltekinni merkingu, skulum við gæta þess að varðveita hana í frumskránni. Röðunin þjónar þá einvörðungu sem nauðsynlegt milliskref við sjálfa tíðnitalninguna.

cut og afmarkarar

Afmarkari (e. delimiter) gegnir því hlutverki að skilja að einstaka reiti innan línunnar. Þegar um ræðir hversdagslegan texta þar sem tvípunktur þjónar sem skilmerki:

anna:OPEN:blue
katla:CLOSE:red

reynist okkur unnt að velja annan reitinn:

cut -d: -f2 gogn.txt
OPEN
CLOSE

Verkfærið cut telst vera skjótvirkt og tært þegar svo ber undir að:

  • afmarkarinn skipar eitt stakt tákn,
  • sérhver reitur fylgir nákvæmlega sama og einfalda sniðinu,
  • enginn vafi leikur á því að afmarkarinn skjóti síst upp kollinum innan sjálfra gildanna.

Hafa ber í huga að það megnar engan veginn að ráða sjálfkrafa úr flóknum gæsalappareglur CSV-sniðsins.

awk: meðferð dálka og hversdagslegir útreikningar

Þegar glímt er við dálka sem aðskildir eru með millibilum:

anna OPEN 12
katla CLOSE 7
anna OPEN 5

reynist okkur unnt að leggja saman gildi þriðja dálks fyrir allar OPEN-færslur:

awk '$2 == "OPEN" { summa += $3 } END { print summa }' gogn.txt

Niðurstaðan hljóðar svo:

17

Í sinni sjálfgefnu mynd styðst awk við samfelldar runur bila og dálkmerkja sem skilmerki. Stillingin -F gerir okkur hins vegar fært að velja annan afmarkara:

awk -F: '$2 == "OPEN" { print $1 }' gogn.txt

Þegar við mætum flóknum CSV-skrám, JSON-gögnum eða hreinlega óreglulegum upplýsingum, skulum við heldur leita á náðir sniðsértækra verkfæra.

CSV: kommur kunna að leynast innan sjálfra reitanna

CSV (e. comma-separated values) er textasnið fyrir töflugögn. CSV-skrá getur litið svona út:

name,place,score
anna,"Reykjavík, Ísland",10
katla,Akureyri,12

Skipunin:

cut -d, -f2 tafla.csv

túlkar hina fyrri gagnalínu á rangan hátt sem "Reykjavík, enda brestur cut-verkfærið vitneskju um að komman innan gæsalappanna telst vera órjúfanlegur hluti af sjálfu gildinu.

Python-málið skartar hins vegar vönduðum CSV-lesara:

import csv

with open("tafla.csv", newline="", encoding="utf-8") as f:
    for lina in csv.DictReader(f):
        print(lina["name"], lina["place"], int(lina["score"]))

Niðurstaðan hljóðar svo:

anna Reykjavík, Ísland 10
katla Akureyri 12

Varað er við því að rita eigin CSV-skipti með tilstyrk split(',') aðferðarinnar

Gæta verður þess að CSV-sniðið heimilar gæsalappir, kommur og jafnvel línuskipti innan sjálfra reitanna. Styðjist því ætíð við csv-forritasafnið, nema því aðeins að lýsing verkefnisins kveði á um einfaldara og sérsniðið gagnasnið.

Æfing: hvaða tól?

Æfing 3

Leiddu í ljós hvaða verkfæri telst farsælast fyrir sérhvert þessara ílaga:

  1. anna:OPEN:blue, þar sem gengið er út frá því sem vísu að tvípunktur skjóti síst upp kollinum innan sjálfra gildanna.
  2. Hefðbundið CSV-snið þar sem gæsalappir og kommur leynast innan reita.
  3. JSON-færsla sem skipar sérhverja línu.
  4. Þrír töludálkar, aðskildir með millibilum, sem útheimta samlagningu.
Lausn við æfingu 3
  1. cut -d:, eða awk -F:, fellur prýðilega að hinu einfalda sniði.
  2. Python csv virðir allar CSV-reglur til hlítar.
  3. jq þjónar JSON Lines-sniðinu best.
  4. awk reynist vel við hversdagslega dálkaútreikninga.

Þótt mörg verkfæri megi nýta við sömu úrlausnina, skulum við ætíð velja það tól sem gætir þess að skilja sjálfa innri gerð sniðsins, fremur en að reiða okkur á eina tóma heppni.

JSON-gögn og verkfærið jq

JSON (e. JavaScript Object Notation) hlutast til um að varðveita skipulögð gögn í formi hluta, fylkja og hvers kyns frumgilda. Sniðið JSON Lines (JSONL) hýsir eina staka JSON-einingu í sérhverri línu. JSON Lines-skrá hefur eina JSON-einingu í hverri línu:

{"user":"anna","action":"OPEN","score":10}
{"user":"katla","action":"CLOSE","score":7}
{"user":"anna","action":"OPEN","score":5}

Látum okkur nú velja OPEN-færslur og prenta út notandanafnið á formi hreinrættaðs texta:

jq -r 'select(.action == "OPEN") | .user' atburdir.jsonl
anna
anna

Látum okkur nú leggja saman stigin:

jq -s 'map(select(.action == "OPEN") | .score) | add' atburdir.jsonl
15

Stillingin -s (e. slurp) hlutast til um að lesa allar línurnar yfir í eitt samfellt JSON-fylki. Slík tilhögun reynist okkur hægðarauki þegar glímt er við hófleg gögn í gagnaglímum, en getur hins vegar lagt óhóflegt álag á vinnsluminnið séu skrárnar um of viðamiklar.

Meðferð óvissra eða fjarverandi reita

jq -r 'select(.action? == "OPEN") | .user // "<vantar>"' atburdir.jsonl
  • .action? girðir fyrir villur komi í ljós að innri gerðin sé síst eins og ætlað var,
  • // hlutast til um val á varagildi hvenær sem vinstri niðurstaðan reynist vera null eða hreinlega fjarverandi.

Okkur ber þó engu að síður að halda tölu á ógildum línum. Verkfærið jq nemur staðar með villumeldingu verði það vart við gallað JSON-gögn; gæta verður þess að sú viðvörun hverfi síst sjónum okkar innan viðamikils vinnsluferlis.

Æfing: síaðu JSON

Æfing 4

Skráin dyr.jsonl inniheldur:

{"door":"A","open":true,"user":"anna"}
{"door":"B","open":false,"user":"katla"}
{"door":"A","open":true,"user":"bjarni"}
{"door":"C","open":true,"user":"anna"}

Ritið niður þá jq-skipun sem leiðir í ljós notendur er opnuðu dyr merktar A. Hvaða niðurstöðu færir þetta okkur?

Lausn við æfingu 4

Skipunin hljóðar svo:

jq -r 'select(.door == "A" and .open == true) | .user' dyr.jsonl

Niðurstaðan hljóðar svo:

anna
bjarni

Óskum við þess að hljóta einvörðungu einstök nöfn, er okkur fært að bæta við skipuninni | sort -u; hafa ber þó í huga að slíkt hróflar við röðun og þurrkar út endurtekningar. Við skulum einvörðungu grípa til þess ráðs, fari rannsóknarspurningin sérstaklega fram á slíkt.

Beiting reglulegra segða innan Python-málsins

Python-málið reynist okkur einkar giftudrjúgt þegar okkur er nauðsynlegt að:

  • skeyta saman fjölmörgum útdregnum gildum,
  • færa sönnur á heilar textalínur,
  • halda tölu á þeim frávikum sem fram koma,
  • vinna með Unicode,
  • rita vandaðar og skýrar prófanir.
import re

mynstur = re.compile(r"^user=([a-z]+) action=(OPEN|CLOSE) score=([0-9]+)$")

for nr, lina in enumerate(open("gogn.txt", encoding="utf-8"), start=1):
    lina = lina.rstrip("\n")
    samsvorun = mynstur.fullmatch(lina)

    if not samsvorun:
        print(f"Ógild lína {nr}: {lina!r}")
        continue

    notandi, adgerd, stig = samsvorun.groups()
    print(notandi, adgerd, int(stig))

Aðferðin fullmatch() gerir kröfu um að textalínan í heild sinni falli að mynstrinu. Það kemur í veg fyrir að við samþykkjum óvænt rusl fyrir eða eftir gildin.

Vandaðri mynstursmíði með nafngreindum hópum

Þegar glímt er við flóknari mynstur, er okkur fært að styðjast við (?P<user>...) og síðan samsvorun.group("user"). Skýr og skiljanleg heiti vega að jafnaði þyngra en sá hagnaður sem fæst við að spara örfáa stafi.

Talning með Counter

from collections import Counter

notendur = ["anna", "katla", "anna", "bjarni", "anna"]
talning = Counter(notendur)

for nafn, fjoldi in talning.most_common():
    print(fjoldi, nafn)
3 anna
1 katla
1 bjarni

Beri svo undir að tvö gildi skarti nákvæmlega sömu tíðni, ríður á að búa yfir vitneskju um hvernig leysa skuli úr slíku jafntefli. Hafa ber í huga að most_common() aðferðin í nútímalegum útgáfum Python-málsins varðveitir þá röð sem táknin birtust í fyrst, þegar talning reynist jöfn. Verkefnið gæti hins vegar gert kröfu um stafrófsröðun. Látum okkur því skilgreina regluna á eigin spýtur:

for nafn, fjoldi in sorted(talning.items(), key=lambda x: (-x[1], x[0])):
    print(fjoldi, nafn)

Gagnahreinsun með mælingum

Gagnahreinsun (e. data cleaning) felst í því að hafa uppi á og sýsla við gögn sem teljast ófullkomin, ósamræmd eða á annan hátt óæskileg, í samræmi við fastmótaðar reglur.

Vandað forrit í gagnaglímu heldur ávallt tölu á eftirfarandi:

heild = 0
gildar = 0
ogildar = 0

with open("gogn.txt", encoding="utf-8") as f:
    for lina in f:
        heild += 1
        lina = lina.strip()

        if not lina or lina.startswith("#"):
            continue

        if "=" not in lina:
            ogildar += 1
            continue

        gildar += 1

print({"heild": heild, "gildar": gildar, "ogildar": ogildar})

Við skulum sömuleiðis gera greinarmun á milli:

  • auðra lína sem heimilt er að láta sem vind um eyru þjóta,
  • athugasemda sem rúmast innan gagnasniðsins,
  • hreinna og klárra ógildra færslna,
  • gildra færslna sem uppfylla síst af öllu tiltekin síuskilyrði.

Að öðrum kosti gætu ummæli á borð við „100 færslur fundust“ í raun hylja þá staðreynd að þar var um að ræða 100 af 150 mögulegum, þar sem 50 færslur glötuðust sökum villna.

Varað er við skeljarvillum sem hrófla við niðurstöðum

Gætið þess að rita mynstur innan gæsalappa

grep -E 'gg\{[^}]+\}' skra.txt

Séu gæsalappirnar fjarverandi, blasir við sú hætta að skelin taki sig til og túlki tákn á borð við *, ?, hornklofa og önnur merki, löngu áður en grep-verkfærið fær þau til úrvinnslu.

Meðferð skráarheita sem hefjast á bandstriki

grep 'mynstur' -- -undarlegt-nafn.txt

-- gefur til kynna að upptalningu valkosta (e. options) sé lokið.

Hnökrar innan vinnsluferlisins

Innan Bash-umhverfisins getur svo farið að lokaskipunin teljast hafa tekist, þrátt fyrir að hnökrar hafi verið á fyrri vinnuskrefum. Þegar kemur að ritun lausnarskýrslna, er giftudrjúgt að hefja leikinn á:

set -euo pipefail

pipefail hlutast til um að vinnsluferlið skili villumeldingu undireins og hnökrar koma í ljós innan þess. Við skulum þó gæta þess að beita þessu af tilskildum vitsmunum: grep skilar til dæmis stöðunni 1 hvenær sem engin samsvörun finnst, sem í sjálfu sér gæti talist fyllilega eðlilegt í tilteknum skriftum.

Lokaverkefni: hávaðasama sendingin

Æfing 5

sending.txt inniheldur:

# synthetic CTF data
2026-07-10 user=anna part=2 value=gna
rusl
2026-07-10 user=katla part=1 value=ekki
2026-07-10 user=anna part=1 value=gg{ga
2026-07-10 user=anna part=4 value=xti}
2026-07-10 user=anna part=3 value=te
2026-07-10 user=anna part=x value=VILLA
2026-07-10 user=katla part=2 value=svar

Verkefnið segir:

Veldu notandann sem á fjóra tölusetta hluta, raðaðu eftir hlutanúmeri og límdu value saman.

Ritið niður lausn sem færir sönnur á gagnasniðið og skilar okkur hinni endanlegu niðurstöðu.

Möguleg lausn við æfingu 5

Python-málið reynist hér einkar vel fallið til verksins, enda ríður þar á að færa sönnur á gögnin, hópa þau, raða og loks skeyta þeim saman:

import re
from collections import defaultdict

mynstur = re.compile(
    r"^2026-07-10 user=(?P<user>[a-z]+) "
    r"part=(?P<part>[0-9]+) value=(?P<value>\S+)$"
)

hlutar = defaultdict(dict)
ogildar = []

with open("sending.txt", encoding="utf-8") as f:
    for nr, lina in enumerate(f, start=1):
        lina = lina.rstrip("\n")

        if not lina or lina.startswith("#"):
            continue

        m = mynstur.fullmatch(lina)
        if not m:
            ogildar.append((nr, lina))
            continue

        user = m.group("user")
        part = int(m.group("part"))
        value = m.group("value")

        if part in hlutar[user]:
            raise ValueError(f"Tvítekið part={part} hjá {user}")

        hlutar[user][part] = value

for user, gildi in hlutar.items():
    if sorted(gildi) == [1, 2, 3, 4]:
        svar = "".join(gildi[nr] for nr in range(1, 5))
        print(user, svar)

print("Línur sem dæmdar voru ógildar:", ogildar)

Niðurstaðan er:

anna gg{gagnatexti}

Ógildu línurnar eru rusl og línan með part=x. Katla hefur aðeins tvo hluta og uppfyllir því ekki skilyrðið.

Okkur auðnaðist að færa sönnur á að Anna byggi yfir nákvæmlega hlutunum frá þeim fyrsta til hins fjórða, og við gættum þess jafnframt að vísa öllum tvíteknum hlutanúmerum á bug. Hversdagsleg beiting sort-skipunarinnar, án þess að hugað sé að þessum athugunum, gæti hæglega hulið sjónum okkar gallaðar upplýsingar.

Samantekt

Í þessum kafla höfum við kynnst því hvernig á að:

  • hefja leikinn á skýrri rannsóknarspurningu og rýna í sýnishorn af ílagsgögnum,
  • tvinna saman hnitmiðuð verkfæri með tilstyrk pípa,
  • beita grep og reglulegum segðum við síun og útdrátt gagna,
  • beita röðun löngu áður en uniq -c skipunin leggur mat á tíðni,
  • styðjast við tölulega eða á annan hátt fyrirsjáanlega röðun þegar þess er krafist,
  • velja á milli cut og awk þegar glímt er við hversdagsleg og afmörkuð snið,
  • nýta Python-forritasafnið csv eigi að virða allar reglur CSV-sniðsins til hlítar,
  • beita síun og umbreytingum á JSON-gögn með tilstyrk jq-verkfærisins,
  • færa sönnur á heilar textalínur með re.fullmatch() aðferðinni,
  • halda tölu á niðurstöðum og leysa úr jafntefli í samræmi við fastmótaðar reglur,
  • mæla hve margar línur voru gildar, hunsaðar eða ógildar,
  • gæta þess að varðveita ætíð frumgögnin og færa sérhverja reglu við gagnahreinsun rækilega til bókar.

Í eftirfarandi kafla munum við glíma við það verkefni að varpa verkefnalýsingum yfir á reiknirit, herma eftir hnitmiðuðum ferlum og leysa úr margvíslegum forritunar- og rökþrautum.