Perl (18) - Regulární výrazy - množiny znaků

Ve 3. pokračování o regulárních výrazech jsou představeny třídy znaků.

3.11.2005 13:00 | Jiří Václavík | přečteno 42471×

Množina znaků je skupina obyčejných znaků. Pokud ji uvedeme ve vzoru, potom se taková množina chová jako 1 ze znaků, které obsahuje. Máme několik možností, jak množinu definovat.

libovolný znak

Začneme tím nejjednodušším. Znak tečka zastupuje 1 libovolný znak mimo konce řádků.

Poznámka - Pokud chcete zahrnout do množiny znaků, které tečka zastupuje i znak konce řádku, použijte přepínač /s.

Příklad vypisuje slova s názvem o délce 4:

  @slova = qw(C++ Perl Python Ruby Java);
  foreach $slovo (@slova){
      print $slovo, "\n" if ($slovo =~ /^....$/); #vypíší se slova, které zabírají 4 znaky
  }

Poznámka - Nutno ale poznamenat, že místo cyklu foreach bychom mohli jednoduše použít funkci grep:

  $, = "\n";
  print grep /^....$/, @slova;

Výčet znaků

Je možné specifikovat užší množinu znaků než libovolný znak. K tomu slouží hranaté závorky. Skupina znaků, které jsou v hranatých závorkách, tvoří tuto užší množinu. Znaky nejsou nijak speciálně odděleny, protože to není potřeba.

  print "MATCHED" if "SUSE Linux 9.2" =~ /S[Uu]SE Linux 9\.[123]/;
  print "MATCHED" if "SuSE Linux 9.3" =~ /S[Uu]SE Linux 9\.[123]/;
  print "MATCHED" if "SUSE Linux 9.4" =~ /S[Uu]SE Linux 9\.[123]/;

1. a 2. příklad vrací 1, poslední příklad prázdný řetězec. Číslo 4 totiž není v dané množině povolených znaků.

Pomocí alternace můžeme vytvořit vzor, kterému vyhoví i "SUSE Linux 10.0".

  print "MATCHED" if "SUSE Linux 10.0" =~ /S[Uu]SE Linux (9\.[123]|10\.0)/;

Poznámka - Zde návratová hodnota obsahuje zapamatované hodnoty. Pamatování jsme zatím neřešili.

Zkusme si spočítat, kolika různým řetězcům náš poslední vzor vyhoví. Aby to nebylo nekonečno, vzor nejdříve ukotvíme:

  /^S[Uu]SE Linux (9\.[123]|10\.0)$/

Nyní pojďme znak po znaku (resp. množinu po množině), u každého zjistíme počet možností, a tyto počty mezi sebou vynásobíme. 1. znak, S, je samostatný znak - tedy množina jediného znaku. [Uu] je množina 2 znaků a jsou tedy 2 možnosti. Dále máme až k závorce vždy 1 možnost. Potom následuje 1 z řetězců 9.1, 9.2, 9.3 nebo 10.0. Tedy 4 možnosti. Velikosti množin mezi sebou vynásobíme, a tím získáváme 1*2*1*1*1*1*1*1*1*1*1*4 = 8. Naší šabloně tak vyhoví 8 řetězců.

Pro výčet znaků existuje několik usnadnění. Představte si, že chcete definovat množinu, které vyhovuje libovolné anglické písmeno. Řešení může vypadat například takto: [abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ]. Jistě uznáte, že to není to pravé. Je možné si ale nadefinovat proměnné, kde budou skupinu znaků obsahovat a následně je použít asi takto ve vzoru:

  $pismeno = "[abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ]";
  print "r" =~ /$pismeno/;

Znaky v množinách se speciálním významem

Stále to je příliš těžkopádné. Regulární výrazy nabízejí mnohem lepší řešení. Existuje totiž speciální operátor rozsahu. Ten se zapisuje v syntaxi regulárních výrazů pomlčkou. Předchozí problém s množinou všech písmen se tímto způsobem řeší mnohem pohodlněji. Do vzoru stačí uvést [a-zA-Z]. To samé lze dělat s čísly. [2-5] je totéž, co [2345]. Množninu znaků, které vyhoví číslice šetnáckového zápisu čísla, tak zapíšeme jako [0-9a-fA-F].

Zkuste si tipnout, co všechno vyhoví zápisu ^[x|y]$. Ač to možná intuitivně vypadá jako znak x nebo znak y, má tento zápis úplně jiný význam. Znak | nemá na tomto místě žádnou speciální funkci, proto celému regulárnímu výrazu vyhovuje znak x, y nebo |.

Jedním z těchto speciálních znaků je ^ (negace - pozor!, se začátkem řetězce má společné jen to, že používá stejný symbol). Užijete ji, pokud například chcete definovat množinu, které mají vyhovovat všechny znaky kromě velkých písmen. Píše se bezprostředně za úvodní hranatou závorku (pokud je jinde, svoji funkci ztrácí). Má ten efekt, že to, co je obvykle množinou znaků, je jejím doplňkem. Náš problém s vynecháním velkých písmen vyřešíme takto: [^A-Z] (oproti tomu v zápisu [a-c^d-f] stříška speciální význam nemá, protože již není na 1. pozici).

Stejně jako stříška ztrácí uvnitř hranatých závorek svoji funkci na jiném než 1. místě, ztrácí ji i uzavírací závorka na 1. místě. Pokud je tam uvedete, bere se jen jako znak z množiny. Je-li umístěna jinam, musíte jí předřadit zpětné lomítko. To samé platí pro pomlčku - speciální funkci má pouze, pokud dostane z obou stran nějaké smysluplné hodnoty. V opačném případě totiž buď hlásí Invalid range (je-li na jedné straně písmeno, na druhé číslo, nebo když jsou strany prohozeny), a nebo bere pomlčku jako znak bez speciálního významu (pokud je uvedena na začátku nebo na konci řetězce v hranatých závorkách).

Nejen na alfanumerické znaky se může rozsah vztahovat. Lze zadat osmičkové rozpětí znaků z ASCII tabulky. Například znaky ( (osmičkový kód 050), ) (051), * (052), + (053). Následující 2 zápisy jsou ekvivalentní:

  /^[()*+]$/
  /^[\050-\053]$/

čeština

Operátor rozsahu se na české znaky nevztahuje. Jinak je to ale s předdefinovanými množinami znaků. Použijete-li příkaz use locale; a máte-li správně nastavené proměnné prostředí, vztahují se množiny i na národní znaky.

  use locale;
  print "MATCHED" if "č" =~ /\w/;

Je vytisknuto MATCHED, protože znak s háčkem množině vyhovuje. Zkuste nyní odstranit use locale;. Potom toto tvrzení platit nebude.

Předdefinované množiny

Další elegantní řešení nabízejí předdefinované množiny znaků. Existují speciální escape sekvence, které je zastupují. Přitom se nemusejí uzavírat hranatými závorkami. Pro srovnání do tabulky zahrnuji i tečku.

Escape znakEkvivalentní zápisVýznam
\d[0-9]Číslice
\D[^0-9] nebo [^\d]Cokoliv mimo číslici
\w[_0-9a-zA-Z]Znaky identifikátorů
\W[^_0-9a-zA-Z] nebo [^\w]Cokoliv mimo znaků identifikátorů
\s[\ \n\r\t\f]Bílý znak
\S[^ \n\r\t\f] nebo [^\s]Cokoliv mimo bílého znaku
.[^\n]Libovolný znak mimo znaku nového řádku (chování se dá pozměnit přepínačem s)

Poznámka - Ekvivalenty \w a [_0-9a-zA-Z]) resp. \W a [^_0-9a-zA-Z] v poslední tabulce nejsou skutečně 100% ekvivalenty. Vzory [_0-9a-zA-Z] a \w se mají chovat stejně, ale v případě použití locales tomu tak není. Předveďme si důkaz:

  use locale;
  print "MATCHED" if "č" =~ /\w/;           #true
  print "MATCHED" if "č" =~ /[_0-9a-zA-Z]/; #false

Nyní zapíšeme regulární výraz pro formát 12hodinového zápisu času.

  /^[01]\d[:\.][0-5]\d[:\.][0-5]\d[ ]?[pPaA][mM]$/;

Vysvětleme si, jak se tento regulární výraz vyhodnocuje:

Je tu problém. Našemu výrazu vyhoví i čas 13 - 19 hodin. Musíme použít znak |. 1. operand bude hledat, zda jsou hodiny mezi 00 a 09, pokud ne, 2.operand zjistí zda nejsou ještě mezi 10 až 12.

  /^((0\d)|(1[0-2]))[:\.][0-5]\d[:\.][0-5]\d[ ]?[pPaA][mM]$/;

Regulárnímu výrazu ((0\d)|(1[0-2])) vyhovují čísla 01 až 12, zbytek je stejný jako v minulém příkladu. Celý výraz ještě o něco zjednoduší přepínač /i. Zápis [pPaA][mM] pak může vypadat o něco pohodlněji.

  /^((0\d)|(1[0-2]))[:\.][0-5]\d[:\.][0-5]\d[ ]?[pa]m$/i;

Sice ani toto není dokonalé řešení (vyhoví i takové věci jako "10.10:40pM"), ale pro představu nám posloužilo.

Unicode třídy

Toto asi nebudete nikdy potřebovat, ale koho to zajímá ať čte dál. Třída znaků se zadává pomocí zápisu \p{třída}. Pokud chceme doplněk třídy (tedy vše mimo toho, co do třídy patří), použijeme zápis \P{třída}.

V 1. tabulce jsou jen pro představu 2 Unicode třídy. Existuje jich ale mnohem více (man perlretut).

TřídaVýznam
IsSmmatematický symbol
IsScznaky měny

Znakem měny je například dolar:

  print "MATCHED" if "r" =~ /^\p{IsSc}$/ #nevyhovuje
  print "MATCHED" if "\$" =~ /^\p{IsSc}$/; #vyhovuje

Dále si Perl některé třídy sám definuje.

Mají 2 možnosti zápisu. Buď \p{třída} nebo [:třída:]. Zápis [:třída:] je možný jen uvnitř dalších [ ]. Samotné [:třída:] by totiž znamenalo množinu znaků :, t, ř, í, d, a. Je tedy nutný zápis [[:třída:]].

UNICODE \p{třída}POSIX [:třída:]Význam
IsAlphaalphaAnglická písmena
IsAlnumalnumAnglická písmena a číslice
IsASCIIasciiZnaky ASCII 0-127
IsSpaceblankMezera, tabulátor
IsCntrlcntrlŘídící znaky
IsDigitdigitČíslice
IsGraphgraphGrafické
IsLowerlowerMalá anglická písmena
IsPrintprintTisknutelné znaky a mezera
IsPunctpunctInterpunkce a pomocné znaky
IsSpacePerlspaceBílý znak
IsUpperupperVelká anglická písmena
IsWordwordAlfanumerický znak nebo podtržítko
IsXDigitxdigitŠestnáckové číslo

Použití ukazuje následující kód. Všimněte si odlišných barev hranatých závorek. Vnější definují množinu znaků, jejíž obsahem je třída xdigit.

  print "MATCHED" if "5" =~ /^[[:xdigit:]]$/; #vyhovuje - 5 je hexadecimální
  print "MATCHED" if "a" =~ /^[[:xdigit:]]$/; #vyhovuje - a je hexadecimální
  print "MATCHED" if "g" =~ /^[[:xdigit:]]$/; #nevyhovuje - g není hexadecimální

Perl umožňuje POSIX třídy negovat. Negovaná [:třída:] je [:^třída:].

Na závěr ještě něco k definici unicode tříd. Je nutné vytvořit podprogram se stejným jménem jako třída. Jeho návratovou hodnotou jsou řádky. Je-li na řádku 1 číslo, určuje ASCII znak, určený 16kově. Jsou-li na řádku 2 čísla, oddělená mezerou, jde o rozsah. Definujeme si třídu IsZavorka, které vyhoví 1 ze znaků (, ), {, }, [, ], <, > - tedy závorka kulatá, složená, hranatá nebo lomená.

  print "MATCHED" if "(" =~ /^\p{IsZavorka}$/; #vyhovuje
  
  sub IsZavorka {
      return <<END;
  28 29
  3C
  3E
  5B
  5D
  7B
  7D

  END
  }

Třídy lze definovat i pomocí již definovaných tříd. V takovém případě je do návratové hodnoty podprogramu třeba uvést řádek [+!-&]balík::Trida. Implicitní unicode třídy jsou v balíku utf8. + (sjednocení) funguje pro přidání znaků z příslušné třídy, ! (rozdíl) pro odebrání znaků, - (doplněk) pro všechny znaky mimo znaky z této třídy a & (průnik) vybere pouze znaky, které jsou v této třídě a třídě jiné. Pro definici třídy IsBracket, která má stejný význam jako IsZavorka stačí psát toto:

  sub IsBracket {
      return <<END
  +main::IsZavorka
  END
  }

Další možností definice unicode tříd je podle typu písma. Existují například třídy InHebrew, InMathematicalOperators. Celý seznam je na manuálové stránce perlunicode.

Zdroje

Toto téma by zabralo několik samostatných článků. Mnohem více o unicode třídách a jejich definicích na manuálových stránkách

Online verze článku: http://www.linuxsoft.cz/article.php?id_article=1000