Siivoamista: sed

Tähän asti olemme oppineet, että suomenkielisen Wikipedian yleisin sana näyttäisi olevan "ref" ja kärjessä on myös sen tyylisiä sanoja kuin "http", "www" ja "fi". Syy selviää äkkiä, kun hakee tiedostosta sanan "ref" esiintymiä, esimerkiksi tyyliin grep --color '\<ref\>'. Wikipedian lähdekoodi on täynnä tämän kaltaisia lähdeviitteitä:

<ref name="kuinkajohdetaan">{{Verkkoviite | Osoite = https://dev.hel.fi/paatokset/media/att/1c/1c3497c4d905d089f74c946d08268dd468c63f20.pdf| Nimeke = Kuinka eurooppalaisia kaupunkeja johdetaan?| Tekijä = Backman, Katri| Tiedostomuoto = pdf| Julkaisupaikka = Helsinki| Julkaisija = Helsingin kaupunki, Tietokeskus| Viitattu = 17.5.2018}}</ref>

<ref>[http://www.mersenne.org/ http://www.mersenne.org]</ref>

<ref name=mj2>Majaranta, Leo, s. 12–13</ref>

<ref name="tulokset" />

Saisimmeko siivottua nämä kaikki pois sotkemasta? Tämä on taas hiukan huonosti määritelty tehtävä, kun Wikipedia on lopulta ihmisten käsin kirjoittamaa koodia, <ref>-komentoja on käytetty aika sekalaisesti ja koodissa on kirjoitusvirheitäkin. Mutta yritetään siivoilla näistä silti suurin osa pois. Karkeasti ottaen haluaisimme siis poistaa tämän kaltaisia rimpsuja:

<ref>...</ref>
<ref name=...>...</ref>
<ref name=... />

Mutta ollaan huolellisia: jos tekstissä lukee vaikkapa

a <ref>b</ref> c <ref>d</ref> e

emme halua siivota pois koko pätkää <ref>b</ref> c <ref>d</ref> vaikka se onkin periaatteessa muotoa <ref>...</ref>.

Etsi-korvaa

Haluaisimme siis tehdä jonkinlaista automaattista etsi-korvaamista tekstitiedostolle. Tähän hyvä työkalu on sed. Tämä on awk:n tapaan hyvinkin monipuolinen väline, mutta käytännössä useimmat käyttävät vain yhtä sed:n toimintoa:

sed 's#hahmo#korvaus#'

Tämä lukee syötettä, etsii jokaiselta hahmon ensimmäisen esiintymän, ja korvaa sen halutulla merkkijonolla. Alussa oleva komento s kertoo, että olemme tekemässä juuri tätä etsi-korvaa-toimintoa (monia muitakin komentoja olisi). Erotinmerkin # paikalla voi käyttää melkein mitä tahansa välimerkkiä, esimerkiksi tämä toimii myös (kunhan hahmossa ja korvauksessa ei esiinny pilkkuja):

sed 's,hahmo,korvaus,'

Tässä "hahmo" on säännöllinen lauseke samaan tapaan kuin grep-komennossakin. Kokeillaan! Esimerkiksi

echo lapioijat | sed 's#i#XXX#'

tulostaa "lapXXXoijat": ensimmäinen hahmon "i" osuma siis korvattiin merkkijonolla "XXX". Jos haluat korvata kaikki osumat, lisää loppuun valitsin "g" (global):

sed 's#hahmo#korvaus#g'

Esimerkiksi tämä siis tuottaa tuloksen "lapXXXoXXXjat"; jokainen "i":n esiintymä on nyt korvattu (kokeile!):

echo lapioijat | sed 's#i#XXX#g'

Normaalisti merkkikoolla on väliä, mutta valitsin "I" (ignore case) muuttaa tämän:

sed 's#hahmo#korvaus#g'

Esimerkiksi tämä tulostaa "LAPIOIJAT":

echo LAPIOIJAT | sed 's#i#XXX#g'

Kun taas tämä tulostaakin "LAPXXXOXXXJAT":

echo LAPIOIJAT | sed 's#i#XXX#gI'

Turhien tägien siivoilua

Lähdetään nyt miettimään, miten nuo turhat <ref>-tägit saisi siivottua pois. Luo ensin tiedosto ref.txt, jossa on muutama perusesimerkki:

hyvä <ref>huono</ref> hyvä <ref>huono</ref> hyvä

Yksinkertaisimmillaan voisimme ensin poistaa merkkijonot <ref> ja </ref>, vaikkapa näin:

sed 's#<ref>##gI' ref.txt | sed 's#</ref>##gI'

Tässä siis ensin etsitään hahmoa <ref> (kaikki osumat, merkkikoosta välittämättä) ja korvataan ne tyhjällä, ja lopputuloksessa tehdään sama hahmolle </ref>. Lopputulos on tällainen:

hyvä huono hyvä huono hyvä

Haluaisimme kuitenkin poistaa myös kaiken roskan, mitä on tagien <ref> ja </ref> välissä. Ensimmäinen ajatus olisi varmaankin luoda hahmo <ref>.*</ref>:

sed 's#<ref>.*</ref>##gI' ref.txt

Tässä ollaan kuitenkin liiankin ahneita ja jäljelle jää vain:

hyvä  hyvä

Hahmo osui koko pätkään <ref>huono</ref> hyvä <ref>huono</ref>. Emme siis oikeasti halua etsiä <ref> + ihan mitä tahansa + </ref>, vaan hiukan rajallisemmin. Suhteellisen hyvä kompromissi voisi olla tällainen:

sed 's#<ref>[^<>]*</ref>##gI' ref.txt

Säännöllisissä lausekkeissa [^abc] tarkoittaa "mikä tahansa merkki paitsi a, b ja c" ja tässä siis [^<>] tarkoittaa "mikä tahansa merkki paitsi < ja >". Tuttuun tapaan * tarkoittaa "toista kuinka monta kertaa tahansa". Eli etsitään siis <ref> + mikä tahansa tekstinpätkä, joka ei sisällä merkkejä < ja > + </ref>. Ollaan jo lähellä oikeaa, nyt tulostuu sitä mitä pitääkin:

hyvä  hyvä  hyvä

Tämä ei kuitenkaan vielä sellaisiin tapauksiin kuin <ref name=...>...</ref> tai <ref name=... />. Näihinkin voidaan kehittää samantyylisiä sääntöjä pienellä miettimisellä. Nämä näyttäisivät purevan aika hyvin useimpiin tilanteisiin:

  1. <ref name *= *"[^"]*" *>[^<>]*</ref>

  • Tässä siis haetaan ensin <ref name, sen jälkeen mahdollisesti välilyöntejä mielivaltainen määrä (*), tämän jälkeen =, taas välilyöntejä mielivaltainen määrä, sitten lainausmerkki ", sitten mitä tahansa muuta kuin lainausmekkejä ([^"]*), toinen lainausmerkki ", taas mahdollisia välilyöntejä, sitten mitä tahansa muuta kuin merkkejä < ja > ([^<>]*) ja lopuksi </ref>.

  • Tämä siis osuu sellaisiin tekstinpätkiin kuin <ref name="foo">bar</ref> tai <ref name = "foo" >bar</ref>.

  1. <ref name *= *"[^"]*" */>

  • Vastaavaan tapaan tämä osuu sellaisiin tekstinpätkiin kuin <ref name="foo"/> tai <ref name = "foo" />.

  1. <ref name *= *[^"<>]* *>[^<>]*</ref>

  • Tämä taas osuu esimerkiksi sellaiseen tekstinpätkään kuin <ref name=foo>bar</ref>, siis ilman lainausmerkkejä.

  1. <ref name *= *[^"<>]* */>

  • Vastaavaan tapaan tämä osuu sellaisiin tekstinpätkiin kuin <ref name=foo/>.

Siivousskripti

Kootaan nämä kaikki yhdeksi skriptiksi, niin on helpompi kokeilla! Luo tiedosto siivoa.sh, ja kirjoittele siihen nämä kaikki säännöt yhdeksi putkeksi:

#!/bin/bash

sed 's#<ref>[^<>]*</ref>##gI' |
    sed 's#<ref name *= *"[^"]*" *>[^<>]*</ref>##gI' |
    sed 's#<ref name *= *"[^"]*" */>##gI' |
    sed 's#<ref name *= *[^"<>]* *>[^<>]*</ref>##gI' |
    sed 's#<ref name *= *[^"<>]* */>##gI'

Anna taas tiedostolle ajo-oikeudet: chmod +x siivoa.sh. Kokeillaan ensin perustapausta, että tämä edelleen varmasti toimii:

./siivoa.sh < ref.txt

Tulosteena pitäisi olla tuttuun tapaan "hyvä hyvä hyvä". Yritetään nyt suodattaa näytepala tämän skriptin avulla:

./siivoa.sh < osa.txt > osa-siivottu.txt

Tutkitaan, auttoiko!

Laske kuinka monta kertaa merkkijono <ref (isoilla tai pienillä kirjaimilla kirjoitettuna) esiintyy alkuperäisessä tiedostossa osa.txt ja kuinka monta kertaa se esiintyy tiedostossa osa-siivottu.txt. Muista, että grep -c laskee rivien määrää eikä kaikkien esiintymien määrää, joten voit joutua käyttämään tässä esimerkiksi komentoa grep -o hakemaan kaikki esiintymät ja putkittamaan tuloksen komennolle wc -l niiden laskemiseksi.

Näyttäisi siis, että mennään hyvään suuntaan! Tutkitaan, miten tämä korjaus vaikuttaa sanamäärätilastoihin. Lasketaan ensin näytepalasta sanamäärät:

./sanat.sh < osa-siivottu.txt > osa-siivottu-sanat.txt

Katso tiedostoa osa-siivottu-sanat.txt. Nyt ainakin kaksi yleisintä sanaa näyttäisi olevan järkeviä suomen sanoja: "ja" ja "on". Jäljellä olisi vielä paljon siivottavaa, esimerkiksi wikitekstissä esiintyvät tägit <br> (rivinvaihto) ja &nbsp; (katkeamaton välilyönti) sotkevat tilastoja, mutta jätetään näiden siivoilu myöhemmäksi oman harrastuneisuuden varaan.

Koko aineiston siivoaminen

Kokeillaan nyt tätä koko aineistoon! Jos levytilasta ei ole pulaa, voit edetä kahdessa vaiheessa:

./siivoa.sh < fiwiki.txt > fiwiki-siivottu.txt
./sanat.sh < fiwiki-siivottu.txt > fiwiki-sanat-siivottu.txt

Tai vaihtoehtoisesti voit koota kaiken yhteen putkeen, jolloin vältetään muutaman gigatavun kokoisen tilapäistiedoston luominen:

./siivoa.sh < fiwiki.txt | ./sanat.sh > fiwiki-sanat-siivottu.txt

Taas voi olla hyvä hetki käydä kahvilla tai tehdä vaikka lumityöt…

Tutki näin syntynyttä tiedostoa fiwiki-sanat-siivottu.txt.

Etsi tiedostosta sana "ja"; kuinka monta kertaa tämä esiintyi ja millä sijalla se oli?

Entä sana "suomalainen"?

Entä sana "lapio"?

Mikä näistä sanoista oli yleisin?

Palautusta lähetetään...