PHP/Przetwarzanie tekstu

< PHP
Poprzedni rozdział: Ćwiczenia
Następny rozdział: Podstawy wyrażeń regularnych

Przetwarzanie tekstu edytuj

Największa ilość wykonywanych w PHP operacji dotyczy danych tekstowych oraz ich obróbki. Poznamy tutaj kilkanaście funkcji, które będą nam pomocne. W następnym rozdziale zaznajomimy się natomiast z podstawami wyrażeń regularnych pozwalających na znacznie bardziej zaawansowane manipulacje oraz dokładniejszą kontrolę poprawności.

Wyszukiwanie ciągów edytuj

Podstawową funkcją wyszukiwania jednego ciągu w drugim jest strpos() posiadająca także wariant stripos(), w którym nie gra roli wielkość liter. Użyliśmy jej już raz podczas pisania księgi gości do sprawdzenia, czy wprowadzony adres WWW zawiera identyfikator protokołu. Przypomnijmy jeszcze raz ten skrypt:

<?php
 
 	$adresy = array(0 =>
 		'http://www.wikibooks.pl',
 		'www.wikibooks.pl'	
 	);
 	
 	foreach($adresy as $adres)
 	{
 		if(strpos($adres, 'http://') === 0)
 		{
 			// jest http://
 			echo '<p>'.$adres.'</p>';
 		}
 		else
 		{
 			// nie ma http://
 			echo '<p>http://'.$adres.'</p>';
 		}	
 	}
 
?>

Funkcje działają następująco: za pierwszy parametr podajemy ciąg do przeszukania, za drugi - szukany, a następnie patrzymy na rezultat. strpos() powinien zwrócić nam pozycję, od której zaczyna się tekst, którego szukamy, przy czym znaki liczone są od zera. Jeżeli nic nie zostanie znalezione, dostaniemy wartość false. Z tego powodu powinniśmy zawsze wykorzystywać te funkcje z operatorami === oraz !== sprawdzającymi nie tylko wartość, ale i typ zwracanych danych. Oto interesująca sztuczka, która w jednej linijce pozwoli nam na sprawdzenie, czy ciąg został znaleziony i EWENTUALNE pobranie pozycji jego wystąpienia:

<?php
 
 	$zrodlo = 'Litwo, ojczyzno moja!';
 	$szukany = 'moja';
 	
 	if(($id = strpos($zrodlo, $szukany)) !== false) // wlasnie tu
 	{
 		echo 'Szukany ciąg zaczyna się w znaku '.$id;
 	}
 	else
 	{
 		echo 'Nie znaleziono szukanego ciągu';
 	}
 
?>

Jeśli nie rozumiesz zastosowanej tu sztuczki, przypomnij sobie zabawy z wyrażeniami na początku podręcznika. PHP najpierw wykona przypisanie do zmiennej $id: $id = strpos($zrodlo, $szukany). Operator zwróci przypisywaną wartość, która zostanie wykorzystana z kolei do sprawdzenia, czy coś zostało znalezione: [wyrażenie] !== false. W ten sposób mamy jednocześnie wykonane i sprawdzenie, i pozycję pierwszego wystąpienia w zmiennej $id.

Powyższe dwie funkcje znajdują pierwsze wystąpienie szukanego ciągu. Jeżeli chcemy znaleźć ostatnie, musimy posłużyć się funkcjami strrpos() i strripos().

Modyfikacja ciągów edytuj

Zajmiemy się teraz dosyć rozległą kwestią modyfikacji oraz obróbki tekstu. Jest ona niezwykle ważna, jeżeli zamierzasz pisać aplikacje typu forum dyskusyjnego czy księgi gości. Z wiadomych przyczyn nigdy nie powinniśmy zezwolić internaucie na wypisywanie wszystkiego, co mu się podoba i musimy poddać jego tekst elektronicznej obróbce. Za jej pomocą można również ułatwiać formatowanie tekstu. Przykładowo, treść źródłowa tego podręcznika zapisana jest w specjalnym zestawie znaczników dostosowanym do specyfiki Wikibooks. Odpowiednia biblioteka zajmuje się jego przetwarzaniem na język HTML. Tak złożonymi kwestiami nie będziemy się tu jednak zajmować. Wspomniane zostaną przede wszystkim te funkcje dostępne w PHP. Bardziej zaawansowane algorytmy musisz już zaprogramować samodzielnie.

Nagłówki na stronach internetowych przybierają różne style, w zależności od pełnionych funkcji. Część z nich pisana jest kapitalikami, w innych z dużych liter zaczyna się każdy wyraz. Wcale nie musimy zmuszać redaktorów, by to oni zajmowali się wprowadzaniem tytułów w wymaganej postaci. Istnieją odpowiednie algorytmy, które wykonają to za nich. Choć obecnie zadanie sformatowania tytułów można z powodzeniem zrealizować już z wykorzystaniem CSS-a, PHP posiada szereg funkcji do konwersji dużych liter na małe. Pamiętajmy wszak, że PHP to nie tylko strony internetowe.

<?php
 
 	$tekst = 'php jest językiem programowania skryptowego zaprojektowanego dla stron internetowych';
 	
 	echo '<p>Kapitaliki: '.strtoupper($tekst).'</p>';
 	echo '<p>Dużą literą: '.ucfirst($tekst).'</p>';
 	echo '<p>Każdy wyraz dużymi literami: '.ucwords($tekst).'</p>';
?>

Mamy tutaj tekst zapisany w zmiennej (symulującej jakieś źródło danych, ponadto wykorzystamy go kilkakrotnie, stąd ta zmienna) złożony z samych małych liter. Za pomocą funkcji strtoupper() zamieniamy je wszystkie na duże. Odwrotną operację wykonuje strtolower(). ucfirst() zamienia na dużą literę początek pierwszego wyrazu ciągu, a ucwords() - początek każdego wyrazu. Doświadczenie podpowiada nam, że otrzymywane dane nie zawsze są tak klarowne. Jeżeli potrzebne nam automatyczne kapitalizowanie początków zdań, musimy sami napisać odpowiedni algorytm.

Jeżeli chcemy wyciąć fragment jednego ciągu, aby np. poddać go bardziej szczegółowej obróbce, z pomocą przychodzi nam substr(). Podajemy w niej pozycję, od której zamierzamy ciąć oraz (opcjonalnie) ilość znaków do pobrania. Domyślnie funkcja kończy wycinanie na końcu ciągu. W przykładzie połączymy ją z funkcją strpos() do wycięcia nazwiska z personaliów pewnego człowieka.

<?php
 
 	$nazwa = 'Janusz Kowalski';
 	
 	if(($id = strpos($nazwa, ' ')) !== false)
 	{
 		$nazwisko = substr($nazwa, $id + 1); // +1, by zacząć wycinanie od jednego miejsca za znalezioną spacją
 	}
 	else
 	{
 		$nazwisko = '';
 	}
 	
 	echo $nazwisko;
 
?>

W personaliach odnajdujemy spację, która posłuży nam jako marker. strpos() zwróci nam jej pozycję. Przekazujemy ją do substr() jako punkt startowy wycinania i pobieramy ilość znaków równą długości personaliów minus pozycji spacji. W ten sposób w nasze ręce wpadnie nazwisko i będziemy mogli zrobić z nim, co tylko chcemy. Alternatywny sposób rozwiązania tego problemu polega na rozbiciu tego ciągu na tablicę poznaną już funkcją explode(). Przydaje się on, kiedy oprócz nazwiska pragniemy otrzymać także drugi ciąg z imieniem - tu pasuje ona, jak znalazł.

Teraz coś bardziej praktycznego. Na wielu witrynach internetowych można spotkać emotikonki zamieniane na obrazki. Jeżeli nie interesują nas żadne dodatkowe fajerwerki, napisanie konwertera jest bajecznie proste. PHP posiada funkcję str_replace() zamieniającą jeden fragment ciągu na drugi (jej wariant, str_ireplace() nie rozróżnia wielkości liter).

<?php
 
 	$post = 'Tak, zaiste jesteś bardzo zdolny :). Jeszcze nad praktyką popracuj :].';
 
 	// Pierwszy sposób zamiany
 	
 	echo '<p>'.str_replace(':)', '<img src="smile.gif"/>', $post).'</p>';
 	
 	// Drugi, kilka naraz
 	
 	echo '<p>'.str_replace(array(
 		':)',
 		':]',
 		':('	
 	), array(
 		'<img src="smile.gif"/>',
 		'<img src="eye.gif"/>',
 		'<img src="sad.gif"/>'	
 	), $post).'</p>';
 
?>

Pierwszym parametrem jest szukany ciąg, drugim - ciąg docelowy, a trzecim - ciąg, na którym operujemy. Możemy zdefiniować tylko jeden wzorzec do podmiany albo całą grupę, którą podajemy w postaci tablic, jak na przykładzie. Zwróć uwagę, że w przeciwieństwie do omawianego wcześniej substr(), tutaj ciąg, na którym operujemy, wskazywany jest dopiero na końcu.

Zajmując się księgą gości, dbaliśmy, aby do naszych wpisów nie przedostał się HTML. Funkcja htmlspecialchars() zamieniała wtedy znaki specjalne HTML-a na encje, przez co nie mogły być one przetworzone i pojawiały się we wpisie jako statyczny tekst. Okazuje się, że nie jest to jedyne dostępne nam rozwiązanie. Możemy znaczniki wyrzucić całkowicie. Różne warianty prezentuje poniższy przykład:

<?php
 
 	$post = '   To jest post
 	kilkulinijkowy. Który należy przerobić tak,
 	aby go <b>fajnie wyświetlać</b>';
 	
 	// Wariant 1
 	
 	echo '<p>'.nl2br(htmlspecialchars(trim($post))).'</p>';
 	
 	// Wariant 2
 	
 	echo '<p>'.nl2br(strip_tags(trim($post))).'</p>';
 
?>

Zarówno strip_tags(), jak i trim() mają pewne ciekawe dodatkowe parametry, o których nie wszyscy wiedzą. Przy wycinaniu znaczników możemy określić, które z nich mają być zostawiane, np. strip_tags($tekst, '<b><i><u><a>') pozostawi nam pogrubianie, kursywę, podkreślanie oraz linki. trim() natomiast niekoniecznie musi wycinać białe znaki - wystarczy, że podamy nasz własny zestaw jako drugi parametr, a możemy oszczędzić sobie dużo pracy.

ASCII edytuj

Kiedy powstawały komputery, powstała konieczność stworzenia uniwersalnego standardu kodowania znaków alfabetu za pomocą kodów liczbowych rozumianych przez maszyny. Umożliwiłoby to pisanie dokumentów, które da się przenosić między maszynami bez konieczności ich konwersji. Tak narodził się siedmiobitowy standard ASCII zawierający 128 kodów. Jego cechą szczególną było istnienie tzw. kodów sterujących (od 0 do 32), dzięki którym można było nawet sterować pracą niektórych urządzeń. W latach późniejszych wykorzystano także ósmy bit (kody od 128 do 255). Jego wykorzystanie różni się w zależności od wariantu systemu kodowania.

PHP udostępnia kilka funkcji ułatwiających pracę z ASCII. Na początku zapoznamy się z funkcjami chr() oraz ord() konwertujących kod na odpowiadający mu znak i vice versa. Oto prosty skrypt , wykonujący to zadanie i pobierający dane z formularza HTML.

<?php
 
 	if($_SERVER['REQUEST_METHOD'] == 'POST')
 	{ // 1
 		if($_POST['kierunek'] == 0) // 2
 		{
 			if(strlen($_POST['dane']) != 1)
 			{
 				die('<p>Nieprawidłowe dane. Proszę podać JEDEN znak!</p>');			
 			}
 			echo '<p>Kod znaku <b>'.$_POST['dane'].'</b> to '.ord($_POST['dane']).'</p>';
 		}
 		else
 		{ // 3
 			if(!ctype_digit($_POST['dane']))
 			{
 				die('<p>Nieprawidłowe dane. Proszę podać kod znaku!');			
 			}
 			echo '<p>Kodowi <b>'.$_POST['dane'].'</b> odpowiada znak '.chr($_POST['dane']).'</p>';		
  		}	
 	}
 	else
 	{ // 4
 		echo '<form method="post" action="ascii.php">
 		<h2>Informator ASCII</h2>
 		<select name="kierunek">
 		<option value="0">Sprawdź kod znaku</option>
 		<option value="1">Sprawdź znak pod kodem</option>
 		</select>
 		<input type="text" name="dane"/>
 		<input type="submit" value="OK"/>		
 		</form>';	
 	}
?>

Formularz przesyła nam dwie informacje:

  • Kierunek - określa, czy konwertujemy znak na kod, czy też kod na znak.
  • Dane - znak albo kod do zamiany.

Oto omówienie działania:

  1. Dotarły do nas dane z formularza.
  2. Konwersja ze znaku na kod. Sprawdzamy, czy pole dane zawiera dokładnie jeden znak. Jeśli tak, wyświetlamy wynik.
  3. Konwersja z kodu na znak. Funkcją ctype_digit() sprawdzamy, czy wprowadzony kod składa się wyłącznie z cyfr. Jeśli tak, wyświetlamy wynik.
  4. Formularz do komunikacji ze skryptem.

W powyższym przykładzie wykorzystaliśmy funkcję ctype_digit(), która szybko bada, czy podany ciąg składa się wyłącznie z liczb. Istnieje kilkanaście funkcji z serii ctype. Oto kilka z nich:

  • ctype_alpha($tekst) - sprawdza, czy tekst składa się wyłącznie z liter.
  • ctype_lower($tekst) - sprawdza, czy tekst składa się wyłącznie z małych liter.
  • ctype_upper($tekst) - sprawdza, czy tekst składa się wyłącznie z dużych liter.
  • ctype_xdigit($tekst) - sprawdza, czy tekst zawiera wyłącznie znaki do zapisu liczb w systemie szesnastkowym (heksadecymalnym).

Jeśli żądany przez Ciebie zestaw znaków nie jest udostępniany przez żadną z funkcji, możesz skorzystać z odpowiedniego algorytmu:

<?php
 
 	function ctype($ciag, $zestaw)
 	{
 		for($i = 0; $i < strlen($ciag); $i++)
 		{
 			if(strpos($zestaw, $ciag{$i}) === FALSE)
 			{
 				return false;
 			}
 		}
 		return true;		
 	} // end ctype();
 
?>

Algorytm jest bardzo prosty. Przeszukujemy wprowadzony ciąg znaków $ciag i sprawdzamy funkcją strpos(), czy każdy jego znak zawiera się w zestawie $zestaw. Jeśli nie, przerywamy pracę i zwracamy false. Oto przykładowe użycie:

echo ctype('555-1234', '0123456789-');

Jeżeli poszukujesz jeszcze bardziej złożonych związków między znakami, musisz posłużyć się wyrażeniami regularnymi, z których podstawami zaznajomi Cię następny rozdział.

Formatowanie tekstu edytuj

Korzenie PHP znajdują się w języku C, więc nic dziwnego, że dziedziczy on po nim funkcję printf() służącą do prezentacji sformatowanych danych.

<?php
 
 	printf('Liczba szesnastkowa: %x', 3342);
 
?>

Funkcja odnajduje w podanym ciągu specjalne kody formatujące rozpoczynające się od znaku procentu i umieszcza na ich miejscu dane z kolejnych parametrów. W powyższym przykładzie zastosowaliśmy kod %x, który spowoduje wyświetlenie się liczby 3342 w systemie szesnastkowym. Przeglądarka wyświetli zatem:

Liczba szesnastkowa: d0e

Za pomocą kodów formatujących możemy też np. określić precyzję wyświetlania ułamków. Aby wyświetlić liczbę π do 4 miejsc po przecinku, napiszemy:

<?php
 
 	printf('Liczba PI: %0.4f', M_PI);
 
?>

To są jedynie podstawy kodów formatujących. Szczegółowy ich opis znajduje się w dokumentacji PHP. Warto nadmienić także istnienie odmian funkcji printf(), np.

  • sprintf() - zwraca wynik jako ciąg tekstowy.
  • vprintf() - pobiera dane do kodów z tablicy przekazanej jako drugi parametr.
  • vsprintf() - pobiera dane do kodów z tablicy przekazanej drugim parametrem i zwraca wynik jako ciąg tekstowy.

Kodowanie edytuj

Teraz nieco o przechowywaniu haseł użytkowników przez PHP. Generalnie nigdy nie składuje się ich w postaci jawnej oraz nie koduje się algorytmem dwukierunkowym (czyli dającym teoretyczną możliwość ich rozszyfrowania). Powszechna praktyka poleca stosowanie tzw. funkcji haszujących generujących unikalne, równej długości sygnatury niszczące oryginalny przekaz, przez co nie da się ich już rozszyfrować metodą inną, niż sprawdzenie wszystkiego na wszystkim. Wbrew początkowemu wrażeniu sens takiego działania jest bardzo oczywisty. Kiedy użytkownik rejestruje się w serwisie, haszujemy jego hasło i zapisujemy w profilu. Próbując się zalogować, przysyła nam swoje hasło jeszcze raz. Haszujemy je i wynik porównujemy z tym, co mamy w bazie. Jeżeli uzyskamy identyczne hasze, znaczy to, że użytkownik podał hasło poprawnie i może zostać zalogowany.

PHP posiada wbudowanych kilka algorytmów haszujących:

  • crypt() - najstarszy, generuje 8-znakowe hasze. Nie polecamy do celów autoryzacji.
  • md5() - do niedawna najpopularniejszy algorytm. Generował 32-znakowe ciągi, lecz niedawno ujawniono w nim poważne dziury.
  • sha1() - aktualnie najbezpieczniejszy algorytm haszujący dostępny domyślnie w PHP. Generuje 40-znakowe ciągi.

Zwróć uwagę na jedną rzecz: hasze mają stałą długość, dlatego ilość możliwych kombinacji jest ograniczona. Tymczasem możliwych ciągów jest nieskończenie wiele. Dlatego może się zdarzyć, że dwa różne ciągi generują ten sam hasz i zwiemy to kolizją. W dobrym algorytmie, aby doszło do takiej sytuacji, muszą to być naprawdę różne ciągi. Ponadto zmiana już pojedynczego znaku wewnątrz haszowanego ciągu musi powodować utworzenie zupełnie innego wyniku. Siła algorytmu zależy od tego, jak dużo czasu potrzeba na wykrycie kolizji metodą brute-force, czyli sprawdzenia wszystkiego na wszystkim. Algorytm MD5 został już złamany na tyle, że zwyczajny komputer PC jest w stanie znaleźć kolizję już w ciągu zaledwie ośmiu godzin. Dla SHA1 ilość niezbędnych do sprawdzenia kombinacji jest rzędu 2^64, z czym nie jest w stanie poradzić sobie żaden istniejący obecnie komputer.

Przypuśćmy, że $uzytkownicy jest tablicą asocjacyjną taką, że indeks jest nazwą użytkownika, a wartość hasłem zaszyfrowanym w SHA1. Aby sprawdzić, czy użytkownik wpisał poprawne hasło, musimy wykonać następującą czynność:

<?php

    if(isset($uzytkownicy[$_POST['login']]) && $uzytkownicy[$_POST['login']] == sha1($_POST['haslo']))
    {
        echo 'Dziękujemy, podałeś dobre hasło.';
    }
    else
    {
       echo 'Nieprawidłowy login i/lub hasło.';
    }

?>

Skonstruowanie systemu autoryzacji wymaga także zaimplementowania mechanizmu sesji, aby przekazać fakt bycia zalogowanym między poszczególnymi żądaniami. Zajmiemy się tym w dalszych rozdziałach.