Portál AbcLinuxu, 1. května 2025 07:01

Ruby pro začátečníky - 6 (regulární výrazy)

9. 8. 2006 | Jaromír Hradílek
Články - Ruby pro začátečníky - 6 (regulární výrazy)  

Dnes si stručně ukážeme, co to jsou regulární výrazy, a jak je v Ruby použít.

Obsah

1. Stručný úvod
2. Operátor =~
3. Metoda sub (sub!)
4. Metoda gsub (gsub!)
5. Pamatování si vyhovujících řetězců

1. Stručný úvod

Jste-li uživatelem Linuxu nebo jiného systému unixového typu a nebojíte se příkazové řádky, pravděpodobně jste se už s regulárními výrazy setkali. Využívá jich napřiklad streamový editor sed, vyhledávací program grep a spousta textových editorů (vim, kate) a dalších programů (less, awk). Regulární výraz je jakýsi vzor, podle nějž se vyhledává řetězec v textu, obvykle za účelem provedení nějaké operace, typicky náhrady za jiný řetězec nebo extrakce určitých údajů.

Regulární výrazy jsou samy o sobě látkou dosti obsáhlou a jejich detailní výuka není účelem tohoto seriálu – od toho jsou zde povolanější, například vynikající seriál Pavla SatrapyRegulární výrazy. Pokud jste se s nimi tedy dosud nesetkali, doporučuji po přečtení tohoto článku prostudovat výše zmíněné materiály. Ačkoli jejich zvládnutí není pro další pokračování v jazyku Ruby nezbytné, jejich ignorací se připravíte o velmi silný nástroj, a to nejen v Ruby.

Jak už jsem řekl v úvodu, regulární výraz je vlastně vzor pro vyhledávání v textu a v jazyce Ruby se uzavírá mezi dopředná lomítka /. Nejjednodušším vzorem je libovolný znak, tyto znaky pak lze zřetězit do slov:

/a/     # Tomuto vzoru vyhovuje libovolný výskyt písmene a.
/ahoj/  # Tomuto vzoru vyhovuje libovolný výskyt slova ahoj.
/1984/  # Tomuto vzoru vyhovuje libovolný výskyt čísla 1984.

Je třeba podotknout, že regulární výrazy jsou ve výchozím stavu case-sensitive, tedy rozlišující velikost písmen. Výrazu /ahoj/ tedy vyhovuje ahoj, zatímco Ahoj nebo AHOJ už ne.

Toto dozajista není nijak zvlášť oslňující. Pravá kouzla však přichází až s výčtem speciálních vzorů, které Ruby rozeznává:

zápis: význam:
[] Výčet znaků, které se mohou na daném místě vyskytovat, včetně rozsahů.
Např. [a-z] vyhovuje všem znakům malé (anglické) abecedy, [Aa] vyhovuje malému i velkému písmenu a.
^ Uvedena jako první znak výrazu zastupuje začátek řádku.
Uvedena bezprostředně za otevírací hranatou závorkou výčtu plní funkci negace, např. [^a] vyhovuje jakémukoli znaku krom malého a.
$ Uveden na konci regulárního výrazu zastupuje konec řádku.
\w Libovolný alfanumerický znak. Ekvivalentní zápisu [a-zA-Z0-9]
\W Cokoli jiného než alfanumerický znak. Ekvivalentní zápisu [^a-zA-Z0-9]
\s Tzv. bílý znak, tedy mezera, tabulátor, nový řádek,… Ekvivalentní [ \t\n\r\f]
\S Cokoli jiného než bílý znak. Ekvivalentní [^ \t\n\r\f]
\d Libovolná jedna cifra. Ekvivalentní [0-9]
\D Cokoli jiného než numerický znak. Ekvivalentní [^0-9]
. Libovolný jeden znak.
* Žádný nebo libovolný počet výskytů předchozího znaku.
+ Jeden nebo libovolný počet výskytů předchozího znaku.
? Žádný nebo jeden výskyt předchozího znaku.
{m,n} Nejméně m, nejvýše n výskytů předchozího znaku.
| Slouží jako logické nebo.
Např.: /maly|velky/ vyhovuje řetězci maly, ale také velky
() Slouží k seskupování znaků, např. /(ha)+/ vyhovuje ha, haha atd.
Text vyhovující výrazu v závorkách je zároveň ukládán do paměti, viz dále.

Jakkoli to zpočátku vypadá komplikovaně, když vám přejdou do krve, stanou se regulární výrazy vítaným pomocníkem, a to zdaleka nejen v Ruby. A abychom nezůstali jen u teorie, pojďme se společně podívat na několik ukázek použití.

Řekněme, že chceme v konfiguračním souboru lokalizovat IP adresu. IPv4 se skládá ze čtyř čísel v rozsahu od 0 do 255, vzájemně oddělených tečkami (např. 192.168.1.3). Způsobů, jak napsat vyhovující vzor, je pochopitelně více, uveďme si jen pár z nich:

/\d+\.\d+\.\d+\.\d+/                              # Nepřesné.
/\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}/              # Lepší.
/[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}/  # Extrém!

První případ je jednoduchý a stručný, není však dostatečně přesný, vzoru totiž vyhoví i neplatná adresa (např. 192.1689.1.3), často však postačí. Druhý příklad je už přísnější, ačkoli stále připouští neplatné adresy, kdy cifra přesahuje 255 (např. 192.999.1.3), což se dá ovšem snadno ošetřit dále v programu. Třetí příklad je přepisem druhého aby bylo vidět, že to lze i složitěji.

Povšimněte si, že před každou tečkou je zpětné lomítko. Samostatná tečka má totiž ve výrazu význam libovolného znaku. Předcházejícím zpětným lomítkem explicitně říkáme, že chceme skutečně tečku. Podobně bychom postupovali i v případě ostatních rezervovaných znaků, včetně zpětného lomítka:

/\d+\s*\+\s*\d\s*=\s*\d+/   # Vyhovuje zápisu součtu dvou čísel, např.:
                            # 15 + 9 = 24
/[a-zA-Z]:\\/               # Vyhovuje uživatelskému promptu Windows,
                            # např.: C:\
/\*[A-Z][A-Z ]+\*/          # Vyhovuje textovým ICQ5 emotikonám, např.:
                            # *THUMBS UP* nebo *JOKINGLY*

Říkal jsem, že Ruby implicitně rozlišuje velikost písmen. Co když ale nevíme, jakým způsobem bude daný text zapsán (typicky přípony souborů stažených z internetu)? Představme si, že máme soubor, kde je na každém řádku uveden název souboru a my chceme vyhledat jen ty ve formátu Ogg Vorbis. Jednou z možností by bylo uvést všechny možnosti:

/^[\w ]+.[Oo][Gg][Gg]/

Toto je sice v případě třípísmenné přípony ještě únosné, kdybychom ale hledali delší text, asi bychom se uzávorkovali. Ruby naštěstí umožňuje rozlišování velikosti písmen explicitně vypnout a to uvedením volby i (case insensitive), již zapisujeme za uzavírací lomítko:

/^[\w ]+.ogg/i

Přehlednější, že ano?

2. Operátor =~

Po zvládnutí základů je na čase ukázat si, jak jich využít přímo v jazyce. Velmi často se používá operátor =~ který zjistí, zda se v řetězci vyskytuje nějaká část vyhovující vzoru. Pokud ano, vrátí číselný index prvního znaku nalezeného řetězce, v opačném případě vrátí hodnotu nil. Ukažme si to třeba na našem příkladu s ICQ emotikony:

irb(main):001:0> "tak to uz je spatny*TIRED*:-P" =~ /\*[A-Z][A-Z ]+\*/
=> 19
irb(main):002:0> ":-* ;-) :-*" =~ /\*[A-Z][A-Z ]+\*/
=> nil
irb(main):003:0>

Protože jako false je v Ruby krom false samotného vyhodnoceno už jen nil a cokoli jiného se automaticky vyhodnocuje jako true, lze operátor =~ bez obav použít k větvení programu:

irb(main):003:0> my_ip = "192.168.1.3"
=> "192.168.1.3"
irb(main):004:0> if my_ip =~ /^\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}$/
irb(main):005:1>   puts "Ok."
irb(main):006:1> else
irb(main):007:1*   print "Zadejte prosim svou IP: "
irb(main):008:1>   my_ip = gets.chomp
irb(main):009:1> end
Ok.
=> nil
irb(main):010:0>

Tento příklad má jednu vadu a to tu, že dále nezkoumá uživatelský vstup. To řeší třeba následující ukázka s cyklem:

irb(main):010:0> my_ip = ""
=> ""
irb(main):011:0> until my_ip =~ /^\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}$/
irb(main):012:1>   print "Zadejte prosim svou IP: "
irb(main):013:1>   my_ip = gets.chomp
irb(main):014:1> end
Zadejte prosim svou IP: Nepovim. :-P
Zadejte prosim svou IP: localhost
Zadejte prosim svou IP: 192.168.1.3    
=> nil
irb(main):015:0> puts my_ip
192.168.1.3
=> nil
irb(main):016:0>

A když už jsme u těch řídících struktur, byla by škoda si neukázat, že lze regulární výrazy bez obav použít i v konstrukci case:

irb(main):016:0> vypocet = "19 - 7 = 12"
=> "19 - 7 = 12"
irb(main):017:0> case vypocet
irb(main):018:1>   when /\d+\s*\+\s*\d\s*=\s*\d+/
irb(main):019:1>     puts "Soucet."
irb(main):020:1>   when /\d+\s*-\s*\d\s*=\s*\d+/
irb(main):021:1>     puts "Rozdil."
irb(main):022:1>   when /\d+\s*\*\s*\d\s*=\s*\d+/
irb(main):023:1>     puts "Soucin."
irb(main):024:1>   when /\d+\s*[:\/]\s*\d\s*=\s*\d+/
irb(main):025:1>     puts "Podil."
irb(main):026:1>   else
irb(main):027:1*     puts "Juj, tak ted jsi me dostal. :-o"
irb(main):028:1> end
Rozdil.
=> nil
irb(main):029:0>

3. Metoda sub (sub!)

Dalším běžným úkonem bývá náhrada textu za jiný. Pro datový typ String je proto definována metoda sub, jejíž zápis je následující:

řetězec.sub(vzor, náhrada)

Představte si, že chcete přenést skript pro nastavení pravidel iptables ze svého laptopu s Debianem na PC se Slackware. Jenže ejhle, všude voláte iptables s absolutní cestou a ta je ve Slackware jiná. Necháme-li stranou, že by byl na takový úkon patrně vhodnější sed (nebo mít ve skriptu cestu uloženou v proměnné), mohla by část vykonávající náhradu vypadat třeba takto:

irb(main):029:0> radek = "/sbin/iptables -A INPUT -i $IN -p tcp --syn -j dos"
=> "/sbin/iptables -A INPUT -i $IN -p tcp --syn -j dos"
irb(main):030:0> radek.sub(/\/sbin\/iptables/, "/usr/sbin/iptables")
=> "/usr/sbin/iptables -A INPUT -i $IN -p tcp --syn -j dos"
irb(main):031:0>

Teoreticky tedy procházíme skript, kdy každý řádek načítáme do proměnné radek a tu pak zpracováváme. Metoda sub nám nicméně obsah proměnné nemění, pozměněný řetězec předává jako návratovou hodnotu. Kdybychom chtěli záměnu aplikovat přímo na proměnnou, použijeme verzi s vykřičníkem:

irb(main):031:0> radek.sub!(/\/sbin\/iptables/, "/usr/sbin/iptables")
=> "/usr/sbin/iptables -A INPUT -i $IN -p tcp --syn -j dos"
irb(main):032:0> puts radek
/usr/sbin/iptables -A INPUT -i $IN -p tcp --syn -j dos
=> nil
irb(main):033:0>

4. Metoda gsub (gsub!)

Metoda sub má jen jednu „nevýhodu“ – uplatňuje se pouze na první výskyt vyhovujícího řetězce:

irb(main):033:0> ":p :p :p".sub(/:p/, ":-P")
=> ":-P :p :p"
irb(main):034:0>

Jsou situace, kdy nám to nevadí nebo toho s výhodou využijeme, jindy se nám to ale nemusí vůbec hodit. Naštěstí existuje metoda gsub, která provede náhradu všech řetězců vyhovujících vzoru:

irb(main):034:0> ":p :p :p".gsub(/:p/, ":-P")
=> ":-P :-P :-P"
irb(main):035:0>

Stejně jako v případě sub má i gsub variantu s vykřičníkem.

5. Pamatování si vyhovujících řetězců

V tabulce jsem se u závorek zmiňoval, že řetězec vyhovující vzoru mezi nimi je ukládán do paměti. Existují dva způsoby, jak se k nim dostat, a prvním z nich je prostřednictvím proměnné $n, kde n je číslo udávající pořadí závorek:

irb(main):035:0> udaj = "NAME=Feyd-Rautha Harkonnen"
=> "NAME=Feyd-Rautha Harkonnen"
irb(main):036:0> if udaj =~ /^NAME=([^ ]+) (.*)$/
irb(main):037:1>   puts "Krestni jmeno: " + $1
irb(main):038:1>   puts "Prijmeni:      " + $2
irb(main):039:1> end
Krestni jmeno: Feyd-Rautha
Prijmeni:      Harkonnen
=> nil
irb(main):040:0>

Druhý způsob – zápis ve tvaru \n – se používá přímo v regulárních výrazech. Dejme tomu, že máme v textovém souboru seznam jmen ve tvaru jméno příjmení, ovšem kvůli snazšímu vyhledávání bychom chtěli příjmení na prvním místě. Toho dosáhneme třeba takto:

irb(main):040:0> jmeno = "John Ronald Reuel Tolkien"
=> "John Ronald Reuel Tolkien"
irb(main):041:0> jmeno.sub!(/^(.*) ([^ ]+)$/, '\2 \1')
=> "Tolkien John Ronald Reuel"
irb(main):042:0>

Seriál Ruby pro začátečníky (dílů: 8)

První díl: Ruby pro začátečníky - 1, poslední díl: Ruby pro začátečníky - 8 (soubory, výjimky, kontakt s OS).
Předchozí díl: Ruby pro začátečníky - 5 (řídící struktury)
Následující díl: Ruby pro začátečníky - 7 (metody a třídy)

Související články

Ruby pro začátečníky - 1
Ruby pro začátečníky - 2 (komentáře, aritmetika, proměnné)
Ruby pro začátečníky - 3 (datové typy)
Ruby pro začátečníky - 4 (vstup, výstup)
Ruby pro začátečníky - 5 (řídící struktury)
Seriál: BASH
Python a PyQt - 1 (úvod)
Python a PyQt - 2 (podmínky, cykly, tlačítka)
Začínáme programovat v jazyce Python
Kommander - 1 (Skriptované GUI)
Kommander - 2 (Starý parser)
Kommander - 3 (Nový parser)
Seriál: Začíname KProgramovať
Programujeme v PERLu - I
Programujeme v PERLu - II

Další články z této rubriky

LLVM a Clang – více než dobrá náhrada za GCC
Ze 4 s na 0,9 s – programovací jazyk Vala v praxi
Reverzujeme ovladače pro USB HID zařízení
Linux: systémové volání splice()
Programování v jazyce Vala - základní prvky jazyka

Diskuse k tomuto článku

Marek Bernát avatar 9.8.2006 07:06 Marek Bernát | skóre: 17 | blog: Arcadia
Rozbalit Rozbalit vše Re: Ruby pro začátečníky - 6 (regulární výrazy)
Odpovědět | Sbalit | Link | Blokovat | Admin
Chápem, že regulárne výrazy sú dôležité, ale neviem prečo treba ich základy duplikovať do každého tutoriálu o programovacích jazykoch. Nebolo by lepšie dať odkaz na (lepší) tutoriál pre regulárne výrazy a zamerať sa len na špecifické operácie konkrétneho jazyka?

Poprosím len koštruktívne komentáre, toto má byť návrh na vylepšenie, ak to nie je zrejmé.
physics.stackexchange.com -- Q&A stránky o fyzike v štýle StackOverflow.
9.8.2006 09:52 Robert Krátký | skóre: 94 | blog: Robertův bloček
Rozbalit Rozbalit vše Re: Ruby pro začátečníky - 6 (regulární výrazy)
Nebolo by lepšie dať odkaz na (lepší) tutoriál pre regulárne výrazy a zamerať sa len na špecifické operácie konkrétneho jazyka?
Mám za to, že to takhle článek udělal. Lehký úvod do problematiky, seznámení, odkaz na tutoriál a popis toho, jak výrazy používat v Ruby (zkratky, operátory atd.).
Marek Bernát avatar 9.8.2006 20:15 Marek Bernát | skóre: 17 | blog: Arcadia
Rozbalit Rozbalit vše Re: Ruby pro začátečníky - 6 (regulární výrazy)
Ospravedlňujem sa autorovi, tú linku som prehliadol :-(
physics.stackexchange.com -- Q&A stránky o fyzike v štýle StackOverflow.
17.8.2006 13:28 Miroslav Pecka
Rozbalit Rozbalit vše Re: Ruby pro začátečníky - 6 (regulární výrazy)
Odpovědět | Sbalit | Link | Blokovat | Admin
Téměř vše o regulárních výrazech (články, hotová řešení, testery, diskuzní fórum) najdete na webu Regulární výrazy.

ISSN 1214-1267, (c) 1999-2007 Stickfish s.r.o.