Kihagyás

3. gyakorlat

Non-capturing groupok

Mivel a capture tárolása is memóriát igényel illesztés közben, egyfajta extra bookmarkingot, továbbá a gond, hogy ha zárójeles regexet kell másoljunk egy meglévőben refaktoráláskor, elronthatja a csoportok indexelését, végül azért, mert sok nyelv csak legfeljebb tíz csoport capture-ülését támogatja, felmerül az igény, hogy legyen olyan zárójelünk is, ami tényleg csak a műveleti precedenciáért felel, és nem capture-öl. Ezek a non-capturing groupok és a jelük rendszerint a (?: nyitójel és ) csukójel, így Javában is.

Így például a ^(?:blue|red)*(green(?:blue|red|green)*)*$ regex egy olyan stringre illeszkedik, melyet a red, blue, green szavak valamilyen sorrendben, bármelyik bárhányszor, alkotnak, pl. redredgreenredgreenblue, illeszkedés esetén ez az eredeti string kerül a nulladik, és az első green, valamint az azt követő rész kerül majd az egyes csoportba, mert az első green előtti rész non-capturing groupban van. Továbbá szintén non-capturing groupban van a green utáni rész - az iterálton belüli group egyébként is csak egyet kapna el az illeszkedő substringekből, mégpedig a legutolsót.

Java regex alapok

Javában a String osztály boolean matches(String regex) függvényével végezhetünk illesztést egy regexre, pl. "alma".matches(".*m.*") igaz lesz, "alma".matches("a") pedig hamis (ellentétben a greppel, az itteni match függvény a teljes inputra próbálja illeszteni a regexet).

Ha ennél összetettebb feladatot szeretnénk elvégezni (mint pl. capturing groupokkal dolgozni), akkor a java.util.regex.Pattern és a java.util.regex.Matcher osztályok lesznek a barátaink, erre példa:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
String text = "  sanyi";
String regex = "\\s*(.*)";  //note: dupla escape kell, hogy így a regex \s*(.*) legyen
// statikus factory metódussal készítünk egy Pattern-t a regex szöveges reprezentációjából
Pattern pattern = Pattern.compile( regex );
// ezt a patternt használhatjuk az input szövegen egy illesztésre, eredménye egy Matcher
Matcher matcher = pattern.matcher( text );

// ami egyrészt tárolja, hogy illeszkedett-e a pattern a textre
if( matcher.matches() )
{
  System.out.println("A minta illeszkedik a textre. Csoportok: ");
  // és ha igen, akkor pl. a captured groupokat is elkérhetjük.
  for( int groupID = 0; groupID <= matcher.groupCount(); groupID++ )
  { 
    System.out.println( "Group " + groupID + ": " + matcher.group( groupID ) );
    // 0. csoport: "  sanyi", 1. csoport: "sanyi"
  }
} else {
  System.out.println( "A minta nem illeszkedik a textre" );
}

A Java java.util.regex package

A lentebbi kódok egyben itt:

match

A Matcher objektumunk matches() függvénye egy boolt ad vissza, igaz, ha a string, mellyel paraméterezve a matchert létrehoztuk, illeszkedik a reguláris kifejezésre, akinek a matcher függvényével őt létrehoztuk.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
// elkészül a Pattern a regex stringből
Pattern theRegex = Pattern.compile(".*\\btelefon\\b.*");

// ez lesz az input szöveg
String  theText = "alma körte telefon barack";            

// elkészítjük a Matchert
Matcher theMatcher = theRegex.matcher( theText );

// matches() true, ha a szöveg illik a regexre
if( theMatcher.matches() ) {                              
  System.out.println("a regex illik a szövegre");
}else{
  System.out.println("a regex NEM illik a szövegre");
}

groups

A matches() függvény hívása után a Matcher objektumtól, ha volt illeszkedés, el tudjuk kérni az elkapott csoportok tartalmát: groupCount() mondja meg a csoportok számát, group(i) pedig az i-edik csoport tartalmát (ami egy string lesz, 0 <= i <= groupCount(), itt is a 0. csoport a teljes illeszkedő string.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
// regex, mely első és egyetlen capturing groupjába teszi az inputot, az elejéről-végéről trimve a whitespace-t:
Pattern theRegex = Pattern.compile( "\\s*((?:\\S+(?:\\s+\\S+)*)?)\\s*" );

// az input szöveg
String theText = "   a whitespace duplaplusz nemjó   ";

// matchelünk
Matcher results = theRegex.matcher( theText );

if( results.matches() )
{
  // ha volt illeszkedés, elkérhetjük a csoportok számát
  System.out.println( "we have a match! Groups count: " + results.groupCount() );
  // és maguknak a csoportoknak a tartalmát is
  // note: 0-tól <= groupCount()-ig!
  for( int i = 0; i <= results.groupCount(); i++ )
  {
    System.out.println("Group " + i + " is *" + results.group(i) + "*");
  }
} else {
  System.err.println( "Sumthin is wrong" );
}

Ha nem az egész stringre akarjuk illeszteni a regexet, hanem keresünk benne illeszkedő substringet, az a Matcher objektumunk find() metódusával megoldható, szintén booleant ad vissza, és találat esetén feltölti a csoportokat:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
//számjegy-sorozat, esetleg előjellel
Pattern theRegex = Pattern.compile( "[+-]?\\d+" );
String theText = "Ebben a szövegben 3 szám és 12 szó van, vagy még több";
Matcher results = theRegex.matcher( theText );

// a find() metódus matchelő substringet keres
if( results.find() )
{
  // a group() ugyanaz, mint a group(0) : maga a match
  System.out.println( "Egy szám: " + results.group() ); 
  // prints Egy szám: 3
}

Ha mindent meg akarunk keresni, ahhoz a Matcher következő metódusait használhatjuk:

  • A Matcher objektumot létrehozásakor a teljes input texthez kötöttük. A find( int from ) metódusának meg tudjuk adni, hogy hanyadik pozíciótól kezdve keressen találatot.
  • Ha pedig a Matcher talált egy matchet, akkor az end() metódusával kaphatjuk meg azt a pozíciót, ami a vége (és már nincs benne a matchben).

Ezt a kettőt összekombinálva találhatunk egy grep-like searchAll-t:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
Pattern theRegex = Pattern.compile( "[+-]?\\d+" );
String theText = "Ebben a szövegben 3 szám és 12 szó van, vagy -1 több";
Matcher results = theRegex.matcher( theText );

// lastPos-ban tároljuk, hogy honnan kezdje keresni a következőt
int lastPos = 0;                                        
while( results.find( lastPos ) )
{
  // prints '3', then prints '12', then prints -1
  System.out.println( "Egy szám: " + results.group() );
  // a következő keresést a mostani match végétől kezdje el
  lastPos = results.end();                              
}

replace

Flagek helyett Javában a Matcher osztálynak van külön egy replaceAll és egy replaceFirst metódusa, mindkettő megkapja argumentumként, hogy mire cserélje ki a találato(ka)t:

1
2
3
4
5
6
7
8
Pattern theRegex = Pattern.compile( "[+-]?\\d+" );
String theText = "Ebben a szövegben a 27 és a -42 fordulnak elő számként.";
Matcher results = theRegex.matcher( theText );

String replaced = results.replaceAll( "SZÁM" );

// prints Ebben a szövegben a SZÁM és a SZÁM fordulnak elő számként.
System.out.println( replaced );
1
2
3
4
5
6
7
8
Pattern theRegex = Pattern.compile( "[+-]?\\d+" );
String theText = "Ebben a szövegben a 27 és a -42 fordulnak elő számként.";
Matcher results = theRegex.matcher( theText );

String replaced = results.replaceFirst( "SZÁM" );

// prints Ebben a szövegben a SZÁM és a -42 fordulnak elő számként.
System.out.println( replaced ); 

A replacement stringben használhatóak a $0, $1 stb. meták is, melyek helyére a megfelelő captured groupok kerülnek be (note: míg a regexen belül itt is \k jelzi a k. captured csoportot, replace-ben $ a prefix!):

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
// az esetleges előjelet kitesszük egy csoportba, a számot egy másikba
Pattern theRegex = Pattern.compile( "([+-]?)(\\d+)" );
String theText = "Ebben a szövegben a 27 és a -42 fordulnak elő számként.";
Matcher results = theRegex.matcher( theText );

// az előjelet változatlanul, a szám részt zárójelben írjuk ki
String replaced = results.replaceAll( "$1($2)" );

//prints Ebben a szövegben a (27) és a -(42) fordulnak elő számként.
System.out.println( replaced );
Note: a non-capturing zárójelek használata pl. azért is fontos lehet, mert itt a dollár metában csak 0 és 9 közti indexeket parsol fel, tehát maximum kilenc captured grouppal tudunk dolgozni!

Lookahead

Van, amikor jól jön, ha egy-egy pozícióján a stringnek szeretnénk tesztelni, hogy a pozíció utáni rész(nek egy prefixe) illik-e egy mintára, és ha illik, akkor nem feldolgozni azt a részt, hanem onnan folytatni az illesztést, ahol tartottunk, mintha csak "előrenéznénk" az inputot feldolgozás nélkül. Ezt lookaheadnek hívják és a jele Javában (és C++ban is) a (?= nyitó- és ) zárójel. Tehát amikor egy (?=R)T regexet illesztünk egy pozíción, akkor

  • először a motor megnézi, hogy ezen a pozíción kezdődik-e olyan substringje a teljes inputnak, ami illeszkedik R-re;
  • ha nem, akkor itt nincs illeszkedés, ha viszont igen, akkor megpróbálja ugyanerről a pozícióról illeszteni T-t (az már fel fogja dolgozni a karaktereket, ahogy szokta is, amikre illeszkedik).

Hívják a lookaround műveleteket non-consuming csoportnak is, mert nem használja fel az inputból a karaktereket, melyekre illeszkedik.

(note: a grep/sed nem támogatja a lookaheadet -- amennyire hinni lehet az internetnek, a körbenéző operátorokat, mint a lookahead is, csak a PCRE backtracking motorok támogatják)

Van még:

  • negatív lookahead: egy pozíción akkor illeszkedik a (?!R) regex, ha a pozíció utáni stringnek nincs olyan prefixe, melyre illeszkedik R.
  • (pozitív) lookbehind: egy pozíción akkor illeszkedik a (?<=R) regex, ha a pozíció előtti stringnek van olyan suffixe, amire illeszkedik R.
  • negatív lookbehind: egy pozíción akkor illeszkedik a (?<!R) regex, ha a pozíció előtti stringnek nincs olyan suffixe, amire illeszkedik R.

Messze nem minden motor támogatja ezeket a lookaround műveleteket: a std::regex pl. nem támogatja a lookbehindot. A Java motor is csak akkor, ha tud kiszámolni egy előre ismert felső korlátot a lookbehind hosszára, így pl. csillagot nem írhatunk lookbehind kifejezés belsejébe (de pl. egymilliószor való ismétlést igen).

A negatív lookahead hasznos lehet, ha pl. olyan feltételünk van, ami azt mondja, hogy a mintánkat nem követi pl. valamilyen karakter: a q[^u] mintára findolva megkapjuk az összes olyan q betűt az input stringben, ami után jön egy karakter, és az nem u, de egyrészt ez lenyeli ezt a második karaktert is (tehát pl. ha qqx van a stringünkben, akkor csak az első q-ra jelez találatot), másrészt a szó végén lévő q-ra nem illik (mert a [^u]-nak kell egy karakter, ami nem u, de akkor is kell neki egy karakter). Ehelyett a q(?!u) regexet illesztve minden olyan q karaktert megtalálunk a stringben, ami után nem jön u betű, akár a string végén van, akár nem.

Feladatok

  • Javában mérjük ki egy komplikáltabb reguláris kifejezés sok rövid sorra való illesztésével, hogy ha csak matchesre vagyunk kíváncsiak, akkor gyorsabb lehet-e a Pattern egyszeri létrehozása, majd illesztése a String.matches(String regex) függvény sorozatos hívásánál.
  • Írjunk függvényt C++-ban vagy Javában, mely az inputként kapott stringből készít egy olyan stringet, ami az eredeti string minden számjában hármasával pontokat helyez el a szokásos módon! Pl. a van 1001 kiskutyám és 1234567 hópihém, nem csak 4000 stringből készítse el a van 1.001 kiskutyám és 1.234.567 hópihém, nem csak 4.000 stringet.
  • Oldjuk meg kedvenc programozási nyelvünkön a következő dekódoló feladatot (from ggmarkk prog1 stepik):
    • egy, csak kis- és nagybetűket (szorítkozhatunk mondjuk az angol ábécé kis- és nagybetűire) tartalmazó string tömörített változatát kapjuk
    • a tömörítés lényege, hogy az ismétlődő stringeket ismétlésszám + string alakba konvertálja valamilyen módon rekurzívan
    • ha egy karakter ismétlődik valahányszor, az csak egy szám + egy karakter egymás után írva, így pl. 4a az aaaa string egy tömörítése, 21b pedig a bbbbbbbbbbbbbbbbbbbbb stringet kódolja el
    • hosszabb ismétlődő stringeket egy szám + nyitójel + string + csukójel kódol, így pl. 3(ab) az ababab stringet, 4(a) az aaaa stringet, 0(abc) és 65() pedig az üres stringet kódolják
    • lehetnek egymásba ágyazva is a tömörítő jelzetek, így pl. ab3(c10a3(b)) az abcaaaaaaaaaabbbcaaaaaaaaaabbbcaaaaaaaaaabbb stringet kódolja.
    • a feladat egy ilyen módszerrel elkódolt string dekódolása, regex és capturing group használatával.

Utolsó frissítés: 2021-10-23 18:39:09