Mihai Budiu -- mihaib+@cs.cmu.edu
http://www.cs.cmu.edu/~mihaib/
martie 2000
De la prima întîlnire cu calculatoarele am fost fascinat de ele. Pentru mine întîlnirea asta s-a produs destul de tîrziu, după standardele generației actuale de elevi, prin clasa a opta. Firește, primul lucru pe care l-am văzut a fost un joc; calculatorul cu pricina este o specie deja dispărută, numit Sinclair Spectrum ZX, după care românii au făcut o copie destul de reușită, numită HC 85 (de fapt a existat o a doua copie, TIM-S, dar care s-a bucurat de mai puțin succes). Am avut imediat dorința de a construi și eu un joc.
Realizez că începutul ăsta poate induce în eroare cititorul, pentru că nu despre jocuri vreau să vorbesc aici. De fapt, dacă ați trecut peste dezamăgirea produsă de titlul anost, cred că veți rezista și la restul textului.
De la început am fost fascinat de calculatoare, și fascinația mea a rămas neschimbată, chiar dacă unele dintre motivele fascinației s-au modificat cu timpul. La început am fost entuziasmat de faptul că exista o mică lume asupra căruia sunt demiurg, stăpîn absolut, o lume care se mișcă după legile pe care i le trasez eu. Probabil ca într-o oarecare măsură acest motiv este prezent și acum în atracția mea pentru calculatoare, dar într-o măsură mult diminuată.
Cu timpul am învățat că demiurgul-programator nu este de fapt atotputernic; într-o serie de articole precedente din PC Report, am încercat să ilustrez unele dintre limitările calculatoarelor însele: de exemplu am văzut într-un articol despre teoria complexității și calculabilitate (PC Report din Decembrie 1999, o copie fiind disponibilă din pagina mea de web) că există lucruri pe care calculatoarele nu le pot calcula, și în trei articole din octombrie, noiembrie și februarie am încercat să arăt că există probleme care se pot rezolva, dar pentru care oricît de multe resurse computaționale am avea la dispoziție, răspunsuri practice probabil nu vor putea fi obținute vreodată.
Acum calculatoarele sunt profesia mea și ca atare am de-a face cu ele zi de zi. De fapt studiez calculatoarele de 13 ani, în sensul cel mai concret al cuvîntului: am fost elev la Liceul de Informatică din București, student în calculatoare la Politehnica din București pentru șase ani (din care unul de master), și acum sunt în al treilea an de doctorat; cu toate acestea, mai am multe de învățat! Acum știu că de fapt calculatoarele nu sunt un haos primitiv, în care eu, programatorul, atotputernic, pot face ordine, ci că există multe limite peste care nu pot trece, că unele lucruri se pot face mai bine sau mai rău, cîteodată foarte bine și foarte rău, și am mai învățat că sunt enorm de multe lucruri pe care nimeni nu știe încă să le facă. De fapt asta e o veste bună pentru mine: principala mea activitate este cercetarea, deci faptul că sunt multe lucruri de aflat înseamnă că pentru o vreme o să am ceva pîine pe masă.
Am ajuns pe ocolite la ideea pe care vreau să-mi bazez introducerea. După cum am spus și în alte articole, știința calculatoarelor (informatica în terminologie franco-română) are două ramuri mari: partea teoretică și cea aplicată. În general cercetătorii pot fi atribuiți destul de clar uneia dintre ramuri. Cei dintr-o ramură uneori îi privesc cu superioritate pe cei din cealaltă. De fapt ambele ramuri sunt extrem de importante, și deloc ușoare; efortul intelectual depus în oricare dintre ele este substanțial, și există talent specific fiecăreia dintre ele.
Dar rezultatele cele mai spectaculoase se obțin atunci cînd teoria și practica concură; situația este oarecum asemănătoare cu fizica, în care teoreticienii și experimentaliștii sunt două triburi separate; dar revoluțiile sunt cele în care teoria și practica explică un același fenomen, confirmîndu-se și întărindu-se reciproc.
De fapt atunci cînd teoria concură cu practica obținem un al treilea rezultat, cel mai palpabil, și cel care de fapt schimbă societatea în mod dramatic: este vorba de tehnologie. Cîți dintre cei care folosesc astăzi televizoarele sunt conștienți că existența lor se bazează pe teoria cîmpului electromagnetic a lui Maxwell, și pe efectul fotoelectric descoperit experimental de Hertz (și pentru a cărei explicație Einstein a primit premiul Nobel)?
La fel stau lucrurile și în calculatoare: tehnologia la îndemîna (aproape a) fiecăruia, care acum schimbă societatea într-un mod fundamental (de exemplu prin Internet), nu ar fi fost posibilă fără rezultate atît din teorie cît și din practică. De fapt acest articol va ilustra o problemă foarte simplă, la care teoria a adus niște contribuții majore, dar care are enorm de multe aplicații în practică. De fapt, această teorie are un succes atît de mare, încît probabil că nu există calculator digital care să nu o folosească în mai multe feluri, de la cele din PC-uri pînă la microcontrolerele care controlează utilaje industriale sau reglează injecția automobilelor. Din necesitate însă, prezentarea noastră va fi foarte sumară, și nu va acoperi decît puține aspecte ale problemei.
Un calculator abstractizat teoretic poate fi văzut ca un aparat care prelucrează limbaje. Toate informațiile pe care le prelucrează sunt exprimate în semne discrete, pe care le vom numi ``litere'' (de exemplu în biți, în zero și unu, sau dacă preferați în octeți). Astfel, datele de la intrare și de la ieșire sunt toate șiruri de litere, pe care le numim ``cuvinte''. Noțiunea aceasta de cuvînt este mai largă decît cea obișnuită: în româna nu numim șirul de litere ``jjj'' un cuvînt, dar în terminologia calculatoarelor, da. Fiecare program procesează anumite cuvinte și dă ca rezultat alte cuvinte. Adesea, pentru un program nu orice cuvînt venit la intrare are sens: unele vor fi pur și simplu eronate; de exemplu un program care sortează un șir de numere nu se așteaptă să primească alte simboluri. Numim o mulțime de cuvinte ``limbaj''.
De exemplu, limbajul Pascal constă în colecția tuturor cuvintelor care reprezintă programe corecte Pascal (observați că un întreg program Pascal este deci un singur cuvînt în această terminologie!). O întrebare este cum poți descrie în mod succint un întreg limbaj? De exemplu, cum poți spune cuiva ce este un program Pascal corect? Pentru asta trebuie să folosești reguli care descriu cum se pot construi programele corecte. Regulile care descriu cum arată un limbaj se numesc sintaxa limbajului.
|
Aceste reguli la rîndul lor trebuie scrise cumva. Limbajul care descrie regulile poate fi numit ``meta-limbaj'', pentru că este un limbaj care descrie alte limbaje.
Desigur, ne putem pune întrebarea: cum descriem un meta-limbaj? În mod tradițional meta-limbajele erau descrise pe hîrtie, folosind o notație simbolică; prima probabil în ordine cronologică a fost notația Backus-Naur, care a fost printre altele folosită pentru a descrie limbajul Pascal. Un exemplu este prezent în figura 1.
Au apărut apoi programe speciale, care permit descrierea structurii altor limbaje; există multe astfel de scule folosite pe scară largă; iată cîteva dintre ele:
![]() |
Pare complicat? Este, cel puțin pînă vă obișnuiți cu ideea programelor care prelucrează alte programe. Despre XML au mai apărut articole în PC Report; poate în viitor o să scriu un articol despre yacc sau prietenul lui. În textul de față o să vedem însă soluția la o problemă mai simplă.
Atunci cînd eu vorbesc cu cineva, acel cineva desface textul spus de mine în cuvinte, pe care le analizează. Vorbitorul unei limbi străine are adesea probleme în a segmenta textul auzit în cuvinte independente. În scris problema este mai simplă, cel puțin pentru limbile moderne care folosesc alfabetul latin: prin convenție punem între două cuvinte un spațiu.
Dacă ideea vi se pare evidentă, aflați că lucrurile nu au stat întotdeauna așa: romanii și grecii nu foloseau spații (de asemenea foloseau extrem de multe prescurtări, pentru că hîrtia era scumpă; din lipsa tipografiilor, încă ne-inventate, copiștii2 aveau probleme de segmentare, și făceau adesea erori de interpretare a textelor cînd le copiau). Dacă vi se pare floare la ureche așa ceva, atunciîncercațisăcitițipropozițiaastașivedețicîtdeușorvăeste.
Atunci cînd unui calculator3 îi este prezentat un program, el are de făcut o analiză similară: trebuie să despartă textul în bucățele separate și să identifice semnificația fiecăreia; aceasta este analiza lexicală; bucățelele obținute sunt numite lexeme. După aceea calculatorul verifică dacă aceste bucățele sunt îmbinate corect (nu orice succesiune de cuvinte din română formează o propoziție), folosind analiza sintactică. Figura 2 arată cum aceste faze sunt folosite în compilatoare.
![]() |
Nici în calculatoare lucrurile nu au fost prea clare de la început: limbajele primitive, ca Fortran-ul, nu aveau reguli complet ne-ambigue pentru a discerne într-un text care este fiecare cuvînt. De exemplu, în Fortran spațiile nu contează (din motive pragmatice: aparatele de găurit cartele nu erau prea fiabile în tastatul de spații). Folclorul spune că americanii au pierdut un satelit din cauza asta, pentru că compilatorul de Fortran a interpretat un program altfel decît utilizatorul care l-a scris.
Dacă acestea vi se par aberații îndepărtate, aflați că nici limbajele cele mai folosite la ora actuală pe scară largă nu sunt lipsite de ele: de exemplu în C++ o problemă specială apare din cauza construcției template, care se scrie astfel a<b>. Problema apare în cazul unor template imbricate (incluse una într-alta), ca în a<b<c>>. Problema e cu semnele >>: reprezintă ele două template închise, sau semnul ``shift-right''? Analizorul lexical nu poate decide de unul singur ce înseamnă aceste semne, are nevoie de ajutor din partea analizorului sintactic.
Am văzut deci că prima etapă în înțelegerea unui program este descompunerea lui în lexeme. Atunci cînd implementăm un limbaj nou de programare, cum ar fi oare cel mai eficace să descriem toate lexemele posibile? De exemplu, în C avem lexeme de forma for, while, etc., dar nu lexeme de forma %$#@. 'In plus, într-un program C putem întîlni lexeme de genul variabila_cea_mare. Există deci un număr potențial infinit de lexeme (dacă presupunem că numele de variabile nu au nici o limită pentru lungime). Totalitatea tuturor lexemelor legale este la rîndul ei un limbaj; acesta nu trebuie confundat cu limbajul C: în limbajul C ``cuvintele'' sunt programele corecte, în limbajul lexemelor C, cuvintele legale sunt toate lexemele care pot apărea în vreun program C.
Teoreticienii au propus cu mult timp în urmă (în anii '60) un meta-limbaj extrem de concis pentru a descrie lexeme. Limbajul acesta este limbajul expresiilor regulate. O expresie regulată este un șir de caractere care descrie o mulțime de cuvinte posibile (poate chiar o mulțime infinită). Lexemele tuturor limbajele de programare moderne pot fi descrise prin expresii regulate; vom folosi de aici înainte abrevierea ER pentru ``expresii regulate''.
Cred că cel mai simplu este să vedem niște exemple. Pentru început voi folosi notația cea mai economică, care este folosită de teoreticieni. Vom vedea apoi tot felul de variante, folosite de tot felul de ``scule'' de procesare de texte.
Să zicem că vrem să descriem limbaje care folosesc litere din
alfabetul {a,b,c,d}, pentru a simplifica lucrurile. Perfect. ER
folosesc toate aceste caractere, plus o listă de caractere
suplimentare:
. Voi explica
prin exemple ce înseamnă fiecare. Observați că scriu fiecare
expresie regulată cu caractere cursive, iar cuvintele din limbajul pe
care-l descriem cu caractere drepte. Asta pentru a preveni
confuziile, pentru că există caractere comune în cele două
mulțimi. (Voi abandona această convenție în secțiunile care
descriu ``sculele'', pentru că acestea oricum folosesc alte notații.)
În fine, să ne apucăm de ceva serios. Celelalte trei semne care au rămas ne permit să creăm limbaje mai complicate din limbaje simple. Astea vor face ER o sculă foarte puternică.
De exemplu, expresia regulată 4 descrie
un limbaj cu trei cuvinte: { , a, b }. Primul cuvînt este
cuvîntul fără nici o literă; mai avem apoi alte două cuvinte, de
cîte o literă. De îndată ce vom vedea celelalte două operații o
să apreciați mai tare semnul |, pentru că împreună ele putem
exprima limbaje din ce în ce mai interesante.
De exemplu, expresia regulată
descrie un limbaj
compus din patru cuvinte: {aa, ac, ba, bc}. Fiecare cuvînt este
compus dintr-un cuvînt din limbajul
{a,b} concatenat cu un
cuvînt din limbajul
{a,c}.
Deja putem folosi ER pentru a ne exprima foarte succint. De pildă,
dacă limbajul R are 2 cuvinte, atunci
are 2 x 2 x 2 x 2 = 16 cuvinte!
Limbajul este un limbaj ale cărui cuvinte sunt formate dintr-un
număr arbitrar (inclusiv zero!) de cuvinte din R, concatenate. De
exemplu, limbajul descris de
este { , a, aa, aaa,
aaaa,...}, care conține cuvîntul fără nici o literă (obținut
din zero copii ale lui ``a''), cuvîntul a, cuvîntul aa, etc., și nu
vă puteți aștepta să scriu aici toate cuvintele, pentru că sunt
în număr infinit, și PC Report recomandă o limită de 3500 de
cuvinte pe articol (pe care oricum o să o depășesc, mi-e teamă).
Iată deci că fiecare expresie regulată descrie un întreg limbaj, poate chiar infinit. Vom vedea aici mai multe exemple de expresii regulate interesante. De acum înainte voi folosi tot setul de caractere ASCII, și nu mă voi mai limita la literele a,b,c,d. Pentru eficiență voi denumi ER care apar și pe care vreau să le refolosesc; voi folosi semnul egal =; acesta nu este un semn care poate fi folosit pentru a scrie expresii regulate.
Pentru cei doritori de formalisme, există tot felul de reguli care ne permit să simplificăm ER complicate. Iată cîteva dintre ele (semnificația semnului egal este: limbajul descris de expresia din stînga este același cu limbajul descris de expresia din dreapta):
Primele nouă din regulile acestea ne spun că mulțimea tuturor
cuvintelor împreună cu aceste operațiile și
este un
semi-inel idempotent. De aici un amator de algebră poate deriva tot
felul de alte proprietăți interesante, pe care acum o să le trecem
cu vederea.
Am văzut tot felul de ER, și o să mai vedem și alte cîteva. Cu cîteva semne simple putem descrie într-un mod finit limbaje complicate infinite. Dar nu orice limbaj se poate descrie cu ER. Limbajele care se pot descrie cu ER se numesc limbaje regulate. Puterea ER este destul de limitată, dar suficient de mare pentru a le face extrem de utile. Vom vedea mai jos că sculele care folosesc ER adaugă o serie de abrevieri utile, și că uneori extind ER în moduri care permit descrierea unor limbaje care nu sunt de fapt regulate.
Iată niște exemple de limbaje care nu se pot descrie cu ER:
Ce facem cu o expresie regulată? Problema principală este cea de recunoaștere: dacă avem o expresie regulată și un text, reprezintă acel text un cuvînt din limbajul descris de expresia regulată? Spunem că expresia regulată acceptă acel cuvînt.
De exemplu, cuvîntul ``141231'' este în limbajul descris de expresia regulată NZ de mai sus, dar cuvîntul ``-+-+131'' nu este.
O problemă înrudită este cea a analizei lexicale, pe care am
descris-o mai sus: dacă se dă un text (de exemplu în Pascal) și
mai multe ER (de exemplu ID, NN, etc.), să-l descompunem într-o
secvență de cuvinte astfel încît fiecare cuvînt este acceptat de
una din expresii. De exemplu textul acum := 51; este compus din
patru cuvinte: un identificator, acceptat de ID, semnul de
atribuire, acceptat de expresia , pe care nu am descris-o,
numărul 51, acceptat de expresia NN și semnul punct-și-virgulă,
care este acceptat de o expresie regulată foarte simplă ``;''.
De fapt cele două probleme de mai sus (acceptarea și analiza lexicală) se pot ambele rezolva cu aceeași tehnologie: dacă putem recunoaște un cuvînt, atunci putem ciopîrți și textul în bucăți, recunoscînd primul cuvînt, și după aia continuînd descompunerea cu restul textului. Vestea cea bună este că putem rezolva problema recunoașterii limbajelor regulate în mod foarte eficient!
Aparatele care recunosc limbaje regulate se numesc automate finite. Putem vedea aceste automate sub forma unor dispozitive fizice, reale, sau sub forma unor programe extrem de simple.
Teoreticienii arată (și studenții învață la cursurile de teoria limbajelor formale) că o expresie regulată se poate traduce într-un automat finit, care operează asupra unui cuvînt, și care produce un rezultat da/nu, dacă cuvîntul este sau nu în limbajul descris de expresia regulată. În acest articol nu o să ne preocupăm despre cum se face asta, și nici despre tot felul de alte rafinamente, (cum sunt automatele finite nedeterministe). O să vă arăt doar un automat finit și să vă explic cum funcționează el. Vom observa că automatul finit se uită la fiecare literă din cuvîntul de recunoscut o singură dată, și face o singură operație internă pentru fiecare literă. Asta înseamnă că procesează fiecare cuvînt în timp linear, ceea ce îl face foarte eficient.
![]() |
De fapt așa procedează toate sculele pe care le expun mai jos: cînd primesc o expresie regulată, ``compilează'' această expresie într-un automat finit. Automatul este reprezentat sub forma unui progrămel foarte rapid. Apoi acest progrămel primește cuvîntul de testat, îl studiază, și produce diagnosticul. Automatele folosite în analizoarele lexicale indică cum programul de la intrare poate fi descompus în lexeme, și ce expresie regulată s-a potrivit cu fiecare lexemă.
Figura 3 arată un automat finit care operează cu cuvinte peste alfabetul {a,b}. Acest automat are o mulțime finită de stări, notate prin cercuri. În fiecare clipă automatul se află într-o unică astfel de stare. La începutul calculului automatul se află în starea marcată de o săgeată care vine ``de nicăieri''.
Între două stări există săgeți etichetate cu litere; acestea se numesc tranziții. De exemplu, între starea 1 și starea 2 există o tranziție etichetată cu a: asta înseamnă că automatul, dacă vede litera a la intrare și se află în starea 1, va trece în starea 2.
Unele dintre stările automatului sunt marcate cu două cercuri; astfel de stări sunt numite stări finale. Un cuvînt este prin definiție acceptat dacă atunci cînd este prezentat la intrare cauzează automatul să treacă într-o stare finală atunci cînd cuvîntul a fost în întregime prelucrat.
![]() |
De exemplu, figura 4 arată evoluția în timp a automatului anterior.
În această secțiune voi discuta pe scurt mai multe programe care folosesc ER. Fiecare din ele are notații puțin diferite de celelalte, ceea ce e foarte neplăcut dacă doriți să folosiți mai multe dintre ele.
Toate aceste programe prelucrează fișiere text, adică fișiere în care informația este într-o formă citibilă de către oameni. E o tradiție în lumea Unix ca datele să fie menținute în formă textuală; Microsoft aparent și-a făcut un scop să creeze formate binare (adică ne-textuale) de stocare a datelor, care mai sunt și secrete pe deasupra! Din cauza asta, multe din aceste scule sunt de utilitate redusă în lumea Windows. Din fericire, una dintre formele cele mai prevalente de prezentare a informației este limbajul HTML, care are o reprezentare pur textuală. Pentru prelucrări de HTML, prelucrarea textelor de programe, programe automate de colectat informații de pe Internet, sculele descrise de mai jos sunt extrem de utile.
În acest text eu am folosit litere cursive pentru a scrie expresiile
regulate, și litere drepte pentru cuvinte. Din păcate în lumea
programelor se folosește un același set de caractere pentru a denota
literele și expresiile regulate. Asta ridică o problemă
suplimentară, pentru că nu avem noi semne,
pentru a construi expresiile regulate, deci trebuie să folosim tot
unele dintre litere. Dar atunci e o ambiguitate între litera
*
și semnul folosit pentru expresii regulate.
Toate programele de mai jos rezolvă această problemă în același
fel, și anume folosesc mai multe simboluri consecutive pentru una
dintre semnificații, și un singur simbol pentru cealaltă. De
exemplu, primul program, grep, utilizează semnul * pentru
asterisc (operația) și \*
pentru caracterul steluță.
Am scris în iunie 1997 un articol despre shell-ul din Unix (mai curînd ca să explic ce este, decît ca să arăt cum se folosește un shell modern sofisticat). Pe scurt, shell-ul este un program care poate fi folosit pentru a da comenzi sistemului, de obicei în mod interactiv.
Shell-ul Unix tradițional recunoaște niște forme extrem de primitive de expresii regulate, care sunt abreviate într-un mod nu tocmai natural. Pe de altă parte shell-ul permite doar forme restrînse de expresii, așa că voi începe cu el, pentru că e mai simplu.
Următoarele construcții în shell sunt pe post de expresii regulate:
\?
.
\
trebuie să scriem \\
.
a???b
reprezintă toate cuvintele de cinci
litere care încep cu ``a'' și se termină cu ``b''.
a*b
'' asta înseamnă: toate șirurile
care încep cu ``a'' și se termină cu ``b''.
{R,S,T}
. Astfel, expresia {a,b}{c,d}
reprezintă patru șiruri: ac, bc, ad, bd.
În mod normal atunci cînd shell-ul primește o comandă de la
utilizator, încearcă să expandeze expresiile regulate în toate
numele de fișiere de pe disc care se potrivesc. Astfel comanda:
ls .??*
va afișa (comanda ls) toate fișierele al căror
nume începe cu semnul punct și are cel puțin trei caractere.
Chiar și MS-DOS aparent oferea astfel de expresii, dar de fapt era
mult mai limitat: puteai avea cel mult o steluță pentru numele
fișierului și una pentru extensie. În Unix poți spune ceva de
genul rm *a*b*
, care înseamnă ``șterge toate fișierele care
au un a și un b în nume, în ordinea asta''. Deși expresia
*a*b*
pare mult mai complicată, teoria automatelor finite ne
spune că ea poate fi analizată și folosită la fel de rapid ca
orice expresie mai simplă.
În Unix poți spune chiar lucruri de genul: wc -l */*/*.cc
,
pentru a număra liniile (wc -l = word count lines) din toate
fișierele C++ aflate la două directoare adîncime.
grep este un utilitar extrem de util, care a fost proiectat special pentru a manipula expresii regulate. Numele său vine de la Get Regular Expression Patterns, adică ``caută textele descrise de o anumită expresie regulată''. grep primește o expresie regulată și o listă de fișiere, și caută în acele fișiere liniile care conțin cuvinte descrise de acea expresie regulată.
Există mai multe specialități de grep, și cel mai comun dintre ele are tot felul de opțiuni (de genul: tipărește numerele de linie, ignoră diferența între literele mari și mici, tipărește numai ce nu se potrivește, etc.). Noi vom studia doar funcționalitatea de bază.
grep oferă o paletă foarte largă de operații pentru a descrie expresiile regulate, care se dovedesc a fi foarte utile. Pentru că unele semne sunt atît litere cît și operatori în ER, regulile sunt cam încîlcite; le voi prezenta aici pe cele mai importante:
.
(punct) ține locul oricărui caracter, mai
puțin sfîrșitul de linie. De exemplu, expresia ``...
''
descrie orice cuvînt de trei litere.
a.b
înseamnă orice
cuvînt de trei litere care începe cu ``a'' și se termină cu ``b''.
\(
și \)
, și se pot
folosi pentru a schimba ordinea de aplicare a operațiilor.
\|
, ex. \(a\|b\)c
reprezintă cuvintele ac și bc.
(ab)*
.
\
pentru a fi
exprimate; de exemplu, \*
reprezintă chiar o steluță.
[a-f]
pentru a indica toate caracterele
între a și f, inclusiv (a,b,c,d,e,f).
[^a-f]
pentru toate caracterele care nu
sunt între a și f.
[acfz]
pentru unul dintre caracterele a, c, f
sau z.
^
este începutul de linie.
$
este sfîrșitul de linie. \?
se aplică ca steluța, după o expresie, și
înseamnă: ``expresia anterioară de zero sau una ori''. Cu alte
cuvinte, expresia ab\(c\?\)
reprezintă cuvintele ab și abc.
R\{2,4\}
. De exemplu, expresia \(ab\)\{1,3\}
descrie cuvintele ab, abab și ababab.
Dacă invocăm grep astfel: grep ER fisier, atunci se întîmplă următoarele lucruri:
^.*
ER.*$
.
De ce schimbă grep expresia regulată în acest fel? Prin definiție, grep tipărește toate liniile care conțin un cuvînt care se potrivește cu expresia regulată. Faptul că linia conține un cuvînt potrivit este același lucru cu faptul că linia are niște caractere oarecare, cuvîntul și apoi alte caractere. Ceea ce e totuna cu a zice că linia însăși se potrivește cu expresia regulată expusă mai sus!
Notația asta pare complicată, și chiar este. grep este însă o sculă de neînlocuit, pe care eu o folosesc zilnic.
În realitate, problema este și mai complicată: adesea tastăm comanda grep chiar în linia de comandă a shell-ului. De aceea, shell-ul, înainte de a executa comanda, face anumite substituții în linia de comandă, și abia apoi pasează programului grep argumentele.
De exemplu, dacă tastăm în shell grep a\* fisier
, shell-ul
va interpreta caracterele \*
drept semnul steluță (protejată
pentru a nu fi interpretată ca o expresie regulată, ci ca un
caracter), și ca atare va trimite drept argumente lui grep doar
șirul a*
, care pentru grep înseamnă altceva decît
a\*
.
Această complicație suplimentară face programarea în shell cu
expresii regulate o treabă foarte neplăcută. Există însă o
soluție simplă: întotdeauna înveliți argumentele lui grep
cu ghilimele simple ''
, și atunci shell-ul nu se va mai atinge
de ele. Veți scrie deci
grep 'a\*' fisier
.
Dacă sunteți amator de expresii încîlcite, puteți scrie și
grep a\\\* fisier
, pentru că shell-ul va trimite lui
grep din \\
doar un \
, și din \*
doar
steluța.
awk este un limbaj de programare foarte simpatic, creat de trei inși celebri: Aho, Weinberger și Kernighan (ultimul autor a creat și limbajul C); inițialele celor trei au dat și numele limbajului. awk seamănă puțin cu grep, în sensul că procesează fiecare linie din fișier separat. Spre deosebire de grep, awk este un limbaj complet de programare, cu variabile, bucle, etc.
În prezent limbajul awk a fost subsumat complet de Perl, care este mult mai eficient, așa că nu-l voi mai discuta aici.
sed înseamnă Stream EDitor, adică un editor de texte ne-interactiv. În sed ``bagi'' un fișier și un program cu reguli de ``re-scriere''. sed parcurge fișierul de obicei linie cu linie și îl rescrie conform cu aceste reguli.
Ca și awk, sed este complet surclasat de limbajul Perl.
Voi indica aici doar una din trăsăturile lui, care este foarte
folosită în Perl: comanda s///
. Această comandă are forma
s/ER/inlocuire/
. s vine de la substituție.
Expresiile regulate sunt foarte asemănătoare cu cele ale lui
grep.
Iată algoritmul după care operează sed:
Iată un exemplu, pentru clarificare:
sed 's/\([^ ]*\)[ ]*\(.*\)/\2 \1#/' fisier
. Asta
se citește așa:
^
) o
secvență de caractere care nu sunt spații ([^ ]*
).
\( \)
).
[ ]*
.
.*
.
\( \)
).
\2 \1#
. Adică a doua variabilă este pusă la început,
urmată de un spațiu, urmată de prima, urmată de semnul #
.
Dacă aplicăm acest program următorului fișier:
Acesta este un mic fisier pe care incercam un mic program sedobținem
este un mic Acesta# pe care incercam fisier# mic program sed un#
Am mutat astfel primul din linie cuvînt la sfîrșitul liniei!
vi este un editor de texte primitiv, probabil primul editor de texte vizual (numele înseamnă VIsual editor). vi a fost conceput și implementat de Bill Joy, care este unul dintre fondatorii companiei Sun Microsystems, pe vremea cînd era încă student la Universitatea din California Berkeley.
Cu toate că este relativ primitiv, vi oferă o serie de prelucrări cu expresii regulate extrem de puternice, care într-o anumită măsură nu mai sunt oferite de nici un editor actual!
Expresiile regulate și comenzile în vi sunt foarte asemănătoare cu cele din sed. Iată doar un exemplu:
:/Introducere/,/Capitolul/s/<I>\(.*\)<\/I>/<B>\1<\/B>/g
Această comandă complicată trebuie citită după cum urmează:
:/Introducere/,/Capitolul/
Pentru toate liniile între
cea care conține cuvîntul ``Introducere'' și cea care conține
cuvîntul ``Capitolul'' aplică comanda care urmează fiecărei linii.
s
/<I>\(.*\)<\/I>
se citește astfel: orice șir de forma
<I>ceva caractere</I>
5 este salvat în
variabila 1, lucru indicat de paranteze \( \)
.
<B>\1<\/B>
, adică: păstrează textul neschimbat (variabila
\1
) însă înlocuiește I-ul cu un B6.
Figura 2 arată un exemplu de text înainte și după prelucrare.
|
Editorul de texte emacs este un editor non-wyswyg extrem de puternic, și editorul meu favorit7; acest text a fost scris cu emacs; Ca și vi, emacs oferă expresii regulate puternice în comenzi de genul search-and-replace. Nu le voi discuta aici, însă; sintaxa și expresivitatea sunt foarte asemănătoare cu vi.
lex și flex sunt două generatoare de analizoare lexicale pentru construcția de compilatoare. flex este o generație mai nouă. Aceste programe primesc un fișier cu descrieri de expresii regulate, și cu acțiuni asociate fiecărei expresii. Apoi aceste programe generează programe C sau C++ (există și jflex pentru Java mai nou) care implementează automatele finite care recunosc aceste expresii regulate, și execută acțiunile indicate de fiecare dată cînd întîlnesc una din ele.
Dar aceste programe se pot folosi și în alte scopuri pentru prelucrarea de texte; de pildă eu am scris un program în întregime în flex care translatează texte scrise într-un subset al limbajului LATEX(un limbaj pentru typesetting -- aranjare în pagină) în text chior. Eu îmi scriu toate articolele pentru PC Report în LATEX, dar le transform în text înainte de a le trimite revistei.
Iată un exemplu de program simplu scris în lex adună toate numerele naturale dintr-un fișier, indiferent unde apar ele:
%{ #include <stdio.h> #include <stdlib.h> int suma=0; %} CIFRA [0-9] NUMAR {CIFRA}+ %% {NUMAR} { suma += atoi(yytext); } . /* ignora orice nu e un numar */ %% int yywrap() { return 1;} int main() { yylex(); printf("Suma %d\n", suma); return 0; }
În fine, voi încheia cu o sumară prezentare a expresiilor regulate în Perl. PC Report a publicat cel puțin un articol despre acest interesant limbaj, și poate va continua seria.
Perl vine de la Practical Extraction and Report Language: un limbaj pentru extras date și produs rapoarte. Este un limbaj minunat pentru a procesa fișiere text și a face transformări sofisticate. Pînă de curînd era limbajul preferat pentru a genera pagini dinamice la serverele de web, deși în ultima vreme are concurenți serioși în limbaje și sisteme ca Python, PHP, Visual Basic, Zope, etc.
Perl este departe de a fi un limbaj ideal, dar repară cu succes problemele expresiilor regulate din celelalte limbaje și scule în mod normal folosite. Perl are următoarele avantaje:
\
în fața unui operator pentru
expresii regulate. Dacă puneți acest semn, caracterul devine un
caracter obișnuit, și nu un operator.
Voi menționa o construcție Perl care depășește puterea expresiilor regulate. Din această cauză, algoritmii eficienți cunoscuți, care generează automate finite, nu mai funcționează pentru astfel de expresii. Ele trebuie deci folosite cu cumpătare, pentru că anumite cazuri patologice vor necesita extrem de mult timp.
Perl ne permite să scriem ceva de genul: (.*)\1
; asta
înseamnă un șir de caractere urmat de el însuși, deci un cuvînt
care este format din concatenarea a două șiruri identice. Această
exprimare simplifică multe operații, dar nu este o ER în sensul
strict al cuvîntului, deși manualul Perl o numește așa.
Închei acest paragraf cu o listă a operatorilor și prescurtărilor cele mai importante din Perl (nu toate!). ER din Perl sunt extrem de bogate, dar învățate încetul cu încetul sunt foarte abordabile. Dacă faceți multe prelucrări pe fișiere, Perl este o sculă absolut necesară în arsenalul dumneavoastră.
Expresie | Semnificație |
\ |
Următorul meta-caracter devine un caracter obișnuit |
^ |
Început de linie |
. |
Orice caracter în afară de newline |
$ |
Sfîrșit de linie |
| |
Alternanță (![]() |
() |
Indică precedența operațiilor |
|
Concatenarea este implicită (nu folosim nici un semn) |
[] |
O mulțime de caractere (ca la grep) |
* |
Operatorul star: R* = R de zero sau mai multe ori la rînd |
+ |
R+ = RR* (R cel puțin o dată) |
? |
R+ = ![]() |
{n} |
R{n} = R exact de n ori |
{n,} |
R{n,} = R de cel puțin n ori |
{n,m} |
R{n,m} = R de cel puțin n dar cel mult m ori la rînd |
\t |
Caracterul tab |
\n |
Caracterul sfîrșit de linie |
\r |
Caracterul retur de car |
\x1B |
Caracter descris în hexazecimal |
\l |
Următorul caracter micșorat (majuscule devin minuscule) |
\u |
Următorul caracter majorat (minusculele devin majuscule) |
\w |
Orice caracter care formează ``cuvinte'' (alfanumerice și _) |
\W |
Orice caracter care nu formează cuvinte |
\s |
Orice spațiu |
\S |
Orice caracter care nu e spațiu |
\d |
Orice cifră |
\D |
Orice caracter care nu e cifră |
\b |
O poziție dintre un cuvînt și un non-cuvînt (zero caractere) |
\B |
O poziție care nu este între un cuvînt și un non-cuvînt |
Există o grămadă de informație despre aceste programe. Pentru utilitarele descrise (grep, sh, sed, awk), paginile de manual interactiv (man) au o grămadă de informații. De asemenea, implementările ``free'' din proiectul GNU (menționat adesea în PC Report, vedeți de pildă articolul meu din iunie 1998) ale acestor utilitare vin cu documentații excelente în formă electronică.
Limbajul Perl are o mulțime de informații interesante on-line, la www.perl.com; toate manualele de bază sunt acolo în formă electronică.
Dacă vreți să învățați mai mult despre expresii regulate, există o grămadă de cărți utile. De exemplu ``Cartea Dragon'', a lui Aho, Sethi și Ullman ``Compilers : Principles, Techniques, and Tools'', de la Addison-Wesley din 1985, care este o carte clasică despre teoria și construcția calculatoarelor. Un tratament foarte teoretic și elegant puteți găsi în cartea lui Dexter Kozen ``Automata and Computability'', de la Springer Verlag, 1999.
Echivalentul lui lex pentru Java are o pagina de web la http://www.jflex.de/, cu tot cu documentație.
Textul asta prezintă o grămadă de detalii; mă și întreb dacă vor fi folositoare cuiva...Cred ca unele dintre lucrurile de aici sunt însă demne de reținut:
Orice programator poate deci folosi expresiile regulate, fără să știe tot ce se ascunde în spatele lor. Nici articolul acesta nu face decît să ridice puțin voalul, făcînd aluzie la o combinație de solide rezultate teoretice și inginerești. Fundamentele solide sunt cele care de fapt fac această ``tehnologie'' atît de eficace.
<I>ceva caractere</I>
5
<I>
în HTML descrie
cuvinte scrise cu litere cursive (I = italic).
<B>
indică caractere grase (B = bold).