Hoppa yfir í efnið

Textamynstur og gagnahreinsun

Sum Misc-verkefni afhenda okkur ekki fallega mynd eða dularfullt hljóð. Þau gefa okkur 50.000 línur af texta og eina spurningu:

Hvaða auðkenni birtist oftast rétt áður en orðið OPEN kom fyrir?

Við gætum lesið línurnar handvirkt. Betra er að smíða lítið færiband:

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 að breyta hávaða í svar. Öll dæmi nota tilbúin CTF-gögn.

Hreinsun er túlkun

Þegar við fjarlægjum línur, breytum hástöfum eða köstum ógildum færslum erum við að taka ákvörðun. Við varðveitum upprunalega skrá, skráum reglurnar og teljum hversu mörg gögn hurfu.

Byrjum á spurningu og sýnishorni

Segjum að atburdir.txt innihaldi:

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

Áður en við skrifum flókna skipun skoðum við:

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

Við spyrjum:

  • Er ein færsla í hverri línu?
  • Eru hauslínur eða auðar línur?
  • Er afmarkari alltaf sá sami?
  • Eru öll gildi á sama sniði?
  • Hvaða lína brýtur mynstrið?
  • Þurfum við allar línur til að svara spurningunni?

Skoðaðu lítið sýni, en staðfestu á öllu safninu

Fyrstu tíu línurnar geta litið reglulega út þótt lína 20.000 sé öðruvísi. Notaðu sýnið til að smíða aðferðina og keyrðu síðan talningar og villupróf á heildinni.

Pípur sem lítil færibönd

Við höfum áður lært um pípur og staðalstrauma. Píputáknið | sendir úttak einnar skipunar í næstu:

grep 'action=OPEN' atburdir.txt | head

Lengra færiband:

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

Úttak fyrir sýnidæmið:

      2 anna
      1 katla

Skrefin eru:

  1. halda aðeins OPEN-línum,
  2. draga út user= og gildið fram að næsta bili,
  3. fjarlægja user=,
  4. raða notandanöfnum,
  5. telja samliggjandi eins línur,
  6. raða talningunni stærstu fyrst.

Við getum prófað hvert skref eitt og sér. Það er auðveldara að finna villu í sex litlum skrefum en einni óútskýrðri galdraskipun.

grep: finnum línur og textabrot

Einföld leit:

grep 'OPEN' atburdir.txt

Óháð há-/lágstöfum:

grep -i 'open' atburdir.txt

Með línunúmerum:

grep -n 'BROKEN' atburdir.txt

Andhverf leit, það er línur sem passa ekki:

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

-E virkjar útvíkkaðar reglulegar segðir (e. extended regular expressions). -o prentar aðeins textabrotið sem passaði:

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

Mynstrið merkir:

gg\{      bókstaflegi textinn gg{
[^}]+     einn eða fleiri stafir sem eru ekki }
\}        bókstaflegur }

Gráðug mynstur

Mynstrið:

gg\{.*\}

getur tekið of mikið úr línunni:

gg{fyrri} rusl gg{seinni}

.* er gráðugt og getur passað frá fyrsta { til síðasta }. [^}]+ stöðvar við fyrsta lokaslaufusviga og er skýrara fyrir einfalt fánasnið.

Æfing: dragðu út gilda frambjóðendur

Æfing 1

bland.txt inniheldur:

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

Hvað skilar eftirfarandi skipun?

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

Úttakið er:

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

gg{vantar_lok hefur engan } og passar ekki. gg{} hefur engan staf milli sviganna, en + krefst eins eða fleiri. grep -o getur skilað fleiri en einu samsvörunarbrotum úr sömu línu.

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

uniq sameinar aðeins samliggjandi eins línur. Því röðum við venjulega fyrst:

sort notendur.txt | uniq -c

Algengt tíðnifæriband:

sort notendur.txt | uniq -c | sort -nr
  • uniq -c setur talningu fremst,
  • sort -n raðar tölulega,
  • sort -r snýr röðinni við.

Án -n er textaröðun:

10
2
9

Með sort -n fæst töluleg röð:

2
9
10

Staðfærsla og endurtekning

Röðun getur ráðist af tungumála- og staðfærslustillingum. Þegar CTF-lausn þarf nákvæma bætaröð getum við notað:

LC_ALL=C sort gildi.txt

Skráðu stillinguna í lausninni. 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 er munurinn á:

uniq -c notendur.txt

og:

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

Fyrri skipunin telur aðeins samliggjandi runur:

      1 anna
      1 katla
      2 anna
      1 katla

Seinni skipunin setur öll eins nöfn saman:

      3 anna
      2 katla

Ef upprunaleg röð hefur merkingu varðveitum við hana í frumskránni. Röðunin er aðeins afleitt skref fyrir tíðnitalningu.

cut og afmarkarar

Afmarkari (e. delimiter) skilur reiti að. Fyrir einfaldan tvípunktaskiptan texta:

anna:OPEN:blue
katla:CLOSE:red

getum við valið annan reit:

cut -d: -f2 gogn.txt
OPEN
CLOSE

cut er fljótt og skýrt þegar:

  • afmarkarinn er eitt tákn,
  • allir reitir fylgja sama einfalda sniði,
  • afmarkarinn getur ekki komið fyrir inni í gildi.

Það skilur ekki sjálfkrafa gæsalappareglur CSV.

awk: dálkar og einfaldir útreikningar

Fyrir bilaskipta dálka:

anna OPEN 12
katla CLOSE 7
anna OPEN 5

getum við lagt saman þriðja dálk fyrir OPEN:

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

Úttak:

17

awk notar sjálfgefið runur bila og dálkmerkja sem skil. Með -F veljum við annan afmarkara:

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

Fyrir flókið CSV, JSON eða óregluleg gögn veljum við hins vegar sniðsértækt tól.

CSV: kommur geta verið inni í reit

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

les fyrri gagnalínuna rangt sem "Reykjavík, því cut skilur ekki að komman inni í gæsalöppum tilheyri reitnum.

Python hefur 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"]))

Úttak:

anna Reykjavík, Ísland 10
katla Akureyri 12

Ekki skrifa þinn eigin CSV-skipti með split(',')

CSV getur haft gæsalappir, kommur og línuskipti inni í reitum. Notaðu csv-safnið nema verkefnið skilgreini einfaldara sérsnið.

Æfing: hvaða tól?

Æfing 3

Veldu hentugt tól fyrir hvert inntak:

  1. anna:OPEN:blue, þar sem tvípunktur má aldrei vera inni í gildi.
  2. Staðlað CSV með gæsalöppum og kommum inni í reitum.
  3. JSON-færsla í hverri línu.
  4. Þrír bilaskiptir töludálkar sem á að leggja saman.
Lausn við æfingu 3
  1. cut -d:, eða awk -F:, hentar einfalda sniðinu.
  2. Python csv hentar CSV-reglunum.
  3. jq hentar JSON Lines.
  4. awk hentar einföldum dálkaútreikningi.

Fleiri en eitt tól getur leyst sum atriði, en við veljum það sem skilur uppbyggingu sniðsins í stað þess að reiða sig á heppni.

JSON og jq

JSON (e. JavaScript Object Notation) geymir skipulögð gögn sem hluti, fylki og frumgildi. JSON Lines (JSONL) geymir eina JSON-einingu í hverri 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}

Veljum OPEN og prentum notandann sem hráan texta:

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

Leggjum saman stig:

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

-s (e. slurp) les allar línurnar í eitt JSON-fylki. Það er þægilegt fyrir lítil CTF-gögn en getur notað mikið minni á mjög stórri skrá.

Óvissir eða vantaðir reitir

jq -r 'select(.action? == "OPEN") | .user // "<vantar>"' atburdir.jsonl
  • .action? forðast villu ef uppbygging er óvænt,
  • // velur varagildi þegar vinstri niðurstaðan er null eða vantar.

Við teljum samt ógildar línur. jq hættir með villu á brotnu JSON og sú villa má ekki hverfa óséð í löngu færibandi.

Æ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"}

Skrifaðu jq-skipun sem skilar notendum sem opnuðu dyr A. Hvert er úttakið?

Lausn við æfingu 4

Skipunin er:

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

Úttak:

anna
bjarni

Ef við viljum einstök nöfn getum við bætt við | sort -u, en þá breytum við röð og fjarlægjum endurtekningar. Það gerum við aðeins ef spurningin biður um það.

Reglulegar segðir í Python

Python hentar þegar við þurfum að:

  • tengja mörg útdregin gildi,
  • staðfesta heila línu,
  • telja frávik,
  • vinna með Unicode,
  • skrifa 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))

fullmatch() krefst þess að öll línan passi. Það kemur í veg fyrir að við samþykkjum óvænt rusl fyrir eða eftir gildin.

Nafngreindir hópar skýra mynstrið

Fyrir flóknara mynstur getum við notað (?P<user>...) og síðan samsvorun.group("user"). Skýr heiti eru oft verðmætari en örfáir sparaðir stafir.

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

Ef tvö gildi hafa sömu tíðni þurfum við að vita hvernig jafntefli á að leysa. most_common() varðveitir röð fyrstu birtingar fyrir jafna talningu í nútíma Python, en verkefnið gæti krafist stafrófsröðunar. Skilgreinum regluna sjálf:

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) er að finna og meðhöndla ófullkomin, ósamræmd eða óæskileg gögn samkvæmt skýrum reglum.

Gott CTF-forrit telur:

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ð greinum líka á milli:

  • auðrar línu sem má hunsa,
  • athugasemdar sem sniðið leyfir,
  • ógildrar færslu,
  • gildrar færslu sem uppfyllir ekki síuskilyrði.

Annars getur „100 færslur fundust“ í raun þýtt „100 af 150; 50 hurfu vegna villu“.

Skeljarvillur sem breyta niðurstöðu

Setjum mynstur í gæsalappir

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

Án gæsalappa getur skelin túlkað *, ?, hornklofa og önnur tákn áður en grep sér þau.

Skráarheiti sem byrjar á bandstriki

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

-- segir að valkostum sé lokið.

Villur í pípu

Í Bash getur síðasta skipunin tekist þótt fyrri skipun mistakist. Fyrir lausnarskrift er gagnlegt að byrja á:

set -euo pipefail

pipefail lætur pípuna skila villu ef skref inni í henni mistekst. Við notum þetta með skilningi: grep skilar stöðunni 1 þegar engin samsvörun finnst, sem gæti verið eðlileg niðurstaða í sumum 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.

Skrifaðu lausn sem staðfestir sniðið og skilar niðurstöðu.

Möguleg lausn við æfingu 5

Python hentar vel því við þurfum að staðfesta, hópa, raða og líma:

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("Ógildar línur:", 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ð.

Við staðfestum að Anna hafi nákvæmlega hlutana 1–4 og höfnum tvíteknum hlutanúmerum. Einföld sort-skipun án þessara athugana gæti falið gölluð gögn.

Samantekt

Í þessum kafla lærðum við að:

  • byrja á skýrri spurningu og skoða sýni af inntakinu,
  • tengja lítil tól með pípum,
  • nota grep og reglulegar segðir til að sía og draga út,
  • raða áður en uniq -c telur tíðni,
  • nota tölulega eða fyrirsjáanlega röðun þegar þess þarf,
  • velja cut og awk fyrir einföld afmörkuð snið,
  • nota Python csv fyrir raunverulegar CSV-reglur,
  • sía og umbreyta JSON með jq,
  • staðfesta heilar línur með re.fullmatch(),
  • telja og leysa jafntefli með skýrum reglum,
  • mæla hve margar línur voru gildar, hunsaðar eða ógildar,
  • varðveita frumgögn og skrá allar hreinsunarreglur.

Í næsta kafla breytum við verkefnalýsingum í reiknirit, hermum eftir litlum ferlum og leysum afmarkaðar forritunar- og rökþrautir.