{"id":8576,"date":"2019-12-19T09:06:40","date_gmt":"2019-12-19T08:06:40","guid":{"rendered":"https:\/\/sii.pl\/blog\/?p=8576"},"modified":"2025-05-07T11:19:25","modified_gmt":"2025-05-07T09:19:25","slug":"sciaganie-danych-o-planszowkach-z-internetu-czyli-web-scraping-w-praktyce","status":"publish","type":"post","link":"https:\/\/sii.pl\/blog\/sciaganie-danych-o-planszowkach-z-internetu-czyli-web-scraping-w-praktyce\/","title":{"rendered":"\u015aci\u0105ganie danych o plansz\u00f3wkach z Internetu, czyli web scraping w praktyce"},"content":{"rendered":"\n<p>Wiele os\u00f3b narzeka na brak ciekawych zbior\u00f3w danych, na kt\u00f3rych mogliby po\u0107wiczy\u0107 swoje zdolno\u015bci analizy danych. Cz\u0119sto te\u017c s\u0142ysz\u0119, \u017ce\u00a0<em>szkoda, \u017ce dana strona nie udost\u0119pnia jakich\u015b danych w prostej formie<\/em>. Ale to, \u017ce kto\u015b nie udost\u0119pnia jakich\u015b danych wprost nie znaczy, \u017ce nie da si\u0119 ich zdoby\u0107 inaczej.<\/p>\n\n\n\n<div class=\"wp-block-image\"><figure class=\"aligncenter size-full is-resized\"><a href=\"https:\/\/sii.pl\/blog\/wp-content\/uploads\/2019\/12\/przyklad-serwisu-z-ktorego-sciagamy-informacje-Scraping-690x1024-1.png\"><img decoding=\"async\" src=\"https:\/\/sii.pl\/blog\/wp-content\/uploads\/2019\/12\/przyklad-serwisu-z-ktorego-sciagamy-informacje-Scraping-690x1024-1.png\" alt=\"przyklad serwisu z ktorego sciagamy informacje Scraping 202x300 - \u015aci\u0105ganie danych o plansz\u00f3wkach z Internetu, czyli web scraping w praktyce\" class=\"wp-image-18706\" width=\"345\" height=\"512\" srcset=\"https:\/\/sii.pl\/blog\/wp-content\/uploads\/2019\/12\/przyklad-serwisu-z-ktorego-sciagamy-informacje-Scraping-690x1024-1.png 690w, https:\/\/sii.pl\/blog\/wp-content\/uploads\/2019\/12\/przyklad-serwisu-z-ktorego-sciagamy-informacje-Scraping-690x1024-1-202x300.png 202w\" sizes=\"(max-width: 345px) 100vw, 345px\" \/><\/a><\/figure><\/div>\n\n\n\n<p>Czasem dane udost\u0119pniane s\u0105 w inny spos\u00f3b, np. za pomoc\u0105 API. Ale nawet je\u015bli serwis nie wystawia \u017cadnego, nie stoimy na straconej pozycji, bo mamy do dyspozycji jeszcze web scraping, czyli pisanie paj\u0105czk\u00f3w internetowych. Co to s\u0105 te paj\u0105czki i czemu tak si\u0119 nazywaj\u0105? S\u0105 to programy, kt\u00f3re chodz\u0105 po Sieci (crawl the net) i gromadz\u0105 dane poprzez parsowanie kodu html. No bo je\u015bli macie serwis internetowy, w kt\u00f3rym tysi\u0105ce stron ma tak\u0105 sam\u0105 struktur\u0119, to mo\u017ce da si\u0119 z nich wyci\u0105gn\u0105\u0107 informacje w jaki\u015b zorganizowany spos\u00f3b? Da si\u0119\u00a0\ud83d\ude42\u00a0I mo\u017ce nawet nie zdajecie sobie sprawy z tego, jakie to proste!<\/p>\n\n\n\n<p>Korzysta\u0142em z Anacondy zainstalowanej pod Windowsem i bior\u0105c pod uwag\u0119, \u017ce wi\u0119kszo\u015b\u0107 tutoriali pisanych jest z perspektywy linuxa, gdzie instalowanie pakiet\u00f3w i praca w konsoli jest zdecydowanie \u0142atwiejsza, to my\u015bl\u0119, \u017ce ten artyku\u0142 mo\u017ce przyda\u0107 si\u0119 osobom, kt\u00f3re czu\u0142y si\u0119 odstraszone pisaniem w shellu. Scrapy jeszcze jaki\u015b czas temu nie dzia\u0142a\u0142 pod pythonem 2, tak\u017ce je\u015bli zamierzacie dopiero zainstalowa\u0107 Anacond\u0119 b\u0105d\u017a samego pythona, zdecydujcie si\u0119 na wersj\u0119 opatrzon\u0105 numerkiem 3.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">Scrapy \u2013 podstawy<\/h3>\n\n\n\n<p>Po zainstalowaniu pakietu Scrapy, czy to przez mened\u017cera Anacondy czy przez pipa, mo\u017cemy uruchomi\u0107 interaktywny shell, w kt\u00f3rym b\u0119dziemy mogli spr\u00f3bowa\u0107 podstawowych funkcjonalno\u015bci pakietu. Aby to zrobi\u0107, w Anaconda prompt wpiszmy:<\/p>\n\n\n<div class=\"wp-block-syntaxhighlighter-code \"><pre class=\"brush: plain; title: ; notranslate\" title=\"\">\npython \u2013m scrapy shell\n<\/pre><\/div>\n\n\n<p>I na pr\u00f3b\u0119 \u015bci\u0105gnijmy sobie jak\u0105\u015b stron\u0119 internetow\u0105, np.:<\/p>\n\n\n<div class=\"wp-block-syntaxhighlighter-code \"><pre class=\"brush: plain; title: ; notranslate\" title=\"\">\nfetch(\"https:\/\/boardgamegeek.com\/boardgame\/242639\/treasure-island\")\n<\/pre><\/div>\n\n\n<p>Je\u015bli jeszcze tego nie m\u00f3wi\u0142em ani nie zorientowali\u015bcie si\u0119 \u2013 b\u0119dziemy \u015bci\u0105gali dane o plansz\u00f3wkach\u00a0\ud83d\ude09\u00a0Dlaczego? Bo plansz\u00f3wki s\u0105 super, a boardgamegeek jest por\u00f3wnywalnie dobrym (je\u015bli nie lepszym) \u017ar\u00f3d\u0142em danych o grach jak imdb o filmach.<\/p>\n\n\n\n<p>Po wykonaniu instrukcji:<\/p>\n\n\n<div class=\"wp-block-syntaxhighlighter-code \"><pre class=\"brush: plain; title: ; notranslate\" title=\"\">\nprint(response.text)\n<\/pre><\/div>\n\n\n<p>uka\u017ce nam si\u0119 kod html \u015bci\u0105gni\u0119tej strony. Od teraz b\u0119dziemy mogli \u201edobra\u0107 si\u0119\u201d do poszczeg\u00f3lnych element\u00f3w strony na dwa sposoby: za pomoc\u0105 selektor\u00f3w css b\u0105d\u017a xpath. W og\u00f3lno\u015bci pierwszy pozwala nam na odwo\u0142ywanie si\u0119 do takich element\u00f3w jak atrybut class kodu CSS, a drugi pozwala na budow\u0119 hierarchicznej \u015bcie\u017cki, kt\u00f3ra identyfikuje konkretne elementy kodu xml. Brzmi \u015bwietnie, ale powinni\u015bmy zacz\u0105\u0107 od jednej wa\u017cnej informacji:&nbsp;<strong>w dzisiejszych czasach strony internetowe s\u0105 cz\u0119sto generowane dynamicznie i to, co widzimy w przegl\u0105darce niekoniecznie jest tym samym, co b\u0119dziemy widzieli \u015bci\u0105gaj\u0105c kod tej strony<\/strong>. \u017beby to zobrazowa\u0107 na stronie powy\u017cej kliknijmy w przegl\u0105darce prawym przyciskiem gdzie\u015b w okolicach Description i wybierzmy Zbadaj\/Inspect. Otworzy nam si\u0119 konsola developera z zaznaczonym fragmentem kodu odpowiadaj\u0105cym elementowi, na kt\u00f3ry klikn\u0119li\u015bmy. Ale wy\u015bwietlaj\u0105c \u017ar\u00f3d\u0142o strony spr\u00f3bujmy znale\u017a\u0107 ten sam fragment kodu\u2026 Mo\u017ce by\u0107 ci\u0119\u017cko. A to dlatego, \u017ce du\u017ca cz\u0119\u015b\u0107 znacznik\u00f3w html generowana jest skryptem napisanym w Java Script. Wiele informacji, kt\u00f3re b\u0119dziemy chcieli wydoby\u0107 jest przekazywanych jako argumenty tego\u017c skryptu, natomiast znacznik\u00f3w html widocznych z poziomu narz\u0119dzi przegl\u0105darki tutaj nie u\u015bwiadczymy.<\/p>\n\n\n\n<p>Maj\u0105c to na uwadze spr\u00f3bujmy zobaczy\u0107 jak dzia\u0142aj\u0105 te dwa selektory. Najpierw css, kt\u00f3ry wygl\u0105da przyk\u0142adowo tak:<\/p>\n\n\n<div class=\"wp-block-syntaxhighlighter-code \"><pre class=\"brush: plain; title: ; notranslate\" title=\"\">\nresponse.css(\"meta\")\n<\/pre><\/div>\n\n\n<p>To zwr\u00f3ci nam d\u0142ug\u0105 list\u0119 r\u00f3\u017cnych rzeczy. Dlaczego? Ano dlatego, \u017ce znacznik\u00f3w meta mamy ca\u0142kiem sporo w kodzie strony\u2026 no to mo\u017ce spr\u00f3bujmy wyci\u0105gn\u0105\u0107 tylko jeden, najlepiej pierwszy? Prosz\u0119 bardzo:<\/p>\n\n\n<div class=\"wp-block-syntaxhighlighter-code \"><pre class=\"brush: plain; title: ; notranslate\" title=\"\">\nresponse.css(\"meta\").extract_first()\n<\/pre><\/div>\n\n\n<p>Co powinno nam da\u0107:<\/p>\n\n\n\n<p>&lt;meta charset=<strong>\u2019utf-8\u2032<\/strong>&gt;<\/p>\n\n\n\n<p>Rzeczywi\u015bcie, to jest pierwszy znacznik meta wyst\u0119puj\u0105cy na tej stronie. Super, w takim razie teraz zauwa\u017cmy, \u017ce w g\u00f3rnej cz\u0119\u015bci kodu, w sekcji &lt;head&gt; mamy kilka informacji, kt\u00f3re da\u0142oby si\u0119 \u0142atwo wydoby\u0107. W tym skr\u00f3cony opis gry, do kt\u00f3rego odwo\u0142ujemy si\u0119 dzi\u0119ki znacznikowi o atrybucie property=\u201eog:description\u201d:<\/p>\n\n\n\n<p>&lt;meta property=<strong>\u201eog:description\u201d<\/strong>&nbsp;content=<strong>\u201eLong John Silver\u2019s 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;amp;mdash; or perhaps his misdirections? Who knows\u2026 The old sea dog is surely planning an escape, after all, after which he will definitely try to get his treasure back.<\/strong><\/p>\n\n\n\n<p><strong>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\u2019s escape, when he will make a final run to get the booty for himself!\u201d<\/strong>&nbsp;\/&gt;<\/p>\n\n\n\n<p>Selektor css pozwoli nam \u0142atwo opisa\u0107 taki znacznik:<\/p>\n\n\n<div class=\"wp-block-syntaxhighlighter-code \"><pre class=\"brush: plain; title: ; notranslate\" title=\"\">\nresponse.css(\"meta&#x5B;property='og:description']\").extract_first()\n<\/pre><\/div>\n\n\n<p>No ale to pokazuje nam opis gry razem z ca\u0142ym znacznikiem\u2026 Aha, no tak, bo przecie\u017c opis kryje si\u0119 w atrybucie content. Jak dosta\u0107 si\u0119 do warto\u015bci atrybutu? Tak:<\/p>\n\n\n<div class=\"wp-block-syntaxhighlighter-code \"><pre class=\"brush: plain; title: ; notranslate\" title=\"\">\nresponse.css(\"meta&#x5B;property='og:description']::attr(content)\").get()\n<\/pre><\/div>\n\n\n<p>Widzimy te\u017c, \u017ce zamiast extract_first() mo\u017cemy u\u017cy\u0107 funkcji get().<\/p>\n\n\n\n<p>A teraz\u00a0 dla odmiany zobaczmy jak dzia\u0142a selektor xpath:<\/p>\n\n\n<div class=\"wp-block-syntaxhighlighter-code \"><pre class=\"brush: plain; title: ; notranslate\" title=\"\">\nresponse.xpath(\"\/\/html\/body\").get()\n<\/pre><\/div>\n\n\n<p>Ok, czyli tutaj te\u017c w \u0142atwy spos\u00f3b dostali\u015bmy si\u0119 do ca\u0142ej sekcji body. A spr\u00f3bujmy czego\u015b innego:<\/p>\n\n\n<div class=\"wp-block-syntaxhighlighter-code \"><pre class=\"brush: plain; title: ; notranslate\" title=\"\">\nresponse.xpath(\"\/\/html\/head\/script\/text()\").extract_first()\n<\/pre><\/div>\n\n\n<p>Whoa! Co tu si\u0119 wydarzy\u0142o\u2026 ot\u00f3\u017c jak widzimy w kodzie strony du\u017ca cz\u0119\u015b\u0107 informacji jest przekazywana jako argumenty skryptu Java Script. Chc\u0105c si\u0119 dosta\u0107 do tych informacji konstruujemy selektor przechodz\u0105c przez cz\u0119\u015b\u0107 html, sekcj\u0119 head, nast\u0119pnie znacznik script, a potem korzystaj\u0105c z funkcji text(), aby zobaczy\u0107 co jest wewn\u0105trz znacznika script. Du\u017co tekstu, ale wszystko da si\u0119 sparsowa\u0107. Oczywi\u015bcie znacznik\u00f3w script wewn\u0105trz &lt;html> mo\u017ce by\u0107 du\u017co, dlatego xpath() zwraca nam list\u0119 wszystkich fragment\u00f3w kodu pasuj\u0105cych do tego schematu, a nast\u0119pnie funkcj\u0105 extract_first() wybieramy tylko pierwszy z nich, bo akurat w tym przypadku mamy pewno\u015b\u0107, \u017ce chodzi nam o pierwszy skrypt wyst\u0119puj\u0105cy w kodzie\u2026 Pierwszy, kt\u00f3ry ma tekst\u00a0\ud83d\ude09\u00a0Bo tak naprawd\u0119 to drugi znacznik script, ale pierwszy nie ma \u017cadnego tekstu. Xpath() pomija wi\u0119c pusty script, bo nie pasuje on do wzorca.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">Pisanie paj\u0105czk\u00f3w<\/h3>\n\n\n\n<p>Teraz kiedy ju\u017c wiemy jakie mo\u017cliwo\u015bci z grubsza posiada Scrapy, mo\u017cemy napisa\u0107 prostego paj\u0105czka, kt\u00f3ry sam przepe\u0142znie przez kilka stron internetowych. W dedykowanym folderze utw\u00f3rzmy plik SiiSpider.py, a w nim umie\u015b\u0107my nast\u0119puj\u0105cych kilka linijek kodu:<\/p>\n\n\n<div class=\"wp-block-syntaxhighlighter-code \"><pre class=\"brush: python; title: ; notranslate\" title=\"\">\nimport scrapy  \n   \nclass SiiSpider(scrapy.Spider):  \n    name = \"planszoPajak\" \n    start_urls = &#x5B;'https:\/\/boardgamegeek.com\/browse\/boardgame?sort=rank&rankobjecttype=subtype&rankobjectid=1']  \n    download_delay = 5 \n    def parse(self, response):  \n        zestaw=response.css(\".collection_table\").xpath(\".\/\/tr\")  \n        for item in zestaw:  \n            i = {}  \n            i&#x5B;'tytul'] = item.css(\".collection_objectname a::text\").extract_first()  \n            i&#x5B;'url'] = item.css(\".collection_objectname a::attr(href)\").extract_first()  \n            yield i\n<\/pre><\/div>\n\n\n<p>Oczywi\u015bcie importujemy pakiet scrapy, a nast\u0119pnie tworzymy klas\u0119 SiiSpider, kt\u00f3ra dziedziczy po klasie scrapy.Spider. Nast\u0119pnie ustalamy trzy wa\u017cne parametry:<\/p>\n\n\n\n<ul class=\"wp-block-list\"><li>Nazw\u0119, kt\u00f3ra b\u0119dzie identyfikowa\u0142a naszego paj\u0105czka<\/li><li>List\u0119 adres\u00f3w url, na podstawie kt\u00f3rych paj\u0105czek ma rozpocz\u0105\u0107 scraping. My zaczniemy od listy najlepszych gier wg serwisu BoardGameGeek.<\/li><li>Op\u00f3\u017anienie pomi\u0119dzy scrapingiem kolejnej strony. P\u00f3ki co ustawiam na 5s, potem om\u00f3wi\u0119 czemu to takie istotne.<\/li><\/ul>\n\n\n\n<p>Nast\u0119pnie mamy metod\u0119 parse(), kt\u00f3rej rola w og\u00f3lno\u015bci polega na zebraniu danych i znalezieniu sobie nowego adresu url do scrapowania. Na razie skupimy si\u0119 na tej pierwszej cz\u0119\u015bci. W 8 linijce zmienna zestaw stanie si\u0119 list\u0105 wierszy w tabeli z grami. Selektor .collection_table odwo\u0142uje si\u0119 do atrybutu class, kt\u00f3ry w tym przypadku wyst\u0119puje w znaczniku table. Skoro nie u\u017cy\u0142em funkcji extract_first(), selektory wybieraj\u0105 wszystkie znaczniki pasuj\u0105ce do tr, czyli wiersza w tabeli. Nast\u0119pnie w p\u0119tli dla ka\u017cdej linii zape\u0142niam s\u0142ownik i tytu\u0142ami i adresami url. Ostatecznie yield zwraca nasz s\u0142ownik (i sprawia jednocze\u015bnie, \u017ce parse() staje si\u0119&nbsp;<a href=\"https:\/\/pl.python.org\/docs\/whatsnew\/section-generators.html\" rel=\"nofollow\" >funkcj\u0105 generuj\u0105c\u0105<\/a>, ale to ju\u017c opowie\u015b\u0107 na inn\u0105 okazj\u0119). Oczywi\u015bcie pisz\u0105c tego typu p\u0119tl\u0119 korzystam z interaktywnego shella i testuj\u0119 wszystkie selektory na konkretnych przyk\u0142adach.<\/p>\n\n\n\n<p>Jak uruchomi\u0107 naszego paj\u0105ka? Nie wystarczy skompilowa\u0107 kodu. Musimy go zapisa\u0107, w linii komend Anacondy wej\u015b\u0107 do katalogu z programem, a nast\u0119pnie uruchomi\u0107 go za pomoc\u0105 komendy:<\/p>\n\n\n<div class=\"wp-block-syntaxhighlighter-code \"><pre class=\"brush: plain; title: ; notranslate\" title=\"\">\npython \u2013m scrapy runspider SiiScraper.py\n<\/pre><\/div>\n\n\n<p>Zobaczymy w linii komend zalogowane \u015bci\u0105gni\u0119cia informacji, o kt\u00f3re nam chodzi, ale dobrze by\u0142oby je gdzie\u015b zapisa\u0107. Dlatego spr\u00f3bujmy tak:<\/p>\n\n\n<div class=\"wp-block-syntaxhighlighter-code \"><pre class=\"brush: plain; title: ; notranslate\" title=\"\">\npython \u2013m scrapy runspider SiiScraper.py \u2013o probka.csv\n<\/pre><\/div>\n\n\n<p>Dostaniemy plik csv z zapisanym s\u0142ownikiem i. Wygl\u0105da nie\u017ale, ale to tylko 100 gier \u2013 wy\u0142\u0105cznie pierwsza strona. Co zrobi\u0107, \u017ceby paj\u0105k poszed\u0142 dalej? Dodajmy par\u0119 linijek do metody parse():<\/p>\n\n\n<div class=\"wp-block-syntaxhighlighter-code \"><pre class=\"brush: python; title: ; notranslate\" title=\"\">\ndef parse(self, response):  \n    zestaw=response.css(\".collection_table\").xpath(\".\/\/tr\")  \n    for item in zestaw:  \n        i = {}  \n        i&#x5B;'tytul'] = item.css(\".collection_objectname a::text\").extract_first()  \n        i&#x5B;'url'] = item.css(\".collection_objectname a::attr(href)\").extract_first()  \n        yield i  \n           \n    next_page = response.xpath(\"\/\/a&#x5B;@title='next page']\").css(\"a::attr(href)\").extract_first()  \n    if next_page and '3' not in str(next_page):  \n        yield scrapy.Request(  \n            response.urljoin(next_page),  \n            callback=self.parse  \n        ) \n<\/pre><\/div>\n\n\n<p>Warto\u015b\u0107 w zmiennej next_page b\u0119dzie adresem url kolejnej strony. W tym przypadku je\u015bli nast\u0119pna strona nie jest trzeci\u0105 (czyt. Chc\u0119 sko\u0144czy\u0107 na drugiej), wywo\u0142amy funkcj\u0119 Request() i przeka\u017cemy j\u0105 do yield, co sprawi, \u017ce w rekurencyjnym stylu po sko\u0144czeniu scrapingu obecnej strony scrapy b\u0119dzie pr\u00f3bowa\u0142 scrapowa\u0107 kolejn\u0105. Po ponownym uruchomieniu paj\u0105ka dostaniemy ju\u017c 200 wierszy zamiast 100. Ale uwa\u017cajcie \u2013 je\u015bli podali\u015bcie t\u0119 sam\u0105 nazw\u0119 pliku, scrapy dorzuci nowe dane na ko\u0144cu istniej\u0105cego pliku, a nie zast\u0105pi go.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">Jak to tak naprawd\u0119 dzia\u0142a?<\/h3>\n\n\n\n<p>Wszystko fajnie, ale w ten spos\u00f3b dysponujemy tylko tytu\u0142ami i adresami url gier\u2026 nic przesadnie ciekawego. Co innego, jakby wej\u015b\u0107 w te adresy i pobra\u0107 jakie\u015b konkretne dane ze strony gry. Wi\u0119c co stoi na przeszkodzie? Zr\u00f3bmy to! =D<\/p>\n\n\n<div class=\"wp-block-syntaxhighlighter-code \"><pre class=\"brush: python; title: ; notranslate\" title=\"\">\nimport scrapy  \n   \nclass SiiSpider(scrapy.Spider):  \n    name = \"planszoPajak\" \n    start_urls = &#x5B;'https:\/\/boardgamegeek.com\/browse\/boardgame?sort=rank&rankobjecttype=subtype&rankobjectid=1']  \n    download_delay = 5 \n    def parse(self, response):  \n        zestaw=response.css(\".collection_table\").xpath(\".\/\/tr\")  \n        for item in zestaw:  \n            i = {}  \n            i&#x5B;'tytul'] = item.css(\".collection_objectname a::text\").extract_first()  \n            i&#x5B;'url'] = item.css(\".collection_objectname a::attr(href)\").extract_first()  \n            request = scrapy.Request(\"https:\/\/boardgamegeek.com\"+str(i&#x5B;'url']),  \n                             callback=self.parse_page2)  \n            request.meta&#x5B;'i'] = i  \n            yield request  \n               \n        next_page = response.xpath(\"\/\/a&#x5B;@title='next page']\").css(\"a::attr(href)\").extract_first()  \n        if next_page and '3' not in str(next_page):  \n            yield scrapy.Request(  \n                response.urljoin(next_page),  \n                callback=self.parse  \n            )  \n               \n    def parse_page2(self, response):  \n        i = response.meta&#x5B;'i']  \n        script = str(response.xpath(\"\/\/html\/head\/script\/text()\").extract_first())  \n        x = script.find('\"description\":')  \n        y = script.find('\"wiki\":')  \n        i&#x5B;'overview'] = script&#x5B;(x+15):(y-2)].replace(',','')  \n        yield i \n<\/pre><\/div>\n\n\n<p>Dodali\u015bmy metod\u0119 parse_page2() oraz jej wywo\u0142anie wewn\u0105trz metody parse(). To dobry moment, \u017ceby dok\u0142adniej om\u00f3wi\u0107 dzia\u0142anie Request\u00f3w. W linii 13 tworzymy obiekt Request, kt\u00f3ry jako argumenty przyjmuje adres url oraz nazw\u0119 metody, kt\u00f3ra b\u0119dzie mia\u0142a obs\u0142u\u017cy\u0107 odpowied\u017a (callback), czyli z grubsza kod strony. Kluczowa rzecz dzieje si\u0119 w linijce 16 \u2013 kiedy stawiamy obiekt Request po yield sprawiamy, \u017ce scrapy kolejkuje nasze zg\u0142oszenie, \u015bci\u0105ga odpowiedni\u0105 stron\u0119 i tworzy obiekt Response, gdzie zapisuje dane. &nbsp;Ten obiekt od razu przekazywany jest do funkcji przekazanej do argumentu callback. To samo dzieje si\u0119 domy\u015blnie na samym pocz\u0105tku dzia\u0142ania paj\u0105ka: automatycznie wywo\u0142uje on domy\u015bln\u0105 funkcj\u0119 callback \u2013 parse() do automatycznie stworzonego requesta z pierwszym adresem wzi\u0119tym ze zmiennej start_urls. To tworzy obiekt Response, kt\u00f3ry jest przekazywany metodzie parse(). St\u0105d wszystkie nasze selektory dzia\u0142aj\u0105 w\u0142a\u015bnie na zmiennej response. Teraz mo\u017cemy zwr\u00f3ci\u0107 r\u00f3wnie\u017c uwag\u0119 na to, \u017ce w linijce 20 wyst\u0119puje dok\u0142adnie taka sama sytuacja, jednak tam u\u017cywamy jeszcze funkcji urljoin(). To dlatego, \u017ce linki do stron mog\u0105 by\u0107 relatywne, a my chcemy pos\u0142ugiwa\u0107 si\u0119 absolutnymi, co zapewni wspomniana funkcja.<\/p>\n\n\n\n<p>Kiedy zrozumieli\u015bmy ju\u017c jak dzia\u0142a system Request\/Response (niczym Brygada RR) mo\u017cemy przyjrze\u0107 si\u0119 metadanym. Powiedzmy, \u017ce na naszej li\u015bcie gier s\u0105 informacje, kt\u00f3rych nie ma na stronie konkretnej gry. Chcieliby\u015bmy zatem przekaza\u0107 informacje zgromadzone podczas scrapingu listy do requesta scrape\u2019uj\u0105cego konkretn\u0105 gr\u0119 lub na odwr\u00f3t. Jak to zrobi\u0107? W\u0142a\u015bnie dzi\u0119ki metadanym. Ot\u00f3\u017c atrybut meta obiektu Request jest kopiowany do response.meta. Dzi\u0119ki temu mo\u017cemy przekazywa\u0107 informacje mi\u0119dzy requestami. Tak w\u0142a\u015bnie robimy tutaj: w linii 13 tworzymy obiekt Request, ale nic si\u0119 z nim jeszcze nie dzieje. W linii 15 przypisujemy metadanym s\u0142ownik i ze zgromadzonymi przez nas danymi. Nast\u0119pnie w linijce 16 \u015bci\u0105gana jest strona okre\u015blona przez request. Tworzony jest obiekt Response i do response.meta kopiowane s\u0105 metadane z requesta. Dzi\u0119ki temu kiedy wywo\u0142a si\u0119 parse_page2() w linii 26 mo\u017cemy zadeklarowa\u0107 lokaln\u0105 zmienn\u0105 i, kt\u00f3r\u0105 mo\u017cemy zainicjalizowa\u0107 naszym uzupe\u0142nionym ju\u017c troch\u0119 s\u0142ownikiem. W dalszej cz\u0119\u015bci metody zapisujemy jeszcze kr\u00f3tki opis gry do klucza overview, a nast\u0119pnie przekazujemy s\u0142ownik do yield. Nie jest to obiekt Request, wi\u0119c scrapy wie, \u017ce jest to zestaw danych, kt\u00f3ry ma by\u0107 naszym wynikiem.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">Kulturalne korzystanie z robot\u00f3w<\/h3>\n\n\n\n<p>Na koniec par\u0119 s\u0142\u00f3w o legalno\u015bci i przyzwoito\u015bci u\u017cywania paj\u0105czk\u00f3w. Musimy sobie odpowiedzie\u0107 na dwa pytania:<\/p>\n\n\n\n<ul class=\"wp-block-list\"><li>Czy mo\u017cemy z nich korzysta\u0107?<\/li><li>Jak bardzo mo\u017cemy z nich korzysta\u0107?<\/li><\/ul>\n\n\n\n<p>Odpowied\u017a na pierwsze znajdziemy zwykle w jakiej\u015b cz\u0119\u015bci Terms, w tym konkretnym przypadku mamy:<\/p>\n\n\n\n<blockquote class=\"wp-block-quote is-layout-flow wp-block-quote-is-layout-flow\"><p><em>You shall not use or launch any automated system, including without limitation \u201erobots,\u201d \u201espiders,\u201d or \u201eoffline readers,\u201d 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.<\/em><\/p><\/blockquote>\n\n\n\n<p>To odpowiada nam w sumie na oba pytania, tzn. \u017ce mo\u017cemy korzysta\u0107 z robot\u00f3w tylko wtedy, je\u015bli nie wysy\u0142amy zbyt du\u017co zapyta\u0144\u2026 ale co to w praktyce oznacza? No w\u0142a\u015bnie, \u017ceby odpowiedzie\u0107 na pytanie jak bardzo mo\u017cemy odpytywa\u0107 dany serwis, z pomoc\u0105 przychodzi plik&nbsp;<a href=\"https:\/\/boardgamegeek.com\/robots.txt\" rel=\"nofollow\" >https:\/\/boardgamegeek.com\/robots.txt<\/a>. Zwyczajowo serwisy umieszczaj\u0105 go w swojej g\u0142\u00f3wnej domenie i mo\u017cna w nim znale\u017a\u0107 nast\u0119puj\u0105ce dwie interesuj\u0105ce nas informacje \u2013 jakie powinni\u015bmy ustawi\u0107 op\u00f3\u017anienie (download_delay) oraz jakich cz\u0119\u015bci serwisu nie powinni\u015bmy scrapowa\u0107 \u2013 to oczywi\u015bcie te wzorce poprzedzone s\u0142\u00f3wkiem Disallow. Jak widzimy informacje dotycz\u0105ce gier nie s\u0105 tam wymienione, czyli bez przeszk\u00f3d mo\u017cemy puszcza\u0107 naszego paj\u0105ka. Byle nie chodzi\u0142 za szybko, dlatego delay ustawiamy mu na 5s.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">Podsumowanie<\/h3>\n\n\n\n<p>Bardzo przyjemn\u0105 w\u0142asno\u015bci\u0105 paj\u0105czk\u00f3w jest to, \u017ce po napisaniu pierwszego bardzo \u0142atwo dopasowa\u0107 go do swoich potrzeb i napisa\u0107 kolejne. Wiele serwis\u00f3w internetowych b\u0119dzie wygl\u0105da\u0142o podobnie, a nawet je\u015bli wizualnie b\u0119d\u0105 si\u0119 r\u00f3\u017cni\u0107, to selektor\u00f3w wsz\u0119dzie u\u017cywa si\u0119 tak samo. Mo\u017cna wi\u0119c powiedzie\u0107, \u017ce jest to pot\u0119\u017cna maszyna, kt\u00f3rej u\u017cycie \u0142atwo opanowa\u0107, ale nale\u017cy te\u017c pami\u0119ta\u0107 (wybaczcie bana\u0142),&nbsp; \u017ce z wielk\u0105 si\u0142\u0105 wi\u0105\u017ce si\u0119 wielka odpowiedzialno\u015b\u0107 \u2013 szanujcie zasady serwis\u00f3w, kt\u00f3re chcecie scrapowa\u0107, bo w wi\u0119kszo\u015bci przypadk\u00f3w \u0142ami\u0105c je nie zostaniecie zbanowani ani nikt Was nie b\u0119dzie \u015bciga\u0142\u2026 Ale po prostu b\u0119dziecie burakami.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">Par\u0119 przydatnych link\u00f3w<\/h3>\n\n\n\n<ul class=\"wp-block-list\"><li><a href=\"https:\/\/docs.scrapy.org\/en\/latest\/intro\/tutorial.html\" rel=\"nofollow\" >https:\/\/docs.scrapy.org\/en\/latest\/intro\/tutorial.html<\/a>&nbsp;\u2013 dok\u0142adniejszy tutorial z oficjalnej strony ScraPy,<\/li><li><a href=\"https:\/\/boardgamegeek.com\/\" rel=\"nofollow\" >https:\/\/boardgamegeek.com\/<\/a>&nbsp;\u2013 serwis-encyklopedia o plansz\u00f3wkach,<\/li><li><a href=\"https:\/\/pl.python.org\/docs\/whatsnew\/section-generators.html\" rel=\"nofollow\" >https:\/\/pl.python.org\/docs\/whatsnew\/section-generators.html<\/a>&nbsp;\u2013 by\u0142 ju\u017c wy\u017cej, ale powt\u00f3rz\u0119 \u2013 o generatorach w Pythonie, \u017ceby zrozumie\u0107 jak dzia\u0142a yield.<\/li><\/ul>\n\n\n<div class=\"kk-star-ratings kksr-auto kksr-align-left kksr-valign-bottom\"\n    data-payload='{&quot;align&quot;:&quot;left&quot;,&quot;id&quot;:&quot;8576&quot;,&quot;slug&quot;:&quot;default&quot;,&quot;valign&quot;:&quot;bottom&quot;,&quot;ignore&quot;:&quot;&quot;,&quot;reference&quot;:&quot;auto&quot;,&quot;class&quot;:&quot;&quot;,&quot;count&quot;:&quot;2&quot;,&quot;legendonly&quot;:&quot;&quot;,&quot;readonly&quot;:&quot;&quot;,&quot;score&quot;:&quot;5&quot;,&quot;starsonly&quot;:&quot;&quot;,&quot;best&quot;:&quot;5&quot;,&quot;gap&quot;:&quot;11&quot;,&quot;greet&quot;:&quot;&quot;,&quot;legend&quot;:&quot;5\\\/5 ( votes: 2)&quot;,&quot;size&quot;:&quot;18&quot;,&quot;title&quot;:&quot;\u015aci\u0105ganie danych o plansz\u00f3wkach z Internetu, czyli web scraping w praktyce&quot;,&quot;width&quot;:&quot;139.5&quot;,&quot;_legend&quot;:&quot;{score}\\\/{best} ( {votes}: {count})&quot;,&quot;font_factor&quot;:&quot;1.25&quot;}'>\n            \n<div class=\"kksr-stars\">\n    \n<div class=\"kksr-stars-inactive\">\n            <div class=\"kksr-star\" data-star=\"1\" style=\"padding-right: 11px\">\n            \n\n<div class=\"kksr-icon\" style=\"width: 18px; height: 18px;\"><\/div>\n        <\/div>\n            <div class=\"kksr-star\" data-star=\"2\" style=\"padding-right: 11px\">\n            \n\n<div class=\"kksr-icon\" style=\"width: 18px; height: 18px;\"><\/div>\n        <\/div>\n            <div class=\"kksr-star\" data-star=\"3\" style=\"padding-right: 11px\">\n            \n\n<div class=\"kksr-icon\" style=\"width: 18px; height: 18px;\"><\/div>\n        <\/div>\n            <div class=\"kksr-star\" data-star=\"4\" style=\"padding-right: 11px\">\n            \n\n<div class=\"kksr-icon\" style=\"width: 18px; height: 18px;\"><\/div>\n        <\/div>\n            <div class=\"kksr-star\" data-star=\"5\" style=\"padding-right: 11px\">\n            \n\n<div class=\"kksr-icon\" style=\"width: 18px; height: 18px;\"><\/div>\n        <\/div>\n    <\/div>\n    \n<div class=\"kksr-stars-active\" style=\"width: 139.5px;\">\n            <div class=\"kksr-star\" style=\"padding-right: 11px\">\n            \n\n<div class=\"kksr-icon\" style=\"width: 18px; height: 18px;\"><\/div>\n        <\/div>\n            <div class=\"kksr-star\" style=\"padding-right: 11px\">\n            \n\n<div class=\"kksr-icon\" style=\"width: 18px; height: 18px;\"><\/div>\n        <\/div>\n            <div class=\"kksr-star\" style=\"padding-right: 11px\">\n            \n\n<div class=\"kksr-icon\" style=\"width: 18px; height: 18px;\"><\/div>\n        <\/div>\n            <div class=\"kksr-star\" style=\"padding-right: 11px\">\n            \n\n<div class=\"kksr-icon\" style=\"width: 18px; height: 18px;\"><\/div>\n        <\/div>\n            <div class=\"kksr-star\" style=\"padding-right: 11px\">\n            \n\n<div class=\"kksr-icon\" style=\"width: 18px; height: 18px;\"><\/div>\n        <\/div>\n    <\/div>\n<\/div>\n                \n\n<div class=\"kksr-legend\" style=\"font-size: 14.4px;\">\n            5\/5 ( votes: 2)    <\/div>\n    <\/div>\n","protected":false},"excerpt":{"rendered":"<p>Wiele os\u00f3b narzeka na brak ciekawych zbior\u00f3w danych, na kt\u00f3rych mogliby po\u0107wiczy\u0107 swoje zdolno\u015bci analizy danych. Cz\u0119sto te\u017c s\u0142ysz\u0119, \u017ce\u00a0szkoda, &hellip; <a class=\"continued-btn\" href=\"https:\/\/sii.pl\/blog\/sciaganie-danych-o-planszowkach-z-internetu-czyli-web-scraping-w-praktyce\/\">Continued<\/a><\/p>\n","protected":false},"author":106,"featured_media":8684,"comment_status":"open","ping_status":"open","sticky":false,"template":"","format":"standard","meta":{"_acf_changed":false,"_editorskit_title_hidden":false,"_editorskit_reading_time":0,"_editorskit_is_block_options_detached":false,"_editorskit_block_options_position":"{}","inline_featured_image":false,"footnotes":""},"categories":[1314],"tags":[421,870],"class_list":["post-8576","post","type-post","status-publish","format-standard","has-post-thumbnail","hentry","category-development-na-twardo","tag-bi","tag-web-scraping"],"acf":[],"aioseo_notices":[],"republish_history":[],"featured_media_url":"https:\/\/sii.pl\/blog\/wp-content\/uploads\/2019\/12\/bi-plansz\u00f3wki.jpg","category_names":["Development na twardo"],"_links":{"self":[{"href":"https:\/\/sii.pl\/blog\/wp-json\/wp\/v2\/posts\/8576"}],"collection":[{"href":"https:\/\/sii.pl\/blog\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/sii.pl\/blog\/wp-json\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/sii.pl\/blog\/wp-json\/wp\/v2\/users\/106"}],"replies":[{"embeddable":true,"href":"https:\/\/sii.pl\/blog\/wp-json\/wp\/v2\/comments?post=8576"}],"version-history":[{"count":4,"href":"https:\/\/sii.pl\/blog\/wp-json\/wp\/v2\/posts\/8576\/revisions"}],"predecessor-version":[{"id":18709,"href":"https:\/\/sii.pl\/blog\/wp-json\/wp\/v2\/posts\/8576\/revisions\/18709"}],"wp:featuredmedia":[{"embeddable":true,"href":"https:\/\/sii.pl\/blog\/wp-json\/wp\/v2\/media\/8684"}],"wp:attachment":[{"href":"https:\/\/sii.pl\/blog\/wp-json\/wp\/v2\/media?parent=8576"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/sii.pl\/blog\/wp-json\/wp\/v2\/categories?post=8576"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/sii.pl\/blog\/wp-json\/wp\/v2\/tags?post=8576"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}