Uvod
Prošlim člankom, započeli smo mini-serijal članaka u kojima ćemo se (na ovaj ili onaj način) baviti obradom teksta (ili, ako želite da sve lepše zvuči, a pri tom je još i tačno - programskog koda).
U ovom članku, implementiraćemo Python skriptu za uklanjanje komentara iz programskog koda, prvenstvano za 'C-olike' programske jezike, kao što su C/C++, C#, Java, JavaScript i PHP.
Kažemo tako (uz napomenu), jer u slučaju jezika kao što su Python i SQL, u kojima su zastupljeni samo linijski komentari, pronalaženje (i uklanjanje) komentara je jednostavna operacija, dok je, kada su u pitanju jezici u kojima se pored linijskih komentara koriste i blok komentari, situacija ipak malo kompleksnija.
Takođe, ovaj članak će biti dobra prilika da se pobliže upoznamo sa konecptom vođenja računa o kontekstu čitanju koda (iako detaljniju razradu "zvanično" ostavljamo za sledeći članak) ....
Uklanjanje linijskih komentara i osnovna razmatranja pri pronalaženju komentara u tekstu
Za sam početak, dva pitanja: kako možemo ukloniti linijske komentare i da li postoji jednostavan obrazac po kome možemo prepoznavati obe vrste komentara (pri čemu je drugo pitanje od većeg značaja)?
Prepoznavanje linijskih komntara jeste jednostavno, a verujemo da iskusnijim čitaocima odmah pada na pamet da se obe vrste komentara mogu pronalaziti preko regularnih izraza, pri čemu se početni tekst može podeliti na tri (2 + 1) grupe tokena:
- one koji počinju sa
/*
i završavaju se sa*/
(blok komentari za C-olike jezike) - one koji počinju sa
//
(C/C++, C#, Java, JavaScript, PHP),#
(Python), ili--
(SQL) i završavaju prelaskom u novi redn\
(linijski komentari) - sve "ostale" (kod koji nije pod komentarima)
.... ali - i dalje ostaje pitanje da li se posle toga komentari mogu jednostavno ukloniti iz liste.
Pogledajmo prvo kako stvari stoje sa linijskim komentarima.
Uklanjanje linijskih komentara
Donja skripta pronalazi tokene koji počinju sa //
(ili #
, ili --
) i završavaju se sa \n
i potom ih zamenjuje tokenom \n
:
# ----- konfiguracija ---------------------------------------- #
datoteka_ulaz = "naziv_datoteke"
datoteka_izlaz = "naziv_datoteke" # ista ili različita datoteka,
# po vašem izboru
regex_clike = "//.*\n"
regex_python = "#.*\n"
regex_sql = "--.*\n"
# ----- funkcije --------------------------------------------- #
def uklanjanje_linijskih komentara(tekst, regex):
return tekst.replace(regex, "\n")
# ----- obrada ----------------------------------------------- #
f = open(datoteka_ulaz, "rb")
s = f.read().decode("utf-8")
f.close()
s = uklanjanje_linijskih_komentara(s, regex_clike)
f = open(datoteka_izlaz, "wb")
f.write(s)
f.close()
Bili smo praktični i u ovom (vrlo jednostavnom) slučaju, nismo pravili listu, već smo zamene obavili direktno.
(Moguće je da se već pitate i zašto smo onda uopšte i spominjali liste?!)
Postupak koji smo upravo razmotrili je krajnje adekvatan za zamenu linijskih komentara (što znači da je odgovor na prvo od dva pitanja sa početka potvrdan), ali, ako pokušamo da takav isti ("naivan") pristup primenimo za uklanjanje blok komentara, naići ćemo odmah na određene zamke ....
(Možda ne baš odmah, možda će na samom početku sve i dalje delovati lepo. Videćemo ....)
Šta se dešava ako pokušamo da primenimo naivan pristup za uklanjanje blok komentara
Uzmimo kao primer sledeći C kod ....
/*
Blok komentar
*/
#include<stdio.h>
void main() // Glavna funkcija
{
printf("Dobar dan! :)");
}
Ako definišemo regularni izraz koji pronalazi obe vrste komentara ....
regex_clike = "//.*\n||\/\*[\s\S]*?\*\/"
.... i primenimo ga u naredbi replace
(kao što smo radili u prvoj skripti za uklanjanje linijskih komentara), skripta će na gornjem primeru C koda svoj posao (i ovog puta) obaviti adekvatno.
Ali, šta ako se prebacimo na C++ (što, samo po sebi nije od prevelikog začaja) i malo promenimo kod (što jeste od značaja)?
#include<iostream>
using namespace std;
/* ----- Komentar ----- */
int main()
{
string s = "/* ----- Komentar ----- */";
}
Jasno je da će obe niske /* ----- Komentar ----- */
(gde je, u kontekstu primera, samo prva niska zapravo komentar, a druga deo niske) - biti uklonjene ....
#include<iostream>
using namespace std;
int main()
{
string s = "";
}
.... što praktično obesmišljava naredbu dodele u 8. redu (naredba, sama po sebi, jeste u redu, ali, to nije naredba koju smo hteli da izvršimo)!
Nije teško zaključiti da problem nastaje usled toga što program ne vodi računa o kontekstu čitanja tokena (odnosno - tokena), to jest, o tome da li se tokeni nalaze unutar komentara, niski i sl. (ili izvan).
Odgovor na drugo od dva pitanja koja smo postavili na početku je dakle - odričan. Vidimo da ne postoji (skroz) jednostavan obrazac po kome možemo prepoznavati blok komentare i jasno je da je potreban drugačiji pristup.
Srećom, algoritam za prepoznavanje blok komentara nije posebno komplikovan ....
Uklanjanje blok komentara
Korišćenje regularnih izraza je i dalje (krajnje) adekvatan pristup za skriptu koja polako dobija konačni oblik, ali, moramo biti precizniji i moramo kreirati mehanizam koji prepoznaje kontekst u kome se token koji trenutno razmatramo pojavljuje.
(Takođe, trebaće nam i liste. Zato smo ih spominjali od početka (kada nam još uvek nisu trebale). Tada nam nisu trebale, ali - sada će nam trebati.)
U prepoznavanju konteksta, pomoći će nam (zapravo prilično jednostavna) pravila jezika, pri čemu znamo sledeće:
- linijski komentari počinju sa
//
i završavaju se prelaskom u sledeći red - blok komentari počinju sa
/*
i završavaju se sa*/
- niske počinju i završavaju se sa
"
(mogu naravno počinjati i sa'
(apostrof), kao i`
(backtick), ali ćemo se u ovom članku, zarad jednostavnosti, zadržati samo na navodnicima) - znak
"
(navodnik), ne može se samostalno pojaviti unutar niske (koja počinje i završava navodnicima), već isključivo kao deo escape sekvence:\"
Dakle, svi tokeni koji se pojave posle tokena /*
predstavljaju deo komentara (sve do pojave tokena */
), kao i svi tokeni posle tokena //
(sve do pojave tokena \n
).
To znači da je naš osnovni zadatak da (preko regularnih izraza), u tekstu pronađemo sledeće tokene: //
, \n
, /*
, */
i "
(kao što smo već pominjali, inače se za niske mogu - a praktično i moraju - koristiti i tokeni '
(apostrof) i `
(backtick)).
Ovoga puta, podelićemo ulazni tekst u listu tokena, u kojoj je svaki element:
- neki od navedenih tokena (ili)
- tekst koji se nalazi između dva tokena iz gore navedene grupe (ili, između početka ili kraja niske i nekog od navedenih tokena)
Preko novog regularnog izraza ....
import re
regex_clike = "(//|\n|/*|*/|\")"
def rastavljanje_teksta(tekst, regex):
lista = re.split(regex, tekst)
return lista
.... dobijamo sledeću listu (primer C++ koda koji smo prethodno koristili):
#include<iostream>
-----------------------------
using namespace std;
-----------------------------
/*
-----------------------------
----- Komentar -----
-----------------------------
*/
-----------------------------
int main()
{
string s =
-----------------------------
"
-----------------------------
/*
-----------------------------
----- Komentar -----
-----------------------------
*/
-----------------------------
"
-----------------------------
;
-----------------------------
}
-----------------------------
Da bismo bolje razumeli kako treba protumačiti ovakvu listu, pogledajmo nekoliko jednostavnijih primera ....

Izdvojeni su i ostali tokeni (operatori, white space znakovi i sl), što smo učinili da bi primeri bili zanimljiviji, ali, princip prepoznavanja niski i komentara se ne menja.
U prvom primeru ....

.... prvo se pojavljuju se dva tokena koja ne menjaju kontekst čitanja.
Treći token ....

.... uvodi čitač u režim prepoznavanja niski, što znači da će svi tokeni

.... sve do pojave sledećeg tokena "\""
....

.... biti prepoznati kao deo niske.
Ove informacije (inače) možemo iskoristiti da (po želji), saržaj niske grupišemo u jedan token:

.... ili da celu nisku smestimo u jedan token:

U našim primerima, ostavićemo (ipak) tokene u originalnom rasporedu.
Spajanje tokena nije ni od kakvog značaja za skriptu koju pravimo, jer će tokeni koji bi inače bili spajani - jednostavnobiti zanemareni (odnosno, neće biti kopirani u izlaznu datoteku).

U drugom primeru, prvi token od značaje je token "/*", koji uvodi čitač u režim prepoznavanja komentara ( u nekoj drugoj implementaciji, kao što smo već videli, to bi mogao biti i režim spajanja komentara).

To znači da će svi tokeni do pojave tokena "*/" (uključujući i taj token, biti prepoznati kao komentar).

(U ovom primeru, ostatak naredbe je algebarski izraz sa promenljivom.)

U sledećem primeru, koji predstavlja složeniju situaciji (kada se među tokenima pojavljuju, i tokeni koji otvaraju / zatvaraju komentare, i tokeni koji otvaraju / zatvaraju niske), prvi token od značaja je token "/*"
, koji uvodi čitač u režim prepoznavanja komentara, što znači da se dolazeći tokeni tretiraju kao komentari ....

Može se postaviti pitanje, da li token "\"", koji je prethodno imao poseban značaj ....

.... ima poseban značaj i u ovakvoj situaciji, ali, odgovor je - ne - i token "\""
se sada prepoznaje (samo) kao deo niske.

U slučaju da određeni token (u ovoj situaciji je to bio token "/*"
) izvede čitač iz osnovnog režima, svi tokeni koji inače imaju posebno značenje, postaju obični tokeni i jedini token od značaja je token koji vraća čitač u osnovni režim (u ovom slučaju, token "*/"
).

U trećem primeru, token "\""
uvodi čitač u režim perpoznavanja niske ....

To (ponovo) znači da će određeni token ....

.... koji u osnovnom režimu ima specijalno značenje, biti prepoznat shodno trenutnom režimu čitača (ovoga puta, kao deo niske).

.... što važi i za prepostale tokene (dok drugi token "\"" ne zatvori nisku).

Implementacija
Ostaje da sve što smo videli pretočimo u funkcionalnu skriptu. Koristićemo ideje koje smo razmotrili na pređašnjim primerima, s tom razlikom što smo u primerima komentare označavali, a u skripti ćemo ih uklanjati (to jest, deo teksta koji je prepoznat kao komentar, jednostavno neće biti kopiran u izlaznu datoteku)
Prvo ćemo precizno definisati pravila za premeštanje tokena.
Pravila za premeštanje tokena
- preko steka ćemo voditi računa o kontekstu
- kontekst 0 - token koji se čita je van komentara i niski
- kontekst 1 - token je deo linijskog komentara
- kontekst 2 - token je deo blok komentara
- kontekst 3 - token je deo niske
- proći ćemo redom kroz listu tokena
- tokeni "//", "\n", "/*", "*/" i "\"" (ovoga puta znak navoda, a ne escape sekvenca za znak navoda), menjaju stanje steka, ali se svi osim znaka navoda uklanjaju iz liste
- ostali tokeni prate kontekst
- ako je kontekst 0 ili 3, biće ostavljeni
- ako je kontekst 1 ili 2, biće uklonjeni (odnosno, neće biti kopirani)
Na kraju, napisaćemo i Python skriptu za uklanjanje programskog koda:
Python skripta za uklanjanje komentara iz programskog koda
# ----- konfiguracija ---------------------------------------- #
import re
datoteka_ulaz = "naziv_datoteke"
datoteka_izlaz = "naziv_datoteke" # ista ili različita datoteka,
# po vašem izboru
regex_clike = "(//|\n|/*|*/|\")"
regex_python = "#.*\n"
regex_sql = "--.*\n"
# ----- funkcije --------------------------------------------- #
def rastavljanje_teksta(tekst, regex):
lista = re.split(regex, tekst)
return lista
def obrada_pojedinacnog_tokena(token, stek, lista, nova_lista):
kontekst = stek[len(stek) - 1]
if token == "": return
if token == "/*":
if kontekst == 0:
stek.append(1)
return
if token == "*/":
if kontekst == 1:
stek.pop()
return
if token == "//":
if kontekst == 0:
stek.append(2)
return
if token == "\n" or token == "\r":
if kontekst == 2:
stek.pop()
nova_lista.append(token)
if token == "\"":
if kontekst == 0: stek.append(3)
if kontekst == 3: stek.pop()
if kontekst == 0 or kontekst == 3:
nova_lista.append(token)
def obrada_liste_tokena(lista):
nova_lista = []
stek = [ 0 ]
for token in lista:
obrada_pojedinacnog_tokena(token, stek, lista, nova_lista)
return nova_lista
def formatiranje_teksta(lista):
s = ""
for token in lista:
s = s + token
return s
# ----- obrada ----------------------------------------------- #
f = open(datoteka_ulaz, "rb")
s = f.read().decode("utf-8")
f.close()
lista = rastavljanje_teksta(s, regex_clike)
lista = obrada_liste_tokene(lista)
tekst =
f = open(datoteka_izlaz, "wb")
f.write(s)
f.close()
Ovoga puta, ako skripti damo na obradu C kodove koje smo prethodno koristili, biće uklonjene samo niske koje predstavljaju komentar, a ne i one koje samo "liče" na komentare.
Ideje za unapređenje skripte
Verujemo da, ako ste se zainteresovali za obradu teksta, nećete imati manjak ideja za samostalno isprobavanje, ali, spomenućemo 'najočigledniju' ideju.
Uklanjanjem linijskog komentara koji nije bio "slepljen" za početak reda (već je bio odvojen jednim ili više razmaka / tabova), ostaće whits space, a uklanjanje blok komentara (koji su se prostirali u više redova) ostavljaće prazne redove.
Ostavljamo vama, našim čitaocima, da samostalno implementirate jednu takvu jednostavnu (ali, ni izdaleka "banalnu") skriptu koja će rešiti navedene probleme ....