Czy liczba jest liczbą? (PHP)

Nie dawno chciałem się dowiedzieć, jak najlepiej jest sprawdzać, czy tekst jest liczbą w Javie. Szybko trafiłem na ten oto wpis, którego autor sprawdził dostępne metody pod względem szybkości wykonywania. Wyszło, że najlepiej jest chałupniczo – przejrzeć znaki w stringu po kolei.

Zamieszczony wyżej link grzebie koncepcje używania wyrażeń regularnych do prostych celów w Javie. Zalecano mi ją w jednej z moich poprzednich notek, choć nawet wtedy całość nie wydawał mi się dobrym pomysłem (nie tylko mi).

Jak jednak wyrażenia regularne radzą sobie w językach skryptowych? Na warsztacie dzisiaj PHP.

Test jest prosty, kod zamieszczam poniżej. Poniżej przykładowe wyniki. Jak widać, nie ma w tym wielkiej filozofii, najlepiej sprawują się metody predefiniowane do wykrywania liczby w stringu. Wyrażenia regularne (zarówno preg_match jak i eregi) są gorsze. Najgorzej radzi sobie moja własnoręcznie napisana funkcja.

ctype_digit = 5.85733890533
is_numeric = 6.05098295212
preg_match = 8.32741999626
isNumber = 10.1650650501
eregi = 9.09768795967

Wnioski: jeśli można, używamy funkcji wbudowanych! Ale odkrycie. A wyrażanie regularne itp. zostawiamy na złożone zadania. Nie ma więc sensu wpychać ich wszędzie gdzie się da. Najgorzej jest wyłamywać otwarte drzwi – czyli klepać od początku takie funkcje których implementacje już istnieją.

Może ktoś, albo ja w wolnym czasie zajrzę jak sobie radzi z tym Python (i jego kompilowane wzorce). Można też sprawdzać jak radzą sobie funkcje wykrywające liczby z przecinkiem, albo heksadecymalne.

A i jeszcze jedno – w niektórych próbach eregi miało wyniki słabsze od preg_match, choć rzadziej.

Na koniec kod (choć nie jest zbyt odkrywczy).

<?php
$amount = 999999;
$strings = array();

for($i = 0; $i < $amount; $i++) {
        $strings[] = (string)(rand(111111,999999) * (rand(0,1)) ? -1 : 1);
}

$start = microtime(get_as_float);
for($i = 0; $i < $amount; $i++) {
        ctype_digit($string[i]);
}
echo 'ctype_digit = '.(microtime(get_as_float) - $start) ."\n";

$start = microtime(get_as_float);
for($i = 0; $i < $amount; $i++) {
        is_numeric($string[i]);
}
echo 'is_numeric = '.(microtime(get_as_float) - $start) ."\n";

$start = microtime(get_as_float);
for($i = 0; $i < $amount; $i++) {
        preg_match('!^[-+]?[0-9]+$!', $string[i]);
}
echo 'preg_match = '.(microtime(get_as_float) - $start) ."\n";

function isNumber($string)
{
        if (($string[0] !== '-') && ($string[0] !== '+') && ($string[0]  9))
                return false;

        for($i = 1; $i < strlen($string); $i++)
                if ($string[$i]  9)
                        return false;
        return true;
}

$start = microtime(get_as_float);
for($i = 0; $i < $amount; $i++) {
        isNumber($string[i]);
}
echo 'isNumber = '.(microtime(get_as_float) - $start) ."\n";

$start = microtime(get_as_float);
for($i = 0; $i 

PS. zżera sporo pamięci.

“Kompilacja” pliku konfiguracyjnego – PHP

Dzisiejszy wpis ma wbrew pozorom niewiele wspólnego z tym wpisem, pomijając to, że to jakiś tam związek z plikami ini.

Najłatwiej plik konfiguracyjny jest zrobić w postaci pliku php, który sobie przyłączamy do projektu. Praktyczne nie ma z tym problemów. No może po za jednym; jak już go przyłączysz do wielu plików i w zasadzie oprzesz o niego zasadę działania całości skryptu, może się okazać, że warto by użyć owego nieszczęsnego (“wymarłego“) pliku ini do przechowywania konfiguracji.

W pierwszym odruchu chciałem parsować plik – nazwijmy go config.ini – za każdym razem i przypisywać odpowiednie wartości odczytane z niego, pewnym zmiennym dostępnym globalnie. No ale rozwiązanie to jest trochę nieoptymalnie. Stąd pomysł, by zorganizować całość na podobieństwo Smarty, czyli na podstawie pliku pliku ini generować sobie plik php do bezpośredniego przyłączenia.

Edit (2008.09.09 17:32): zaprezentowane rozwiązanie, po części wynikało z twz. “kompatybilności wstecznej”. Może być jednak ulepszone – zapoznaj się z komentarzami do wpisu.

Tak więc zakładamy, ot mamy plik ini (config.ini), mamy plik config.php który jest skryptem przeprowadzającym “kompilacje”, oraz plik który ma zostać w jej wyniku wygenerowany – config.inc.php. Tutaj uwaga: includować do skryptów będziemy tylko plik config.php.

Pierwsze co trzeba zrobić po wywołaniu pliku config.php to sprawdzić, czy mamy wygenerowany plik config.inc.php i czy ma późniejszą datę modyfikacji niż plik config.ini. Wykorzystamy do tego funkcję filemtime:

Nie trzeba sprawdzać, czy plik config.inc.php w ogóle istnieje, dzięki @ nie zgłosi ona błędu, a w wyniku da wartość FALSE (które zostanie potraktowane jako 0).

Przejdźmy do wczytania pliku konfiguracyjnego, to też trudne nie jest:

{geshi lang=php}$configArray = parse_ini_file('config.ini.php', true);

Teraz w zmiennej $configArray mamy wczytany w postaci tablicy wielowymiarowej (dzięki 2giemu parametrowi – szczegóły w opisie funkcji parse_ini_file). Otwieramy plik wynikowy:

Można zapisywać zmienne. Szkoda by jednak robić to z palca dla każdej osobno. Napisałem więc małą funkcję:

{geshi lang=php}function writeSection($defaultArray, $sectionName) { global $configArray, $configFile; foreach($defaultArray as $key => $value) { if (isset($configArray[$sectionName][$key])) $entry = $configArray[$sectionName][$key]; else $entry = $value; if (!is_numeric($entry)) $entry = '\''.$entry.'\''; fwrite($configFile, "\$$key = ".$entry.";\n"); } }

To teraz jak to działa: pierwszy parametr to tablica wartości domyślnych, drugi parametr to nazwa sekcji (konsekwencja drugiego parametru w funkcji parse_ini_file. Skupmy się na tym pierwszym. Funkcja przegląda każdą z par klucz => wartość tej tablicy, sprawdzając, czy klucz został też zdefiniowany w pliku konfiguracyjnym (config.ini). Jeśli tak, funkcja pobiera jego wartość z tablicy $configArray, jeśli nie, wstawia wartość domyślną z tablicy jaką dostarczyliśmy. Pytania?

Po co jest is_numeric?
To taka tylko zmiana w sumie kosmetyczna – sprawia, że liczby nie są zapisywane jako tekst w pojedyńczych cudzysłowach.

Czy drugi parametr jest konieczny?
Nie, ale też wczytać plik konfiguracyjny trzeba bez podziału na sekcje.

Jakiś przykład?

Trzeba pamiętać oczywiście jeszcze o zamknięciu pliku config.inc.php

{geshi lang=php}fwrite($configFile, '?>'); fclose($configFile);

To jeszcze nie wszystko w pliku config.php na końcu, za tym całym głównym ifem (patrz wyżej), trzeba wczytać plik config.inc.php:

<pre lang="php}require_once('config.inc.php'){/geshi}

To”>;

Jako komentarz nie zostanie sparsowane, a sam plik wywołany jako php wyświetli tylko średnik no i tekst komunikatu die.

Są inne metody, ale mi to akurat wystarczyło.

Zalety takiego rozwiązania, są takie, że w pliku config.ini nie trzeba zamieszczać wszystkich parametrów i ich wartości, a i tak będę one widoczne w skrypcie. Po za tym to na pewno jest ciut szybsze niż wczytywanie za każdym razem konfiguracji.

Wady: trochę to naciągane;) Nie zawsze trzeba kombinować z plikami ini.

Chętnie poczytam jakiś konstruktywnych uwag. Pozdrawiam.

Dziękuje też za nie wszystkie.

Ochrona skryptów PHP przed wywołaniem

Skrypt nad którym pracowałem sobie dzisiaj, wymaga aby pewna jego czynność była wykonywana codziennie – w sumie to nie zaszkodziło by jakby wykonywała się więcej niż jeden raz – ale tak dla przyszłych, być może, rozwiązań trzeba by nieco zawyżyć wymagania.

Na szczęście na serwerze dostępny był cron, którym można by wywoływać skrypty php (czasem są z tym małe problemy) – coś takiego jak tutaj mamy. Pojawił się inny problem – jak zabezpieczyć skrypt przed wywołaniem go paska adresu – w przeglądarce.

Jednym ze sposobów jest coś takiego:

/**
* Check remote address (allow only from localhost). If not working - write here 'localhost' or IP your server
*/
if ($_SERVER['REMOTE_ADDR'] != 'localhost')
{
	die("Ciekawe co chciałeś tutaj zrobić?");
}

(zapożyczone ze strony wiki projektu Vallheru)

A… byłbym zapomniał: edycja pliku .htaccess odpada w moim przypadku.

Ja z kolei wymyśliłem coś innego, sprawdzam czy istnieje zmienna (isset) $argv – jeśli tak, plik php został wywołany z linii poleceń (czyli cron np.), jeśli nie, ktoś wywołał go prawdopodobnie przez przeglądarkę.

if (isset($argv))
	echo "Z konsoli.\n";
else
	echo "Server.\n";

Uwaga: jak wykazano w 2gim komentarzu do tej notki nie jest to za najlepsza metoda. Po szczegóły odsyłam do wspomnianego komentarza.

Jeśli ktoś ma jakieś doświadczenia w powyższej materii, chętnie posłucham.