Polskie znaki w PHP – dramat w czterech aktach

Modyfikuję ostatnio fragment serwisu WWW zakodowanego w standardzie ISO-8859-2 na co nie mam zupełnie wpływu (nie jestem autorem i nie mogę przerobić całości). Wszystko działało super, bo przecież ten standard na ogół nie sprawia problemów, póki nie pojawiła się konieczność zastosowania pluginu jQuery (o początkach pisałam kilka dni temu) za pomocą którego realizuję autouzupełnianie w polach formularza. Okazało się, że ten plugin korzysta z kodowania w standardzie utf-8 a to już prowadzi do konieczności konwersji łańcuchów znaków w te i wewte.

Konwersja stringów – akt pierwszy

Ponieważ raczkuję zarówno jeśli chodzi o jQuery jak i o JavaScript w ogóle a w manualu pluginu nie mogłam się tego doszukać, to metodą dedukcji wywnioskowałam, że dane wpadają do tego modułu w kodowaniu utf-8. Ha… Ale ja muszę je zaprząc do przeczesania bazy, która jest w ISO-8859-2! No to konwertujemy ciągi. W PHP w wersji 4.0.5 i w starszych mamy do dyspozycji zgrabną funkcję iconv(). Funkcja ta przyjmuje trzy parametry: format z jakiego kodujemy, format do jakiego kodujemy i string, który chcemy przekodować (po szczegóły odsyłam do manuala).

Super, tylko okazało się, że niestety na serwerze na którym pracuje modyfikowana przeze mnie aplikacja jest PHP w wersji 4.0.0 i nie ma możliwości aby to zmienić. Cóż było robić. Trzeba zastosować stworzyć odpowiednią funkcję. Poniżej kod:

define('UTF8_TO_ISO88592', 1);
define('ISO88592_TO_UTF8', 2);
define('WIN1250_TO_UTF8', 3);
define('UTF8_TO_WIN1250', 4);
define('ISO88592_TO_WIN1250', 5);
define('WIN1250_TO_ISO88592', 6);

function plCharset($string, $type = ISO88592_TO_UTF8) {
	$win2utf = array("\xb9" => "\xc4\x85", 	"\xa5" => "\xc4\x84", 
	"\xe6" => "\xc4\x87", "\xc6" => "\xc4\x86", 
	"\xea" => "\xc4\x99", "\xca" => "\xc4\x98", 
	"\xb3" => "\xc5\x82", "\xa3" => "\xc5\x81", 
	"\xf3" => "\xc3\xb3", "\xd3" => "\xc3\x93", 
	"\x9c" => "\xc5\x9b", "\x8c" => "\xc5\x9a", 
	"\xbf" => "\xc5\xbc", "\x8f" => "\xc5\xbb", 
	"\x9f" => "\xc5\xba", "\xaf" => "\xc5\xb9", 
	"\xf1" => "\xc5\x84", "\xd1" => "\xc5\x83");
	$iso2utf = array("\xb1" => "\xc4\x85", "\xa1" => "\xc4\x84", 
	"\xe6" => "\xc4\x87", "\xc6" => "\xc4\x86", 
	"\xea" => "\xc4\x99", "\xca" => "\xc4\x98", 
	"\xb3" => "\xc5\x82", "\xa3" => "\xc5\x81", 
	"\xf3" => "\xc3\xb3", "\xd3" => "\xc3\x93", 
	"\xb6" => "\xc5\x9b", "\xa6" => "\xc5\x9a", 
	"\xbc" => "\xc5\xba", "\xac" => "\xc5\xb9", 
	"\xbf" => "\xc5\xbc", "\xaf" => "\xc5\xbb", 
	"\xf1" => "\xc5\x84", "\xd1" => "\xc5\x83");
	$win2iso = array("\xa5" => "\xa1", "\x8c" => "\xa6",
	"\x8f" => "\xac", "\xb9" => "\xb1",
	"\x9c" => "\xb6", "\x9f" => "\xbc");
	
	switch($type){
		case UTF8_TO_ISO88592:
			$tab_conw = array_flip($iso2utf);	break;
		case ISO88592_TO_UTF8:
			$tab_conw = $iso2utf;	break;
		case WIN1250_TO_UTF8:
			$tab_conw = $win2utf;	break;
		case UTF8_TO_WIN1250:
			$tab_conw = array_flip($win2utf);	break;
		case ISO88592_TO_WIN1250:
			$tab_conw = array_flip($win2iso);	break;
		case WIN1250_TO_ISO88592:
			$tab_conw = $win2iso;	break;
	}
	return strtr($string, $tab_conw);
}

Na początku (jeszcze przed funkcją) znajduje się definicja sześciu stałych, których nazwy mówią z jakiego do jakiego standardu będziemy kodować a wartości będą wykorzystywane w funkcji. Następnie zdefiniowano trzy tablice zawierające komplety odpowiadających sobie znaków przy przejściu z WIN1250 do UTF-8, z ISO 8859-2 do UTF-8 oraz z WIN1250 do ISO 8859-2. Pozostałe trzy kombinacje można otrzymać odwracając tablice. Następnie trzeba dokonać wyboru tablicy w zależności od parametru wejściowego w tym celu pojawia się konstrukcja switch. Na koniec funkcją strtr() zamieniamy odpowiednie znaki na ich zamienniki i zwracamy przekonwertowany string poprzez instrukcje return.

Oczywiście zamiast tablic można było użyć stringów, bo funkcja strtr() przyjmuje również takie parametry, zamiast funkcji strtr() można było użyć czegoś bazującego na wyrażeniach specjalnych aby było szybciej. Tym razem postawiłam na prostotę i przejrzystość konstrukcji. Przykładowy skrypt pokazuje jak działa konwersja stringów w różne strony (najpierw string w ISO-2 konwertowany jest do utf-8 i win a potem te stringi konwertowane z powrotem do iso).

Jedna uwaga. Nie przez niechlujstwo użyłam cudzysłowów (“) zamiast apostrofów (‘). Nie użyłam znaków ale ich kodów stąd konieczność korzystania z cudzysłowów. Zresztą można sprawdzić samemu, że z apostrofami to nie zadziała.

Kłopotliwe strtolower() i reszta rodzinki – akt drugi

Chcąc zastosować Autocomplete, plugin jQuery, o którym już wspominałam, posiłkowałam się przykładami i demo zamieszczonymi w jego dokumentacji. Podeszłam bardzo optymistycznie do sprawy i po prostu skopiowałam sobie fragment kodu:

$q = strtolower($_GET["q"]);
if (!$q) return;
$items = array("Great <em>Bittern</em>"=>"Botaurus stellaris",
"Little <em>Grebe</em>"=>"Tachybaptus ruficollis");

foreach ($items as $key=>$value) {
	if (strpos(strtolower($key), $q) !== false) {
		echo "$key|$value\n";
	}
}

Jak widać po pobraniu zmiennej ‘q’ z tablicy $_GET łańcuch znaków od razu jest konwertowany do małych liter za pomoca funkcji strtolower(). Zupełnie nie wiem po co, bo jak się potem okazało bez względu na to co się wpisuje w okienko to i tak jest w samum pluginie konwertowane do małych liter. Tak więc coś w stylu masło maślane.

Nie mniej jednak trochę mnie ta linijka kodu przećwiczyła. Dlaczego? Proszę rzucić okiem na tabelę. Dane w trzeciej kolumnie wyprodukowane są w następujący sposób. W pierwszych trzech wierszach zrobiono najpierw konwersję a potem strtolower() uzyskanego skryptu. W dwóch ostatnich wierszach zrobiono powrotną konwersję do ISO tych stringów, które wcześniej zostały potraktowane funkcją strtolower(). Jak Wam się podoba efekt? No właśnie to mnie przećwiczyło, bo straciłam trochę czasu nim doszłam do tego, czemu konwersja wywala mi krzaki bez względu na to z czego na co konwertuję. Jak widać w tabeli w kolumnie czwartej ten sam problem dotyczy funkcji strtoupper() i analogicznie funkcji ucfirst(), ucwords(), lcfirst(), dlatego doradzam dużą ostrożność w korzystaniu z tych funkcji.

Własna funkcja do zmieniania wielkości znaków – akt trzeci

Co zatem zrobić, żeby mieć jednak możliwość zmiany wielkości znaków? Nie ma rady trzeba samemu napisać funkcję. Dla uproszczenia pokażę funkcję, która zmienia pierwszą literę każdego wyrazu w tekście na wielką, taki odpowiednik funkcji ucwords() w przypadku stringu zakodowanego w trybie ISO 8859-2.

function FirstUp($string){
	$l2u = array('±'=>'ˇ',	'ę'=>'Ę',	'ó'=>'Ó',
			'¶'=>'¦',	'ł'=>'Ł',	'ż'=>'Ż',
			'Ľ'=>'¬',	'ć'=>'Ć',	'ń'=>'Ń');

	$a_String = explode(' ',$string);
	for($i=0;$i<count($a_String);$i++){
		$iso_l = array_key_exists($a_String[$i][0],$l2u);
		$iso_u = in_array($a_String[$i][0],$l2u);
		if($iso_l)	$a_String[$i][0] = $l2u[$a_String[$i][0]];
		elseif(!$iso_u) $a_String[$i] = ucfirst($a_String[$i]);
	}
	
	$string = implode(' ',$a_String);
	return $string;
}

Najpierw definiujemy tablicę ‘$l2u’, której kluczami są małe polskie litery zakodowane w ISO 8859-2 a wartościami odpowiednie wielkie litery. Następnie string przekazany do funkcji dzielony jest na poszczególne wyrazy (tu dla uproszczenia dzielenie względem spacji, ale żeby to zrobić uczciwie trzeba by było uwzględnić różne dziwne sytuacje), które zostają umieszczone w tablicy.

Po podzieleniu stringu pętla analizuje kolejne wyrazy i sprawdza, czy pierwszy znak wyrazu jest małą polską literą. Robi to analizując klucze tablicy ‘$l2u’ za pomocą funkcji array_key_exists() Jeśli tak, zamienia ten znak na jego wielki odpowiednik. W przeciwnym razie sprawdza, czy pierwszy znak nie jest przypadkiem wielką polską literą (tu wykonujemy analizę zawartości tablicy za pomocą funkcji in_array()). Jeśli nie to znaczy, ze można bezpiecznie zastosować do tego wyrazu funkcję ucfirst(). Po przetworzeniu wszystkich wyrazów składamy je z powrotem do stringu funkcją implode() i tak powstały string jest przez funkcję zwracany. Jak to działa można zobaczyć uruchamiając demo.

Co jQuery wyprawia do spółki z Apachem – akt czwarty

Wszystko by się ładnie potoczyło dalej, bo już zapanowałam nad konwersją polskich znaków między różnymi formatami a nawet napisałam sobie funkcję zmieniającą mi prawidłowo pierwsze litery wyrazów na wielkie (wielce przydatne w moim formularzu z nazwami miejscowości i ulic), gdyby nie to, że całą operację robiłam na lokalnym dysku (tak lubię bo tak mi wygodniej). Po wrzuceniu na serwer docelowy okazało się, że wszystko się kompletnie posypało a w polach podpowiedzi mam znów krzaki.. Dokument HTML kodowany w ISO 8859-2, stringi prawidłowo konwertowane do tego samego formatu a na liście podpowiedzi niemiłe czarne rąby ze znakami zapytania,. W czym rzecz?

Mówią, że diabeł tkwi w szczegółach. W tym jednak przypadku tkwił w nagłówkach. Oczywiście sam dokument HTML był zakodowany w odpowiednim formacie, ale już pliki generujące listę podpowiedzi (po szczegóły odsyłam do artykułów o pluginie Autocomplete Odc.1, Odc.2 i Odc.3) wyświetlały polskie znaki w domyślnym kodowaniu. Od administratora serwera uzyskałam informację, że ichniejszy Apache ma standardowo ustawione kodowanie ISO 8859-2. A jak miał mój? Nic nie miał, znaczy chyba Windows, bo nie przypuszczam, żeby było inaczej.

Na szczęście udało mi się zidentyfikować odpowiedni plik konfiguracyjny. Na ogół jest to plik apache\conf\extra\httpd-languages.conf. Jeśli chcemy mieć z definicji kodowanie wISO-8859-2 to powinien się tam znaleźć wpis:

AddDefaultCharset ISO-8859-2

Oczywiście nie zawsze mamy wpływ na konfigurację serwera. Co wtedy? Na szczęście nie jesteśmy bezbronni. Zawsze w pliku php można ręcznie ustawić odpowiedni nagłówek za pomocą funkcji header(). Wtedy dostosujemy skrypty do odpowiedniego kodowania i nic nam się nie posypie po przeniesieniu na inny serwer.

header("Content-Type: text/html; charset=ISO-8859-2");

Epilog

Taka mnie na koniec naszła refleksja. Różnorodność to piękne zjawisko, na ogół, ale nie koniecznie jeśli chodzi o standardy kodowania polskich znaków. Coś czuję, ze mnie to jeszcze nie raz umęczy.

23 komentarze do wpisu „Polskie znaki w PHP – dramat w czterech aktach”

  1. Ciekawy artykuł :) Ja ostatnio wprowadzałem małe zmiany w stopce forum,
    styl z phpbb by przemo – i kodowałem w UTF-8. Uniwersalnie.
    Jednak cały czas krzaczki się pojawiały. Dlaczego?
    Bo w pliku header.tpl jako kodowanie podane było ISO-8859-2. :)

  2. Heh widze, ze nie tylko ja mam problemy z kodowaniem, a juz najczesciej php->mysql kiedy klient uzywa polskiego iso, baza zapisuje w swedish, a potrzebuje z tego uzyskac utf :F

  3. Właśnie walczyłam ostatnio z taką sytuacją, że w MySQL było w swedish a na stronie ISO 8859-2 (phpBB by Przemo) no po prostu masakra.

  4. Ja mam teraz taki problem – robiłem logowanie,
    użytkownik podaje login i hasło, po czy sprawdzane są z tymi,
    które znajdują się w MySQL. Jednak nie działa to w wypadku
    polskich liter, np. użytkownik ‘Jacuś’ nie może się zalogować.
    Wiesz może dlaczego?

  5. A jesteś pewien, że skrypt, do logowania jest w ISO 88-59-2?
    Dokument musisz mieć w ISO. Czasem trzeba wymusić kodowanie całego pliku zapisując go odpowiednio. Możesz też spróbować wymusić nagłówek albo spróbować wymusić odpowiednie kodowanie (nie wiem jak masz poustawiane wartości dla tej tabeli, dla tego pola i dla całej bazy) przy zapytaniach wysyłając do bazy wpierw zapytanie SET NAMES ‘latin2’. Nie wiem jak masz skonstruowane pliki, więc na dobrą sprawę zgaduję.

  6. Witaj! Coś mi się wydaje, ze uratowałaś mi, jeśli nie życie, to ładnych parę godzin pocenia w związku z ‘strtolower’ w autocomplete jQuery. A wydawało mi się, że niewiele mnie w życiu zaskoczy :) Dzięki za dzielenie się swoimi spostrzeżeniami.

  7. Rozumiem, że zamiast ‘strtolower’ (linia 20 listingu we wskazanym przez ciebie artykule) stosujesz inną funkcję. Jeśłi nie to masz ten sam problem co w wypadku ‘strtoupper’

  8. Witam ;)
    Ciekawa seria artykułów o jQuery Autocomplete, na pewno skorzystam w jakiś sposób ;) Co do polskich znaków to czasem przerzucenie skryptu na utf-8 nie działa, warto też przekonwertować wszystko w notepad++, po tym zawsze jest okej. Nawet pliki javascriptowe, ostatnio alerty w okienkach wyskakiwały mi z krzakami i zastosowałem na nich konwersję utf-8 i poszło, jest normalnie.
    pozdrawiam :)

  9. Ogolnie dosc przyjemny artykul, z tym ze funkcje do zamiany kodowania juz kilka lat temu widzialem (bodajze w manualu php) – troche nieladnie przypisywac sobie jej autorstwo …

    Nie rozumiem jak na serwerze mozna miec wersje php 4.0.0 – kwestie bezpieczenstwa itp … a ta wersja to ile ma lat ? z 10 chyba ???

    Zwroc uwage jeszcze na bardziej optymalne budowanie petli, probuj taka postac:

    for($i=0,$count=count($a_String);$i<$count;$i++){
    …..

  10. Oj nieładnie nieładnie. A pokaż paluszkiem gdzie przypisałam sobie jej autorstwo? :P

    Po drugie nie każdy ma wpływ na to jaka wersja php jest na serwerze.

    Po trzecie kod, który pokazałeś, w kwestii budowy pętli jest super nieoptymalny funkcja count musi się wykonywać za każdym “okrążeniem”. Nie wiem, gdzie tu optymalność.

  11. Wpisując do pola input z autocomplete zapytanie z polskimi znakami – niestety są uwzględniana duże i małe litery. Dla wszystkich pozostałych znaków (nie diakrytycznych PL) działa OK.
    Przykład:
    W bazie siedzi “słowo” i próbujemy go wyszukać:
    Wpisujemy: “słowo” –> Otrzymujemy sugestię: “słowo” i jest OK, ale gdy: Wpisujemy” “SŁOWO” –> Otrzymujemy brak sugestii (ponieważ szukamy wyraz z “Ł” zamiast z “ł”).

    Miałem parę pomysłów na rozwiązanie tej zagadki ale żaden nie był wart opisania go tutaj.

    Poddaję ten temat pod dyskusję.
    Pozdrawiam.

  12. Dobry artykuł, nie tylko dla początkujących. Problem ze znakami miałem w dodatku autocomplete w jquery. Przeglądarka wyświetla sam plik z wynikami poprawnie, a pobrane w jquery podpowiedzi już nie. Dopiero nagłówek header w pliku wyników php eliminije krzaki.

Leave a Reply

%d bloggers like this: