Kirjoittaja: fergusq
Kirjoitettu: 12.06.2013 – 21.08.2013
Tagit: teksti, koodi näytille, vinkki
Tekstiä voidaan jäsennellä monella eri tavalla. Yksi suosituimmista on tekstin jakaminen lekserillä sanoihin ja merkkeihin (tokeneihin), ja näiden analysointi. On kuitenkin myös muita tapoja.
Lekseri on ohjelma, joka lukee merkkejä ja palauttaa listan avainsanoista, nimistä, merkkijonoista, operaattoreista, yms. Teksti if a != b then print "a ei ole b"
lekseröidään luultavasti Avainsana(if), Nimi(a), Operaattori(!=), Nimi(b), Avainsana(then), Avainsana(print), Merkkijono("a ei ole b")
. Eri ohjelmointikielet käyttävät yleensä hieman toisistaan poikkeavia leksereitä riippuen syntaksista. Anna-Liisa
voi jossain kielessä takoittaa nimeä Anna-Liisa, ja jossain toisessa kielessä vähennyslaskua Anna miinus Liisa.
Ilman lekseriä parsiminen on yleensä hankalaa ja kömpelöä. On kuitenkin joitain tilanteita, jolloin lekserin käyttäminen olisi turhaa ja liian monimutkaista. Esimerkiksi pienissä komentosarjakielissä yksinkertaiset merkkijono-operaatiot riittävät vallan hyvin parsimiseen.
Oletetaan, että komentosarjakieli koostuu riveistä. Jokaisella rivillä on komento, ja mahdollinen lista putkitetuista komennoista. Tämän tapainen ohjelmointikieli on helppo toteuttaa ilman lekseriä käyttäen merkkijono-operaatiota split
, joka jakaa tekstin tietyn erotinmerkin avulla ja palauttaa arrayn. Toteutus voisi näyttää tältä:
// Jaa koodi riveihin var rivit = koodi.split("\n"); for (var i = 0; i < rivit.length; i++) { // Jaa lista putkituksista komentoihin var komennot = rivit[i].split("|"); var putki = prompt("Ensimmäisen ohjelman sisääntulo."); for (var j = 0; j < komennot.length; j++) { // Komennon nimi ja argumentit on eroteltu välilyönneillä var komento = komennot[j].split(" "); // Siivoa tyhjät kohdat arraysta for (var k = 0; k < komento.length; k++) { // Poista tyhjät kohdat if (komento[k] == "") { komento.splice(k, 1); k--; } } // Suorita komento putki = suoritaKomento(komento, putki); } alert(putki); }
Funktio suoritaKomento
ottaa argumentteina komennon ja sisääntulon, ja palauttaa ulostulon.
Tämän koodin tapa jäsennellä tekstiä on vastakkaisten sulkujen etsiminen. Koodi etsii tekstistä viimeisen (
-sulun, ja sen edestä seuraavan )
-sulun. Tämän jälkeen sulkujen sisältä luetaan välilyönnillä erotellut sanat ja laitetaan tauluun. Sitten sulut sisältöineen poistetaan alkuperäisestä tekstistä ja korvataan viittauksella taulussa olevaan arvoon. Prosessi aloitetaan alusta.
Ohjelma ei ymmärrä merkkijonoja, joten esimerkiksi ("Volkswagen (auto)" ("Fiat (auto)"))
jäsennellään ["Volkswagen, [auto], ", ["Fiat, [auto], "]]
, eikä ["Volswagen (auto)", ["Fiat (auto)"]]
kuten voisi luulla.
Teksti (a b c (d e f) (g (h)))
jäsennellään ja palautettu array on ["a", "b", "c", ["d", "e", "f"], ["g", ["h"]]]
.
// Parsii sulkujen sisällä olevan datan arrayyn function parsiKoodi(str) { // Varmista, että koodi sisältää vain sallittuja merkkejä. // $-merkit voivat sekoittua tmp$ -viittauksiin if (str.indexOf("$") != -1) throw "Koodi sisältää kiellettyjä merkkejä!"; // Taulu väliaikaisia arvoja varten var table = new Array(); var counter = 0; while (true) { // Etsi viimeinen ( sulku var sulku1 = str.lastIndexOf("("); // Etsi sitä vastaava ) sulku var sulku2 = str.indexOf(")", sulku1); // Jos sulkuja ei löytynyt, poistu loopista -- koko teksti on parsittu if (sulku1 == -1 || sulku2 == -1) break; // Sulkujen sisällä oleva data eroteltuna välilyönneillä var sis = str.substring(sulku1+1, sulku2).trim().split(" "); // Nimi väliaikaista arvoa varten var name = "tmp$" + ++counter; for (var i = 0; i < sis.length; i++) { // Korvaa viittaukset tauluun oikeilla arvoilla taulusta if (table[sis[i]] != undefined) { sis[i] = table[sis[i]]; } // Poista tyhjät kohdat else if (sis[i] == "") { sis.splice(i, 1); i--; } } // Lisää data tauluun väliaikaisesti table[name] = sis; // Korvaa sulut ja niiden sisältö viittauksella taulun väliaikaiseen arvoon str = str.substring(0, sulku1) + " " + name + " " + str.substring(sulku2+1, str.length); } // Palauta viimeisin väliaikainen arvo, eli uloimmat sulut return table["tmp$" + counter]; }
Merkki kerrallaan parsiminen on verrattavissa lekserin avulla parsimiseen, mutta erillistä lekseriä ei kuitenkaan tarvita, koska merkkiyhdistelmiä ei ole. Kieliä, joissa yksittäiset merkit ovat komentoja (kuten Brainfuck) voi parsia vain tällä tavalla.
Esimerkkinä dc:tä muistuttava kieli, joka koostuu numeroista ja komennoista, ja perustuu pinoon. Jokainen numero työnnetään pinoon, ja jokainen operaattori ottaa pinosta kaksi ylintä numeroa ja työntää vastauksen pinoon. Välilyönnit ja kirjaimet ohitetaan.
Pseudokoodina:
Niin kauan kuin sisääntulossa on merkkejä Jos seuraava merkki on välilyönti, tabi, uusirivi, tms, siirry seuraavaan merkkiin Jos seuraava merkki on numero, työnnä se pinoon Jos seuraava merkki on operaattori + ota pinosta 2 ja työnnä summa - ota pinosta 2 ja työnnä erotus jne...
Lisäksi lekserin ja parserin yhdistäminen on mahdollista, jolloin lekseri itse on merkki-kerrallaan parseri. Esimerkkinä yllä oleva sulku-parseri toteutettuna tällä tavalla.
var pino = new Array(); var taulu = new Array(); var nykyinenSana = ""; // Käy koodin merkit läpi for (var i = 0; i < koodi.length; i++) { // kaikki muut merkit paitsi välilyönnit ja sulut nykyiseen sanaan if (koodi.charAt(i).match(/[^\s\(\)]/)) nykyinenSana += koodi.charAt(i); else { // Muut merkit tarkoittavat, että sana loppui // Jos nykyinen sana ei ole tyhjä, lisää se tauluun if (nykyinenSana.length != 0) { taulu.push(nykyinenSana); nykyinenSana = ""; } // Uusi taulu alkaa if (koodi.charAt(i) == "(") { var uusiTaulu = new Array(); // Lisää uusi taulu tauluun taulu.push(uusiTaulu); // Talleta nykyinen taulu pinoon pino.push(taulu); // Korvaa taulu uudella taululla taulu = uusiTaulu; } // Uusi taulu loppuu if (koodi.charAt(i) == ")") { // Ota vanha taulu takaisin pinosta taulu = pino.pop(); } } } // Lisää mahdollinen viimeinen sana tauluun if (nykyinenSana.length != 0) { taulu.push(nykyinenSana); nykyinenSana = ""; } return taulu;
Tämän jälkeen parserin työ on hieman helpompaa, kun sulut on parsittu jo valmiiksi.
Erittäin laajalti käytetty lekserittömän parsinnan muoto on säännöllinen lauseke. Regex-lauseet ovat ohjeita merkki-kerrallaan parsereille.
Ohjelmointiputkassa: