Wprowadzenie do Puppeteer - okładka

Wprowadzenie do Puppeteer

Opublikowano Kategorie BackendCzas czytania 11min

Puppeteer to biblioteka języka JavaScript umożliwiająca pracę z przeglądarkami Chrome i Firefox w trybie headless. W tym artykule dowiesz się, czym jest tryb headless, kiedy się przydaje oraz jak zacząć pracę z Puppeteerem.

Czym jest headless i Puppeteer?

Tryb headless to uruchomienie przeglądarki w trybie bez powłoki graficznej. Komunikacja z nimi może odbywać się za pośrednictwem CLI czy dedykowanych bibliotek, takich jak Puppeteer. Mogą to być powszechnie znane przeglądarki jak Chrome czy Firefox, ale istnieje też kilka mniej znanych rozwiązań.

Puppeteer to biblioteka JavaScript dostarczająca API do pracy z przeglądarkami w trybie headless. Dzięki temu można w prosty sposób wchodzić w interakcję z przeglądarkę z poziomu kodu JavaScript. W dalszej części artykułu będę wykorzystywał Puppeteera w wersji 23.5.0. Na przestrzeni czasu niektóre elementy API Puppeteera mogły ulec zmianie. Przed skorzystaniem, oprócz tego artykułu, koniecznie sprawdź dokumentację dla używanej przez Ciebie wersji.

Pierwsze uruchomienie Puppeteera

Puppeteer to zwykła biblioteka JavaScript i instaluje się ją tak samo, jak każdą inną. Możesz wykorzystać do tego dowolny menadżer pakietów. Puppeteera można zainstalować w dwóch wariantach. Pierwszy doinstalowuje kompatybilną wersję Chrome. Możesz również zainstalować puppeteer-core, który nie pobiera Chrome przy instalacji. Jeśli zdecydujesz się na ten drugi wariant, będziesz musiał(a) doinstalować Chrome samodzielnie. Warto wtedy zwrócić uwagę na instalowaną wersję Chrome, ponieważ każda wersja Puppeteera ma rekomendowaną wersję przeglądarki dla danej wersji.

Praca z niekompatybilną wersją może sprawić, że pewne funkcje Puppeteera nie będą działały poprawnie. Bazując na moich doświadczeniach, mogę potwierdzić, że nie jest to tylko teoretyczny problem. Szczególnie dotyczy to przypadków, gdy korzystamy ze starszej wersji niż rekomendowana. Zachęcam do pilnowania, by używana wersja Chrome była zgodna z rekomendowaną. Niekoniecznie będzie to najnowsza wersja przeglądarki. Wersję Chrome adekwatną do danej wersji Puppeteera znajdziesz w release notesach.

By ułatwić sobie życie i ograniczyć ewentualne problemy z wersją Chrome warto uruchomić aplikację w kontenerze. Dzięki temu unikniemy ewentualnych komplikacji z przeinstalowywaniem Chrome na lokalnej maszynie.

Mając już zainstalowaną bibliotekę i Chrome można uruchomić przeglądarkę metodą launch(). W tym momencie możemy zdefiniować dodatkowe opcje np. dodatkowe argumenty, które mają zostać przekazane do Chrome. Po uruchomieniu przeglądarki możemy tworzyć strony metodą browser.newPage(). Działa to analogicznie do kart w przeglądarkach w trybie UI. Na każdej stronie możemy niezależnie wykonywać akcje.


const browser = await puppeteer.launch( {
    executablePath: process.env.PUPPETEER_EXECUTABLE_PATH,
    args: [ '--no-sandbox', '--disable-gpu',  '--mute-audio' ]
} );

const page = await browser.newPage();

Zastosowania

Głównym zastosowaniem przeglądarek w trybie headless jest automatyzacja pracy wykonywanej w przeglądarkach. W dalszej części artykułu znajdziesz kilka konkretnych zastosowań wraz z przykładami kodów do wykorzystania.

Scraping danych

Przeglądarki w trybie headless nadają się do scrapowania danych z aplikacji webowych. Mają zastosowanie szczególnie w przypadku, gdy interesujące nas dane pojawiają się dopiero po wykonaniu jakiejś akcji na stronie np. po kliknięciu przycisku. W przypadku prostych statycznych stron, headless browser jest armatą na muchę. Wtedy prostszym rozwiązaniem może być skorzystanie np. z cURL-a. Jednak statyczne strony są coraz rzadziej spotykane i proste zapytania HTTP stają się niewystarczające.

Mówiąc o scrapingu, trzeba wspomnieć o potencjalnych problemach i ograniczeniach. Na mój obecny stan wiedzy nie ma przepisów zabraniających scrapowania danych. Mam tu na myśli dane dostępnie publicznie, a nie np. w wyniku luki bezpieczeństwa. Jednak podkreślam, że jestem programistą, a nie prawnikiem. Jeśli scrapujesz, to robisz to na własną odpowiedzialność. Problemy prawne mogą jednak wynikać np. wykorzystania pozyskanych danych w sposób niezgodny z prawem. Ponadto nadmierne obciążenie scrapowanej aplikacji może zostać protraktowane jako DDoS, co również może rodzić potencjalne konsekwencje prawne.

Wiele stron i aplikacji walczy ze scrapowaniem danych. Zakaz scrapowania może być np. zapisany w regulaminie korzystania z aplikacji. Scrapowanie może wtedy zakończyć się blokadą naszego konta. Kolejnym z ograniczeń jest rate limit, który ogranicza maksymalną liczbę zapytań w danym okresie. Niektóre aplikacje mogą sprawdzać User-Agent i odrzucać zapytania, gdzie wartość tego parametru wskazuje na ruch niewynikający z naturalnego wykorzystania aplikacji. Puppeteer umożliwia ustawienie wartości User-Agenta za pomocą metody page.setUserAgent(). W jednym z moich projektów trafiłem właśnie na taki przypadek i pomogło ustawienie wartości z poniższego przykładu.


await page.setUserAgent(
    'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/95.0.4638.69 Safari/537.36'
);

Korzystając z poszczególnych metod, upewnij się czy obsługujesz poprawnie zwrcany rezultat. Większość metod klasy Page jest asynchroniczna i zwraca Promise, który należy rozpakować, np. korzystając z await.

Przydatne może okazać się również ustawienie dodatkowych nagłówków HTTP np. by uzyskać treść w konkretnym języku.


await page.setExtraHTTPHeaders( { 'Accept-Language': 'en-US,en;q=0.9' } );

Do nawigacji służy metoda page.goto(), gdzie należy podać docelowy URL. Przy korzystaniu z page.goto() polecam, by dodatkowo skorzystać z opcji waitUntil. Sprawi ona, że metoda poczeka na wskazane zdarzenie na stronie. Przy moich pierwszych krokach z Puppeteerem zamiast tego mechanizmu wykorzystywałem metodę page.waitForTimeout(). Jedak jest to nieefektywne rozwiązanie i nie gwarantuje, że wszytko na stronie załaduje się we wskazanym czasie.


 await page.goto( url, { waitUntil: 'domcontentloaded' } );

Po udaniu się na interesującą nas stronę możemy zacząć scrapowanie. Najprostszym sposobem na pozyskanie zawartość strony jest skorzystanie z page.content(), które zwróci całą zawartość scrapowanej strony w formie HTML. Tak pobraną zawartość można parsować za pomocą np. wyrażeń regularnych.

Zwykle scrapowanie całości strony często mija się z celem i interesować nas będzie zawartość konkretnych elementów na stronie. By wydobyć informację z konkretnych elementów HTML, możemy wykorzystać metodę page.waitForSelector() i selektory znane z CSS. Możesz również skorzystać ze składni XPath czy wyszukiwania tekstem. Będzie to przydatne na przykład w sytuacji, gdy interesuje nas element HTML zawierający w sobie konkretny fragment tekstu. Przydaje się to również w sytuacjach, gdy nazwy klas i identyfikatorów na scrapowanej stronie generowane są w sposób losowy.


// Przykład z selektorami CSS
const selector = await page.locator( '#news ul li .title' ).waitHandle();
        
const title = await selector?.evaluate( el => el.textContent );

// Przykład z XPath
const xpathSelector = await page.locator(
    `::-p-xpath(//a[contains(text(),'Text content')])`
).waitHandle();

const titleXpath = await xpathSelector?.evaluate( el => el.textContent );

// Przykład z -p-text
const textSelector = await page.locator(
    'div ::-p-text(Aktualności)'
).waitHandle();

const titleTextSelector = await textSelector?.evaluate( el => el.textContent );

Oprócz wyszukiwania selektorów można również wykonywać na nich akcje uzupełniania czy klikania.


await page.locator( 'selector' ).fill( 'searched phrase' );

await page.locator( 'selector' ).click();

Na uwagę zasługuje również mechanizm request interceptor. Polega on na tym, że zapytania wysyłane przez stronę można przechwycić i np. je przerwać. Możemy to wykorzystać np. do filtrowania ruchu czy optymalizacji scrapowania.

Przykładowo, gdy interesuje nas jedynie fragment tekstu z danej strony, to nie potrzebujemy dociągać stylów, obrazków czy skryptów. W tym celu można właśnie wykorzystać interceptor. By skorzystać z interceptora, trzeba najpierw go włączyć metodą page.setRequestInterception( true ). Następnie należy zaprogramować funkcję, która ma się wykonać w momencie wysłania zapytania przez odwiedzoną stronę. W poniższym przykładzie wszystkie zapytania po skrypty, obrazki i arkusze stylów są odrzucane.


await page.setRequestInterception( true );

page.on( 'request', interceptedRequest => {
    const resourcesToSkip = [ 'image', 'script', 'stylesheet' ];

    if ( resourcesToSkip.includes( interceptedRequest.resourceType() ) ) {
        interceptedRequest.abort();
    } else {
        interceptedRequest.continue();
    }
} );

Uruchamianie kodu JS wykorzystującego API przeglądarek na backendzie

Jedną z różnic między kodem uruchamianym w Node.js a w przeglądarce jest dostępność poszczególnych API. Kod wykorzystujący API dostępne w przeglądarce może nie zadziałać w środowisku Node.js i vice versa. Jeśli istnieje potrzeba uruchomienia na backendzie kodu dedykowanego przeglądarkom, to Puppeteer jest jedną z opcji. W tym celu można wykorzystać metodę page.evaluate(), która oczekuje przekazania kodu w formie stringa czy funkcji. Poniższe przykłady zwrócą identyczne rezultaty.


const selector = await page.locator( '#news ul li .title' ).waitHandle();
        
const title = await selector?.evaluate( el => el.textContent );

const titleEvaluate = await page.evaluate(
    `document.querySelector('#news ul li .title').textContent`
);

const titleEvaluateFn = await page.evaluate( () => {
    return document.querySelector( '#Aktualnosci ul li .title' )?.textContent
} );

Do funkcji page.evaluate() należy podchodzić ostrożnie. Jeśli użytkownik aplikacji będzie w stanie przekazać tam dowolną wartość, to stanowi to dość istotną lukę bezpieczeństwa.

Testowanie

Dzięki swoim możliwościom interakcji Puppeteer może służyć automatyzacji testów E2E. Korzystając z wcześniej przedstawionych metod, możemy zaprojektować scenariusze testowe i testować wybrane flow. Puppeteera można też wykorzystać do automatyzacji testów kodu rozszerzeń dla Google Chrome.

Jednak to, że jest to możliwe, nie znaczy jeszcze, że jest to dobry pomysł. Na rynku istnieją wygodniejsze i lepsze rozwiązania, które również działają na przeglądarkach w trybie headless. W przeciwieństwie do Puppeteera dostarczają one m.in. możliwość budowania asercji czy framework do budowania i setupowania zestawów testów.

Zachęcam do przetestowania kilku rozwiązań i wybrania tego, które Ci najbardziej przypadnie do gustu:

W ostateczności Puppeteer również nada się do prostych testów czy weryfikacji.  Jednak w moim przypadku nie byłby to pierwszy wybór.

Generowanie plików PDF i zrzutów ekranu

Kolejnym ciekawym zastosowaniem Puppeteera jest generowanie plików PDF z odwiedzanych stron. Wygenerowanie pliku PDF sprowadza się do wywołania metody page.pdf() z odpowiednimi parametrami. Generowanie plików PDF w Puppeteerze daje sporo możliwości konfiguracji i personalizacji. Można zdefiniować szablon nagłówka i stopki, rozmiaru dokumentu (w postaci formatów papierowych lub wymiarów — szerokości i wysokości), marginesów czy orientacji.

Analogicznie możesz również zrobić zrzut ekranu odwiedzonej strony metodą page.screenshot(). Metoda ta pozwala na wybranie formatu czy jakości wygenerowanego screenshota.

Wygenerowane pliki możesz uzyskać w formie Buffera lub bezpośrednio zapisać na dysku. Jeśli uruchomiłeś(aś) aplikację w kontenerze, to jako ścieżkę zapisu (parametr path) możesz wskazać katalog, który jest podpięty jako volume kontenera, co pozwoli na wyciągnięcie ich.

Podsumowanie

Puppeteer to świetne narzędzie o bardzo szerokim zakresie możliwości. Zachęcam do samodzielnego przetestowania jego możliwości. Przygotowałem dla Ciebie repozytorium ze startowym kodem, który możesz wykorzystać jako punkt wyjścia do zabawy z Puppeteerem. Odsyłam również do oficjalnej dokumentacji Puppeteera, gdzie znajdziesz wszystkie potrzebne informacje oraz do sekcji z przykładami, które pomogą Ci przetestować omówione możliwości.

Jeśli już miałeś(aś) styczność z Puppeteerem, to daj znać, do czego go wykorzystałeś(aś).

Źródła i materiały dodatkowe

Dominik Szczepaniak

Zawodowo Senior Software Engineer w CKSource. Prywatnie bloger, fan włoskiej kuchni, miłośnik jazdy na rowerze i treningu siłowego.

Inne wpisy, które mogą Cię zainteresować

Zapisz się na mailing i odbierz e-booka

Odbierz darmowy egzemplarz e-booka 106 Pytań Rekrutacyjnych Junior JavaScript Developer i realnie zwiększ swoje szanse na rozmowie rekrutacyjnej! Będziesz też otrzymywać wartościowe treści i powiadomienia o nowych wpisach na skrzynkę e-mail.

Subscribe
Notify of
guest

3 komentarzy
Most Voted
Newest Oldest
Inline Feedbacks
View all comments
Sławek
1 month ago

Hej!
Mam stary serwer NAS, który ma tę przypadłość, że co jakiś czas coś się psuje w harmonogramie uruchomien/wyłączeń. W wyniku tego po włączeniu o okreslonej godzinie NAS uruchamia się i po krótkim czasie następuje jego wyłączenie. Interfejs webowy jest dostęny przez kilka sekund. Dotychczas radziłem sobie w ten sposób, że jak najszybciej logowałem się do urządzenia, z menu wybierałem harmonogram i klikałem w „Wyłącz”. Niestety czasu było na tyle mało, że trzeba było robić to kilkanascie, a nawet dziesiąt razy. Każde kolejne właczenie było uczeniem się, w które dokładnie miejsce kliknąć aby wybrać dopowiedni przycisk czy link 🙂 Niedawno znalazłem Puppeteera i napisałem skrypt automatyzujący tę czynność. Jeszcze nie miałem okazji użyć go w sytuacji awaryjnej ale w trakcie testów wszystko działa.

Sławek
1 month ago

Nie znalazłem dokumentacji do API. To dość stary sprzęt, do domowego użytku i nie wiem czy w ogóle API jest dostępne. Prawdę mówiąc nie szukałem zbyt intensywnie 🙂
Kiedy trafiłem na opis Puppeteera wpadło mi do głowy aby użyć go do tego przypadku i przy okazji nauczyć się obsługi przeglądarki od innej strony.