Chyba już nikt nie ma wątpliwości co do tego iż wyszukiwarka Google jest największą i najbardziej zaawansowaną wyszukiwarką w całym internecie. Fajnie by było mieć taką technologię również i w swoim serwisie i z taką samą skutecznością przeszukiwać własne zasoby serwera.

 

Możesz od razu pobrać całą bibliotekę GoogleSearch-src.zip lub zobaczyć go na Githubie https://github.com/MateuszManaj/GoogleSearch/

 

Google oczywiście udostepnia własne API za darmo (https://developers.google.com/custom-search/json-api/v1/overview). Jest w tym ok 100 wyszukiwań / dzień więc nie ma tego zbyt wiele a za $5 możemy mieć ten limit zwiększony do 1000.

Czy to drogo czy nie pozostawiam Wam do oceny. Ja jednak nie lubie płacić za coś co i tak otrzymuję za darmo wpisując wyrażenia do wyszukiwarki. A jeśli mogę ten proces zautomatyzować znając sposoby wyszukiwania to nie pozostaje nic innego jak enkapsulacja do obiektu.

 

Pierwszy algorytm wyszukujący i parsujacy dane powstał pod koniec 2014 roku i miał ok. 60 linii; była to zwykła funkcja napisana "na kolanie" podczas robienia projektu dla klienta, który był fanem google'a i jego pełnotekstowego wyszukiwania pogrubiającego dopasowane wyrażenia.
Biblioteki które zostały wykorzystane to cURL, DOMDocument, XPath.

Ten system wyszukiwania (crawler) sprawdził się w kilku kolejnych projektach i postanowiłem aby go usprawnić, dodać nowe funkcje i upublicznić. Aktualna nowa wersja daje takie możliwości jak:

  • Wyniki standardowe
  • Wyniki z ogłoszeń płatnych
  • Wyrażenia podobne
  • Statystyka wyszukiwania
  • Cache'owanie
  • Paginacja wyników
  • Limitowanie rezultatów wyszukiwania bezpośrednio w zapytaniu Google
  • Regionalizacja wyszukiwania


Posiada więc wszystkie podstawowe funkcje jakie zwykle są przydatne podczas pracy.

Crawler - to nic innego jak program lub jego część, która "potrafi" przetwarzać wyniki działania innego programu i wyciągać potrzebne dane.

 

Pierwszą rzeczą od której trzeba zacząć pisanie każdej dobrej aplikacji to pierwszy pomysł i dalsza koncepcja. Zwykle namawiam ludzi do tego aby tworzyli koncepcje za pomocą klasycznej metody kartki i ołówka (lub jak kto woli kartki i długopisu :)). Generalnie chodzi o to aby opisać swoje myśli.
Główne założenia przedstawiłem powyżej a zasada działania jest bardzo prosta. Względem podanego ciągu wysyłane jest zdalne żądanie cURL'em a odpowiedź parsowana i zamykana w obiekcie oraz kolekcji obiektów.

Chciałbym, aby całość biblioteki działała tak:

    $gs1 = new GoogleSearch("Skup złomu");  // Wyszukiwanie podanego wyrażenia w pierwszych 10 wynikach.
    $gs2 = new GoogleSearch("Koszulki z nadrukiem", 2);     // Wyszukiwanie podanego wyrażenia w drugiej dziesiątce wyników.
    $gs3 = new GoogleSearch("wędki i kołowrotki olsztyn", 1, 50);   // Wyszukiwanie podanego wyrażenia w pierwszych 50 wynikach.

Każda linia przedstawia nową instancję klasy GoogleSearch, która jest osobnym obiektem a wyszukiwanie jest wykonywane tylko raz. Oznacza to, że dla jednej instancji klasy nie będzie możliwości zmiany szukanego wyrażenia czy jego parametrów. Jeśli zechcemy zmienić wyrażenie koniecznym będzie utworzenie nowej instancji.

Wyniki chcę zwracać jako kolekcję obiektów GoogleSearchResults, której klasa implementuje interfejsy Iterator oraz Countable przez co cały obiekt kolekcji zachowuje się jakby był tablicą - jest iterowalny i zliczalny jak tablica ale jest pełnoprawnym obiektem. Do tego obiektu kolekcji chcę wprowadzić metody dodania na stos kolejnego elementu GoogleSearchResult oraz dodać klika usprawniających metod jak: Remove($key) - usuwa element o podanym kluczu kolekcji, ContainsKey($key, $recursive) - sprawdza istnienie elementu o podanym kluczu w kolekcji, ContainsValue($value) - sprawdza istnienie konkretnej wartości w kolekcji, Get($key) - zwraca wartość elementu kolekcji na podstawie podanego klucza, Import(array $array) - zastępuje całą kolekcję na element podany w parametrze, Extend(array $array) - rozszerza obecną kolekcję o element podany w parametrze

    public function __construct($query, $page = 1, $results_number = 10)
    {
        if(is_null(static::$_extensionExists)) static::$_extensionExists = extension_loaded("dom") && extension_loaded("curl");
        if(!static::$_extensionExists) throw new ExtensionException("GoogleSearch needs DOM and CURL extension loaded in your environment");
        $this->CacheDirectory = rtrim($this->CacheDirectory, "/")."/";
        if(!file_exists($this->CacheDirectory))
        {
            if(!mkdir($this->CacheDirectory, 0777, true)) throw new CacheDirectoryException("Unable to create cache directory at '".$this->CacheDirectory."'");
            if(!is_writable($this->CacheDirectory)) throw new CacheDirectoryException("Directory '".$this->CacheDirectory."' isn't writable");
        }
        $this->Query = $query;
        $this->ResultsNumber = is_numeric($results_number) && $results_number > 0 ? intval($results_number) : 10;
        $this->Page = ((is_numeric($page) && $page > 0 ? intval($page) : 1) - 1) * $this->ResultsNumber;
    }

 

Odpowiednio w linijkach sprawdzam czy w systemie zainstalowane są rozszerzenia "DOM" oraz "CURL". Warto zauważyć, że robię to tylko raz. Korzystając z właściwości zmiennej statycznej dla każdych kolejnych instancji klasy to sprawdzenie nie będzie miało miejsca. Zmienna $_extensionExists nie będzie nullem i warunek zostanie pominięty.

Następnie sprawdzam czy istnieje zdefiniowany katalog na pliki cache. Robię to za każdym razem ze względu na to, że użytkonik mógł ten katalog usunąć podczas wykonywania programu a jest on niezbędny stąd konieczność sprawdzania przy tworzeniu każdej instancji osobno. Jeśli wspomniany katalog nie istnieje następuje próba jego utworzenia. Generowane są odpowiednio wyjątki przerywające wykonywanie dalej aplikacji w momencie jeśli utworzenie katalogu nie powiodło się lub po utworzeniu katalog nie jest zapisywalny.

Od linii 11 do 13 do zmiennych członkowskich wpisuję podane w konstruktorze parametry takie jak szykane wyrażenie, ile wyników na stronę oraz która strona.

 

[...]
    public function GetLink()
    {
        $uri = Array("q" => $this->Query, "start" => $this->Page, "pws" => 0, "num" => $this->ResultsNumber);
        if(!is_null($this->ResultLanguage)) $uri['lr'] = $this->ResultLanguage;
        $uri = array_merge($uri, $this->Options);
        return "http://google.com/search?".http_build_query($uri);
    }
[...]

Metoda tworząca link jaki wykorzystany będzie później do wykonania wyszukiwania.

 

[...]
    private function _doFind()
    {
        // Check whether DOMDocument exists in memory previously created.
        if(is_null($this->_finder_handle))
        {
            $this->_stringResults = $this->_doSearch();
            // Prepend content type for further DOMDocument parsing
            $this->_stringResults = '<meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />' .$this->_stringResults;
            $dom = new \\DOMDocument('1.0', 'UTF-8');
            $dom->substituteEntities = true;
            @$dom->loadHTML($this->_stringResults);
            $this->_finder_handle = new \\DomXPath($dom);
        }
    }
[...]

Jeśli w danej instancji utworzonej klasy nic nie szukaliśmy warunek z linii 5 zostanie spełniony. Istotne jest tutaj to, że bez większych przeszkód moglibyśmy pominąć ten warunek i za każdym razem wykonywać jego treść na nic nie bacząc. Z punktu widzenia architektury oprogramowania jest to nieoptymalne stąd zaprezentowana konstrukcja.
Pewnie zastanawiasz się czemu w linii 10 dodaję sekcję meta. Cóż... bez niej (jeśli w naszych wynikach brakuje tej sekcji, w innym przypadku nie zaszkodzi) DOMDocument nie poradzi sobie aby sparsować wygenerowany dokument - uzna go za niepoprawny i wyrzuci wyjątek.
W linii 13 do zmiennej członkowskiej _finder_handle przypisujemy wartość sparsowanego poprzez klasę DOMDocument obiektu naszej strony dodatkowo zamieniając ją w "przeszukiwalny" (traversable) obiekt DOMXPath - takie jQuery dla PHP.

[...]
    // Opis najważniejszych metod...
    $gs1 = new GoogleSearch("sprzęt wędkarski");
    
    echo "<pre>";
    print_r($gs1->Find());      // Wyszukuje zwykłych rezultatów
    print_r($gs1->FindAll());   // Wyszukuje zwykłe i płatne rezultaty
    print_r($gs1->FindAdvertisements());    // Wyszukuje wyłącznie płatne wyniki
    print_r($gs1->FindSimilarQueries());    // Wyszukuje sugestie podobnych słów kluczowych
    echo "</pre>";
[...]

Odpowiedniki metod prywatnych, które wykonują całą pracę to kolejno: _find_regular(), _find_regular() + _find_ads(), _find_ads(), _find_similar()

[...]
        // Regular results
        private $_mainResultsQuery1 = "//div[contains(@class, 'srg')]/*[contains(@class, 'g')]/div[contains(@class, 'rc')]/h3[contains(@class, 'r')]/a|//ol[contains(@id, 'rso')]/*[contains(@class, 'g')]/div[contains(@class, 'rc')]/h3[contains(@class, 'r')]/a";
        private $_mainResultsQuery2 = "//ol[contains(@id, 'rso')]/div[contains(@class, 'srg')]/*[contains(@class, 'g')]/div[contains(@class, 'rc')]/h3[contains(@class, 'r')]/a";
        private $_mainResultsQuery3 = "//ol[contains(@id, 'rso')]/*[contains(@class, 'g')]/div/div[contains(@class, 'rc')]/h3[contains(@class, 'r')]/a";
        private $_descriptionResultsQuery = ".//*[contains(@class, 's')]/div/span[contains(@class, 'st')]";
        private $_innerLinksResultsQuery1 = ".//*[contains(@class, 's')]/div/div[contains(@class, 'osl')]/a";
        private $_innerLinksResultsQuery2 = ".//div[contains(@class, 'sld vsc')]";
        private $_innerLinksResultsQuery2link = ".//*[contains(@class, '_Tyb')]/h3/a";
        private $_innerLinksResultsQuery2desc = ".//*[contains(@class, 's')]/*[contains(@class, 'st')]";
        private $_breadcrumbsResultsQuery = ".//*[contains(@class, 's')]/div/div[contains(@class, 'f')]/cite";
        
        // Advertisements results
        private $_advMainResultsQuery = "//li[contains(@class, 'ads-ad')]";
        private $_advMainLinks = ".//h3/a";
        private $_advPhoneNumber = ".//*[contains(@class, '_r2b')]";
        private $_advDescription1 = ".//*[contains(@class, 'ads-creative')]";
        private $_advDescription2 = ".//*[contains(@class, '_knd _Tv')]";
        private $_advInternalLinks = ".//*[contains(@class, '_MEc _LEc')]/li|.//*[contains(@class, '_gBb')]/li";
        private $_advInternalLinksLink = ".//a";
        private $_advBreadcrumbs = ".//*[contains(@class, 'ads-visurl')]/cite";
        private $_advAddress = "(.//*[contains(@class, '_H2b')])[last()]/a";
        private $_advAddressPhNu = ".//*[contains(@class, '_xnd')]";
        
        // Similar results
        private $_similarMainResultsQuery = "//div[contains(@id, 'brs')]/div[contains(@class, 'card-section')]/div[contains(@class, 'brs_col')]/p/a";
        
        // Statistics results
        private $_statsMain = "//div[contains(@id, 'resultStats')]";
        private $_statsTimeload = ".//nobr";
[...]

Wszystkie selektory dla obiektu DOMXPath. Muszę przyznać, że wielokrotnie sie zdziwiłem podczas pracy z wynikami Google'a gdyż ich kod HTML'owy był niepoprawny. Np. częto w listach nieuporządkowanych zamiast <li> występował jakiś <div> albo coś innego. Dosyć długo zajęło mi aby zrozumieć dlaczego jeden box jest poprawny to inny zaś ma zmienione klasy i strukturę. Niestety Google zmienia też czasem nazwy klas (atrybut htmlowy) stąd gwiazdki w selektorach aby to uelastycznić.

 

Domyślnie cache zapisuje się w lokalizacji ./GoogleSearch/_cache/ a jego pliki są ważne przez równo 3 doby. Po tym czasie dla wyszukiwanego wyrażenia plik zostanie odświeżony. Cache jest jednym z najważniejszych elementów tej biblioteki gdyż zapobiega blokadzie Google'a na nasz adres IP. Jeśli coś takiego się przydaży to na ekranie zobaczycie wyjątek iż Google nas zablokował na chwilę. Zwykle jest to 24h.

[...]
    $html = curl_exec($c);
    $httpcode = curl_getinfo($c, CURLINFO_HTTP_CODE);
    if($httpcode != 200) throw new HttpCodeException("Google has returned ".$httpcode." code. Make sure that google hasn't block your IP address.");
    curl_close($c);
[...]

Linijka 4 odpowiada za zwrócenie wyjątku podczas blokady ze strony Google'a. Aby taką blokadę dostać trzeba się nieźle natrudzić (w dodatku bez cache'a :)). Podczas pisania tej biblioteki (i to na samym końcu) gdy nie było jeszcze cache'a uruchomionego Google zrobił mi psikusa, ale powiedzmy sobie szczerze... spamowałem Google'a chyba przez kilka dobrych godzin debugując 3 zapytania na krzyż podczas gdy nie miałem pojęcia, że kod Google może zawierać lapsusy i mieć niepoprawną strukturę. To były setki zapytań. Oczywiście, Google nie jest głupi i potrafi dosyć trafnie ocenić czy mamy robota czy ręcznie spamujemy jego wyszukiwarkę :)
Nie warto więc stosować tej biblioteki na otwartym internecie udostepniając w swoim serwisie "nielimitowane" wyszukiwanie Google'a tylko raczej do wyszukiwania wyrażeń na swojej stronie tworząc zapytanie w następujący sposób

    $gs1 = new GoogleSearch("site:example.com sprzęt wędkarski");

Konstrukcja taka zawęża nam wyniki wyszukiwania tylko do naszej strony. Wraz z użyciem cache'a mało prawdopodobne staje się to iż doświadczymy blokady Google'a uzyskując w pełni wartościową wyszukiwarkę tylko dla nas.

 

Btw. A znacie to ? https://www.google.pl/search?q=filetype:bak+inurl:%22passwd|htaccess%22&ie=utf-8&oe=utf-8&gws_rd=cr&ei=JgLOVfzIGsXfU6Psl4AM :) Google jednak rządzi a dostęp do tych danych to nie wina Google'a tylko nasza - ludzi, którzy ich nie zabezpieczą :)

Powodzenia w Google-Hackingu!