PHP/Podstawy wyrażeń regularnych

< PHP
Poprzedni rozdział: Przetwarzanie tekstu
Następny rozdział: Obsługa ciastek

Podstawy wyrażeń regularnych

edytuj

W poprzednim rozdziale poznaliśmy proste techniki wyszukiwania i manipulacji danymi tekstowymi. Ponadto dowiedzieliśmy się o funkcjach z serii ctype pozwalających na prostą kontrolę zawartości ciągu pod kątem występujących w nim znaków. Jak sam zapewne zdążyłeś zauważyć, narzędzia te nie posiadają jednak żadnych zaawansowanych funkcji. Czy wobec tego możliwe jest manipulowanie ciągami o złożonej strukturze? Okazuje się, że tak - dzięki wyrażeniom regularnym, których omówieniem zajmuje się niniejszy rozdział.

Istota wyrażeń regularnych

edytuj

Mechanizm wyrażeń regularnych (ang. regular expressions, czasem skracane do regexp) jest tak naprawdę parserem pewnego języka służącego do precyzyjnego definiowania dozwolonego formatu ciągu. Korzystanie z wyrażeń regularnych polega na stworzeniu za jego pomocą tzw. wzorca, a następnie jego porównania odpowiednimi funkcjami z interesującym nas ciągiem. Na wyjściu otrzymujemy informację, czy ciąg pasuje do wzorca, czy też nie.

Wyrażenia regularne mają jeszcze większe możliwości. Dzięki nim wyciągnięcie dowolnych interesujących nas informacji z ciągu nie stanowi kłopotu. Wystarczy, że znamy wzorzec go opisujący, a system wyrażeń zwróci nam dodatkowo tablicę uzyskanych z jego wnętrza danych, których potrzebujemy. Wyrażenia regularne dają nam także dostęp do znacznie bogatszego w możliwości kuzyna funkcji str_replace() z poprzedniego rozdziału. O ile tamta funkcja bezmyślnie zamieniała wszystkie napotkane wystąpienia jakiegoś fragmentu na inny, dzięki wyrażeniom regularnym możemy zdefiniować naprawdę wymyślne mechanizmy zamiany uwzględniające wiele dodatkowych czynników.

Jak widać, wyrażenia regularne to potężne narzędzie, jednak przez to też skomplikowane. Niemniej każdy szanujący się programista powinien znać przynajmniej jego podstawy, ponieważ praktyka zawodowa pokazuje, iż wykorzystywane są one bardzo często.

Pierwszy przykład

edytuj

Nasze pierwsze spotkanie praktyczne z wyrażeniami regularnymi rozpoczniemy od prostego sprawdzenia, czy wypełnione pole formularza zawiera dokładnie jedną cyfrę. Do porównywania wzorca z ciągiem służy funkcja preg_match(), która zwraca ilość wystąpień ciągu według podanego wzorca.

<?php

	if($_SERVER['REQUEST_METHOD'] == 'POST')
	{
		if(preg_match('/^[0-9]$/D', $_POST['cyfra']))
		{
			echo '<p>Wpisałeś cyfrę '.$_POST['cyfra'].'</p>';
		}
		else
		{
			echo '<p>Nieprawidłowe dane! Skrypt wymaga podania cyfry!</p>';
		}
	}
	else
	{
		echo '<form method="post" action="preg1.php">
			Podaj cyfrę: <input type="text" name="cyfra"/><input type="submit" value="OK"/>
			</form>';	
	}
?>

Wykorzystaliśmy tutaj wzorzec /^[0-9]$/. Zawarty jest on wewnątrz ograniczników /. Poza nimi mogą znajdować się jedynie dodatkowe flagi kontrolne i nic więcej. Znak ^ oznacza początek ciągu, a znak $ koniec lub "prawie" koniec zezwalając na zakończenie wyrażenia przejściem do nowej linii \n. Zastosowane dodatkowo D wymusza interpretację $ jako bezwzględnego końca wyrażenia (możliwość dodania \n w niektórych przypadkach może być luką w bezpieczeństwie skryptu). [0-9] definiuje klasę dozwolonych znaków, jakie mogą pojawić się w danym miejscu. Ostatecznie wzorzec ten opisuje wszystkie ciągi składające się z DOKŁADNIE jednego znaku będącego cyfrą z przedziału 0 do 9.

Istnieje jeszcze jeden sposób powiadomienia parsera, ile znaków chcemy tam widzieć. Jest nim użycie kwantyfikatorów zasięgu. Ich składnia jest następująca:

  • {długość} - dozwolona długość określona jest dokładnie.
  • {długość_min,długość_max} - podany jest przedział dozwolonych długości
  • {długość_min,} - określona jest minimalna długość
  • {,długość_max} - określona jest maksymalna długość

Kwantyfikator umieszczamy po znaku lub klasie dozwolonych znaków, zatem nasze wyrażenie będzie miało postać /^[0-9]{1}$/. W wyrażeniach regularnych można stosować kilka predefiniowanych kwantyfikatorów:

  • * - 0 lub więcej
  • + - 1 lub więcej
  • ? - 0 lub 1 (uwaga: znak ten jest także wykorzystywany w innym kontekście)

Klasy znaków

edytuj

Nauczymy się teraz bardziej dokładnego definiowania klas znaków, jakich można używać w danym miejscu ciągu. Zasada podstawowa jest bardzo prosta: jeśli w jakimś miejscu napiszemy "a", to parser będzie się tam spodziewać litery "a" występującej dokładnie jeden raz. Jeżeli zastosujemy klasę znaków, definiujemy w ten sposób listę dozwolonych na danej pozycji znaków dokładnie jeden raz. W obu przypadkach "dokładnie jeden raz" można zmienić na dowolną inną długość za pomocą omówionych wyżej kwantyfikatorów. Zatrzymajmy się jednak dokładniej przy tym zwrocie. Skoro dokładnie jeden raz, czemu w takim razie podany wyżej przykład dla wyrażenia /[0-9]/ akceptuje ciągi liczb o dowolnej długości? Aby lepiej pokazać, co naprawdę wtedy ma miejsce, wpisz w formularzu tekst "9a" - o dziwo także i on zostanie przyjęty, mimo że na drugiej pozycji mamy literę! Co jest nie tak? Nic - wyrażenie działa prawidłowo. Parser po prostu osiągnął jego koniec przy sprawdzeniu pierwszego znaku ciągu i resztę przepuścił bez żadnej kontroli. Dlatego istotne jest powiadomienie o tym, gdzie ma znajdować się koniec ciągu.

Tworząc klasę znaków, możemy stosować się do następujących reguł:

  • Wypisujemy w nawiasach kwadratowych wszystkie dopuszczalne znaki, np. [abcdefgh]
  • Wprowadzamy zakres: [a-h] (dopuszczalne małe litery od "a" do "h")
  • Wprowadzamy kilka zakresów: [a-hA-H] (dopuszczalne duże i małe litery od "a" do "h" i od "A" do "H").

Aby wprowadzić jakiś znak specjalny do klasy, poprzedzamy go backslashem: [a-hA-H\-] - znaki duże i małe od "a" do "h" wraz z pauzą. Dysponując tymi wiadomościami, jesteśmy już w stanie napisać pierwszą funkcję kontrolującą (w ograniczonym stopniu) poprawność adresu e-mail:

<?php

	if($_SERVER['REQUEST_METHOD'] == 'POST')
	{
		if(preg_match('/^[a-zA-Z0-9\.\-_]+\@[a-zA-Z0-9\.\-_]+\.[a-z]{2,4}$/D', $_POST['email']))
		{
			echo '<p>Wpisałeś e-mail '.$_POST['email'].'</p>';
		}
		else
		{
			echo '<p>Nieprawidłowe dane! Skrypt wymaga podania adresu e-mail!</p>';
		}
	}
	else
	{
		echo '<form method="post" action="preg2.php">
			Podaj adres e-mail: <input type="text" name="email"/><input type="submit" value="OK"/>
			</form>';	
	}
?>

Omówmy sobie poszczególne partie tego wyrażenia:

  • /^[a-zA-Z0-9\.\-_]+ - początek adresu składa się z dowolnych znaków alfanumerycznych, kropki, pauzy oraz podkreślenia i jego długość musi wynosić minimum 1 znak.
  • \@ - później ma być małpa
  • [a-zA-Z0-9\.\-_]+ - analogicznej klasy używamy do zdefiniowania domeny.
  • \.[a-z]{2,4}$/ - domena musi kończyć się kropką, po której spodziewamy się domeny nadrzędnej (np. .pl, .com).

W pełni poprawne wyrażenie sprawdzające poprawność adresu jest znacznie bardziej skomplikowane. Zainteresowanych odsyłamy do odpowiedniego dokumentu RFC definiującego je.

PCRE posiada kilka klas predefiniowanych:

  • . - kropka symbolizuje dowolny znak (za wyjątkiem przełamania linii).
  • \d - dowolna cyfra dziesiętna
  • \D - dowolny znak niebędący cyfrą
  • \s - biały znak (np. spacja, tabulator)

Predefiniowane klasy można ze sobą łączyć wewnątrz nawiasów kwadratowych: [\d\s] - dozwolone cyfry dziesiętne oraz białe znaki. Jeżeli po otwierającym nawiasie kwadratowym pojawi się symbol ^, będzie to oznaczać negację klasy: "wszystkie znaki, które NIE należą do wymienionych". Jak zdefiniowałbyś "dowolny znak niebędący cyfrą", czyli klasę \D w tradycyjny sposób?

Poszczególne fragmenty ciągu mogą być ze sobą łączone w większe grupy, ujmowane w okrągłych nawiasach. Są one wykorzystywane w dwóch celach. Po pierwsze, można do nich zbiorczo zastosować kwantyfikator, żądając, aby np. jakiś fragment powtarzał się od 3 do 5 razy. Za pomocą grup eksportujemy także do PHP interesujące nas dane. Przykładowo, do wyrażenia /^(abc)+$/ pasują ciągi "abc", "abcabc", "abcabcabc" itd.

Przedstawimy teraz, jak wykorzystać wyrażenia regularne w innych dziedzinach, niż tylko kontrola formularzy. Załóżmy, że zlecono nam zadanie przeprojektowania bazy danych, ponieważ stara nie spełnia stawianych jej wymagań. Oczywiście musimy napisać jakiś konwerter, który przeniesie automatycznie dane do nowej bazy. Natknęliśmy się jednak na problem: daty utworzenia rekordów zapisywane są w postaci tekstowej, np. "12 Dec 2006, 16:34", zamiast w łatwych do przetwarzania sekundach od 1.1970. Do rozbicia ciągu na poszczególne fragmenty wykorzystamy wyrażenia regularne:

<?php

	$date = '12 Dec 2006, 16:34';

	if(preg_match('/^(\d{1,2}) (Jan|Feb|Mar|Apr|May|Jun|Jul|Aug|Sep|Oct|Nov|Dec) (\d{4})\, (\d{1,2})\:(\d{1,2})/', $date, $found))
	{
		// Co nam zwrocilo...
		echo '<h3>Dane: "'.$date.'"</h3>';
		echo '<p>Dzien: '.$found[1].'</p>';
		echo '<p>Miesiac: '.$found[2].'</p>';
		echo '<p>Rok: '.$found[3].'</p>';
		echo '<p>Godzina: '.$found[4].'</p>';
		echo '<p>Minuta: '.$found[5].'</p>';

		$monthConverter = array('Jan' => 1, 'Feb' => 2, 'Mar' => 3, 'Apr' => 4, 'May' => 5,
			'Jun' => 6, 'Jul' => 7, 'Aug' => 8, 'Sep' => 9, 'Oct' => 10, 'Nov' => 11, 'Dec' => 12);

		echo '<p>Unix timestamp: '.mktime($found[4], $found[5], 0, $monthConverter[$found[2]], $found[1], $found[3]).'</p>';
	}
	else
	{
		echo '<p>Nieprawidłowy format daty!</p>';
	}

?>

W zastosowanym wyrażeniu regularnym pojawia się symbol | - jest to operator alternatywy. Ciąg Jan|Feb|Mar oznacza, że w tym miejscu możemy mieć "Jan" ALBO "Feb" ALBO "Mar". Zauważ, że wszystkie istotne elementy daty zawarliśmy w grupach, a do samej funkcji preg_match() podaliśmy trzeci parametr. Do podanej tam zmiennej zostanie przypisana tablica z treścią pasującego ciągu na indeksie 0 oraz wartościami wszystkich użytych grup na kolejnych indeksach. Teraz możemy już łatwo przekonwertować funkcją mktime() naszą datę na format uniksowy.


Dodatkowe rozwiązania

edytuj

Z biegiem czasu korzystanie z wbudowanych funkcji w PHP staje się coraz mniej opłacalne ze względu na ich niewiarygodną obsługę błędów (poleganie na warningach i errorach, zamiast na wyjątkach) oraz małe możliwości. W nowych projektach powinniśmy korzystać z bibliotek przeznaczonych specjalnie do tego celu, np. biblioteki T-Regx.

  Ta sekcja jest zalążkiem. Jeśli możesz, rozbuduj ją.