Ile razy natrafiałeś bądź natrafiałaś na problem, że dana witryna udostępnia interesujące Cię dane w skomplikowanej bądź nieczytelnej formie? Że wolałbyś/aś, aby były one zaprezentowane w inny sposób. Wtedy wpadasz na pomysł jak mogłaby wyglądać ekspozycja tych danych i zabierasz się za jej implementację. Natomiast znowu wybrana witryna stwarza problemy, ponieważ nie udostępnia żadnego publicznego API do wykorzystania. Wtedy ostatnią deską ratunku okazuje się wykorzystanie Web Scrapingu.
Web Scraping to bardzo ciekawa metoda polegającą na wyciąganiu danych ze strony internetowej. Jej głównym zastosowaniem jest poszukiwanie w sieci dogodnych okazji takich jak tanie bilety lotnicze, wielkie promocje w sklepach internetowych itd. Jednak web scraping może także posłużyć do zdobywania danych dla naszej aplikacji. Sam tworząc własne aplikacje zawsze generowałem testowe wartości, jednak przychodzi w końcu taki moment, że warto byłoby skorzystać z rzeczywistych danych. Z tego powodu zainteresowałem się zagadnieniem web scrapingu z punktu widzenia Java Developera, który chciałbym Ci teraz przedstawić.
Web Scraping w Javie
Wpisując w wyszukiwarkę frazę „web scraping java” uzyskujemy wiele odnośników prowadzących do możliwych rozwiązań. Na tą chwilę istnieje kilka bibliotek Javowych służących do web scrapingu:

Najbardziej popularnymi rozwiązaniami, sądząc po liczbie wyszukanych fraz, wydają mi się HTMLUnit oraz Jsoup. To właśnie im poświęciłem swoją uwagę i wykorzystałem do przykładowego projektu.
HTMLUnit
W skrócie HTMLUnit można przedstawić jako przeglądarkę bez interfejsu graficznego dla programów napisanych w Javie. Podczas użycia tej biblioteki otrzymujemy modele HTML, z którymi możemy oddziaływać poprzez dostarczone API. Przykładowo możemy przechodzić do podstron, wypełniać formularze itd., czyli wszystko to co robimy w standardowych przeglądarkach.
Należy zaznaczyć, że HTMLUnit wspiera obsługę JavaScriptu na przeglądanych stronach. Zdecydowanie jest to pomocne w przypadku, gdy dana witryna jest generowana dynamicznie. W ten sposób możemy odwołać się do elementów, które nie byłyby dostępne z poziomu statycznego kodu HTML. Warto jeszcze dodać, że biblioteka jest ciągle rozwijana, ostatnia wersja została wypuszczona 3 października 2020r.
Jsoup
Dzięki Jsoup uzyskujemy możliwość pracy z kodem HTML stron internetowych, które nas interesują. Poprzez API biblioteki możemy manipulować danymi poprzez wykorzystanie np. selektorów CSS. Muszę przyznać, że jest to bardzo wygodne zwłaszcza przy wydobywaniu danych tekstowych danego elementu HTML. Należy podkreślić, że biblioteka również jest na bieżąco ulepszana, jej ostatnia wersja pochodzi z początku bieżącego roku.
Wykorzystanie w aplikacji Java
Chciałbym przedstawić użycie wyżej wymienionych bibliotek w projekcie znajdującym się w moim repozytorium Gita. Nosi on nazwę JobWebScrapper i ma za zadanie zbierać dane o interesującym nas stanowisku znajdującym się w internetowych ogłoszeniach o pracę. Rozważyłem w nim trzy witryny: pracuj.pl, praca.pl oraz bulldogjob.pl. Mam pytanie, czy nie uważasz, że w tym projekcie nie zerwałem czasem z zasadą Segragacji Interfejsów? 😉
Analiza przypadku
Na samum początku trzeba sprawdzić jak w ogóle zachowuje się strona internetowa, którą jesteśmy zainteresowani. Przejdźmy, więc do witryny pracuj.pl, gdzie uzyskamy okno z wyszukiwarką. Podajemy interesujące nas stanowisko oraz miasto i klikamy przycisk Szukaj:

Nasza przeglądarka przejdzie pod adres https://www.pracuj.pl/praca/java;kw/warszawa;wp?rd=30. Pogrubiłem specjalnie frazy „java” oraz „warszawa„, aby pokazać gdzie w url znajdują się podane przez nas parametry. To właśnie w te dwa miejsca będziemy mogli wstawiać parametry w programie w zależności od naszych potrzeb.
Na obecnej stronie widzimy sporą ilość ofert pracy dla Java Developera. Zagłębmy się teraz w HTML przedstawionej witryny i znajdźmy sposób w jaki możemy wydobyć z niej dane:

Analizując elementy strony zauważyłem, że odpowiednim selektorem CSS będzie „#results .offer-details__title-link„. W ten sposób uzyskujemy dostęp do wszystkich interesujących nas linków z ogłoszeniami o pracę. Możemy to łatwo zweryfikować poprzez konsolę w przeglądarce i odrobiną JavaScriptu:

Klikając w jeden z prezentowanych linków przejdziemy do oferty pracy zawierającej obszerniejsze informacje o danym stanowisku. Tutaj także musimy przejrzeć źródło strony, aby zweryfikować jak dostać się do informacji np. o nazwie firmy:

Widać, że wystarczy użyć selektora CSS w postaci „h2[data-test=’text-employerName’]„, który wybierze nam element zawierający nazwę firmy. Dzięki poświęceniu chwili na analizę problemu wiemy w jaki sposób musimy zaimplementować web scraping, aby uzyskać potrzebne nam dane.
Implementacja Web Scraping
Na początku należy dodać odpowiednie zależności do naszego projektu. Korzystając z Mavena dokonujemy następujących wpisów w pom.xml:
<dependencies> // miejsce na inne zależności <dependency> <groupId>net.sourceforge.htmlunit</groupId> <artifactId>htmlunit</artifactId> <version>2.44.0</version> </dependency> <dependency> <groupId>org.jsoup</groupId> <artifactId>jsoup</artifactId> <version>1.13.1</version> </dependency> </dependencies>
Teraz możemy korzystać z dobrodziejstw jakie dają nam te dwie biblioteki. W celu uzyskania nazw firm, które ogłaszają się na portalu, należy napisać następujący kod:
package pl.csanecki.jobsearcher; import com.gargoylesoftware.htmlunit.BrowserVersion; import com.gargoylesoftware.htmlunit.WebClient; import com.gargoylesoftware.htmlunit.html.HtmlPage; import org.jsoup.Jsoup; import org.jsoup.nodes.Document; import org.jsoup.nodes.Element; import org.jsoup.select.Elements; import java.io.IOException; import java.util.Set; import java.util.stream.Collectors; public class WebScrapingExample { public static void main(String[] args) { String url = String.format( "https://www.pracuj.pl/praca/%s;kw/%s;wp?rd=30", "java", "warszawa" ); try (WebClient webClient = setUpWebClient()) { HtmlPage htmlPage = webClient.getPage(url); Document parsedDocument = Jsoup.parse(htmlPage.asXml()); Elements jobLinks = parsedDocument .select("#results .offer-details__title-link"); Set<String> employersNames = jobLinks.stream() .map(jobLink -> jobLink.attr("href")) .map(WebScrapingExample::scrapeThroughOfferPage) .collect(Collectors.toSet()); employersNames.forEach(System.out::println); } catch (IOException e) { throw new RuntimeException("Cannot connect to " + url); } } private static String scrapeThroughOfferPage(String subUrl) { try (WebClient subWebClient = setUpWebClient()) { HtmlPage subHtmlPage = subWebClient.getPage(subUrl); Document subParsedDocument = Jsoup.parse(subHtmlPage.asXml()); Element employerElement = subParsedDocument .selectFirst("h2[data-test='text-employerName']"); return employerElement.ownText(); } catch (IOException e) { throw new RuntimeException("Cannot connect to " + subUrl); } } private static WebClient setUpWebClient() { WebClient webClient = new WebClient(BrowserVersion.FIREFOX); webClient.getOptions().setThrowExceptionOnScriptError(false); webClient.getOptions().setThrowExceptionOnFailingStatusCode(false); return webClient; } }
Przejdźmy teraz do omówienia poszczególnych elementów naszego programu. Głównym elementem HTMLUnit jest klasa WebClient, która umożliwia pobranie kontentu interesującej nas strony. Należy zwrócić uwagę na to w jaki sposób jest ona tworzona w metodzie setUpWebClient. Musiałem ustawić dwie opcje, które wyłączą rzucanie wyjątku w przypadku wystąpienia błędów podczas przetwarzania strony internetowej. Dodatkowo ustawiona została przeglądarka Firefox umożliwiająca poprawne załadowanie treści wyszukiwarki.
W liniach 18-22 tworzony jest adres URL, z parametrami „java” oraz „warszawa„, do którego będziemy chcieli się odwołać. Następnie nasz obiekt klasy WebClient przetwarza interesującą nas stronę tworząc z niej XML, który przekazywany jest do metody parse klasy Jsoup. W ten sposób przechodzimy do statycznej analizy pobranej witryny. Dzięki metodzie select i odpowiedniemu selektorowi CSS pobieramy wszystkie linki do ofert znajdujących się w rezultacie wyszukiwania. Kolejnym krokiem jest odpytanie o każdą stronę znajdującą się w naszych odnośnikach i wydobycie z nich nazw firm wystawiających ogłoszenie. Wszystko to na koniec zbierane jest do zbioru, aby uniknąć duplikatów. Rezultat poszukiwania danych przedstawia się następująco:
Avenga HEBE Jeronimo Martins Drogerie i Farmacja DECERTO Sp. z o.o. Cyfrowy Polsat S.A. SII Sp. z o.o. Urząd Komisji Nadzoru Finansowego ASTEK Polska Raiffeisen Bank International AG (Spółka Akcyjna) Oddział w Polsce Diverse CG Sp. z o.o. sp.k. PKO BP Finat sp. z o.o. Accenture Technology T-Mobile Mindbox S.A. Netcompany Poland Sp. z o.o. SORIGO Sp. z o.o. Sp. k. ERGO Digital IT Gmbh Sp. z o.o. Oddział w Polsce Sollers Consulting PKO Bank Polski SA Michael Page Relyon IT Services SoftwarePlant Amelco UK Ltd Acxiom Global Service Center Polska sp. z o. o Wojskowe Zakłady Lotnicze Nr 2 Spółka Akcyjna ING Tech Poland Roche Pharma Poland Pentacomp TRANSACTIONLINK sp. z o.o. Cheil Poland NASK PEOPLE Sp. z o.o. e-point Polska Sp. z o.o. eg+ worldwide Polkomtel Sp. z o.o. EY Global Delivery Services Sportradar Polska Sp. z o.o. Comarch SA DXC Technology Bank Millennium S.A. ProData Consult Polska BNP Paribas Bank Polska S.A.
Kod do przykładu powyżej znajdziesz pod tym adresem na moim GitHubie.
Podsumowanie
Prawda, że jest to fascynujące i bardzo pomocne narzędzie? Spróbuj sam dokonać analizy i pozyskać interesujące Ciebie dane do Twojej aplikacji. W razie jakichkolwiek pytań napisz do mnie. Postaram się pomóc jeśli będę w stanie bądź razem znajdziemy rozwiązanie. Napisz także w komentarzu czy ten wpis był dla Ciebie przydatny albo czego w nim jeszcze brakuje. Czekam na Twoją opinię z niecierpliwością!