Web scraping w Pythonie z wykorzystaniem bibliotek Requests i BeautifulSoup

Web Scraping w Pythonie

Wyniki ankiety przeprowadzanej przez JetBrains wśród programistów Pythona pokazują, że 1/3 developerów używa Pythona do tworzenia web parserów, crawlerów i scraperów. Z całą pewnością programu tego typu są niezwykle przydatne, dlatego chciałbym podzielić się z Wami jednym ze sposobów ich tworzenia.jet brains survey results
link do wyników ankiety: JetBrains survey results.
Zanim przejdziemy do części poradnikowej chciałbym aby wszyscy zrozumieli pojęcia wymienione w ankiecie i dostrzegli różnice między nimi.

web parsing – samo pojęcie parsowania odnosi się do przetwarzania obszernego tekstu na mniejsze fragmenty, łatwiejsze do czytania lub analizowania. Parsowanie może dotyczyć dokumentów np w formacie XML czy HTML lub nawet po prostu obszernego tekstu.

web scraping – W przeciwieństwie do parsowania, web scraping polega na pobieraniu ze stron internetowych konkretnych interesujących nas informacji. Wyobraźmy sobie sytuacje, że przeprowadzając się do innego miasta chcemy poznać ceny mieszkań w jednej z dzielnic. Dobrym rozwiązaniem takiego problemu będzie napisanie scrapera (czyli programu do web scrapingu), który będzie pobierał ceny z portalu z ogłoszeniami, a następnie zapisywał je do odpowiedniego pliku.

web crawling – pojęcie oznacza „chodzenie” po stronach internetowych. Dobrym przykładem crawlerów są boty Google, które cały czas przemierzają internet w celu indeksowania odwiedzonych stron. Web crawler wchodząc na stronę znajduje na niej linki do innych stron, które może odwiedzić.

Wszystkie wyżej wymienione pojęcia najczęściej występują razem. Tworzenie crawlera, który po prostu chodzi po stronach i nic z nich nie pobiera mija się z celem. Mówiąc, że piszemy web scrapera często mamy na myśli, że jest to jednocześnie crawler. Każdy scraper na pewnym poziomie abstrakcji musi również stronę parsować. Brzmi skomplikowanie, nieprawdaż? ;) Na szczęście z punktu widzenia programisty wcale takie nie jest :)

Kolejnym ważnym pojęciem w tej tematyce jest request. Jest to inaczej żądanie dostępu do strony internetowej. Klient, czyli na przykład nasza przeglądarka, wysyła wysyła do serwera żądanie poprzez protkół HTTP. Po otrzymaniu żądania serwer wysyła do klienta odpowiedź, która zawiera odpowiednią wiadomość (treść strony), kod odpowiedzi oraz jej nagłówek. Jest to oczywiście bardzo ogólne wyjaśnienie, jednak zależy mi, aby każdy zrozumiał rolę requestu w kontekście web scrapingu.

request diagram

Biblioteki i frameworki do web scrapingu

    • Requests – według mnie to najlepsza biblioteka do wykonywania requestów (żądań dostępu do strony internetowej). W naszym przypadku request ma zakończyć się pobraniem zawartości strony. Używanie jej jest proste i przyjemne, a jej możliwości są ogromne.
    • urllib – podobnie jak biblioteka wyżej, urllib służy do obsługi requestów. Aktualnie jest to biblioteka wbudowana w nowsze wersje Pythona, jednak osobiście do wykonywania requestów zawsze wybieram bibliotekę Requests. Biblioteka ta przyda nam się do parsowania adresów URL.
    • aiohttp – nieco bardziej skomplikowana biblioteka, jej największą zaletą jest obsługa asynchronicznych requestów.
    • BeautifulSoup – tym razem nie jest to biblioteka do wykonywania żądań do stron internetowych, jednak do parsowania kodu HTML i XML. Jest to zdecydowanie najwygodniejsze narzędzie parsujące dostępne dla Pythona.
    • Requests-HTML – jest to dość świeża sprawa, biblioteka łączy w sobie Requests oraz BeautifulSoup, dodając od siebie kilka ciekawych funkcji, o których napiszę inny post:)
    • Scrapy – w poprzednich punktach mówiliśmy o bibliotekach, tutaj mamy do czynienia z zaawansowanym frameworkiem, którego największymi zaletami są szybkość oraz brak konieczności martwienia się na przykład o kodowanie strony. W tym przypadku próg wejścia i zrozumienia kodu jest dość wysoki, co na początku zdecydowanie może odstraszyć.
    • Selenium Web Driver – narzędzie wykorzystywane do testów automatycznych, jednak można za jego pomocą również tworzyć scrapery.

W tym poście pokażę Wam tworzenie scrapera z wykorzystaniem bibliotek Requests oraz BeautifulSoup. Dlaczego nie Scrapy? W większości przypadków dużo łatwiej i szybciej pożądany efekt można uzyskać korzystając ze wspomnianego zestawu bibliotek, a dodatkowe funkcje Scrapy nie zawsze są konieczne.

Korzystanie z Requests

Bibliotekę Requests będziemy wykorzystywać do pobierania zawartości strony.

działanie pokazanej powyżej metody get można objaśnić jako wejście na stronę, wyświetlenie źródła strony (ctrl+U w Google Chrome) i zapisanie całego znalezionego tam tekstu.
Mając zwrócony obiekt klasy Response możemy pobrać z niego tekst strony, odczytać i zmienić jej kodowanie.

Obiekt ten pozwala również na sprawdzenie kodu zwróconego przez stronę.

Za pomocą biblioteki Requests można wykonywać również requesty metodami POST, PUT, DELETE, na przykład:

User-agent

Omawiana biblioteka pozwala również na wykonywanie requestów ze zdefiniowanymi przez nas nagłówkami. Szczególnie interesującym nagłówkiem jest user-agent. User-agent jest parametrem requestu, po którym strona poznaje z jakim typem użytkownika ma do czynienia. Każda przeglądarka na różnych systemach operacyjnych ma zdefiniowane różne user-agent’y, co pozwala na prowadzenie statystyk przeglądarek i systemów jakich używają użytkownicy strony.
ie6-ping
Po nagłówku user-agent rozpoznawane są także boty Google.
Kiedy może przydać się zmiana domyślnej wartości nagłówka user-agent? Korzystając z publicznie dostępnych API możemy otrzymać kod 429 i odpowiedź o treści Too Many Requests. Dzieje się tak na przykład w przypadku korzystania z Reddita zwracającego dane w formacie json.

Aby temu zapobiec wystarczy, że użyjemy innego user-agenta:

Jeżeli chcemy wygenerować user-agenta podobnego do używanych przez przeglądarki możemy skorzystać z biblioteki user_agent.

Pamiętajcie jednak, aby nie wykorzystywać user-agenta do celów niezgodnych z warunkami korzystania stron internetowych, do których wysyłacie żądania ;)
Omówione przeze mnie funkcje Requests powinny być wystarczające w większości pisanych programów, jednak zachęcam do przejrzenia oficjalnej dokumentacji biblioteki.

Korzystanie z BeautifulSoup

Kiedy w pamięci RAM mamy już zapisany kod strony należałoby wreszcie go przeparsować i wyekstrahować potrzebne nam elementy. Jak już wcześniej mówiłem użyjemy do tego celu biblioteki BeautifulSoup.
Dla lepszego zrozumienia działania bilbioteki przedstawię wszystko na przykładzie. Naszym zadaniem będzie pobranie adresów subredditów wypisanych na liście znajdującej się pod adresem https://www.reddit.com/r/ListOfSubreddits/wiki/listofsubreddits.

Najpierw musimy oczywiście pobrać zawartość strony oraz załadować ją do parsera.

Parsery biblioteki BeautifulSoup

Aktualnie biblioteka bs4 pozwala na korzystanie z jednego z czterech parserów:

  • html.parser – wbudowany parser, nie jest jednak najszybszym rozwiązaniem
  • lxml – najszybszy dostępny parser HTML. Nie jest on zainstalowany domyślnie, należy go doinstalować na przykład korzystając z programu pip:
  • html5lib – jest on najwolniejszy jednak parsuje stronę identycznie do przeglądarek. Podobnie jak w przypadku lxml’a jest to pakiet zewnętrzny, który można zainstalować poleceniem:
  • xml – jedyny obsługiwany parser przeznaczony do dokumentów XML

Zalecam korzystanie z parsera lxml ze względu na jego szybkość. Parser html5lib przyda się, jeżeli zauważymy, że inne parsery błędnie parsują stronę.
Poszukiwane przez nas linki do subredditów znajdują się wewnątrz tagów body dlatego też dostaniemy się do tej sekcji od razu, aby niepotrzebnie nie parsować sekcji head. Możemy również przejść do diva klasy „md wiki” w którym znajdują się linki. Służy do tego metoda find(), która znajduje tylko jeden element spełniający podane kryteria. We wskazanym divie znajduje się tabelka oraz nieuporządkowana lista z linkami oznaczonymi parametrem nofollow, które nie są nam potrzebne. Aby ich nie pobierać możemy więc usunąć niechcianą tabelkę oraz listę.

Szukanie konkrentych znaczników

Przyjrzyjmy się najpierw w jaki sposób pożądane linki zapisane są w kodzie HTML strony. Wykorzystamy do tego funkcję przeglądarki „Zbadaj element”.
inspect element
Niestety linki nie mają żadnej charakterystycznej klasy, którą moglibyśmy wskazać naszemu parserowi, jednak w tym przypadku pomocny będzie parametr rel. Warto zauważyć, że wszystkie linki do subredditów mają ustawioną wartość tego parametru na nofollow.
Powinniśmy więc teraz znaleźć wszystkie znaczniki <a> z parametrem rel=”nofollow”. Służy do tego funkcja find_all(). Metody tej możemy używać zarówno na obiekcie soup jak i na obiektach zwracanych przez inne parsujące metody jak np find().

Zwrócony obiekt jest instancją klasy z biblioteki bs4, jednak można po nim iterować.

Odwołując się do własności text pobieramy to co znajduję się pomiędzy znacznikami <a> i </a>. Jeżeli chcemy pobrać wartość konkretnego atrybutu znacznika możemu wykorzystać metodę get. Jej parametrem jest nazwa atrybuty, który chcemy pobrać zapisana jako string.

Jak pewnie zauważyliście wartości atrybutów ‚href’ nie są poprawnymi URL’ami. Mamy w tym przypadku do czynienia z relative links. Przykładowo:
relative link: /r/Python
absolute link: https://www.reddit.com/r/Python/
Dla nas dużo bardziej przydatne są linki będące poprawnymi URLami z określonym protokołem. Spróbujemy więc stworzyć je sami z relatywnych linków, które już mamy. Wykorzystamy do tego bibliotekę urllib.

Parsowanie linków przy użyciu urllib

Najłatwiejszym sposobem na zamianę relative and absolute link będzie wykorzystanie funkcji urljoin. Jako pierwszy parametr funkcji nalepiej podać stronę, na której dany link został znaleziony. Drugim parametrem jest oczywiście link relatywny.

W tym przypadku absolute link możemy stworzyć sami w bardzo prosty sposób. Wykorzystamy do tego funkcję urlparse z biblioteki urllib.parse. Zastanówmy się co różni dwa wyżej wymienione rodzaje linków. Absolute link rozpoczyna się od protokołu, najczęściej od http lub https w zależności od tego czy strona posiada certyfikat SSL. Po protokole zawsze występują znaki „://” a następnie nazwa domeny (a dokładniej mówiąc lokalizacja sieciowa) wraz z końcówkę określającą domenę najwyższego poziomu. Na końcu takiego linku znajduje się już link relatywny. Musimy więc pobrać protokół strony i jej lokalizację sieciową. Podobnie jak w przypadku funkcji urljoin jako parametr funkcji urlparse przekażemy adres URL, z której pobraliśmy linki relatywne.

Za pomocą tej funkcji możemy pobrać również relative link, parametry w urlu, kwerendę, czy nawet nazwę użytkownika, hasło i port gdy takie występują.

W ramach lepszego poznania biblioteki napiszemy funkcję, która w naszej sytuacji stworzy poprawny URL z podanego relatywnego linku.

Niestety napisana implementacja jest dość „naiwna” i nie sprawdzi się w każdym przypadku. Nie każdy relative link zaczyna się od ukośnika, czasami wskazuję one ścieżkę do pliku wykorzystując notację z kropkami, na przykład „../r/Python”. Zalecam więc korzystanie z funkcji urljoin, która radzi sobie z różnymi wersjami linków relatywnych. Przetwarzając linki znalezione na stronie należy również sprawdzić czy przetwarzany link nie jest poprawnym URL’em, na przykład linkującym do zewnętrznej domeny. Można to na zweryfikować na przykład przez sprawdzenie czy link nie zaczyna się od „http”. W naszym przypadku chodzi o znalezienie urli do subredditów, dlatego też weźmiemy pod uwagę tylko linki zaczynające się od „/r”.

Mamy wypisane wszystkie linki do subredditów, więc nasz cel został osiągnięty :)

Podsumowanie

Starałem się w jak najprzystępniejszy sposób przedstawić Wam proces web scrapingu w Pythonie. Pomimo wielu elementów wspólnych każdy scraper tworzy się trochę inaczej, każdy stawia nowe i często ciekawe wyzwania. Dla mnie największą zaletą scraperów jest to, że pozwalają korzystać z ogromu wiedzy zgromadzonej w internecie. Są podstawą do open source intelligence (o czym planuję niebawem napisać post). Szczególnie docenić to można w dobie ciągle rosnącej popularności analizy danych i uczenia maszynowego.
Wiele stron robi wszystko aby scrapowanie utrudnić, na przykład poprzez blokowanie requestów niepochodzących z przeglądarek i banowanie adresów IP, z których takie żądania zostały wykonane. Należy wtedy uszanować wolę strony, zamiast próbowania oszukania występujących zabezpieczeń ;) Zanim zaczniecie scrapować strony upewniajcie się również czy jest to zgodne z regulaminem korzystania ze strony.

Leave a Reply

Twój adres email nie zostanie opublikowany. Pola, których wypełnienie jest wymagane, są oznaczone symbolem *