BI

Ściąganie danych o planszówkach z Internetu, czyli web scraping w praktyce

Grudzień 19, 2019 0
Podziel się:

Wiele osób narzeka na brak ciekawych zbiorów danych, na których mogliby poćwiczyć swoje zdolności analizy danych. Często też słyszę, że szkoda, że dana strona nie udostępnia jakichś danych w prostej formie. Ale to, że ktoś nie udostępnia jakichś danych wprost nie znaczy, że nie da się ich zdobyć inaczej. przyklad serwisu z ktorego sciagamy informacje Scraping 202x300 - Ściąganie danych o planszówkach z Internetu, czyli web scraping w praktyceCzasem dane udostępniane są w inny sposób, np. za pomocą API. Ale nawet jeśli serwis nie wystawia żadnego, nie stoimy na straconej pozycji, bo mamy do dyspozycji jeszcze web scraping, czyli pisanie pajączków internetowych. Co to są te pajączki i czemu tak się nazywają? Są to programy, które chodzą po Sieci (crawl the net) i gromadzą dane poprzez parsowanie kodu html. No bo jeśli macie serwis internetowy, w którym tysiące stron ma taką samą strukturę, to może da się z nich wyciągnąć informacje w jakiś zorganizowany sposób? Da się 😉 I może nawet nie zdajecie sobie sprawy z tego, jakie to proste!

Korzystałem z Anacondy zainstalowanej pod Windowsem i biorąc pod uwagę, że większość tutoriali pisanych jest z perspektywy linuxa, gdzie instalowanie pakietów i praca w konsoli jest zdecydowanie łatwiejsza, to myślę, że ten artykuł może przydać się osobom, które czuły się odstraszone pisaniem w shellu. Scrapy jeszcze jakiś czas temu nie działał pod pythonem 2, także jeśli zamierzacie dopiero zainstalować Anacondę bądź samego pythona, zdecydujcie się na wersję opatrzoną numerkiem 3.

Scrapy – podstawy

Po zainstalowaniu pakietu Scrapy, czy to przez menedżera Anacondy czy przez pipa, możemy uruchomić interaktywny shell, w którym będziemy mogli spróbować podstawowych funkcjonalności pakietu. Aby to zrobić, w Anaconda prompt wpiszmy:

python –m scrapy shell

I na próbę ściągnijmy sobie jakąś stronę internetową, np.:

fetch("https://boardgamegeek.com/boardgame/242639/treasure-island")

Jeśli jeszcze tego nie mówiłem ani nie zorientowaliście się – będziemy ściągali dane o planszówkach 😉 Dlaczego? Bo planszówki są super, a boardgamegeek jest porównywalnie dobrym (jeśli nie lepszym) źródłem danych o grach jak imdb o filmach.

Po wykonaniu instrukcji:

print(response.text)

ukaże nam się kod html ściągniętej strony. Od teraz będziemy mogli „dobrać się” do poszczególnych elementów strony na dwa sposoby: za pomocą selektorów css bądź xpath. W ogólności pierwszy pozwala nam na odwoływanie się do takich elementów jak atrybut class kodu CSS, a drugi pozwala na budowę hierarchicznej ścieżki, która identyfikuje konkretne elementy kodu xml. Brzmi świetnie, ale powinniśmy zacząć od jednej ważnej informacji: w dzisiejszych czasach strony internetowe są często generowane dynamicznie i to, co widzimy w przeglądarce niekoniecznie jest tym samym, co będziemy widzieli ściągając kod tej strony. Żeby to zobrazować na stronie powyżej kliknijmy w przeglądarce prawym przyciskiem gdzieś w okolicach Description i wybierzmy Zbadaj/Inspect. Otworzy nam się konsola developera z zaznaczonym fragmentem kodu odpowiadającym elementowi, na który kliknęliśmy. Ale wyświetlając źródło strony spróbujmy znaleźć ten sam fragment kodu… Może być ciężko. A to dlatego, że duża część znaczników html generowana jest skryptem napisanym w Java Script. Wiele informacji, które będziemy chcieli wydobyć jest przekazywanych jako argumenty tegoż skryptu, natomiast znaczników html widocznych z poziomu narzędzi przeglądarki tutaj nie uświadczymy.

Mając to na uwadze spróbujmy zobaczyć jak działają te dwa selektory. Najpierw css, który wygląda przykładowo tak:

response.css("meta")

To zwróci nam długą listę różnych rzeczy. Dlaczego? Ano dlatego, że znaczników meta mamy całkiem sporo w kodzie strony… no to może spróbujmy wyciągnąć tylko jeden, najlepiej pierwszy? Proszę bardzo:

response.css("meta").extract_first()

Co powinno nam dać:

<meta charset=‘utf-8’>

Rzeczywiście, to jest pierwszy znacznik meta występujący na tej stronie. Super, w takim razie teraz zauważmy, że w górnej części kodu, w sekcji <head> mamy kilka informacji, które dałoby się łatwo wydobyć. W tym skrócony opis gry, do którego odwołujemy się dzięki znacznikowi o atrybucie property=„og:description”:

<meta property=“og:description” content=“Long John Silver’s crew has committed mutiny and has him cornered and tied up! Round after round, they question him about the location of his treasure and explore the island following his directions &amp;mdash; or perhaps his misdirections? Who knows… The old sea dog is surely planning an escape, after all, after which he will definitely try to get his treasure back.

Treasure Island is a game of bluffing and adventure in which one player embodies Long John, trying to mislead the others in their search for his treasure. The hunt reaches its climax with Long John’s escape, when he will make a final run to get the booty for himself!” />

Selektor css pozwoli nam łatwo opisać taki znacznik:

response.css("meta[property='og:description']").extract_first()

No ale to pokazuje nam opis gry razem z całym znacznikiem… Aha, no tak, bo przecież opis kryje się w atrybucie content. Jak dostać się do wartości atrybutu? Tak:

response.css("meta[property='og:description']::attr(content)").get()

Widzimy też, że zamiast extract_first() możemy użyć funkcji get().

A teraz  dla odmiany zobaczmy jak działa selektor xpath:

response.xpath("//html/body").get()

Ok, czyli tutaj też w łatwy sposób dostaliśmy się do całej sekcji body. A spróbujmy czegoś innego:

response.xpath("//html/head/script/text()").extract_first()

Whoa! Co tu się wydarzyło… otóż jak widzimy w kodzie strony duża część informacji jest przekazywana jako argumenty skryptu Java Script. Chcąc się dostać do tych informacji konstruujemy selektor przechodząc przez część html, sekcję head, następnie znacznik script, a potem korzystając z funkcji text(), aby zobaczyć co jest wewnątrz znacznika script. Dużo tekstu, ale wszystko da się sparsować. Oczywiście znaczników script wewnątrz <html> może być dużo, dlatego xpath() zwraca nam listę wszystkich fragmentów kodu pasujących do tego schematu, a następnie funkcją extract_first() wybieramy tylko pierwszy z nich, bo akurat w tym przypadku mamy pewność, że chodzi nam o pierwszy skrypt występujący w kodzie… Pierwszy, który ma tekst 😉 Bo tak naprawdę to drugi znacznik script, ale pierwszy nie ma żadnego tekstu. Xpath() pomija więc pusty script, bo nie pasuje on do wzorca.

Pisanie pajączków

Teraz kiedy już wiemy jakie możliwości z grubsza posiada Scrapy, możemy napisać prostego pajączka, który sam przepełznie przez kilka stron internetowych. W dedykowanym folderze utwórzmy plik SiiSpider.py, a w nim umieśćmy następujących kilka linijek kodu:

import scrapy  
  
class SiiSpider(scrapy.Spider):  
    name = "planszoPajak"  
    start_urls = ['https://boardgamegeek.com/browse/boardgame?sort=rank&amp;rankobjecttype=subtype&amp;rankobjectid=1']  
    download_delay = 5  
    def parse(self, response):  
        zestaw=response.css(".collection_table").xpath(".//tr")  
        for item in zestaw:  
            i = {}  
            i['tytul'] = item.css(".collection_objectname a::text").extract_first()  
            i['url'] = item.css(".collection_objectname a::attr(href)").extract_first()  
            yield i

Oczywiście importujemy pakiet scrapy, a następnie tworzymy klasę SiiSpider, która dziedziczy po klasie scrapy.Spider. Następnie ustalamy trzy ważne parametry:

  • Nazwę, która będzie identyfikowała naszego pajączka
  • Listę adresów url, na podstawie których pajączek ma rozpocząć scraping. My zaczniemy od listy najlepszych gier wg serwisu BoardGameGeek.
  • Opóźnienie pomiędzy scrapingiem kolejnej strony. Póki co ustawiam na 5s, potem omówię czemu to takie istotne.

Następnie mamy metodę parse(), której rola w ogólności polega na zebraniu danych i znalezieniu sobie nowego adresu url do scrapowania. Na razie skupimy się na tej pierwszej części. W 8 linijce zmienna zestaw stanie się listą wierszy w tabeli z grami. Selektor .collection_table odwołuje się do atrybutu class, który w tym przypadku występuje w znaczniku table. Skoro nie użyłem funkcji extract_first(), selektory wybierają wszystkie znaczniki pasujące do tr, czyli wiersza w tabeli. Następnie w pętli dla każdej linii zapełniam słownik i tytułami i adresami url. Ostatecznie yield zwraca nasz słownik (i sprawia jednocześnie, że parse() staje się funkcją generującą, ale to już opowieść na inną okazję). Oczywiście pisząc tego typu pętlę korzystam z interaktywnego shella i testuję wszystkie selektory na konkretnych przykładach.

Jak uruchomić naszego pająka? Nie wystarczy skompilować kodu. Musimy go zapisać, w linii komend Anacondy wejść do katalogu z programem, a następnie uruchomić go za pomocą komendy:

python –m scrapy runspider SiiScraper.py

Zobaczymy w linii komend zalogowane ściągnięcia informacji, o które nam chodzi, ale dobrze byłoby je gdzieś zapisać. Dlatego spróbujmy tak:

python –m scrapy runspider SiiScraper.py –o probka.csv

Dostaniemy plik csv z zapisanym słownikiem i. Wygląda nieźle, ale to tylko 100 gier – wyłącznie pierwsza strona. Co zrobić, żeby pająk poszedł dalej? Dodajmy parę linijek do metody parse():

def parse(self, response):  
    zestaw=response.css(".collection_table").xpath(".//tr")  
    for item in zestaw:  
        i = {}  
        i['tytul'] = item.css(".collection_objectname a::text").extract_first()  
        i['url'] = item.css(".collection_objectname a::attr(href)").extract_first()  
        yield i  
          
    next_page = response.xpath("//a[@title='next page']").css("a::attr(href)").extract_first()  
    if next_page and '3' not in str(next_page):  
        yield scrapy.Request(  
            response.urljoin(next_page),  
            callback=self.parse  
        ) 

Wartość w zmiennej next_page będzie adresem url kolejnej strony. W tym przypadku jeśli następna strona nie jest trzecią (czyt. Chcę skończyć na drugiej), wywołamy funkcję Request() i przekażemy ją do yield, co sprawi, że w rekurencyjnym stylu po skończeniu scrapingu obecnej strony scrapy będzie próbował scrapować kolejną. Po ponownym uruchomieniu pająka dostaniemy już 200 wierszy zamiast 100. Ale uważajcie – jeśli podaliście tę samą nazwę pliku, scrapy dorzuci nowe dane na końcu istniejącego pliku, a nie zastąpi go.

Jak to tak naprawdę działa?

Wszystko fajnie, ale w ten sposób dysponujemy tylko tytułami i adresami url gier… nic przesadnie ciekawego. Co innego, jakby wejść w te adresy i pobrać jakieś konkretne dane ze strony gry. Więc co stoi na przeszkodzie? Zróbmy to! =D

import scrapy  
  
class SiiSpider(scrapy.Spider):  
    name = "planszoPajak"  
    start_urls = ['https://boardgamegeek.com/browse/boardgame?sort=rank&amp;rankobjecttype=subtype&amp;rankobjectid=1']  
    download_delay = 5  
    def parse(self, response):  
        zestaw=response.css(".collection_table").xpath(".//tr")  
        for item in zestaw:  
            i = {}  
            i['tytul'] = item.css(".collection_objectname a::text").extract_first()  
            i['url'] = item.css(".collection_objectname a::attr(href)").extract_first()  
            request = scrapy.Request("https://boardgamegeek.com"+str(i['url']),  
                             callback=self.parse_page2)  
            request.meta['i'] = i  
            yield request  
              
        next_page = response.xpath("//a[@title='next page']").css("a::attr(href)").extract_first()  
        if next_page and '3' not in str(next_page):  
            yield scrapy.Request(  
                response.urljoin(next_page),  
                callback=self.parse  
            )  
              
    def parse_page2(self, response):  
        i = response.meta['i']  
        script = str(response.xpath("//html/head/script/text()").extract_first())  
        x = script.find('"description":')  
        y = script.find('"wiki":')  
        i['overview'] = script[(x+15):(y-2)].replace(',','')  
        yield i 

Dodaliśmy metodę parse_page2() oraz jej wywołanie wewnątrz metody parse(). To dobry moment, żeby dokładniej omówić działanie Requestów. W linii 13 tworzymy obiekt Request, który jako argumenty przyjmuje adres url oraz nazwę metody, która będzie miała obsłużyć odpowiedź (callback), czyli z grubsza kod strony. Kluczowa rzecz dzieje się w linijce 16 – kiedy stawiamy obiekt Request po yield sprawiamy, że scrapy kolejkuje nasze zgłoszenie, ściąga odpowiednią stronę i tworzy obiekt Response, gdzie zapisuje dane.  Ten obiekt od razu przekazywany jest do funkcji przekazanej do argumentu callback. To samo dzieje się domyślnie na samym początku działania pająka: automatycznie wywołuje on domyślną funkcję callback – parse() do automatycznie stworzonego requesta z pierwszym adresem wziętym ze zmiennej start_urls. To tworzy obiekt Response, który jest przekazywany metodzie parse(). Stąd wszystkie nasze selektory działają właśnie na zmiennej response. Teraz możemy zwrócić również uwagę na to, że w linijce 20 występuje dokładnie taka sama sytuacja, jednak tam używamy jeszcze funkcji urljoin(). To dlatego, że linki do stron mogą być relatywne, a my chcemy posługiwać się absolutnymi, co zapewni wspomniana funkcja.

Kiedy zrozumieliśmy już jak działa system Request/Response (niczym Brygada RR) możemy przyjrzeć się metadanym. Powiedzmy, że na naszej liście gier są informacje, których nie ma na stronie konkretnej gry. Chcielibyśmy zatem przekazać informacje zgromadzone podczas scrapingu listy do requesta scrape’ującego konkretną grę lub na odwrót. Jak to zrobić? Właśnie dzięki metadanym. Otóż atrybut meta obiektu Request jest kopiowany do response.meta. Dzięki temu możemy przekazywać informacje między requestami. Tak właśnie robimy tutaj: w linii 13 tworzymy obiekt Request, ale nic się z nim jeszcze nie dzieje. W linii 15 przypisujemy metadanym słownik i ze zgromadzonymi przez nas danymi. Następnie w linijce 16 ściągana jest strona określona przez request. Tworzony jest obiekt Response i do response.meta kopiowane są metadane z requesta. Dzięki temu kiedy wywoła się parse_page2() w linii 26 możemy zadeklarować lokalną zmienną i, którą możemy zainicjalizować naszym uzupełnionym już trochę słownikiem. W dalszej części metody zapisujemy jeszcze krótki opis gry do klucza overview, a następnie przekazujemy słownik do yield. Nie jest to obiekt Request, więc scrapy wie, że jest to zestaw danych, który ma być naszym wynikiem.

Kulturalne korzystanie z robotów

Na koniec parę słów o legalności i przyzwoitości używania pajączków. Musimy sobie odpowiedzieć na dwa pytania:

  • Czy możemy z nich korzystać?
  • Jak bardzo możemy z nich korzystać?

Odpowiedź na pierwsze znajdziemy zwykle w jakiejś części Terms, w tym konkretnym przypadku mamy:

You shall not use or launch any automated system, including without limitation “robots,” “spiders,” or “offline readers,” that accesses the Geek Websites in a manner that sends more request messages to the BoardGameGeek servers in a given period of time than a human can reasonably produce in the same period by using a conventional online web browser, except as expressly permitted by BoardGameGeek.

To odpowiada nam w sumie na oba pytania, tzn. że możemy korzystać z robotów tylko wtedy, jeśli nie wysyłamy zbyt dużo zapytań… ale co to w praktyce oznacza? No właśnie, żeby odpowiedzieć na pytanie jak bardzo możemy odpytywać dany serwis, z pomocą przychodzi plik https://boardgamegeek.com/robots.txt. Zwyczajowo serwisy umieszczają go w swojej głównej domenie i można w nim znaleźć następujące dwie interesujące nas informacje – jakie powinniśmy ustawić opóźnienie (download_delay) oraz jakich części serwisu nie powinniśmy scrapować – to oczywiście te wzorce poprzedzone słówkiem Disallow. Jak widzimy informacje dotyczące gier nie są tam wymienione, czyli bez przeszkód możemy puszczać naszego pająka. Byle nie chodził za szybko, dlatego delay ustawiamy mu na 5s.

Podsumowanie

Bardzo przyjemną własnością pajączków jest to, że po napisaniu pierwszego bardzo łatwo dopasować go do swoich potrzeb i napisać kolejne. Wiele serwisów internetowych będzie wyglądało podobnie, a nawet jeśli wizualnie będą się różnić, to selektorów wszędzie używa się tak samo. Można więc powiedzieć, że jest to potężna maszyna, której użycie łatwo opanować, ale należy też pamiętać (wybaczcie banał),  że z wielką siłą wiąże się wielka odpowiedzialność – szanujcie zasady serwisów, które chcecie scrapować, bo w większości przypadków łamiąc je nie zostaniecie zbanowani ani nikt Was nie będzie ścigał… Ale po prostu będziecie burakami.

Parę przydatnych linków

Oceń ten post
Kategorie: BI
Krzysztof Suchoński
Autor: Krzysztof Suchoński
Od kilku lat pracuję jako analityk danych programując głównie w SAS, R i różnych dialektach SQLa. Jestem pasjonatem nowych technik wizualizacji danych i algorytmiki, a w wolnych chwilach tańczę i gram w planszówki.

Imię i nazwisko (wymagane)

Adres email (wymagane)

Temat

Treść wiadomości

Zostaw komentarz