Vyhledávání v textu pomocí regulárních výrazů

7. ledna 2004

Občas jsme postaveni před úkol vyhledat v určitém textu konkrétní data určitého typu. Může se jednat například o vyhledání (respektive vypsání) všech odkazů či obrázků obsažených ve zdrojovém kódu webové stránky nebo třeba vyhledání všech e-mailů v XHTML kódu či tisíckrát přeposlaném e-mailu.

Právě takové úkoly bude řešit naše PHP třída cReex (regular expressions extractor). A co že to přesně bude dělat? Konstruktor třídy získá (vyextrahuje) z textu (například z textu e-mailu) požadované údaje (například e-mailové adresy) a uloží je do pole. S tímto polem pak pracují další metody zajišťující:

  • převod všech nalezených údajů na malá písmena – eLower()
  • odstranění duplicitních údajů z pole – eUnique()
  • třídění pole s nalezenými údaji (podle libovolné třídící funkce) – eSort(string třídící_funkce)
  • tisk (zobrazení) nalezených údajů jako seznam s uživatelsky definovaným oddělovačem – ePrint(string oddělovač)

Konstruktor třídy má dva argumenty – název proměnné obsahující text, v němž se má vyhledávat a regulární výraz, který postihuje libovolný konkrétní hledaný údaj (tedy například URL či e-mailovou adresu). Výstupem pak bude pole obsahující všechny údaje (data) obsažené v textu a odpovídající regulárnímu výrazu.

class cReex
{
var $RE; //regulární výraz sloužící k vyhledávání, resp. parsování textu
var $Text; //prohledávaný (vstupní) text
var $arFound; //pole obsahující nalezené údaje (data)
function cReex($InpText, $RegExp)
{
    …
}
…jednotlivé metody
};

Stěžejní práci (tedy extrakci údajů z textu) provede rovnou konstruktor při vytvoření objektu. Tato část si ale zaslouží podrobnější popis. V zájmu srozumitelnosti i pro méně zkušeného programátora si dovolím krátký exkurz do používání funkce eregi.

Funkce eregi() (což je case insensitive verze funkce ereg()) slouží ke zjištění, zda zadaný text odpovídá zadanému regulárnímu výrazu, ale navíc umí „rozsekat“ (rozparsovat) zadaný text podle regulárního výrazu, pokud takový regulární výraz obsahuje „uzávorkované“ subvýrazy.

Jak říká PHP manuál, funkce eregi(string pattern, string string [, array regs]) má tedy dva povinné parametry (pattern – regulární výraz, string – text, který má regulárnímu výrazu odpovídat) a jeden nepovinný. Nepovinný parametr regs označuje proměnnou (pole) do níž mají být uloženy části textu (string) odpovídající jednotlivým částem (subvýrazům) regulárního výrazu (pattern).

Například $pattern="^(.{5})(.*)$", $string je text, v němž budeme vyhledávat, přičemž zavoláme funkci eregi($pattern, $string, $regs). Pokud $string odpovídá regulárnímu výrazu $pattern (v našem případě libovolný řetězec o minimální délce pěti znaků) funkce vrátí TRUE a do pole $regs budou uloženy části textu $string odpovídající „uzávorkovaným“ subvýrazům, přičemž text odpovídající prvnímu subvýrazu bude v $regs[1] a text odpovídající druhému subvýrazu v $regs[2]. Text odpovídající celému regulárnímu výrazu je vždy uložen v $regs[0].

Po exkurzi do používání funkce eregi() (resp ereg()) se můžeme vrátit zpět k našemu problému. Pokud byste však měli zájem o hlubší proniknutí do používání funkce eregi() (resp ereg()) s regulárními výrazy obsahujícími subvýrazy, doporučuji vám článek Regulární výrazy v praxi – subvýrazy.

Obecně použitelné řešení budeme pro názornost demonstrovat na vyhledávání e-mailových adres v libovolném textu. Podívejme se nejdříve na kód konstruktoru:

function cReex($InpText, $RegExp)
{
    $this->RE=“(„.$RegExp.“)(.*)$“;
    $this->Text=$InpText;
    $this->arFound=array();
    
    while(eregi($this->RE,$this->Text,$regs))
    {
        array_push($this->arFound,$regs[1]);
        $this->Text=$regs[2];
    };
}

V proměnné $InpText je uložen text, v němž se vyhledává, $RegExp obsahuje regulární výraz pro e-mail ([a-zA-Z0-9._-]+@[a-zA-Z0-9.-]{2,}\.[a-zA-Z]{2,4}). Proměnná $this->RE je potom složena z regulárního výrazu pro e-mail a regulárního výrazu (.*) pro libovolný text libovolné (i nulové) délky. Pokud se v $InpText nachází alespoň jeden e-mail, bude $this->Text odpovídat regulárnímu výrazu $this->RE a navíc bude samotná e-mailová adresa odpovídat první uzávorkované části (subvýrazu) regulárního výrazu a část (zbytek) textu za nalezeným e-mailem připadne na druhou uzávorkovanou část. V proměnné $regs[1] tak bude právě nalezený e-mail a v $regs[2] veškerý text následující za tímto e-mailem.

Nyní se můžeme podívat na cyklus, který postupně najde a uloží všechny e-maily nalezené ve vstupním textu. Cyklus poběží, dokud text, v němž se má e-mail hledat, odpovídá regulárnímu výrazu $text->RE, tedy dokud (neprohledaný) text obsahuje alespoň jeden e-mail. V každém průchodu cyklem se:

  • nalezený e-mail (který bude vždy v $regs[1]) uloží do pole $this->arFound
  • text, v němž se má v dalším průchodu vyhledávat, zkrátí o právě nalezený e-mail, respektive novým $this->Text textem se stane část textu za nalezeným e-mailem, která je k dispozici v $regs[2]

Stěžejní část máme za sebou, můžeme se tedy podívat na metody, které práci s nalezenými údaji usnadní.

U metody eLower() není snad komentář ani třeba. Pomocí foreach a funkce strtolower() se provede převod na malá písmena u všech prvků pole $this->arFound.

function eLower()
{
    foreach($this->arFound as $key=>$value)
    {
        $this->arFound[$key]=strtolower($this->arFound[$key]);
    }
}

Metoda eUnique() je snad ještě prostší. Použitá funkce PHP array_unique() z pole odstraní duplicitní hodnoty. Konkrétně v případě e-mailů je vhodné nejdříve volat metodu eLower() a až poté eUnique(), protože array_unique() je case sensitive a tak považuje e-maily „honza@inmail.cz“ a „Honza@inmail.cz“ za dva rozdílné.

function eUnique()
{
    $this->arFound=array_unique($this->arFound);
}

Metoda eSort() třídí vyhledané údaje (v našem případě e-maily) s použitím zadané třídící funkce. Název třídící funkce je jediným (nepovinným) parametrem této metody. Pokud nebude parametr zadán nebo bude zadán název neexistující funkce, bude tříděno implicitně pomocí funkce sort(). Pokud budete chtít třídit pomocí jiné třídící funkce, například třídit sestupně pomocí funkce rsort(), stačí zavolat $this->eSort("rsort").

function eSort($sort_function=“sort“)
{
    if (!function_exists($sort_function)) $sort_function=“sort“;
    $sort_function($this->arFound); 
}

Metoda ePrint() by snad také ani komentář nevyžadovala. Jedná se de facto jen o použití funkce implode(string oddělovač, array pole), která spojuje prvky pole do řetězce pomocí oddělovače. Metoda ePrint() má jediný (nepovinný) parametr, a to právě oddělovač jednotlivých nalezených údajů (v našem případě e-mailů).

function ePrint($delim=“\n“)
{
    echo implode($delim,$this->arFound);
}

Tuto třídu si pak můžeme uložit třeba do souboru cReex.php a includovat ji vždy, když budeme potřebovat její služby. Minimalistická ukázka použití by pak mohla mít tento kód:

include(„cReex.php“);
$RegExp=“[a-zA-Z0-9._-]+@[a-zA-Z0-9.-]{2,}\.[a-zA-Z]{2,4}“;
$InpText=“nesmysly honza@inmail.cz taky to není e-mail“;
$seznam=new cReex($InpText, $RegExp);
$seznam->eLower();
$seznam->eUnique();
$seznam->eSort(„natsort“);
$seznam->ePrint(„, „);

Rozhodně netvrdím, že by nebylo ještě co přidat. Jedná se o ukázku řešení, kterou si každý může dotvořit tak, aby vyhovovala jeho specifickým požadavkům . Pro úplnost ještě doplním, že:

  • popsané řešení je jedním z několika možných (alternativou je například použití PCRE funkce preg_match_all() namísto postupného získávání e-mailů pomocí funkce eregi() ve while cyklu)
  • v ukázce uvedený regulární výraz popisující e-mailovou adresu není zcela dokonalý (precizní řešení by totiž vyžadovalo docela obsáhlé vysvětlení, což není předmětem tohoto článku)

Kompletní zdrojový kód třídy je vám k dispozici v ZIP archivu. Co se s touto třídou a trochou snahy dá dokázat, můžete vidět na příkladu aplikace Email Extractor.

Starší komentáře ke článku

Pokud máte zájem o starší komentáře k tomuto článku, naleznete je zde.

Štítky: Články

Mohlo by vás také zajímat

Nejnovější

Napsat komentář

Vaše e-mailová adresa nebude zveřejněna. Vyžadované informace jsou označeny *