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:
- að einskorða úrvinnsluna við
OPEN-línur, - að duga út strenginn
user=og samsvarandi gildi fram að næsta millibili, - að þurrka út forskeytið
user=, - að raða notandanöfnum í rétta röð,
- að telja samfylgjandi og einsleitar línur,
- 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 -ckemur talningunni fyrir í upphafi línunnar,sort -nraðar tölulega,sort -rsný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:
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.- Hefðbundið CSV-snið þar sem gæsalappir og kommur leynast innan reita.
- JSON-færsla sem skipar sérhverja línu.
- Þrír töludálkar, aðskildir með millibilum, sem útheimta samlagningu.
Lausn við æfingu 3
cut -d:, eðaawk -F:, fellur prýðilega að hinu einfalda sniði.- Python
csvvirðir allar CSV-reglur til hlítar. jqþjónar JSON Lines-sniðinu best.awkreynist 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 veranulleð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
valuesaman.
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
grepog reglulegum segðum við síun og útdrátt gagna, - beita röðun löngu áður en
uniq -cskipunin leggur mat á tíðni, - styðjast við tölulega eða á annan hátt fyrirsjáanlega röðun þegar þess er krafist,
- velja á milli
cutogawkþegar glímt er við hversdagsleg og afmörkuð snið, - nýta Python-forritasafnið
csveigi 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.