Przeglądanie stron internetowych za pomocą przeglądarki bez głowy: samouczek lalkarza
Opublikowany: 2022-03-11W tym artykule zobaczymy, jak łatwo wykonać web scraping (automatyzację sieci) za pomocą nieco nietradycyjnej metody korzystania z przeglądarki bezgłowej .
Co to jest przeglądarka bez głowy i dlaczego jest potrzebna?
W ciągu ostatnich kilku lat sieć ewoluowała od prostych stron internetowych zbudowanych z samego HTML i CSS. Obecnie istnieje znacznie więcej interaktywnych aplikacji internetowych z pięknymi interfejsami użytkownika, które często są budowane przy użyciu frameworków, takich jak Angular czy React. Innymi słowy, w dzisiejszych czasach JavaScript rządzi siecią, w tym prawie wszystkim, z czym wchodzisz w interakcję na stronach internetowych.
Dla naszych celów JavaScript jest językiem po stronie klienta. Serwer zwraca pliki JavaScript lub skrypty wstrzyknięte do odpowiedzi HTML, a przeglądarka je przetwarza. To jest problem, jeśli robimy coś w rodzaju web scrapingu lub automatyzacji sieci, ponieważ częściej niż nie, treść, którą chcielibyśmy zobaczyć lub scrape, jest w rzeczywistości renderowana przez kod JavaScript i nie jest dostępna z nieprzetworzonej odpowiedzi HTML dostarczanych przez serwer.
Jak wspomnieliśmy powyżej, przeglądarki potrafią przetwarzać JavaScript i renderować piękne strony internetowe. A co, gdybyśmy mogli wykorzystać tę funkcjonalność do naszych potrzeb związanych ze skrobaniem i mieć sposób na programowe kontrolowanie przeglądarek? Właśnie tam wkracza bezgłowa automatyzacja przeglądarki!
Bezgłowy? Przepraszam? Tak, oznacza to po prostu brak graficznego interfejsu użytkownika (GUI). Zamiast wchodzić w interakcje z elementami wizualnymi w normalny sposób — na przykład za pomocą myszy lub urządzenia dotykowego — automatyzujesz przypadki użycia za pomocą interfejsu wiersza polecenia (CLI).
Bezgłowy Chrome i Lalkarz
Istnieje wiele narzędzi do skrobania stron internetowych, których można użyć do przeglądania bez głowy, takich jak Zombie.js lub bezgłowy Firefox przy użyciu Selenium. Ale dzisiaj będziemy odkrywać bezgłowego Chrome'a za pośrednictwem Puppeteer, ponieważ jest to stosunkowo nowszy odtwarzacz, wydany na początku 2018 roku. Nota redaktora: Warto wspomnieć o Intoli's Remote Browser, kolejnym nowym odtwarzaczu, ale to będzie musiało być tematem dla innego artykuł.
Czym dokładnie jest Lalkarz? Jest to biblioteka Node.js, która zapewnia wysokopoziomowy interfejs API do sterowania bezgłowym Chrome lub Chromium lub do interakcji z protokołem DevTools. Jest utrzymywany przez zespół Chrome DevTools i niesamowitą społeczność open-source.
Dość gadania — wskoczmy do kodu i odkryjmy świat, w jaki sposób zautomatyzować skrobanie stron internetowych za pomocą bezgłowego przeglądania Puppeteer!
Przygotowanie środowiska
Przede wszystkim musisz mieć zainstalowany Node.js 8+ na swoim komputerze. Możesz go zainstalować tutaj lub jeśli jesteś miłośnikiem CLI, tak jak ja i lubisz pracować na Ubuntu, postępuj zgodnie z tymi poleceniami:
curl -sL https://deb.nodesource.com/setup_8.x | sudo -E bash - sudo apt-get install -y nodejs
Będziesz także potrzebować niektórych pakietów, które mogą być lub nie być dostępne w twoim systemie. Aby być bezpiecznym, spróbuj zainstalować te:
sudo apt-get install -yq --no-install-recommends libasound2 libatk1.0-0 libc6 libcairo2 libcups2 libdbus-1-3 libexpat1 libfontconfig1 libgcc1 libgconf-2-4 libgdk-pixbuf2.0-0 libglib2.0-0 libgtk-3-0 libnspr4 libpango-1.0-0 libpangocairo-1.0-0 libstdc++6 libx11-6 libx11-xcb1 libxcb1 libxcursor1 libxdamage1 libxext6 libxfixes3 libxi6 libxrandr2 libxrender1 libxss1 libxtst6 libnss3
Skonfiguruj Headless Chrome i Puppeteer
Polecam zainstalować Puppeteer z npm
, ponieważ zawiera on również stabilną, aktualną wersję Chromium, która gwarantuje współpracę z biblioteką.
Uruchom to polecenie w katalogu głównym projektu:
npm i puppeteer --save
Uwaga: może to chwilę potrwać, ponieważ Puppeteer będzie musiał pobrać i zainstalować Chromium w tle.
Dobra, teraz, gdy wszyscy jesteśmy już przygotowani i skonfigurowani, zacznijmy zabawę!
Korzystanie z interfejsu Puppeteer API do automatycznego pobierania z sieci
Zacznijmy nasz poradnik Puppeteer od podstawowego przykładu. Napiszemy skrypt, który sprawi, że nasza bezgłowa przeglądarka zrobi zrzut ekranu wybranej przez nas strony internetowej.
Utwórz nowy plik w katalogu projektu o nazwie screenshot.js
i otwórz go w swoim ulubionym edytorze kodu.
Najpierw zaimportujmy bibliotekę Puppeteer do twojego skryptu:
const puppeteer = require('puppeteer');
Następnie weźmy adres URL z argumentów wiersza poleceń:
const url = process.argv[2]; if (!url) { throw "Please provide a URL as the first argument"; }
Teraz musimy pamiętać, że Puppeteer jest biblioteką opartą na obietnicach: wykonuje asynchroniczne wywołania do bezgłowej instancji Chrome pod maską. Utrzymujmy kod w czystości, używając async/await. W tym celu musimy najpierw zdefiniować funkcję async
i umieścić w niej cały kod Puppeteer:
async function run () { const browser = await puppeteer.launch(); const page = await browser.newPage(); await page.goto(url); await page.screenshot({path: 'screenshot.png'}); browser.close(); } run();
W sumie ostateczny kod wygląda tak:
const puppeteer = require('puppeteer'); const url = process.argv[2]; if (!url) { throw "Please provide URL as a first argument"; } async function run () { const browser = await puppeteer.launch(); const page = await browser.newPage(); await page.goto(url); await page.screenshot({path: 'screenshot.png'}); browser.close(); } run();
Możesz go uruchomić, wykonując następujące polecenie w katalogu głównym projektu:
node screenshot.js https://github.com
Poczekaj chwilę i bum! Nasza bezgłowa przeglądarka właśnie utworzyła plik o nazwie screenshot.png
i możesz zobaczyć w nim renderowaną stronę główną GitHub. Świetnie, mamy działający skrobak sieciowy Chrome!
Zatrzymajmy się na chwilę i zbadajmy, co dzieje się w naszej funkcji run()
powyżej.
Najpierw uruchamiamy nową instancję przeglądarki bezgłowej, następnie otwieramy nową stronę (zakładkę) i przechodzimy do adresu URL podanego w argumencie wiersza poleceń. Na koniec używamy wbudowanej metody Puppeteer do robienia zrzutu ekranu i musimy tylko podać ścieżkę, w której powinien zostać zapisany. Musimy również upewnić się, że zamknęliśmy bezgłową przeglądarkę po zakończeniu automatyzacji.
Teraz, gdy omówiliśmy podstawy, przejdźmy do czegoś bardziej złożonego.
Drugi przykład skrobania lalkarzy
W następnej części naszego samouczka Lalkarza, powiedzmy, że chcemy zeskrobać najnowsze artykuły z Hacker News.
Utwórz nowy plik o nazwie ycombinator-scraper.js
i wklej następujący fragment kodu:
const puppeteer = require('puppeteer'); function run () { return new Promise(async (resolve, reject) => { try { const browser = await puppeteer.launch(); const page = await browser.newPage(); await page.goto("https://news.ycombinator.com/"); let urls = await page.evaluate(() => { let results = []; let items = document.querySelectorAll('a.storylink'); items.forEach((item) => { results.push({ url: item.getAttribute('href'), text: item.innerText, }); }); return results; }) browser.close(); return resolve(urls); } catch (e) { return reject(e); } }) } run().then(console.log).catch(console.error);
W porządku, dzieje się tu trochę więcej w porównaniu z poprzednim przykładem.
Pierwszą rzeczą, którą możesz zauważyć, jest to, że funkcja run()
zwraca teraz obietnicę, więc prefiks async
został przeniesiony do definicji funkcji obietnicy.
Umieściliśmy również cały nasz kod w bloku try-catch, dzięki czemu możemy obsłużyć wszelkie błędy, które powodują odrzucenie naszej obietnicy.
I na koniec używamy wbudowanej metody Puppeteer o nazwie evaluate()
. Ta metoda pozwala nam uruchomić niestandardowy kod JavaScript tak, jakbyśmy wykonywali go w konsoli DevTools. Wszystko zwrócone z tej funkcji zostanie rozwiązane przez obietnicę. Ta metoda jest bardzo przydatna, jeśli chodzi o zbieranie informacji lub wykonywanie niestandardowych działań.
Kod przekazany do metody evaluate()
to całkiem prosty JavaScript, który tworzy tablicę obiektów, z których każdy ma url
i pola text
reprezentujące adresy URL artykułów, które widzimy na https://news.ycombinator.com/.
Wynik skryptu wygląda mniej więcej tak (ale początkowo z 30 wpisami):
[ { url: 'https://www.nature.com/articles/d41586-018-05469-3', text: 'Bias detectives: the researchers striving to make algorithms fair' }, { url: 'https://mino-games.workable.com/jobs/415887', text: 'Mino Games Is Hiring Programmers in Montreal' }, { url: 'http://srobb.net/pf.html', text: 'A Beginner\'s Guide to Firewalling with pf' }, // ... { url: 'https://tools.ietf.org/html/rfc8439', text: 'ChaCha20 and Poly1305 for IETF Protocols' } ]
Powiedziałbym, że całkiem fajnie!

Dobra, przejdźmy dalej. Zwrócono tylko 30 pozycji, a dostępnych jest znacznie więcej — są one po prostu na innych stronach. Musimy kliknąć przycisk „Więcej”, aby załadować następną stronę wyników.
Zmodyfikujmy nieco nasz skrypt, aby dodać obsługę paginacji:
const puppeteer = require('puppeteer'); function run (pagesToScrape) { return new Promise(async (resolve, reject) => { try { if (!pagesToScrape) { pagesToScrape = 1; } const browser = await puppeteer.launch(); const page = await browser.newPage(); await page.goto("https://news.ycombinator.com/"); let currentPage = 1; let urls = []; while (currentPage <= pagesToScrape) { let newUrls = await page.evaluate(() => { let results = []; let items = document.querySelectorAll('a.storylink'); items.forEach((item) => { results.push({ url: item.getAttribute('href'), text: item.innerText, }); }); return results; }); urls = urls.concat(newUrls); if (currentPage < pagesToScrape) { await Promise.all([ await page.click('a.morelink'), await page.waitForSelector('a.storylink') ]) } currentPage++; } browser.close(); return resolve(urls); } catch (e) { return reject(e); } }) } run(5).then(console.log).catch(console.error);
Przyjrzyjmy się, co tutaj zrobiliśmy:
- Dodaliśmy pojedynczy argument o nazwie
pagesToScrape
do naszej głównej funkcjirun()
. Wykorzystamy to, aby ograniczyć liczbę stron, które nasz skrypt będzie zdrapywał. - Jest jeszcze jedna nowa zmienna o nazwie
currentPage
, która reprezentuje numer strony wyników, którą aktualnie przeglądamy. Początkowo jest ustawiony na1
. Opakowaliśmy również naszą funkcję assessmentevaluate()
w pętliwhile
, aby działała tak długo, jak długocurrentPage
jest mniejsze lub równepagesToScrape
. - Dodaliśmy blokadę przejścia na nową stronę i oczekiwania na załadowanie strony przed ponownym uruchomieniem pętli
while
.
Zauważysz, że użyliśmy metody page.click()
, aby przeglądarka bez nagłówka kliknęła przycisk „Więcej”. Użyliśmy również metody waitForSelector()
, aby upewnić się, że nasza logika jest wstrzymana do momentu załadowania zawartości strony.
Oba są wysokopoziomowymi metodami Puppeteer API, gotowymi do użycia od razu po zainstalowaniu.
Jednym z problemów, które prawdopodobnie napotkasz podczas scrapingu w Puppeteer, jest oczekiwanie na załadowanie strony. Hacker News ma stosunkowo prostą strukturę i dość łatwo było czekać na zakończenie ładowania strony. W przypadku bardziej złożonych zastosowań Puppeteer oferuje szeroki zakres wbudowanych funkcji, które można zbadać w dokumentacji API na GitHub.
To wszystko jest całkiem fajne, ale nasz samouczek lalkarza nie obejmował jeszcze optymalizacji. Zobaczmy, jak sprawić, by Puppeteer działał szybciej.
Optymalizacja naszego skryptu lalkarza
Ogólnym założeniem jest, aby przeglądarka bez głowy nie wykonywała żadnej dodatkowej pracy. Może to obejmować ładowanie obrazów, stosowanie reguł CSS, uruchamianie żądań XHR itp.
Podobnie jak w przypadku innych narzędzi, optymalizacja Puppeteer zależy od konkretnego przypadku użycia, więc pamiętaj, że niektóre z tych pomysłów mogą nie pasować do Twojego projektu. Na przykład, gdybyśmy uniknęli ładowania obrazów w naszym pierwszym przykładzie, nasz zrzut ekranu mógł nie wyglądać tak, jak byśmy chcieli.
W każdym razie te optymalizacje można osiągnąć poprzez buforowanie zasobów przy pierwszym żądaniu lub natychmiastowe anulowanie żądań HTTP, gdy są one inicjowane przez witrynę.
Zobaczmy najpierw, jak działa buforowanie.
Należy mieć świadomość, że po uruchomieniu nowej instancji przeglądarki bezgłowej Puppeteer tworzy tymczasowy katalog dla swojego profilu. Jest usuwany po zamknięciu przeglądarki i nie jest dostępny do użycia po uruchomieniu nowej instancji — w ten sposób wszystkie przechowywane obrazy, CSS, pliki cookie i inne obiekty nie będą już dostępne.
Możemy zmusić Puppeteer do korzystania z niestandardowej ścieżki do przechowywania danych, takich jak pliki cookie i pamięć podręczna, które będą ponownie wykorzystywane za każdym razem, gdy uruchomimy go ponownie — dopóki nie wygasną lub nie zostaną ręcznie usunięte.
const browser = await puppeteer.launch({ userDataDir: './data', });
Powinno to dać nam niezły wzrost wydajności, ponieważ przy pierwszym żądaniu w katalogu danych będzie buforowanych wiele CSS i obrazów, a Chrome nie będzie musiał ich ponownie pobierać.
Jednak te zasoby będą nadal używane podczas renderowania strony. W naszych artykułach prasowych Y Combinator związanych ze skrobaniem nie musimy się martwić o żadne elementy wizualne, w tym obrazy. Zależy nam tylko na samym wyjściu HTML, więc spróbujmy zablokować każde żądanie.
Na szczęście w tym przypadku praca z Puppeteerem jest całkiem fajna, ponieważ obsługuje niestandardowe haki. Możemy zapewnić przechwytywacz na każde żądanie i anulować te, których tak naprawdę nie potrzebujemy.
Interceptor można zdefiniować w następujący sposób:
await page.setRequestInterception(true); page.on('request', (request) => { if (request.resourceType() === 'document') { request.continue(); } else { request.abort(); } });
Jak widać, mamy pełną kontrolę nad inicjowanymi żądaniami. Możemy napisać niestandardową logikę, aby zezwalać lub przerywać określone żądania na podstawie ich resourceType
. Mamy również dostęp do wielu innych danych, takich jak request.url
, więc jeśli chcemy, możemy blokować tylko określone adresy URL.
W powyższym przykładzie zezwalamy na przechodzenie przez nasz filtr tylko żądań z typem zasobu "document"
, co oznacza, że zablokujemy wszystkie obrazy, CSS i wszystko inne poza oryginalną odpowiedzią HTML.
Oto nasz ostateczny kod:
const puppeteer = require('puppeteer'); function run (pagesToScrape) { return new Promise(async (resolve, reject) => { try { if (!pagesToScrape) { pagesToScrape = 1; } const browser = await puppeteer.launch(); const page = await browser.newPage(); await page.setRequestInterception(true); page.on('request', (request) => { if (request.resourceType() === 'document') { request.continue(); } else { request.abort(); } }); await page.goto("https://news.ycombinator.com/"); let currentPage = 1; let urls = []; while (currentPage <= pagesToScrape) { await page.waitForSelector('a.storylink'); let newUrls = await page.evaluate(() => { let results = []; let items = document.querySelectorAll('a.storylink'); items.forEach((item) => { results.push({ url: item.getAttribute('href'), text: item.innerText, }); }); return results; }); urls = urls.concat(newUrls); if (currentPage < pagesToScrape) { await Promise.all([ await page.waitForSelector('a.morelink'), await page.click('a.morelink'), await page.waitForSelector('a.storylink') ]) } currentPage++; } browser.close(); return resolve(urls); } catch (e) { return reject(e); } }) } run(5).then(console.log).catch(console.error);
Bądź bezpieczny dzięki limitom cen
Przeglądarki bezgłowe to bardzo potężne narzędzia. Są w stanie wykonać prawie każde zadanie automatyzacji sieci, a Puppeteer jeszcze bardziej to ułatwia. Pomimo wszystkich możliwości, musimy przestrzegać warunków korzystania z serwisu, aby mieć pewność, że nie nadużywamy systemu.
Ponieważ ten aspekt jest bardziej związany z architekturą, nie będę omawiał tego szczegółowo w tym samouczku Puppeteer. To powiedziawszy, najbardziej podstawowym sposobem spowolnienia skryptu Puppeteer jest dodanie do niego polecenia uśpienia:
js await page.waitFor(5000);
To stwierdzenie zmusi Twój skrypt do uśpienia na pięć sekund (5000 ms). Możesz umieścić to w dowolnym miejscu przed browser.close()
.
Podobnie jak ograniczanie korzystania z usług stron trzecich, istnieje wiele innych, bardziej niezawodnych sposobów kontrolowania korzystania z Puppeteer. Jednym z przykładów może być budowanie systemu kolejkowego z ograniczoną liczbą pracowników. Za każdym razem, gdy chcesz użyć Puppeteer, możesz umieścić nowe zadanie w kolejce, ale liczba pracowników, którzy będą w stanie pracować nad zadaniami w nim zawartymi, będzie ograniczona. Jest to dość powszechna praktyka, gdy mamy do czynienia z limitami szybkości API stron trzecich i może być również zastosowana do skrobania danych internetowych Puppeteer.
Miejsce lalkarza w szybko zmieniającej się sieci
W tym samouczku Puppeteer zademonstrowałem jego podstawową funkcjonalność jako narzędzia do przeszukiwania sieci. Ma jednak znacznie szersze zastosowania, w tym między innymi testowanie przeglądarki bez głowy, generowanie plików PDF i monitorowanie wydajności.
Technologie internetowe szybko się rozwijają. Niektóre witryny są tak zależne od renderowania JavaScript, że wykonanie prostych żądań HTTP w celu ich zeskrobania lub wykonania jakiejś automatyzacji jest prawie niemożliwe. Na szczęście przeglądarki bez głowy stają się coraz bardziej dostępne, aby sprostać wszystkim naszym potrzebom w zakresie automatyzacji, dzięki projektom takim jak Puppeteer i niesamowitym zespołom, które za nimi stoją!