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:

Best java web scraping libraries
Dostępne rozwiązania dla *web scrapingu* w Javie, źródło: https://mobilemonitoringsolutions.com

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:

Web scraping on pracuj.pl

Nasza przeglądarka przejdzie pod adres https://www.pracuj.pl/praca/java;kw/warszawa;wp?rd=30. Istotne są tutaj frazy “java” oraz “warszawa”. To nasze podane parametry w URL. 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:

Web scraping css selector link

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:

Javascript check css selector

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:

Web scraping css selector data

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:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<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:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
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.

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ą!