RkBlog

Hardware, programming and astronomy tutorials and reviews.

Wyrażenia regularne

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.

W PHP zaimplementowane są dwa mechanizmy wyrażeń: wyrażenia kompatybilne z POSIX (nazwy funkcji zaczynają się od prefiksu ereg_) oraz wyrażenia regularne Perla (nazwy funkcji zaczynają się od preg_). W całym podręczniku będziemy używali jedynie tych drugich - nie tylko posiadają większe możliwości, ale również działają znacznie szybciej, co ma niebagatelne znaczenie w przypadku dużej ilości danych. W sieci wciąż spotkać można artykuły demonstrujące wyrażenia POSIX, jednak my odradzamy ich stosowanie.

Pierwszy przykład

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 true, jeżeli zachodzi zgodność.
<?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]$/D. 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 dopuszczając dodatkowo przejście do nowej linii . Dodanie "D" wymusza interpretację $ jako bezwzględnego końca. [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: 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:

Klasy znaków

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ł: 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: 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: 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?

Grupy

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 wyboru. Ciąg Jan|Feb|Mar oznacza, że w tym miejscu chcemy 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.

Na podstawie kursu PHP na Wikibooks, licencja GNU Free Documentation License

RkBlog

Podstawy PHP, 14 July 2008, Piotr Maliński

Comment article