Jak wielowątkowo wczytać linki ze strony pracuj.pl do pliku?

0

Witam, to mój pierwszy post.

Mam problem z wątkami i pętlą. Mam kod, który pobiera linki do ogłoszeń pracy ze strony pracuj.pl. Chciałbym, żeby wszystko działo się naraz z użyciem wielu wątków.

Główna klasa wygląda tak:

package org.example;

import java.io.IOException;
import java.util.ArrayList;
import java.util.Set;
import java.util.TreeSet;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

public class Pracuj {
    private static final String PRACUJ_PL_URL = "https://www.pracuj.pl/praca/warszawa;wp?rd=0&pn=";

    public static void main(String[] args) throws Exception {
        SeparetedLinks separetedLinks = new SeparetedLinks();
        Set<String> links = new TreeSet<>();
        ExecutorService executorService = Executors.newFixedThreadPool(10);

        long start = System.currentTimeMillis();

        try {
            executorService.submit(OperationsOnFiles::deleteFiles);
            executorService.submit(() -> {
                try {
                    getLinks(separetedLinks, links);
                } catch (IOException e) {
                    throw new RuntimeException(e);
                }
            });
        } finally {
            ArrayList<String> listOfLinks = new ArrayList<>(links);
            OperationsOnFiles.saveFiles(listOfLinks);
            System.out.println("Saved " + listOfLinks.size() + " links.");
        }
        long end = System.currentTimeMillis();
        System.out.println(end - start);
        executorService.shutdown();
    }

    private static void getLinks(SeparetedLinks separetedLinks, Set<String> links) throws IOException {
        int i = 1;
        int startValue;
        int endValue;
        
        while (IsUrlValid.isValid(PRACUJ_PL_URL + i)) {
            startValue = links.size();
            separetedLinks.addLinksToList(links, PRACUJ_PL_URL + i);
            endValue = links.size();
            if (startValue == endValue) {
                break;
            }
            i++;
        }
    }
}

Dopiero uczę się o wątkach, a z "programowaniem" mam styczność od pół roku. Bez dodania ExecutorService program działał dobrze i zapisywał do pliku wszystkie linki z pracuj.pl, chcę skrócić jego czas działania, zoptymalizować kod. Dziękuję za wszystkie rady i podpowiedzi.

2

Parę pytań / wątpliwości / porad:

  1. Kiedy są zainicjalizowane links?
  2. Czy metoda separetedLinks.addLinksToList jest thread-safe?
  3. Sytuacja myślę typowa do użycia ForkJoinPool.
1
  1. Masz tylko 2 zadania w powyższym kodzie, obecnie ta wielowątkowość jedno zadanie wykona się na plikach, a drugie na linkach. Chyba chodziło o obsługę "linków" na wielu wątkach?

(nie widać jak inicjalizujesz Set<String> links)

  1. Kolejność zakończenia zadań może być nieokreślona.
     executorService.submit(OperationsOnFiles::deleteFiles);
            executorService.submit(() -> {

np. deleteFile może zacząć się zanim zaczniesz wykonywać pozostałę taski, a zakończyć się po tym jak 70% innych tasków zostanie wykonane.

Czy brak gwarancji co do kolejności zakończenia tasków jest dla Ciebie problemem?

  1. Set<String> links = new TreeSet<>(); nie jest thread safe, a w getLinks będzie aktualizowane przez różne wątki.

  2. Zobacz sobie opis metody shutdown w ExecutorService -> nie czeka na zakończenie zadań, a Ciebie chyba interesuje, żeby zadania zostały zakończone.

  3. Może warto zmienić koncepcję, tak by zadanie zwracało listę przetwrozonych elementów, a nie mutowało przekazane argumenty?

  4. Jak chcesz dystrybuować obciążenie na wiele wątków? Zadania same mają sobie pobierać "następny link do przetworzenia", czy może lista ma przychodzić z zewnątrz?
    Jeśli pobierać, to może przekazuj im kolejkę, z które będą pobierać linki (BlockingQueue)?

  5. Logika while (IsUrlValid.isValid(PRACUJ_PL_URL + i)) { jest podejrzana. Mamy np. zbiór i={1,2,3,5,7 } - załóżmy, że isValid dla 2 jest false, chcesz przerwać przetwarzania? Czy może jednak chciałbyś obsłużyć pozostałe elementy, tj. {3,5,7} ?

1

@sgorski00: wszystko ładnie pięknie, ale zrównolegliłeś nie to, co trzeba. Zanim przejdę do tego co i jak, to mam kilka pytań:

  1. Co oznacza „pobiera linki do ogłoszeń”?
  2. Czy IsUrlValid.isValid(PRACUJ_PL_URL + i) wykonuje jakieś połączenia w tle, które weryfikują poprawność linku, czy to jest robione lokalnie?
  3. Czy separetedLinks.addLinksToList(links, PRACUJ_PL_URL + i); tylko dodaje linki do listy, czy też wykonuje dodatkowe czynności np. łączy się po sieci? Szczególnie w kontekście if (startValue == endValue), bo ten kod wygląda tak, jakby możliwe było niedodanie linku do listy. Wtedy wielkość przed i po dodaniu linku będzie taka sama.

Bo na chwilę obecną ten kod to tak średnio można zrównoleglić bez wiedzy co się dzieje pod spodem.

1

ExecutorService ma być w try with resources - shutdown samo się zrobi i dokończy taski ( shutdownnow wysyła interrupt co również nie oznacza że taski nie zostaną skończone) Links ma być w strukturze która zapewnia nie tylko thread safe addition ale aktualne size z volatile memory visibility. Więc musi robić lock na obiekcie zanim pobierze size. Słaby performance. Getlinks nie zadziała. startvalue, endvalue powinny być atomic i jako pole ( i również jako pole), a nie local variables. W metodzie addLinksToList dodajesz do struktury nie thread safe nie atomowo zaktualizowany URL - kiepsko . Łamie się również warunek if na którym powinien być Lock. Kiepsko poformatowane - z plaży pisze.

0
Koziołek napisał(a):

@sgorski00: wszystko ładnie pięknie, ale zrównolegliłeś nie to, co trzeba. Zanim przejdę do tego co i jak, to mam kilka pytań:

  1. Co oznacza „pobiera linki do ogłoszeń”?
  2. Czy IsUrlValid.isValid(PRACUJ_PL_URL + i) wykonuje jakieś połączenia w tle, które weryfikują poprawność linku, czy to jest robione lokalnie?
  3. Czy separetedLinks.addLinksToList(links, PRACUJ_PL_URL + i); tylko dodaje linki do listy, czy też wykonuje dodatkowe czynności np. łączy się po sieci? Szczególnie w kontekście if (startValue == endValue), bo ten kod wygląda tak, jakby możliwe było niedodanie linku do listy. Wtedy wielkość przed i po dodaniu linku będzie taka sama.

Bo na chwilę obecną ten kod to tak średnio można zrównoleglić bez wiedzy co się dzieje pod spodem.

Dzięki za odpowiedź!

  1. Ten kod przechodzi po stronie i wyszukuje pojedyncze linki do ogłoszeń z serwisu, po czym zapisuje je do pliku .url.
  2. Raczej tylko lokalnie, chciałem skupić się przy tym kodzie na nauce pracy z wątkami. Tutaj kodzik tej klasy:

import java.net.URI;
import java.net.URISyntaxException;

public class IsUrlValid {
    static boolean isValid(String url){
        try{
            new URI(url).toURL().;
            return true;
        } catch (URISyntaxException e) {
            return false;
        }
    }
}

  1. Ta metoda używa innych metod, sama w sobie tylko dodaje do listy ale wykorzystuje inne metody, które przechodzą po całym kodzie strony i wycinają tylko interesujące mnie kawałki kodu. Poniżej klasa SeparatedLinks:

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.net.URI;
import java.net.URISyntaxException;

import java.util.Set;

public class SeparetedLinks {
    private StringBuilder readWebsite(String url) throws IOException, URISyntaxException {
        URI pracuj = new URI(url);
        BufferedReader in = new BufferedReader(new InputStreamReader(pracuj.toURL().openStream()));

        String inputLine;
        StringBuilder stringBuilder = new StringBuilder();
        while ((inputLine = in.readLine()) != null) {
            stringBuilder.append(inputLine);
            stringBuilder.append(System.lineSeparator());
        }
        in.close();

        return stringBuilder;
    }

    private void getAllLinksFromPage(Set<String> links, String offersSection) {
        String splittedToLink;
        for (int i = 0; i < offersSection.length(); i++) {
            if (i < 0) {
                break;
            }
            i = offersSection.indexOf("https://www.pracuj.pl/praca/", i);
            splittedToLink = offersSection.substring(i);
            String finalLink = splittedToLink.split("\"")[0];
            links.add(finalLink);
        }
    }
    private String getOffersDiv(String fullPage) {
        int index = fullPage.indexOf("<body>");
        String substring = fullPage.substring(index);
        return substring.split("<div data-test=\"job-offers-bottom-pagination\"")[0];
    }

    void addLinksToList(Set<String> links, String website) throws IOException {
        try {
            getAllLinksFromPage(links, getOffersDiv(readWebsite(website).toString()));
        } catch (StringIndexOutOfBoundsException | URISyntaxException ignored) {}
    }
}

Wiem, że kod nie najlepszej jakości ale dopiero się uczę... Swoją drogą, jak najlepiej uczyć się efektywnego pisania? W sensie gdzie mogę się nauczyć pisać tak, żeby program nie zabierał zbyt dużo zasobów i żeby nie składał się z miliona linijek kodu? I przede wszystkim, żeby aplikacja działała szybko i dobrze.

1

@sgorski00: czyli próbujesz zrównoleglić nie to, co potrzeba :) Czas na kilka poprawek.

Najpierw kosmetyka i drobna poprawka:

public class IsUrlValid {
    Optional<URL> isValid(String url){
		try{
			return Optional.of(URI.create(url).toURL());
		} catch (MalformedURLException| URISyntaxException e) {
			return Optional.empty();
		}
	}
}

Na początek zrobimy sobie Optional na URLu. Zauważ, że SeparetedLinks.readWebsite ponownie tworzysz URL. Niepotrzebnie :) Teraz, o ile dobrze rozumiem logikę kodu, zaczynasz iść po poszczególnych stronach pracuj.pl i pobierasz wszystkie linki do ofert pracy z danej strony. To nie będzie działać, bo jak masz np. https://www.pracuj.pl/praca/warszawa;wp?rd=0&pn=40000, to i tak otworzy się ostatnia dostępna strona (i teraz rozumiem tego IFa).

Operacją, którą chcesz zrównoleglić, to sama operacja pobrania strony i wybrania ofert. Zatem do ExecutorService chcesz wysłać zadanie w stylu

service.submit(()-> separetedLinks.getAllLinksFromPage(URL));

ale…

No właśnie jest jedno ALE związane z tym, co pisałem wyżej. Gdy przekroczysz liczbę stron, to będziesz dostawał ostatnią. Program będzie działać w nieskończoność. Musisz znaleźć warunek stopu, żeby zakończyć generowanie URLi do sprawdzenia. Rzecz w tym, że tempo generowania URLi jest dużo większe niż czas potrzebny, do pobrania strony. Zatem jak pobierzesz ostatnią stronę, to będziesz miał jeszcze tysiące kolejnych do sprawdzenia. Wygenerują się w czasie, gdy ty będziesz pobierać strony.

Wniosek

Skoro możliwość wygenerowania poprawnego URLa jest uzależniona od tego, co do tej pory pobraliśmy, to nasze zadania nie są niezależne. Inaczej mówiąc, pobranie strony o numerze N, zależy od pobrania strony N-1.

Zatem nie można tego zadania przekształcić na taki, które można wykonać współbieżnie

Inne podejście

Oczywiście wniosek jest mocno na wyrost, bo można pobrać maksymalną ilość stron, ale to wymaga grzebania w HTMLu i nie ma gwarancji, że pobrana wartość będzie poprawna. W każdym razie, żeby ją pobrać, to trzeba znaleźć zawartość elementu

<span data-test="top-pagination-max-page-number">ZZZZ</span>

Gdzie ZZZZ to maksymalna ilość stron. Mając tę wartość, możesz zrobić mniej więcej tak:

public static void main(String[] args){
  var maxPage = getMaxPageNumber(PRACUJ_PL_URL); // pobierasz stronę 0 i wyłuskujesz z niej maksymalną stronę
  ExecutorService service = Executors.newFixedThreadPool(10);
  Map<Integer, Set<String>> linksMap = new ConcurrentHashMap();


  for(int i =1; i<=maxPage; i++){
    service.submit(()-> {
        Set<String> links = IsValidUrl.getValid(PRACUJ_PL_URL+i)
          .map(separetedLinks::getAllLinksFromPage)
          .get();
        linksMap.put(i, links);
      });
  } 

  service.shutdown(); // Nie akceptujemy nowych zadań i muis
  
  // teraz musimy poczekać aż wszystko się skończy

  try {
    service.awaitTermination(120, TimeUnit.SECONDS); // 2 minuty. Ale jak się skończy wcześniej, to ruszy dalej :)
  } catch (InterruptedException e) {
	throw new RuntimeException(e);
  }

  // zbieramy nasze wyniki do jednego setu
  
  Set<String> uniqueLinks =	links.values().stream()
                            .flatMap(Set::stream)
                            .collect(Collectors.toSet());
}

I to jest mocno uproszczone podejście. Inne polega na wykorzystaniu ForkJoinPool, ale to trochę bardziej złożone :)

W ogóle zadanie jest fajne, przez ten warunek stopu. Sam je wymyśliłeś?

Swoją drogą, jak najlepiej uczyć się efektywnego pisania? W sensie gdzie mogę się nauczyć pisać tak, żeby program nie zabierał zbyt dużo zasobów i żeby nie składał się z miliona linijek kodu? I przede wszystkim, żeby aplikacja działała szybko i dobrze.

Czytać. Na początek Clean Code wystarczy. No i pytać tutaj i się nie bać :)

0
Koziołek napisał(a):

@sgorski00: czyli próbujesz zrównoleglić nie to, co potrzeba :) Czas na kilka poprawek.

Czytać. Na początek Clean Code wystarczy. No i pytać tutaj i się nie bać :)

Mega dziękuję za rady! W szoku jestem że są ludzie którym tak chce się pomagać początkującym.
Zadanie zmodyfikowałem z zapisu livestreama od Przemka Bykowskiego na youtubie, On tam pokazywał właśnie jak działają klasy Thread i ExecutorService, oraz jakie są z tego korzyści. Zmieniłem tylko stronę na której działałem i pare rzeczy dodałemod siebie, żeby nie przepisywać kodu słowo w słowo tylko samemu trochę pomyśleć.
Teraz nie będę miał czasu, żeby posiedzieć przy laptopie ale za tydzień tu na pewno wrócę i zdam raport jak mi poszło z twoimi poprawkami!
A ta książka to chyba must have jak ktoś lubi programować, wszyscy ją polecają więc będzie trzeba się zagłębić.
Jeszcze raz dziękuję za poświęcony czas i na pewno zostanę na dłużej na tym forum, pozdrawiam :)

0

Dobra, wróciłem do kodu i namieszałem, było już w miarę okej ale i tak napotkałem pare problemów z którymi nie dałem rady :D
Tutaj niedziałający kod na ten moment, klasa główna:

public class Pracuj {
    private static final String PRACUJ_PL_URL = "https://www.pracuj.pl/praca/wroclaw;wp?rd=0&pn=";

    public static void main(String[] args) throws Exception {
        SeparetedLinks separetedLinks = new SeparetedLinks();
        var listOfLinks = new ArrayList<String>();
        var service = Executors.newFixedThreadPool(10);
        Map<Integer, Set<String>> linksMap = new ConcurrentHashMap<>();
        int maxPage = Integer.parseInt(separetedLinks.getMaxPageNumber(PRACUJ_PL_URL));

        for(int i = 1; i<=maxPage; i++){
            service.submit(()-> {
                 Set<String> linksSet = IsUrlValid.getValid(PRACUJ_PL_URL + i)
                        .map(separetedLinks::getAllLinksFromPage)
                        .get();
                linksMap.put(i, linksSet);
            });
        }

        service.shutdown();

        try{
            service.awaitTermination(120, TimeUnit.SECONDS);
        } catch (InterruptedException e){
            e.printStackTrace();
        }

        Set<String> uniqueLinks = linksMap.values().stream()
                .flatMap(Set::stream)
                .collect(Collectors.toSet());

        try {
            OperationsOnFiles.deleteFiles();
            listOfLinks = new ArrayList<>(uniqueLinks);
        } finally {
            OperationsOnFiles.saveFiles(listOfLinks);
            System.out.println("Saved " + listOfLinks.size() + " links.");
        }
    }
}

I klasa z obrabianiem strony html:

public class SeparetedLinks {
    private StringBuilder readWebsite(String url) throws IOException{
        BufferedReader in = new BufferedReader(new InputStreamReader(IsUrlValid.getValid(url).get().openStream()));

        String inputLine;
        StringBuilder stringBuilder = new StringBuilder();
        while ((inputLine = in.readLine()) != null) {
            stringBuilder.append(inputLine);
            stringBuilder.append(System.lineSeparator());
        }
        in.close();

        return stringBuilder;
    }

    void getAllLinksFromPage(Set<String> links, String website) {
        String splittedToLink;
        for (int i = 0; i < getOffersDiv(website).length(); i++) {
            if (i < 0) {
                break;
            }
            i = getOffersDiv(website).indexOf("https://www.pracuj.pl/praca/", i);
            splittedToLink = getOffersDiv(website).substring(i);
            String finalLink = splittedToLink.split("\"")[0];
            links.add(finalLink);
        }
    }

    private String getOffersDiv(String fullPage) {
        String substring = getElement(fullPage, "<body>");
        return substring.split("<div data-test=\"job-offers-bottom-pagination\"")[0];
    }

    String getMaxPageNumber(String websiteLink) throws IOException {
        String result = getElement(readWebsite(websiteLink).toString(), "\"top-pagination-max-page-number\">");
        result = result.split("</span>")[0];
        result = result.substring(result.indexOf(">") + 1);
        return result;
    }

    private String getElement(String fullPage, String textToGetIndex) {
        int index = fullPage.indexOf(textToGetIndex);
        return fullPage.substring(index);
    }
}

Więc tak, chcę wrócić do momentu w którym zacząłem post, ale na razie opiszę rzeczy, z którymi mam problem.

  1. W tym fragmencie kodu:
for(int i = 1; i<=maxPage; i++){
            service.submit(()-> {
                 Set<String> linksSet = IsUrlValid.getValid(PRACUJ_PL_URL + i)
                        .map(separetedLinks::getAllLinksFromPage)
                        .get();
                linksMap.put(i, linksSet);
            });
        }

Mam problem z inkrementacją zmiennej i. Moje IDE podpowiada tak: Variable used in lambda expression should be final or effectively final. Walczyłem z tym i nic nie wywalczyłem, wydaje mi się, że należy użyć tutaj AtomicInteger, ale nie umiem z tego jeszcze korzystać.
2. Drugim kłopotem jest używanie streamów w tej samej części kodu. Z tego co wywnioskowałem, streamujemy tutaj tego optionala url, ale nie rozumiem dlaczego w tej linijce

.map(separetedLinks::getAllLinksFromPage)

używamy referencji do metody i skąd program ma wiedzieć jakie argumenty wysyłamy do tej metody.
3. Dodatkowo przy włączeniu programu z użyciem wielu różnych wariantów, cały czas wyskakuje mi taki błąd:

java: incompatible types: invalid method reference
method getAllLinksFromPage in class org.example.SeparetedLinks cannot be applied to given types
required: java.util.Set<java.lang.String>,java.lang.String
found: java.net.URL
reason: actual and formal argument lists differ in length

Również próbowałem naprawić to na wiele sposobów, ale za każdym razem rezultat jest podobny.
4. Na domiar złego, jak już myślałem, że jakoś to naprawiłem to wywaliło mi StringIndexOutOfBoundException :(

Ostatecznie powróce do stanu początkowego kodu i będę walczył dalej, ale potrzebuję pomocy. Dziękuję za wszystkie odpowiedzi i pozdrawiam.

0

Dobra dałem d**y :) Rzeczywiście używam zmiennej w streamie :) No ale textbox na forum to nie IDE.

for(int i = 1; i<=maxPage; i++){
  var currentUrl = PRACUJ_PL_URL + i;     
            service.submit(()-> {
                 Set<String> linksSet = IsUrlValid.getValid(currentUrl)
                        .map(separetedLinks::getAllLinksFromPage)
                        .get();
                linksMap.put(i, linksSet);
            });
        }

Znowuż void getAllLinksFromPage(Set<String> links, String website) powinno przyjmować tylko webside i zwracać Set

0

Pociesza mnie to, że dobrze kombinowałem i zrobiłem prawie tak jak Ty :D Też zmieniałem tamtą metodę żeby zwracała Set ale nie dopiąłem tego do końca.
Już wszystko zrobiłem ale dalej wywala błąd... Osiwieje z tym. Do rzeczy, Teraz kod wygląda tak:

public class Pracuj {
    private static final String PRACUJ_PL_URL = "https://www.pracuj.pl/praca/wroclaw;wp?rd=0&pn=";
    private static final SeparetedLinks separetedLinks = new SeparetedLinks();
    private static final Map<Integer, Set<String>> linksMap = new ConcurrentHashMap<>();

    public static void main(String[] args) throws Exception {
        Set<String> uniqueLinks = new HashSet<>();

        try {
            OperationsOnFiles.deleteFiles();
            getLinks();
            uniqueLinks = linksMap.values().stream()
                    .flatMap(Set::stream)
                    .collect(Collectors.toSet());
        } finally {
            ArrayList<String> listOfLinks = new ArrayList<>(uniqueLinks);
            OperationsOnFiles.saveFiles(listOfLinks);
            System.out.println("Saved " + listOfLinks.size() + " links.");
        }
    }

    private static void getLinks() throws IOException, URISyntaxException {
        int maxPage = Integer.parseInt(separetedLinks.getMaxPageNumber(PRACUJ_PL_URL));
        ExecutorService service = Executors.newFixedThreadPool(10);
        for (int i = 1; i <= maxPage; i++) {
            var currentUrl = PRACUJ_PL_URL + i;
            service.submit(() -> {
                Set<String> linksSet;
                linksSet = IsUrlValid.getValid(currentUrl)
                        .map(separetedLinks::getAllLinksFromPage)
                        .isPresent()
                        .get();
                linksMap.put(1, linksSet);
            });
        }
        service.shutdown();

        try {
            service.awaitTermination(120, TimeUnit.SECONDS);
        } catch (InterruptedException e) {
            throw new RuntimeException(e);
        }
    }
}

Wywala "java: incompatible types: java.net.URL cannot be converted to java.lang.String", problem jest w tej linijce:

.map(separetedLinks::getAllLinksFromPage)

Próbowałem zmienić wszystkie argumenty metod w klasie z obrabianiem HTML ze Stringa na URLa, ale nie pomogło. Myślałem, żeby napisać tą linijke bez referencji do metody i wklepać w argumencie zmienną currentUrl, ale też nie pomogło. na razie zostawiam to tak jak jest i będę robił coś nowego. Dziękuję za pomoc i cieszy mnie, że mogę zobaczyć jak eksperci piszą kod :D bardzo to pomaga. Pozdrawiam.

0

Czyli do referencji trafia Optional(URL) z getValid. A w klasie z HTML wszystkie argumenty metod to Stringi z URLem. Metodę getAllLinksFromPage zmieniłem w ten sposób, że zwraca Set<String> a jako argument przyjmuje tylko Stringa z linkiem do strony.

public class SeparetedLinks {
    private StringBuilder readWebsite(String url) throws IOException{
        BufferedReader in = new BufferedReader(new InputStreamReader(IsUrlValid.getValid(url).get().openStream()));

        String inputLine;
        StringBuilder stringBuilder = new StringBuilder();
        while ((inputLine = in.readLine()) != null) {
            stringBuilder.append(inputLine);
            stringBuilder.append(System.lineSeparator());
        }
        in.close();

        return stringBuilder;
    }

     Set<String> getAllLinksFromPage(String website) throws IOException, URISyntaxException {
        String splittedToLink;
        Set<String> links = new TreeSet<>();
        for (int i = 0; i < getOffersDiv(website).length(); i++) {
            if (i < 0) {
                break;
            }
            i = getOffersDiv(website).indexOf("https://www.pracuj.pl/praca/", i);
            splittedToLink = getOffersDiv(website).substring(i);
            String finalLink = splittedToLink.split("\"")[0];
            links.add(finalLink);
        }
        return links;
    }
    private String getOffersDiv(String website) throws IOException, URISyntaxException {
        String result = getElement(readWebsite(website).toString(), "<body>");
        return result.split("<div data-test=\"job-offers-bottom-pagination\"")[0];
    }

    String getMaxPageNumber(String websiteLink) throws IOException, URISyntaxException {
        String result = getElement(readWebsite(websiteLink).toString(), "\"top-pagination-max-page-number\">");
        result = result.split("</span>")[0];
        result = result.substring(result.indexOf(">") + 1);
        return result;
    }

    private String getElement(String fullPage, String textToGetIndex) {
        int index = fullPage.indexOf(textToGetIndex);
        return fullPage.substring(index);
    }
}

1 użytkowników online, w tym zalogowanych: 0, gości: 1