poniedziałek, 26 września 2011

Dzielenie ciągów przy pomocy wielu separatorów

Czasem zdarza się tak, że musimy podzielić nasz ciąg tekstowy w wyznaczonych miejscach na kilka mniejszych. W tej sytuacji funkcja explode() sprawdza się wręcz znakomicie. Problem jednak pojawia się gdy chcemy dzielić za pomocą więcej niż jednego separatora, gdyż samo explode() obsługuje jedynie separator w postaci pojedynczego "stringu". Na poniższych przykładach pokazałem różne sposoby rozbicia ciągu przy użyciu trzech separatorów: & ? /.

Rozwiązania
Najtrywialniejszym sposobem na poprawne rozbicie wg kilku separatorów byłoby wywołanie ponownie explode() dla każdego elementu wynikającego z pierwszego użycia tej funkcji, jednak ten sposób aż bije po oczach swym prostactwem więc nawet nie będę bawił się w jego poprawną implementację, a użyję jedynie takiego (dużego i nierealizującego zadania) przybliżenia:
for ($i = 0; $i < 100000; $i++)
{
	$temp = explode ( "/" , $string);
	foreach($temp as $elem)
	{
		$e = explode("?", $elem);
		$e = explode("&", $elem);
	}
}

Kolejnym pomysłem jest iteracyjne podejście - używanie na przemian explode() i implode() w celu stworzenia pojedynczej tablicy z wynikiem.
for ($i = 0; $i < 100000; $i++)
{
	$temp = explode ( "/" , $string);
	$temp = explode("&", implode("&", $temp));
	$temp = explode("?", implode("?", $temp));
}

Skoro już postanowiliśmy iść w stronę podmian to warto by również rozważyć wielokrotne użycie str_replace().
for ($i = 0; $i < 100000; $i++)
{
	$temp = str_replace("&", "/", $string);
	$temp = str_replace("?", "/", $temp);
	$temp = explode ( "/" , $temp);
}

Nim przejdziemy do wyrażeń regularnych przytoczę tu jeszcze jeden przykład, który niestety działa jedynie w przypadku separatorów będących pojedynczymi znakami. Co ciekawe mimo, iż wykorzystanie funkcji strtok() wydaje się być potwornie nieefektywne to wyniki zbliżone są do naprzemiennego wywoływania funkcji explode() i implode().

for ($i = 0; $i < 100000; $i++)
{
	$tok = strtok($string, "/&?");
	$temp = array();
	while ($tok !== false)
	{
		$temp[] = $tok;
		$tok = strtok("/&?");
	}
	unset($tok);
	strtok('', '');
}

Ale czy mamy ograniczać się jedynie do takich kombinowanych sposobów? Może jest jedna "magiczna" funkcja, która zrobi to o czym myślimy? Ano jest i wykorzystuje magię wyrażeń regularnych, a zwie się ona preg_split().
for ($i = 0; $i < 100000; $i++)
{
	$temp = preg_split("/[\/&?]/",$string);
}

Tutaj jednak warto zwrócić uwagę na fakt, że jeśli naszym separatorem jest znak wielobajtowy to funkcja ta będzie generować dodatkowe puste elementy w wynikowej tablicy (jeden dodatkowy element na każde wystąpienie separatora). Sytuacja ta nie ma jednak miejsca jeśli separatorem nie jest znak a ciąg znaków, nawet jeśli wszystkie zawarte w nim elementy są znakami wielobajtowymi.
Tak czy inaczej remedium na ten problem jest funkcja mb_split():
for ($i = 0; $i < 100000; $i++)
{
	$temp = mb_split("[\/&?]",$string);
}

Ostatnim przytoczonym tu przykładem będzie połączenie wyrażeń regularnych wykorzystanych w funkcji preg_replace() wraz z pojedynczym wywołaniem explode():
for ($i = 0; $i < 100000; $i++)
{
	$temp = preg_replace("/[&?]/","/",$string);
	$temp = explode("/", $temp);
}

Testy wydajności
Jak widać istnieje wiele możliwości wykonania zadania dzielenia ciągu z użyciem wielu równorzędnych separatorów, ale które rozwiązanie jest najefektywniejsze? Popatrzmy na wyniki:

single explode:		    0.1573359966
multiple explode:	    1.0633759499
explode & implode:	    0.7556319237
replace & explode:	    0.4618999958
strtok:			    0.6737470627
preg_split:		    0.9344220161
mb_split:		    1.1594960690
preg_replace & explode:	    1.1028780937

Dla separatorów składających się również z kilku znaków otrzymujemy takie wyniki:

single explode:		    0.1560928822
multiple explode:	    1.3294098377
explode & implode:	    1.1651928425
replace & explode:	    1.0913951397
preg_split:		    1.8065340519
mb_split:		    4.9289889336
preg_replace & explode:	    1.8077979088

Jak widać wszystkie czasy wzrosły, przy czym wzrost mb_split() wynika w dużej mierze z używania wielobajtowych znaków.
Jeśli jednak zostaniemy przy tych trzech jedno-znakowych separatorach, a jedynie zwiększymy rozmiar badanego ciągu otrzymamy takie oto wyniki (10x mniej iteracji pętli testujących):

single explode:		    0.2046608925
multiple explode:	    1.8381199837
explode & implode:	    1.0454828739
replace & explode:	    0.5224039555
strtok:			    1.0597200394
preg_split:		    1.8259940147
mb_split:		    2.1428530216
preg_replace & explode:	    1.8069810867

Jeśli dodatkowo zamiast trzech separatorów użyjemy 5 będziemy mieć takie oto wyniki:

single explode:		    0.2125821114
multiple explode:	    4.1121268272
explode & implode:	    3.3804459572
replace & explode:	    1.6378400326
strtok:			    3.4061551094
preg_split:		    2.7953231335
mb_split:		    6.4811940193
preg_replace & explode:	    3.0788750648

Wnioski
Jak widać wydajność metod różni się w zależności tak od długości badanych ciągów, jak i ilości oraz postaci separatorów. Porównując wyniki możemy jednak dojść do wniosku, że najlepszym rozwiązaniem jest użycie duetu str_replace() wraz z explode(), który to we wszystkich testach okazał się najszybszym rozwiązaniem. Pozostałe metody są w zasadzie porównywalne, choć stosunek ich wydajności różni się w zależności od parametrów zadania (długość ciągu i postać separatorów). Pomimo to możemy jasno stwierdzić, że wielokrotne używanie explode() (multiple explode) jest całkowicie nieefektywne w porównaniu do pozostałych metod (zwłaszcza, że nie zaimplementowano tej funkcji w całości). Podobnie sytuacja wygląda w przypadku mb_split(), którą używać należałoby jedynie w przypadku skomplikowanych podziałów, a nie prostego dzielenia wg podanych słów.

Wykorzystany kod do testowania można pobrać z mojego chomika (link).

Brak komentarzy:

Prześlij komentarz