Wyzwanie Python #7: Web scraping

Maciej Bartoszuk 7 stycznia 2022
 

W dzisiejszym, ostatnim już odcinku naszych wyzwań, poszerzymy nasze “wychodzenie na świat” o wyjście bardzo dalekie, bo w świat Internetu. Nauczymy się, jak połączyć się ze stroną internetową, a następnie jak z tejże strony wydobyć intresujące nas informacje. Da to nam satysfakcję, że nasz wielotygodniowy wysiłek zaowocował bardzo praktyczną umiejętnością.

Interesujące nas dane są na zwyczajnej stronie internetowej, nieprzygotowane do zautomatyzowanej obróbki. Trzeba będzie je dopiero stamtąd “wydobyć”. Aby móc to robić, musimy najpierw omówić podstawy języka HTML. Postawmy sprawę jasno: nie trzeba być dobrym webmasterem, aby scrapować strony internetowe. Wystarczy znać podstawy. Analogicznie, nie trzeba być dobrym pisarzem, aby móc czytać książki. Wystarczy znać litery.

Instalacja pakietów

Zanim przejdziemy dalej, będziemy potrzebowali doinstalować dwa pakiety. Pokażemy, jak zrobić to dla dystrybucji Anaconda. Najpierw trzeba otworzyć terminal, z którego będziemy mieli dostęp do polecenia conda, dostarczanego przez dystrybucję. Na systemie Windows powinniśmy kliknąć prawym przyciskiem myszy na Anaconda Prompt z menu Start i wybrać uruchom jako administrator. W przypadku Linuksa prawdopodobnie mamy dostęp do tego polecenia po otworzeniu zwyczajnego terminala (być może polecenie pokazane za chwilę także trzeba będzie uruchomić z prawami roota). Jeśli nie, musimy przejść do folderu, gdzie zainstalowaliśmy Anacondę.

Po otworzeniu wydajemy następujące dwie komendy:

conda install requests

Po każdej z nich zgadzamy się na zaproponowane zmiany (wpisujemy y i naciskamy enter). Proces może chwilkę zająć. Jeśli po poprawnej instalacji pakietu dalej byłby on niewidoczny w kodzie, należy założyć nowy projekt.

Łączenie się ze stroną internetową

Zobaczmy, jak połączyć się ze stroną internetową z poziomu języka Python. Przyda nam się biblioteka requests:

import requests

Następnie należy wywołać funkcję get() z tego pakietu:

odpowiedz = requests.get("https://pl.wikipedia.org/wiki/Zygmunt_III_Waza")
odpowiedz
<Response [200]>

W zmiennej odpowiedz otrzymaliśmy wszystkie informacje dotyczące tego, co zwrócił serwer. Jest to m.in. kod odpowiedzi. Niektóre kody znamy z codziennego życia: 404 oznacza, że nie znaleziono danej strony, 403 to brak praw dostępu, a 200 zwyczajnie oznacza, że wszystko zostało przetworzone bezbłędnie. Aby dostać się do kodu odpowiedzi, napiszemy:

odpowiedz.status_code
200

Jednak nas będzie przede wszystkim interesować sama zawartość strony internetowej. Znajduje się on w polu text:

odpowiedz.text

To tu znajduje się tekst strony. Aby “dobrać” się do niego, musimy poznać język HTML.

Podstawy HTML

Spójrzmy na przykładową, bardzo prostą stronę internetową. Jeżeli chcielibyśmy zobaczyć ją w praktyce na własnym komputerze, wystarczy utworzyć plik strona.html, następnie otworzyć ją notatnikiem, przepisać podany kod, zapisać, a następnie jeszcze raz otworzyć, ale tym razem przeglądarką internetową.

 <html>
    <head>
       <title>Moja pierwsza strona!</title>
    </head>
    
    <body>
    <p>Lorem ipsum dolor sit amet, consectetur adipiscing elit. Donec posuere elit at malesuada tempor. Donec eget ligula in ante auctor luctus. Phasellus iaculis porttitor gravida. Donec eget sem lorem. Morbi a libero imperdiet, viverra tellus ac, consequat tortor. Suspendisse nibh massa, accumsan non neque a, vestibulum commodo dui.</p>
    <p>Phasellus vestibulum ut <br>erat sit amet ullamcorper. Nam at elit feugiat, dapibus ante vitae, ullamcorper dui. Nunc rutrum at nibh tincidunt mattis. In finibus sed ante vel mollis. Donec at semper metus. Aenean quis consectetur risus. Sed suscipit felis sed ex pretium euismod. In fermentum mi a odio porttitor, dapibus aliquet leo accumsan. Suspendisse pretium augue et faucibus euismod. Quisque risus metus, ultricies nec tortor at, efficitur convallis nunc.</p>
    
    <ul>
        <li>Pierwszy punkt
        <li>Drugi punkt</li>
        <li>Trzeci punkt</li>
    </ul>

    <ol>
        <li>Pierwszy punkt
        <li>Drugi punkt</li>
        <li>Trzeci punkt</li>
    </ol>
    
    <table border="3">
        <tr><th>Naglowek 1</th><th>Naglowek 2</th></tr>
        <tr><td>komorka 11</td><td>komorka 12</td></tr>
        <tr><td>komorka 21</td><td>komorka 22</td></tr>
    </table>
    
    <a href="http://google.pl">Arcyciekawa strona</a>

    </body>
    </html>

Jak widzimy, format składa się z tzw. znaczników, czyli tekstu zamkniętego pomiędzy ostrymi nawiasami: <>. Każdy znacznik ma swoją nazwę, np. w <table border="3"> jest to table. Znacznik musi być domknięty, to znaczy, że dla znacznika: <Znacznik> musi się znaleźć jego domykający odpowiednik: </Znacznik>. Można też jednocześnie otworzyć i zamknąć znacznik, wtedy znak / znajduje się na końcu: <Znacznik/>. Znacznik może mieć atrybuty. Jest to para klucz-wartość, np. w <table border="3"> znajduje się atrybut border o wartości "3". Znacznik może też mieć swoje dzieci. Są to znaczniki zdefiniowane pomiędzy jego znacznikiem otwierającym a zamykającym. Przykładowo wszystkie znaczniki są dziećmi znacznika <html>. Ostatecznie znacznik może mieć też swoją treść, czyli tekst wpisany pomiędzy jego znacznikiem otwierającym a zamykającym. Są to np. Naglowek 1 czy Arcyciekawa strona.

Na początku mamy znacznik <html>, który w ogóle zaczyna dokument. Następnie jego dwojgiem dzieci są: <head>, gdzie znajdują się informacje nagłówkowe strony (np. tytuł strony w <title>, który wyświetla się na zakładce przeglądarki, kodowanie, słowa kluczowe…), a także <body>, gdzie znajduje się sama treść strony.

W ciele strony zamieściliśmy parę typowych znaczników. <p> oznacza akapit (ang. paragraph). Domyślnie w HTML nie łamiemy tekstu, zostawiamy to przeglądarce internetowej. Ale gdybyśmy chcieli wymusić “enter”, możemy to zrobić przy użyciu <br> (ang. break).

Następnie mamy wypunktowanie. Listę nienumerowaną tworzymy przy użyciu <ul> (ang. unordered list), a numerowaną przy użyciu <ol> (ang. ordered list). W obu przypadkach kolejne elementy tworzymy przy użyciu <li> (ang. list item). Zwróćmy uwagę, że przeglądarka poradzi sobie niezależnie od tego, czy domkniemy ten znacznik poprzez </li> czy też nie.

Tabelę tworzymy przez znacznik <table>. Przy okazji widzimy tutaj atrybut: border, który ustala grubość obramowania (domyślnie go nie ma). Pojedynczy wiersz tworzymy przez <tr>. W obrębie wiersza mamy możliwość stworzenia komórki nagłówkowej przez <th>, gdzie zawartość jest domyślnie pogrubiona, oraz zwykłej komórki przez <td>.

Ostatnim przykładem jest utworzenie linku. Służy do tego znacznik <a>. Jego najważniejszym atrybutem jest href, który ustala, gdzie dany link kieruje. Sam tekst, który wyświetla się na stronie, a po naciśnięciu którego przechodzimy (zazwyczaj) na inną stronę, znajduje się pomiędzy znacznikiem otwierającym a zamykającym. Przy okazji omówmy, co może znaleźć się w wartości atrybutu href.

W przykładzie podaliśmy link bezwzględny, kierujący na konkretną, inną, stronę. Jednak jeśli chcemy pokierować na którąś ze swoich podstron w ramach jednego serwisu, możemy użyć linku względnego. Tak też dzieje się na Wikipedii. Przykładowo, na stronie https://pl.wikipedia.org/wiki/Zygmunt_III_Waza znajdziemy takie linki jak <a href="/wiki/Wilno" title="Wilno">Wilnie</a>. Jak widać, link zaczyna się od /: /wiki/Wilno. Należy to rozumieć następująco: ze strony, na której jesteśmy, wybieramy tzw. hosta, czyli początek adresu. Dla jest to https://pl.wikipedia.org. Następnie “doklejamy” link względny.

Istnieją także linki kierujące w obrębie tej samej strony. Na wspomnianym wpisie o Zygmuncie III Wazie jest np. link <a href="\#Rodzina">Rodzina</a>. Gdy adres zaczyna się od znaku #, oznacza to, że po jego kliknięciu zostaniemy przeniesieni (strona się “przescrolluje”) do elementu, którego atrybut id równa się napisowi po #. I rzeczywiście, na stronie znajduje się znacznik: <span class="mw-headline" id="Rodzina">. Jest to funkcjonalność, która najczęśniej jest używana przy spisach treści.

Często na stronach internetowych różne znaczniki będą posiadały atrybut class. Obiekt danej klasy ma określony wygląd (np. kolor czcionki, rozmiar, marginesy…). Wiele elementów na stronie może zawierać jedynie prosty zapis class="LadnaKlasa", podczas gdy definicja znajduje się w jednym miejscu, co pozwala na łatwą zmianę wyglądu całej strony w przyszłości. Co więcej, jeden znacznik może być więcej niż jednej klasy. Wtedy są onne oddzielone spacją: class="LadnaKlasa1 LadnaKlasa2". Same definicje klas znajdują się zazwyczaj w plikach stylu o rozszerzeniu css. My nie będziemy dokładnie omawiać styli, jednak warto wiedzieć, że często po nazwie klasy da się znaleźć “charakterystyczne” miejsce w kodzie, od którego umiemy trafić do interesującej nas zawartości.

Na końcu uwaga praktyczna: gdy będziemy scrapować stronę, będziemy chcieli podejrzeć jej kod źródłowy. Czasami będziemy chcieli obejrzeć całe źródło i wtedy najlepiej jest nacisnąć prawym klawiszem myszy na stronie i z menu kontekstowego wybrać Pokaż źródło strony. Gdy będziemy chcieli jednak obejrzeć kod odpowiedzialny za konkretny fragment, lepiej będzie nacisnąć prawym klawiszem myszy dokładnie na niego, a następnie wybrać Zbadaj element.

Scrapowanie

Prz scrapowaniu warto użyć parsera HTML, który będzie świadom struktury znaczników, jaka znajduje się w kodzie strony. Sparsuje on przetwarzaną stronę, zwracając strukturę danych pozwalającą nam na wybór znacznika po jego nazwie, wartości atrybutów, relacji sąsiedztwa czy zawierania się. Użyjemy parsera BeautifulSoup. Załadujmy go:

from bs4 import BeautifulSoup

Przypiszmy kod źródłowy naszej przykładowej strony do zmiennej:

html_doc="""<html>
<head>
    <title>Moja pierwsza strona!</title>
</head>
<body>
<p>Lorem ipsum dolor sit amet, consectetur adipiscing elit. Donec posuere elit at malesuada tempor. Donec eget ligula in ante auctor luctus. Phasellus iaculis porttitor gravida. Donec eget sem lorem. Morbi a libero imperdiet, viverra tellus ac, consequat tortor. Suspendisse nibh massa, accumsan non neque a, vestibulum commodo dui.</p>
<p>Phasellus vestibulum ut <br>erat sit amet ullamcorper. Nam at elit feugiat, dapibus ante vitae, ullamcorper dui. Nunc rutrum at nibh tincidunt mattis. In finibus sed ante vel mollis. Donec at semper metus. Aenean quis consectetur risus. Sed suscipit felis sed ex pretium euismod. In fermentum mi a odio porttitor, dapibus aliquet leo accumsan. Suspendisse pretium augue et faucibus euismod. Quisque risus metus, ultricies nec tortor at, efficitur convallis nunc.</p>
<ul>
    <li>Pierwszy punkt</li>
    <li>Drugi punkt</li>
    <li>Trzeci punkt</li>
</ul>
<ol>
    <li>Pierwszy punkt</li>
    <li>Drugi punkt</li>
    <li>Trzeci punkt</li>
</ol>
<table border="3" bgcolor="#ff00ff" class="tabela blog">
    <tr><th>Naglowek 1</th><th>Naglowek 2</th></tr>
    <tr><td>komorka 11</td><td>komorka 12</td></tr>
    <tr><td>komorka 21</td><td>komorka 22</td></tr>
</table>
<a href="http://google.pl">Arcyciekawa strona</a>
</body>
</html>"""

Teraz, tak jak zwykle, musimy zacząć od sparsowania kodu źródłowego:

soup = BeautifulSoup(html_doc, 'html.parser')

Zacznijmy od wyboru znacznika po jego nazwie. W tym prostym podejściu, gdy na stronie znajdują się dwa znaczniki o tej samej nazwie, wybierany jest pierwszy z nich:

soup.title
<title>Moja pierwsza strona!</title>

Aby odwołać się do nazwy znacznika, używamy pola name:

soup.title.name
'title'

Do odwołania się do wnętrza znacznika, mamy parę metod:

soup.title.string
'Moja pierwsza strona!'
soup.title.text
'Moja pierwsza strona!'
soup.title.contents
['Moja pierwsza strona!']

Warto tu przede wszystkim wyróżnić contents, które zwraca listę. Lista ta przechowuje rozbite wnętrze znacznika, co jest przydatne, gdy znajdują się w nim zagnieżdżone znaczniki. Przyjrzyjmy się, jak będą te pola działały dla bardziej złożonego znacznika <ul>:

soup.ul.text
'\nPierwszy punkt\nDrugi punkt\nTrzeci punkt\n'
soup.ul.contents
['\n',
<li>Pierwszy punkt</li>,
'\n',
<li>Drugi punkt</li>,
'\n',
<li>Trzeci punkt</li>,
'\n']

Przejdźmy teraz do wydobywania wartości atrybutów:

soup.table["border"]
'3'

Aby sprawdzić, czy dany znacznik w ogóle ma atrybut, użyjemy metody has_attr():

soup.table.has_attr("border")
True
soup.table.has_attr("href")
False

Aby uzyskać informacje o wszystkich atrybutach, zarówno ich nazwach jak i wartościach, odwołamy się do attrs:

soup.table.attrs
{'border': '3', 'bgcolor': '#ff00ff', 'class': ['tabela', 'blog']}

Teraz przejdźmy do znajdowania danego znacznika na stronie. Służy do tego metoda find_all() oraz find(), która zwraca pierwszy znaleziony znacznik:

soup.find_all("p")
[<p>Lorem ipsum dolor sit amet, consectetur adipiscing elit. Donec posuere elit at malesuada tempor. Donec eget ligula in ante auctor luctus. Phasellus iaculis porttitor gravida. Donec eget sem lorem. Morbi a libero imperdiet, viverra tellus ac, consequat tortor. Suspendisse nibh massa, accumsan non neque a, vestibulum commodo dui.</p>,
<p>Phasellus vestibulum ut <br/>erat sit amet ullamcorper. Nam at elit feugiat, dapibus ante vitae, ullamcorper dui. Nunc rutrum at nibh tincidunt mattis. In finibus sed ante vel mollis. Donec at semper metus. Aenean quis consectetur risus. Sed suscipit felis sed ex pretium euismod. In fermentum mi a odio porttitor, dapibus aliquet leo accumsan. Suspendisse pretium augue et faucibus euismod. Quisque risus metus, ultricies nec tortor at, efficitur convallis nunc.</p>]
soup.find("p")
<p>Lorem ipsum dolor sit amet, consectetur adipiscing elit. Donec posuere elit at malesuada tempor. Donec eget ligula in ante auctor luctus. Phasellus iaculis porttitor gravida. Donec eget sem lorem. Morbi a libero imperdiet, viverra tellus ac, consequat tortor. Suspendisse nibh massa, accumsan non neque a, vestibulum commodo dui.</p>

Możemy też szukać po wartościach atrybutów:

soup.find(border="3")
<table bgcolor="#ff00ff" border="3">
<tr><th>Naglowek 1</th><th>Naglowek 2</th></tr>
<tr><td>komorka 11</td><td>komorka 12</td></tr>
<tr><td>komorka 21</td><td>komorka 22</td></tr>
</table>

Warto zauważyć, że w popularnym scenariuszu, gdzie szukamy po nazwie klasy, musimy użyć class_, ponieważ słowo class jest zastrzeżonym słowem języka Python. Co więcej, wystarczy podać jedną klasę:

soup.find(class_="tabela")
<table bgcolor="#ff00ff" border="3">
<tr><th>Naglowek 1</th><th>Naglowek 2</th></tr>
<tr><td>komorka 11</td><td>komorka 12</td></tr>
<tr><td>komorka 21</td><td>komorka 22</td></tr>
</table>

Możemy też szukać jedynie zaznaczając, że dany atrybut w ogóle musi występować, niezależnie od jego wartości:

soup.find(href=True)
<a href="http://google.pl">Arcyciekawa strona</a>

Przejdźmy teraz do next_element i next_sibling. Pierwszy zwraca kolejny element, jaki był sparsowany (kolejność jak w kodzie źródłowym), podczas gdy drugi zwraca kolejny element na tym samym poziomie w drzewie wyrażeń (na tym samym poziomie “zagnieżdżenia”, znacznik, który ma tego samego rodzica, czyli znacznik zawierający). Przykładowo:

soup.tr.next_element
<th>Naglowek 1</th>

<th> nie jest na tym samym poziomie, co <tr> (jak się za chwilę przekonamy, kolejny wiersz jest na tym samym poziomie zagnieżdżenia), ale jest po prostu kolejnym elementem w kodzie. Teraz przetestujmy next_sibling:

soup.tr.next_sibling
'\n'

Nie jest to niestety zbyt interesujący wynik, choć z formalnego punktu widzenia poprawny. Otóż przejście w kodzie do nowej linii jest traktowane jako osobny element, następujący po domknięciu </tr>. Dlatego lepiej spójrzmy na kolejne rodzeństwo:

soup.tr.next_sibling.next_sibling
<tr><td>komorka 11</td><td>komorka 12</td></tr>

Analogicznie, istnieją previous_element i previous_sibling, które na tej samej zasadzie przeszukują wstecz.

Zadanie 7

Linki na stronie

Dla swojej ulubionej strony internetowej napisz kod, który połączy się ze stroną, znajdzie wszystkie linki na stronie (znacznik <a> posiadający atrybut href) a następnie je wypisze. Przyjrzyj się im. Czy są tam linki względne albo do tej samej strony?

Giełda

Pod adresem https://stooq.pl/q/?s=cdr znajdziemy notowania giełdowe spółki CD Projekt Red. Napisz kod, który dla ustalonego trzyliterowego kodu spółki wyświetli jego aktualną cenę, procentową zmianę, bezwzględną zmianę oraz liczbę transakcji.

Filmweb

Dla ustalonego linku do filmu na Filmwebie, np. https://www.filmweb.pl/film/Narodziny+gwiazdy-2018-542576, napisz kod, który zwróci:

  • reżysera
  • datę premiery
  • boxoffice
  • ocenę (to najtrudniejsze)

Poza tym, jak widać, link do filmu nie jest zbyt oczywisty. Czy dla podanego tytułu filmu umiesz znaleźć odpowiadającą mu stronę na Filmwebie?


Gotowe rozwiązanie zadania 7 znajdziecie tutaj.


Wszystkie wpisy z cyklu #pythonowewyzwanie:

Wyzwanie 1 - Instalacja środowiska i pierwsze kroki

Wyzwanie 2 - Podstawowe instrukcje

Wyzwanie 3 - Algorytmy i struktury danych

Wyzwanie 4 - Programowanie obiektowe

Wyzwanie 5 - Zaawansowane aspekty programowania obiektowego

Wyzwanie 6 - Wyjątki i operacje na plikach

Wyzwanie 7 - Web scraping

Maciej Bartoszuk

Ukończył z wyróżnieniem informatykę na wydziale Matematyki i Nauk Informacyjnych Politechniki Warszawskiej, gdzie aktualnie pracuje w zakładzie Sztucznej Inteligencji i Metod Obliczeniowych. Tam też od 2013 roku prowadzi zajęcia dydaktyczne z programowania w R, Pythonie, C/C++, C#. Uczestnik studiów doktoranckich w Instytucie Podstaw Informatyki Polskiej Akademii Nauk w latach 2013-2015. W 2018 roku obronił doktorat z wyróżnieniem na swoim rodzimym wydziale: System do oceny podobieństwa kodów źródłowych w językach funkcyjnych oparty na metodach uczenia maszynowego i agregacji danych, który obejmuje zarówno algorytmy przetwarzania kodów źródłowych programów, jak i data science. Współautor książki Przetwarzanie i analiza danych w języku Python wydanej przez PWN. Ponadto trener na bootcampach Data Science, gdzie uczy programować w języku Python pod kątem analizy danych.
Komentarze
Ostatnie posty
Data Science News #204
Data Science News #203
Data Science News #202
Data Science News #201