Czasem zachodzi konieczność, że potrzebujemy pobrać wszystkie adresy URL z zewnętrznej strony. Piszemy crawler, który przechodzi na wybrany przez nas adres, pobiera wszystkie adresy z elementów A, przechodzi na pobrane adresy i tak dalej, do skutku.
We wpisie tym przyjrzymy się dwóm metodom pobierania odnośników z zewnętrznej strony (czy jakiegokolwiek zbitka kodu HTML). Poznamy sposób wydajny i estetyczny, czyli krótko o DOMDocument vs regex.
Problem pobrania wszystkich adresów z innej strony WWW przewija się bardzo często na różnych forach programistycznych. Najczęściej pytanie dotyczy właściwego ułożenia wyrażenia regularnego, które to niekoniecznie jest proste w tym przypadku.
Na początek należy jednak pobrać kod HTML zewnętrznej strony, do której odnośników chcemy się dostać:
1 2 3 4 5 6 7 8 9 10 | $ch = curl_init(); curl_setopt($ch, CURLOPT_URL, $url); curl_setopt($ch, CURLOPT_FAILONERROR, true); curl_setopt($ch, CURLOPT_FOLLOWLOCATION, true); curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, false); curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false); curl_setopt($ch, CURLOPT_USERAGENT, 'Mozilla/4.0 (compatible; MSIE 7.0; Windows NT 5.1)'); $output = curl_exec($ch); curl_close($ch); |
Aby kod ten zadziałał niezbędna jest instalacja modułu cURL, która zazwyczaj już jest wykonana. Jeśli natomiast nie dysponujemy tym rozszerzeniem i nie możemy doinstalować (lub po prostu nie chcemy) to wpis Jak pobierać zewnętrzne zasoby? może okazać Ci się przydatny.
Pobieranie odnośników z kodu HTML z wykorzystaniem wyrażeń regularnych
Aby prawidłowo obsłużyć pobieranie wszystkich odnośników z wskazanego kodu HTML musimy się niemało natrudzić. Powstałe w ten sposób wyrażenie musi obsługiwać wiele sytuacji i wyjątków.
Na potrzeby niniejszego wpisu stworzyłem proste wyrażenie, niezbyt optymalne, nie wychwytujące zapewne wszystkich adresów URL, jednak musi wystarczyć.
1 | /<a\shref=["\']?([^"]+)["\']?/i |
Teraz już tylko wystarczy z użyciem preg_match_all przejść przed kod HTML i zastosować powyższe wyrażenie. Wygląda to następująco:
1 2 3 4 5 | $pattern = '/<a\shref=["\']?([^"]+)["\']?/i'; preg_match_all($pattern, $text, $matches); // nasze url'e var_dump ($matches[1]); |
Pobieranie odnośników z kodu HTML z wykorzystaniem klasy DOMDocument
Drugim, dużo przyjemniejszym i wygodniejszym sposobem jest wykorzystanie wbudowanej w PHP klasy DOMDocument.
Wspomniana klasa wymaga uruchomienie na serwerze rozszerzenia DOM (który jest domyślnie aktywowany) oraz libxml. Rozszerzenia te są jednak dostępne na większości hostingów (nie mówię tutaj o darmowych hostingach), więc nie ma żadnych problemów.
Przykładowy kod może wyglądać następująco:
1 2 3 4 5 6 7 8 9 10 11 12 13 | // Create a new DOM Document to hold our webpage structure $dom = new DOMDocument(); // Load the url's contents into the DOM @$dom->loadHTML($text); // Empty array to hold all links to return $links = array(); // Loop through each <a> tag in the dom and add it to the link array foreach ($dom->getElementsByTagName('a') as $link) { $links[] = $link->getAttribute('href'); } |
Zamiast metody loadHTML można również wykorzystać loadHTMLFile, podając bezpośredni adres URL, co jeszcze bardziej ułatwia nam zadanie i wyklucza wykorzystanie cURL.
Poza tym warto zauważyć, że „wygłuszamy” błędy przy pomocy znaku @ dla metody loadHTML. Zrobiłem to celowo, aby usunąć niechciane informacje o nieprawidłowo sformatowanym dokumencie XML, złych deklaracjach, etc.
Poza tym, każdemu kto zna język JavaScript powyższa składnia będzie bardzo znajoma. Bowiem w JavaScript istnieją dokładnie takie same metody do obsługi drzewa DOM (z powyższego listingu: getElementsByTagName, getAttribute).
Wyrażenia regularne vs DOM Document
Przeprowadziłem krótki benchmark celem sprawdzenia która metoda jest szybsza. Kod testujący działanie tych metod (zresztą już wcześniej wykorzystywany i testowany):
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 | function Benchmark($function, $iterations=1000, $args=null) { set_time_limit(0); if (is_callable($function) === true) { $result = microtime(true); for ($i = 1; $i <= $iterations; $i++) { call_user_func_array($function, $args); } return round(microtime(true) - $result, 4); } return false; } |
Pełen kod benchmarku: http://pastebin.com/n4YFuTmM
A oto uzyskane wyniki:
Metoda / iteracje | 1 | 10 | 100 |
DOM Document | 3.8939 | 38.4944 | 399.2282 |
Wyrażenia regularne | 0.0383 | 0.1597 | 1.6267 |
Po takich wynikach jakie prezentuję powyżej, nie zwiększałem już ilości wywoływanych funkcji podczas testu. Wyraźnie bowiem widać, jak wielka różnica w wydajności poszczególnych rozwiązań istnieje.
Niewątpliwie przoduje tutaj wyrażenie regularne, które mimo iż nie jest napisane w najlepszy sposób, jest wielokrotnie szybsze (ponad 100 razy). Jest to spory argument ZA!
Podsumowanie
Po powyższym teście może zdziwić, ale mimo wszystko polecam używanie klasy DOMDocument do operacji na drzewie DOM. Dlaczego? Ano ważniejszy niż czas wykonania kodu po stronie serwera jest tylko nasz czas i nerwy na przygotowanie danego kodu.
Zamiast więc pisać bardzo kłopotliwy i skomplikowany kod, w którym ciężko doszukać się błędów i który jest trudno skalowalny, lepiej ten czas wykorzystać na optymalizację kodu w innych miejscach aplikacji.
Co jeśli zamiast linków chciałbyś pobrać wszystkie obrazy z danego kodu HTML? A jeśli chciałbyś pobrać dodatkowo tekst alternatywny z tego obrazka? Nasze wyrażenie regularne mogłoby się rozrastać w nieskończoność powodując coraz większe zaciemnienie kodu. Podczas gdy analizując drzewo DOM dokumentu zrobilibyśmy to w kilkanaście sekund :-)
Kolejnym argumentem przemawiającym za stosowaniem rozszerzenia DOM jest możliwość zintegrowania naszego drzewka z DOMXPath.
Z takimi narzędziami żaden problem nie będzie Ci przeszkodą, a pisanie własnych pajączków sieciowych przyjemnością!
A chyba ktoś tu zapomniał o phpQuery, o znacznie większych możliwościach. ;)
nie słyszałem o tej bibliotece, ale jak widzę to faktycznie jeszcze bardziej umila pracę :-)
wcześniej porównałem klasę DOMDocument do pracy na drzewie DOM w JavaScript (przy czym nie trzeba obsługiwać różnych przeglądarek z osobna) – to teraz phpQuery ułatwia pracę niczym jQuery w JavaScriptowym DOM-ie.
dzięki za zajawkę eRIZ :)
A spóźniłem się, bo też chciałem polecić phpQuery :) Ale fajny post, dzięki! :)
Właśnie szukałem takiego czegoś do mojej strony. phpQuery też próbowałem. Dziękuje bardzo!
Ostatni popełniłem takie coś… bez phpQuery.
Działa, niestety. :)
Ten kod nie zadziala dla <a title=”cos” href=”cos.html”> co tez jest poprawnym zapisem ;)
Ano nie zadziała :-) Niemniej łatwo poprawić ten kod.
Zresztą powyżej napisałem:
Jako przewagę DOM Document nad wyrażeniami regularnymi podałem łatwość napisania odpowiednich regułek pobierających. W przypadku wyrażeń regularnych musimy myśleć o setkach różnych możliwościach zapisu HTML, podczas gdy skacząc po drzewie DOM niczym takim nie musimy się martwić.
do parsowania i wyciagania dowolnych elemetow ze strony polecam JS_Extractor. Bardzo stary ale skuteczny skrypcik. Uzywalem tez jego wczesniejszej wersji „TableExtractor”- jesli trzeba wyciagnac dane tabelaryczne to jest on nie dozastapienia.
Faktycznie, świetne narzędzie! :-)