Panel użytkownika
Nazwa użytkownika:
Hasło:
Nie masz jeszcze konta?
Autor: 'kubawal'
Inne artykuły

[C++] Wyrażenia regularne (C++11 / boost)

[artykuł] Artykuł opisuje co to są wyrażenia regularne oraz jak z nich korzystać w języku C++.

Pierwsze spojrzenie

Niniejszy artykuł zacznę od problemu:
Mianowicie zamierzasz napisać program który będzie wyszukiwał w pewnym pliku kodów pocztowych, a resztę odrzucał. I teraz myślisz i myślisz, i myślisz... Cholera jasna, przecież ktoś już musiał wymyślić rozwiązanie! - nagle krzykniesz. I tutaj właśnie z pomocą przychodzą wyrażenia regularne. Co? Nie zaprzątaj mi głowy takimi bzdurami! - pomyślisz i już będziesz chciał zamykac tę kartę w przeglądarce. Zaczekaj.
Wyrażenie regularne to taki wzorzec do którego da się dopasować tylko pewien tekst.
Przykładem takiego wzorca będzie na przykład "\w* to \w*"

Zaczniesz się drapać po głowie, aż w końcu powiesz: "Nic z tego nie rozumiem!". Całe szczęście, bowiem zamierzam cię oświecić:
\wZnak specjalny.Oznacza dowolną literę, cyfrę lub znak '_'
*oznacza "jeden lub więcej"; działa w trybie przyrostkowym
tozwykłe znaki reprezentują same siebie
Do poniższego wzorca można na przykład dopasować: "janek to głupek", "kuba to cymbał", "irena to mądrala", ale nie "janek nowak to inteligent". Bynajmniej nie z powodu pozytywnego znaczenia. Po prostu "nowak" nie pasuje do wzorca "to"

Piszemy program

W tym przykładzie posłużę się wzorcem przedstawionym na początku. Najpierw kodzik, potem wyjaśnienia. W całym artykule w programach będzie używana biblioteka boost::regex, która moim zdaniem jest najlepsza. Istnieją inne implementacje wyrażeń regularnych, lecz nie będziemy sie w to zagłębiać.
C/C++
#include <iostream>
#include <string>
#include <regex> //jeśli mamy standard C++0x
//#include <boost/regex.hpp> // jeśli nie mamy kompilatora zgodnego z standardem C++0x, musimy sami zainstalować bibliotekę boost
//using namespace boost // jw.;narzędzia biblioteki boost zmajdują się w przestrzeni nazw boost
using namespace std; // w C++0x narzędzia z <regex> znajdują się w  std

int main()
{
    string tekst;
    int line = 0;
    regex wzorzec( "\\w* to \\w*" );
   
    cout << "Witaj!\n\007";
    cout << "Pisz. Aby skończyć wćiśnij [Ctrl] + [z] w nowym wierszu\n\n";
   
    while( getline( cin, tekst ) )
    {
        smatch wynik; // tutaj będzie zapisany wynik
        ++line;
        if( regex_search( tekst, wynik, wzorzec ) )
             cout << "Linia " << line << " : " << wynik[ 0 ] << '\n';
       
        cin >> tekst;
    }
    system( "pause" );
    return 0;
}

Wyjaśnienia

C/C++
#include <iostream>
#include <conio.h>
#include <string>
#include <regex> //jeśli mamy standard C++0x
//#include <boost/regex.hpp> // jeśli nie mamy kompilatora zgodnego z standardem C++0x, musimy sami zainstalować bibliotekę boost
//using namespace boost // jw.;narzędzia biblioteki boost zmajdują się w przestrzeni nazw boost
using namespace std; // w C++0x narzędzia z <regex> znajdują się w  std
Załączamy potrzebne nagłówki. W tym artykule używamy implementacji boost, która wchodzi w standard C++0x. Jeśli mamy starszy kompilator,
musimy sami ją zainstalować.

C/C++
int main()
{
    string tekst;
    int line = 0;
    regex wzorzec( "\\w* to \\w*" );
Deklarujemy zmienne. Tworzymy wzorzec, którego klasa ma taka samą nazwę jak jego biblioteka. W konstruktorze podajemy argument
typu string. A czemu te podwójne ukośniki zamiast pojedyńczych? Ano temu, że w C++ znaki które poprzedza \ są zarezerwowane, więc piszemy ukośnik podwójny.
C/C++
cout << "Witaj!\n\007";
cout << "Pisz. Aby skończyć wćiśnij [Ctrl] + [z] w nowym wierszu\n\n";
Powitania i informacje.
C/C++
while( getline( cin, tekst ) )
{
    smatch wynik; // tutaj będzie zapisany wynik
    ++line;
    if( regex_search( tekst, wynik, wzorzec ) )
         cout << "Linia " << line << " : " << wynik[ 0 ] << '\n';
   
}
Oto rdzeń programu, czyli pętla while.
Najpierw pobieramy wiersz tekstu. Teraz ważny moment. Tworzymy zmienną typu smatch, która będzie przechowywała wyniki.
Następnie zwiększamy licznik liń i sprawdzamy czy coś znaleźliśmy za pomocą funkcji regex_search(). Jest dosyć
skomplikowana, więc podam prototyp:
C/C++
bool regex_search( string &, smatch &, regex & );
Wracając, jeśli wynik jest pozytywny, to wypisujemy na ekranie linię i co znaleziono.
Zauważ, że nie piszemy
C/C++
cout << wynik
tylko
C/C++
cout << wynik[ 0 ]
Narazie zawsze dawaj 0, później to się zmieni.
C/C++
system( "pause" );
return 0;
}
  I kończymy program.

To jest podejście podstawowe, teraz zajmiemy się bardziej zaawansowanymi wzorcami.

Więcej o wzorcach

Znaki specjalne

Oto zbiór znaków specjalnych, które można stosowac we wzorcach:
.dowolny pojedyńczy znak
[[:klasa:]]klasa znakowa
{n}licznik
(wyr)grupa
\znaknastępny znak ma specjalne znaczenie
wyr*0 lub więcej
wyr+1 lub więcej
wyr?opcjonalność(0 lub 1)
wyr|wyrlub (alternatywa)
^wyrnegacja
$koniec wiersza
(wyr = wyrażenie)

Przykład

np. "(.+) | (ads){2}" Wzorec ten oznacza "zero lub więcej znaków, albo ciąg 'adsads' "

Rodzaje znaków

\dcyfra dziesiętna
\lmała litera
\sbiały znak(spacja, tabulator itd.)
\uwielka litera
\wlitera, cyfra lub "_"
\Dnie \d
\Lnie \l
\Snie \s
\Unie \u
\Wnie \w

Przykład

np. "\l\l\d" czyli "dwie małe litery, po których następuje cyfra dziesiętna"

Powtórzenia

{n}dokładnie n razy
{n,}n lub więcej razy
{n,m}przynajmniej n i najwyżej m razy
*to samo co {0,}
+to samo co {1,}
?to samo co {0,1}

Przykład

np. "0x*", co znaczy "cyfra '0', po której następuje jedna lub więcej liter 'x' "

Grupowanie

Aby utworzyć grupę, należy użyć operatora (), np. "(fre)|(gul)" - "ciąg 'fre' lub ciąg 'gul' "
Gdyby usunąć nawiasy znaczyło by to "ciąg 'fr' i litera 'e' lub 'g', po czym jest ciąg 'ul' "

Alternatywa

Znak | określa alternatywę. Np. "(FW:|Re:)?(.*)" co oznacza "opcjonalny ciąg 'FW' lub 'Re', po którym następuje 0 lub więcej znaków"
Ten wzorzec pasuje do: "FW: Siema", "Co u ciebie?", "Re: U mnie wszystko gra", ale nie "Re o co ci chodzi?"
Nie dozwolona jest pusta alternatywa,np. "(|uno)". Można natomiast określic kilka opcji: "(jeden|dwa|trzy|cztery)" czyli "ciąg 'jeden' lub ciąg 'dwa'
lub ciąg 'trzy', lub ciąg 'cztery' "

Zbiory i przedziały

Istnieją wbudowane zbiory, czyli \w,\u,\d itd., ale można stworzyć też własne:
[\w @]litera, spacja lub znak @
[a-z]małe litery od a do z
[a-zA-Z]małe i wielkie litery od a do z
[Pp]mała lub wielka litera p
[^zxc]wszystko oprócz z,x i c
Użyliśmy znaku ^, który oznacza mniej więcej to samo, co operator ! w C++
Nie można jednak napisać "[c-a]", a dlaczego, to chyba sam się domyślisz.

Klasy znakowe

Znaki sterujące to tak naprawdę skróty od klas znakowych.

Jeśli chcemy użyć jakiejś klasy piszemy "[[:nazwa_klasy:]]"
alnumdowolny znak alfanumeryczny (\w)
alphadowolny znak alfabetu
blankdowolny biały znak oprócz \n
cntrldowolny znak streujący (np. [Ctrl] + [z])
ddowolna cyfra dziesiętna(\d)
digitjw.
graphdowolny znak graficzny
lowerdowolny mały znak (\l)
printdowolny znak drukowalny
punctdowolny znak interpunkcyjny
sdowolny biały znak (\s)
spacejw.
upperdowolna duża litera (\u)
wdowolna litera, cyfra lub znak _ (\w)
xdigitdowolna cyfra szesnastkowa
Można także używac operatora ^, np. "[[:^alpha:]]", co oznacza "dowolny znak, który nie jest w alfabecie"
UWAGA: Polskie znaki (ę,ą,ś,ń,ć,ź,ż) nie są w alfabecie.

Błędy

Co się stanie, gdy spróbujemy utworzyć niepoprawny wzorzec, np. "[c-a]", "(|bum)" ?
Otóż, konstruktor klasy regex zgłasza wyjątek typu bad_expression. Takie błędy pojawiają się najczęściej, gdy wzorzec jest pobierany od użytkownika.

Podwzorce

Dawno, dawno temu powiedzieliśmy, żeby wydrukować wynik poszukiwań piszemy
C/C++
cout << wynik[ 0 ]
Ale czemu akurat 0 ? Otóż musze ci najpierw wytłumaczyć co to są podwzorce.
Podwzorzec to każde wyrażenie, które znajduje się w nawiasach.
np. we wzorcu "\w*( )?\d*" jest tylko jeden podwzorzec, "( )"
Co to ma wspólnego z tym 0 ? Ma, bowiem jeśli piszemy wynik[0], to mamy na myśli cały wzorzec, a gdy piszemy wynik[1], mamy na myśli pierwszy jego
podwzorzec. Tak też wynik[2], wynik[3] itd. oznaczają kolejne podwzorce we wzorcu.
A co gdy nawiasy są zagnieżdżone, np. "((\w\w\d )|(\d\d\w ))*" ? A więc tak, wynik[1] reprezentuje "((\w\w\d )|(\d\d\w ))", wynik[2] - "(\w\w\d )", a wynik[3]
"(\d\d\w )"

Ale jak sprawdźić, ile jest podwzorców? Dzięki temu, że typ smatch to w sumie wektor, można zastosować metodę size(), która zwraca liczbę elementów,
czyli w tym przypadku podwzorców + 1 (wynik[0] też się liczy). Np. we wzorcu "\w* (\d*)?", wynik.size() == 2.

Składamy wszystko w całość

Dość teorii, teraz pokażemy przykładowy porgram:
C/C++
#include <iostream>
#include <string>
#include <fstream>
#include <regex>
#include <sstream>
#include <conio.h>

using namespace std;

void exit( void )
{
    getch();
    exit( 1 );
}

int main()
{
    string swzorzec;
    regex wzorzec;
    string in;
    ifstream inf;
    cout << "Witam\n";
    cout << "Podaj wzorzec:";
    getline( cin, swzorzec );
    try
    {
        wzorzec = swzorzec;
        cout << "Wzorzec: " << swzorzec << endl;
    }
    catch(...)
    {
        cerr << endl << swzorzec << " nie jest poprawnym wzorcem";
        exit();
    }
    cout << "Podaj plik wejsciowy:";
    cin >> in;
    inf.open( in );
    if( !inf )
    {
        cerr << "Zly plik";
        exit();
    }
   
    int lineo = 0;
    string line;
    while( getline( inf, line ) )
    {
        ++lineo;
        smatch wynik;
        if( regex_search( line, wynik, wzorzec ) )
        {
            cout << "Wiersz " << lineo << " : " << line;
            for( int i; i < wynik.size(); ++i )
                 cout << "\tpasuje.[" << i << "]: " << wynik[ i ] << endl;
           
        }
        else
             cout << "nie pasuje.\n";
       
    }
    return 0;
}
I to by było na tyle.