Dzięki PW od Semmka dowiedziałem się dokładnie o co chodzi w tym projekcie.
Więc i ja przedstawiam projekt (mam nadzieję) przydatny webmasterom raczkującym w PHP - własna wyszukiwarka (prawie jak Google

)
Zacznijmy od wymagań serwera: MySQL o jak największej pojemności (im większa strona, tym większe będą tabele, przykro mi), PHP w wersji 4.2.
Okej! Do dzieła!
Zacznijmy od tabel. Pozwoliłem sobie dać swój nick przed tabelami, by nikt nie przywłaszczył skryptu.
- Kod: Zaznacz cały
DROP TABLE IF EXISTS MATEUSSSZ_SEARCH_CRAWL;
DROP TABLE IF EXISTS MATEUSSSZ_SEARCH_STOP_WORD;
DROP TABLE IF EXISTS MATEUSSSZ_SEARCH_INDEX;
DROP TABLE IF EXISTS MATEUSSSZ_SEARCH_DOCUMENT;
DROP TABLE IF EXISTS MATEUSSSZ_SEARCH_TERM;
CREATE TABLE MATEUSSSZ_SEARCH_CRAWL (
DOCUMENT_URL VARCHAR(255) NOT NULL
)
ENGINE=InnoDB DEFAULT CHARACTER SET latin1
COLLATE latin1_general_cs;
CREATE TABLE MATEUSSSZ_SEARCH_STOP_WORD (
TERM_VALUE VARCHAR(255) NOT NULL
)
ENGINE=InnoDB DEFAULT CHARACTER SET latin1
COLLATE latin1_general_cs;
CREATE TABLE MATEUSSSZ_SEARCH_DOCUMENT (
DOCUMENT_ID INTEGER UNSIGNED NOT NULL AUTO_INCREMENT,
DOCUMENT_URL VARCHAR(255) NOT NULL,
DOCUMENT_TITLE VARCHAR(255),
DESCRIPTION VARCHAR(255),
PRIMARY KEY (DOCUMENT_ID),
CONSTRAINT UNIQUE (DOCUMENT_URL)
)
ENGINE=InnoDB DEFAULT CHARACTER SET latin1
COLLATE latin1_general_cs AUTO_INCREMENT=0;
CREATE TABLE MATEUSSSZ_SEARCH_TERM (
TERM_ID INTEGER UNSIGNED NOT NULL AUTO_INCREMENT,
TERM_VALUE VARCHAR(255) NOT NULL,
PRIMARY KEY (TERM_ID),
CONSTRAINT UNIQUE (TERM_VALUE)
)
ENGINE=InnoDB DEFAULT CHARACTER SET latin1
COLLATE latin1_general_cs AUTO_INCREMENT=0;
CREATE TABLE MATEUSSSZ_SEARCH_INDEX (
TERM_ID INTEGER UNSIGNED NOT NULL,
DOCUMENT_ID INTEGER UNSIGNED NOT NULL,
OFFSET INTEGER UNSIGNED NOT NULL,
PRIMARY KEY (DOCUMENT_ID, OFFSET),
FOREIGN KEY (TERM_ID)
REFERENCES MATEUSSSZ_SEARCH_TERM(TERM_ID),
FOREIGN KEY (DOCUMENT_ID)
REFERENCES MATEUSSSZ_SEARCH_DOCUMENT(DOCUMENT_ID)
)
ENGINE=InnoDB DEFAULT CHARACTER SET latin1
COLLATE latin1_general_cs;
Tu do wyjaśniania nie ma chyba nic

Tabele już stworzone, pora na kod PHP, który będzie przetwarzał dane dla użytkownika.
Najważniejsze będą tutaj indexer oraz panel użytkownika. Zacznijmy od tego pierwszego:
- Kod: Zaznacz cały
[b]plik indexer.php[/b]
#! /usr/bin/php
<?php
// do³¹czenie kodu wspó³u¿ytkowanego
include 'lib/common.php';
include 'lib/db.php';
// wyczyszczenie tabel indeksu
$query = sprintf('TRUNCATE TABLE %sSEARCH_INDEX', DB_TBL_PREFIX);
mysql_query($query, $GLOBALS['DB']);
$query = sprintf('TRUNCATE TABLE %sSEARCH_TERM', DB_TBL_PREFIX);
mysql_query($query, $GLOBALS['DB']);
$query = sprintf('TRUNCATE TABLE %sSEARCH_DOCUMENT', DB_TBL_PREFIX);
mysql_query($query, $GLOBALS['DB']);
// odczytanie listy s³ów ignorowanych
$query = sprintf('SELECT TERM_VALUE FROM %sSEARCH_STOP_WORD', DB_TBL_PREFIX);
$result = mysql_query($query, $GLOBALS['DB']);
$stop_words = array();
while ($row = mysql_fetch_array($result))
{
// ka¿de s³owo bêdzie sprawdzane, czy wystêpuje na liœcie, dlatego s³owa
// bêd¹ traktowane jak klucze tablicy -- isset($stop_words[<s³owo>]) dzia³a
// bardziej wydajnie ni¿ konstrukcja in_array(<s³owo>, $stop_words)
$stop_words[$row['TERM_VALUE']] = true;
}
mysql_free_result($result);
// otwarcie uchwytu CURL do pobierania dokumentów
$ch = curl_init();
// zdefiniowanie opcji curl
curl_setopt($ch, CURLOPT_FOLLOWLOCATION, true);
curl_setopt($ch, CURLOPT_HEADER, false);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_USERAGENT, 'Search Engine Indexer');
// pobranie listy dokumentów do indeksowania
$query = sprintf('SELECT DOCUMENT_URL FROM %sSEARCH_CRAWL', DB_TBL_PREFIX);
$result = mysql_query($query, $GLOBALS['DB']);
while ($row = mysql_fetch_array($result))
{
echo 'Przetwarzanie: ' . $row['DOCUMENT_URL'] . "...\n";
// pobranie zawartoœci dokumentu
curl_setopt($ch, CURLOPT_URL, $row['DOCUMENT_URL']);
$file = curl_exec($ch);
$file = tidy_repair_string($file);
$html = simplexml_load_string($file);
// lub: $html = @simplexml_load_string($file);
// wyodrêbnienie tytu³u
if ($html->head->title)
{
$title = $html->head->title;
}
else
{
// wykorzystanie nazwy pliku, je¿eli nie istnieje tytu³
$title = basename($row['DOCUMENT_URL']);
}
// wyodrêbnienie opisu
$description = 'Nie podano opisu.';
foreach($html->head->meta as $meta)
{
if (isset($meta['name']) && $meta['name'] == 'description')
{
$description = $meta['content'];
break;
}
}
// dodanie dokumentu do indeksu
$query = sprintf('INSERT INTO %sSEARCH_DOCUMENT (DOCUMENT_URL, ' .
'DOCUMENT_TITLE, DESCRIPTION) VALUES ("%s", "%s", "%s")',
DB_TBL_PREFIX,
mysql_real_escape_string($row['DOCUMENT_URL'], $GLOBALS['DB']),
mysql_real_escape_string($title, $GLOBALS['DB']),
mysql_real_escape_string($description, $GLOBALS['DB']));
mysql_query($query, $GLOBALS['DB']);
// pobranie identyfikatora dokumentu
$doc_id = mysql_insert_id($GLOBALS['DB']);
// usuniêcie z treœci dokumentu znaczników jêzyka HTML
$file = strip_tags($file);
// podzia³ treœci na pojedyncze s³owa
foreach (str_word_count($file, 1) as $index => $word)
{
// aby porównywaæ s³owa, wszystkie powinny byæ zapisane ma³ymi literami
$word = strtolower($word);
// pominiêcie s³owa, jeœli wystêpuje na liœcie s³ów ignorowanych
if (isset($stop_words[$word])) continue;
// sprawdzenie, czy s³owo znajduje siê ju¿ w bazie danych
$query = sprintf('SELECT TERM_ID FROM %sSEARCH_TERM WHERE ' .
'TERM_VALUE = "%s"',
DB_TBL_PREFIX,
mysql_real_escape_string($word, $GLOBALS['DB']));
$result2 = mysql_query($query, $GLOBALS['DB']);
if (mysql_num_rows($result2))
{
// s³owo istnieje w bazie danych - pobranie jego identyfikatora
list($word_id) = mysql_fetch_row($result2);
}
else
{
// dodanie s³owa do bazy danych
$query = sprintf('INSERT INTO %sSEARCH_TERM (TERM_VALUE) ' .
'VALUE ("%s")',
DB_TBL_PREFIX,
mysql_real_escape_string($word, $GLOBALS['DB']));
mysql_query($query, $GLOBALS['DB']);
// ustalenie identyfikatora s³owa
$word_id = mysql_insert_id($GLOBALS['DB']);
}
mysql_free_result($result2);
// dodanie rekordu do indeksu
$query = sprintf('INSERT INTO %sSEARCH_INDEX (DOCUMENT_ID, ' .
'TERM_ID, OFFSET) VALUE (%d, %d, %d)',
DB_TBL_PREFIX,
$doc_id,
$word_id,
$index);
mysql_query($query, $GLOBALS['DB']);
}
}
mysql_free_result($result);
curl_close($ch);
echo 'Indeksowanie zakoñczone.' . "\n";
?>
To kod który wyszukuje słowa i je zapisuje w bazie danych

Teraz pora na panel użytkownika, czyli pole do wyszukiwania. Tworzymy w tym celu folder "public files" i tam plik search.php.
- Kod: Zaznacz cały
[b]plik public_files/search.php[/b]
<?php
// do³±czenie kodu wspó³u¿ytkowanego
include '../lib/common.php';
include '../lib/db.php';
include '../lib/functions.php';
// przyjêcie wyszukiwanych s³ów zawartych w przes³anym zapytaniu
$words = array();
if (isset($_GET['query']) && trim($_GET['query']))
{
$words = explode_items($_GET['query'], ' ', false);
// usuniêcie z zapytania s³ów do zignorowania
$query = sprintf('SELECT TERM_VALUE FROM %sSEARCH_STOP_WORD',
DB_TBL_PREFIX);
$result = mysql_query($query, $GLOBALS['DB']);
$stop_words = array();
while ($row = mysql_fetch_assoc($result))
{
$stop_words[$row['TERM_VALUE']] = true;
}
mysql_free_result($result);
$words_removed = array();
foreach ($words as $index => $word)
{
if (isset($stop_words[strtolower($word)]))
{
$words_removed[] = $word;
unset($words[$index]);
}
}
}
// wygenerowanie formularza HTML
ob_start();
?>
<form method="get"
action="<?php echo htmlspecialchars($_SERVER['PHP_SELF']); ?>">
<div>
<input type="text" name="query" id="query" value="<?php
echo (count($words)) ? htmlspecialchars(join(' ', $words)) : '';?>"/>
<input type="submit" value="Szukaj"/>
</div>
</form>
<?php
// rozpoczêcie przetwarzania zapytania
if (count($words))
{
// sprawdzenie poprawno¶ci pisowni szukanych s³ów
$spell_error = false;
$suggest_words = array();
$ps = pspell_new('pl');
foreach ($words as $index => $word)
{
if (!pspell_check($ps, $word))
{
if ($s = pspell_suggest($ps, $word))
{
if (strtolower($s[0]) != strtolower($word))
{
// pominiêcie b³êdów literowych wynikaj±cych z wielko¶ci znaków
$spell_error = true;
$suggest_words[$index] = $s[0];
}
}
}
}
// sformu³owanie zapytania przeszukuj±cego indeks
// z uwzglêdnieniem wpisanych s³ów i wykonanie zapytania
$join = '';
$where = '';
$query = 'SELECT DISTINCT D.DOCUMENT_URL, D.DOCUMENT_TITLE, ' .
'D.DESCRIPTION FROM MATEUSSSZ_SEARCH_DOCUMENT D ';
foreach ($words as $index => $word)
{
$join .= sprintf(
'JOIN MATEUSSSZ_SEARCH_INDEX I%d ON D.DOCUMENT_ID = I%d.DOCUMENT_ID ' .
'JOIN MATEUSSSZ_SEARCH_TERM T%d ON I%d.TERM_ID = T%d.TERM_ID ',
$index, $index, $index, $index, $index);
$where .= sprintf('T%d.TERM_VALUE = "%s" AND ', $index,
mysql_real_escape_string(strtolower($word), $GLOBALS['DB']));
}
$query .= $join . 'WHERE ' . $where;
// usuniêcie czterech ostatnich znaków - s³owa ' AND'
$query = substr($query, 0, strlen($query) - 4);
$result = mysql_query($query, $GLOBALS['DB']);
// wy¶wietlenie wyników
echo '<hr/>';
$num_rows = mysql_num_rows($result);
echo '<p>Wynik wyszukiwania s³ów <b>' . htmlspecialchars(join(' ', $words)) .
'</b> zawiera ' . $num_rows . ' dokument' .
(($num_rows == 1) ? '' : (($num_rows < 4) ? 'y' : 'ów')) . ':</p>';
// wy¶wietlenie sugrowanego zapytania, je¿eli stwierdzono b³êdy literowe
if ($spell_error)
{
foreach ($words as $index => $word)
{
if (isset($suggest_words[$index]))
{
$words[$index] = $suggest_words[$index];
}
}
echo '<p>Podejrzenie b³êdu. Czy chodzi³o o <a href="' .
htmlspecialchars($_SERVER['PHP_SELF']) .'?query=' .
urlencode(htmlspecialchars(join(' ', $words))) . '">' .
htmlspecialchars(join(' ', $words)) . '</a>?</p>';
}
echo '<ul>';
while ($row = mysql_fetch_assoc($result))
{
echo '<li><b><a href="' .
htmlspecialchars($row['DOCUMENT_URL']) . '">' .
htmlspecialchars($row['DOCUMENT_TITLE']) . '</a></b>- ' .
htmlspecialchars($row['DESCRIPTION']) . '<br/><i>' .
htmlspecialchars($row['DOCUMENT_URL']) . '</i></li>';
}
echo '</ul>';
}
$GLOBALS['TEMPLATE']['content'] = ob_get_clean();
// wy¶wietlenie strony
include '../templates/template-page.php';
?>
I tu kod zachowuje się jak Google, z czego jestem dosyć dumny - jeśli wykaże błąd, sprawdza go i podaje przypuszczaną frazę. A tak wszystko wygląda jak wyszukiwarka Brina i Page'a.
Czas na panel administratora: tutaj kod tworzy dwa formy: do wpisywania linków by przetworzyć słowa oraz do omijania słów, których nie chcemy, by zostały zapisane w bazach. Tzn. nie chcemy by po wyszukaniu słowa "że" pojawiła się setka linków.
- Kod: Zaznacz cały
[b]plik public_files/admin.php[/b]
<?php
// do³±czenie kodu wspó³u¿ytkowanego
include '../lib/common.php';
include '../lib/db.php';
include '../lib/functions.php';
// aby uzyskaæ dostêp do strony, trzeba siê zalogowaæ
//include '401.php';
// przetworzenie danych wej¶ciowych z przes³anego formularza
if (isset($_POST['submitted']))
{
// usuniêcie istniej±cych adresów
$query = sprintf('TRUNCATE TABLE %sSEARCH_CRAWL', DB_TBL_PREFIX);
mysql_query($query, $GLOBALS['DB']);
// dodanie listy adresów do bazy danych
$addresses = explode_items($_POST['addresses'], "\n", false);
if (count($addresses))
{
$values = array();
foreach ($addresses as $address)
{
$values[] = mysql_real_escape_string($address, $GLOBALS['DB']);
}
$query = sprintf('INSERT INTO %sSEARCH_CRAWL (DOCUMENT_URL) ' .
'VALUES ("%s")', DB_TBL_PREFIX,
implode ('"), ("', $values));
mysql_query($query, $GLOBALS['DB']);
}
// usuniêcie istniej±cych s³ów ignorowanych
$query = sprintf('TRUNCATE TABLE %sSEARCH_STOP_WORD', DB_TBL_PREFIX);
mysql_query($query, $GLOBALS['DB']);
// dodanie listy s³ów ignorowanych do bazy danych
$words = explode_items($_POST['stop_words'], "\n", false);
if (count($words))
{
$values = array();
foreach ($words as $word)
{
$values[] = mysql_real_escape_string($word, $GLOBALS['DB']);
}
$query = sprintf('INSERT INTO %sSEARCH_STOP_WORD (TERM_VALUE) ' .
'VALUES ("%s")', DB_TBL_PREFIX, implode ('"), ("', $values));
mysql_query($query, $GLOBALS['DB']);
}
}
// wygenerowanie formularza HTML
ob_start();
?>
<form method="post"
action="<?php echo htmlspecialchars($_SERVER['PHP_SELF']); ?>">
<table>
<tr>
<td style="vertical-align:top; text-align:right">
<label for="addresses">Adresy do uwzglêdnienia</label></td>
<td><small>Wpisz adresy, które maj± zostaæ uwzglêdnione przez robota -
jeden adres w wierszu.</small><br/>
<textarea name="addresses" id="addresses" rows="5" cols="60"><?php
// odczytanie listy adresów
$query = sprintf('SELECT DOCUMENT_URL FROM %sSEARCH_CRAWL ' .
'ORDER BY DOCUMENT_URL ASC', DB_TBL_PREFIX);
$result = mysql_query($query, $GLOBALS['DB']);
while ($row = mysql_fetch_array($result))
{
echo htmlspecialchars($row['DOCUMENT_URL']) . "\n";
}
mysql_free_result($result);
?></textarea>
</td>
</tr><tr>
<td style="vertical-align:top; text-align:right">
<label for="stop_words">S³owa ignorowane</label></td>
<td><small>Wpisz s³owa, które maj± byæ pominiête w indeksie, jedno s³owo w wierszu.</small><br/>
<textarea name="stop_words" id="stop_words" rows="5" cols="60"><?php
// odczytanie listy s³ów ignorowanych
$query = sprintf('SELECT TERM_VALUE FROM %sSEARCH_STOP_WORD ORDER BY ' .
'TERM_VALUE ASC', DB_TBL_PREFIX);
$result = mysql_query($query, $GLOBALS['DB']);
while ($row = mysql_fetch_array($result))
{
echo htmlspecialchars($row['TERM_VALUE']) . "\n";
}
mysql_free_result($result);
?></textarea>
</td>
</tr><tr>
<td> </td>
<td><input type="submit" value="Zapisz"/></td>
<td><input type="hidden" name="submitted" value="1"/></td>
</tr><tr>
</table>
</form>
<?php
$GLOBALS['TEMPLATE']['content'] = ob_get_clean();
// wy¶wietlenie strony
include '../templates/template-page.php';
?>
UWAGA! Każdy link należy wpisać pojedynczo i z "http://www" tzn.
- Kod: Zaznacz cały
http://www.donrosa.pl/
http://www.donrosa.pl/informacje.php itd.
Teraz pora na pliki typowo "funkcyjne"; po wpisaniu nazwy pliku w pasku wyszukiwania w przeglądarce nic nie wyskoczy.
- Kod: Zaznacz cały
[b]plik lib/common.php[/b]
<?php
// true, je¶li ¶rodowisko produkcyjne; w przeciwnym razie false
define ('IS_ENV_PRODUCTION', false);
// ustawienie opcji raportowania b³êdów
error_reporting(E_ALL | E_STRICT);
ini_set('display_errors', !IS_ENV_PRODUCTION);
ini_set('error_log', 'log/phperror.txt');
// ustawienie strefy czasowej, by uniknaæ ostrze¿eñ
// w przypadku u¿ycia funkcji czasu i daty
date_default_timezone_set('Europe/Warsaw');
// uwzglêdnienie "magic quotes" w razie konieczno¶ci
if (get_magic_quotes_gpc())
{
function _stripslashes_rcurs($variable, $top = true)
{
$clean_data = array();
foreach ($variable as $key => $value)
{
$key = ($top) ? $key : stripslashes($key);
$clean_data[$key] = (is_array($value)) ?
stripslashes_rcurs($value, false) : stripslashes($value);
}
return $clean_data;
}
$_GET = _stripslashes_rcurs($_GET);
$_POST = _stripslashes_rcurs($_POST);
// $_REQUEST = _stripslashes_rcurs($_REQUEST);
// $_COOKIE = _stripslashes_rcurs($_COOKIE);
}
?>
Jak widać, są to ułatwienia dla użytkownika.
- Kod: Zaznacz cały
[b]plik lib/db.php[/b]
<?php
// sta³e bazy danych i schematów
define('DB_HOST', 'localhost');
define('DB_USER', 'username');
define('DB_PASSWORD', 'password');
define('DB_SCHEMA', 'db_name');
define('DB_TBL_PREFIX', 'MATEUSSSZ_');
// ustanowienie po³±czenia z serwerem bazy danych
if (!$GLOBALS['DB'] = mysql_connect(DB_HOST, DB_USER, DB_PASSWORD))
{
die('B³±d: Nie uda³o siê nawi±zaæ po³±czenia z baz± danych.');
}
if (!mysql_select_db(DB_SCHEMA, $GLOBALS['DB']))
{
mysql_close($GLOBALS['DB']);
die('B³±d: Nie uda³o siê wybraæ schematu bazy danych.');
}
?>
Ten plik trzeba omówić: w miejscu localhost wpisujemy nazwę serwera mysql (dla cba.pl jest to mysql.cba.pl, ovh.pl to localhost, zwykle localhost będzie działać), USER nazwę użytkownika, PASS - hasło, a db_name nazwę bazy.
Reszta jest nie ważna.
- Kod: Zaznacz cały
[b]plik lib/functions.php[/b]
<?php
// przekszta³cenie listy elementów (domy¶lnie oddzielonych znakiem nowego wiersza)
// do postaci tablicy
// pominiêcie wierszy pustych i warto¶ci zduplikowanych
function explode_items($text, $separator = "\n", $preserve = true)
{
$items = array();
foreach (explode($separator, $text) as $value)
{
$tmp = trim($value);
if ($preserve)
{
$items[] = $tmp;
}
else
{
if (!empty($tmp))
{
$items[$tmp] = true;
}
}
}
if ($preserve)
{
return $items;
}
else
{
return array_keys($items);
}
}
?>
W komentarzach jest wszystko opisane

Najważniejsze kody mamy już opisane, teraz pora na wygląd, czyli pliki templates.
- Kod: Zaznacz cały
[b]plik templates/template-page.php[/b]
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN"
"http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en">
<head>
<meta http-equiv="Content-Type" content="text/html; charset=iso-8859-2" />
<title>
<?php
if (!empty($GLOBALS['TEMPLATE']['title']))
{
echo $GLOBALS['TEMPLATE']['title'];
}
?>
</title>
<link rel="stylesheet" type="text/css" href="css/styles.css"/>
<?php
if (!empty($GLOBALS['TEMPLATE']['extra_head']))
{
echo $GLOBALS['TEMPLATE']['extra_head'];
}
?>
</head>
<body>
<div id="header">
<?php
if (!empty($GLOBALS['TEMPLATE']['title']))
{
echo $GLOBALS['TEMPLATE']['title'];
}
?>
</div>
<div id="content">
<?php
if (!empty($GLOBALS['TEMPLATE']['content']))
{
echo $GLOBALS['TEMPLATE']['content'];
}
?>
</div>
<div id="footer">Copyright ©<?php echo date('Y'); ?> Skrypt by Mateusz Sobieski</div>
</div>
</body>
</html>
Ten plik można edytować bez problemu, nie ruszając kodu php (znając sam HTML), zmieniając wygląd wyszukiwarki.
I tak doszliśmy do końca!
Nasza wyszukiwarka ma wszystkie najpotrzebniejsze funkcje, a da się ją zamknąć w małym paseczku na górze strony.
Kod jest ogólnodostępny - można go rozpowszechniać za darmo i bez mojej zgody, prosiłbym jedynie o wspomnieniu o mnie i nie usuwaniu stopki (chyba że chcecie "zmniejszyć" pole wyszukiwania, to wystarczy na samym dole wyników).
Na końcu przydatne linki:
http://agerwebedytor.com/ - darmowy edytor stron www, wspierający html, js, php, mysql i css.
http://www.kurshtml.boo.pl/ - to jeśli chcielibyście zmienić wygląd, szczególnie przydatna jest kategoria css.
Jeśli macie jakieś pytania, piszcie na PW, email (
mateuszsobieski@op.pl) oraz GG.
Pozdrawiam i połamania klawiatury przy kodowaniu!
P.S. Zauważyłem, że przy kopiowaniu kodu z mojego edytora WWW zanikły polskie znaki, więc trzeba je na nowo wstawić

Bardzo mi przykro z tego powodu.
P.S.2 Jeżeli podobają się wam takie skrypty, mogę przedstawić ich tutaj więcej, wszystko zależy od was.