Opisałem drogę w wyniku której zacząłem używać scrapy, dzisiaj chciałym pokazać jak skonfigurowałem to narzędzie żeby móc pobierać dane z otomoto.
Instalacja i pierwsze testy
Scrapy instalowałem z pomocą pip:
pip install Scrapy
Poprawność instalacji możemy sprawdzić przy pomocy komendy:
C:\>scrapy --version
Scrapy 1.8.0 - no active project
Usage:
scrapy <command> [options] [args]
Available commands:
bench Run quick benchmark test
fetch Fetch a URL using the Scrapy downloader
genspider Generate new spider using pre-defined templates
runspider Run a self-contained spider (without creating a project)
settings Get settings values
shell Interactive scraping console
startproject Create new project
version Print Scrapy version
view Open URL in browser, as seen by Scrapy
[ more ] More commands available when run from project directory
Use "scrapy <command> -h" to see more info about a command
Scrapy - pierwszy test
Jeżeli nie ma potrzeby przetwarzania dużej ilości stron (lub tak jak u mnie chcesz sprawdzić czy to narzędzie zwróci oczekiwany wynik) to nie jest potrzebne konfigurowanie całego projektu. można przeprowadzić testy używając samej konsoli. Wystarczy rozpocząć od komendy scrapy shell
> scrapy shell
2020-01-09 18:40:34 [scrapy.utils.log] INFO: Scrapy 1.8.0 started (bot: scrapybot)
...
2020-01-09 18:40:35 [scrapy.extensions.telnet] INFO: Telnet console listening on 127.0.0.1:6023
[s] Available Scrapy objects:
[s] scrapy scrapy module (contains scrapy.Request, scrapy.Selector, etc)
[s] crawler <scrapy.crawler.Crawler object at 0x00C45C30>
[s] item {}
[s] settings <scrapy.settings.Settings object at 0x04871DB0>
[s] Useful shortcuts:
[s] fetch(url[, redirect=True]) Fetch URL and update local objects (by default, redirects are followed)
[s] fetch(req) Fetch a scrapy.Request and update local objects
[s] shelp() Shell help (print this help)
[s] view(response) View response in a browser
>>>
Pojawi się wtedy lista potrzebnych komend, spróbujmy pobrać przykładowa stronę komendą
>>> fetch("https://www.otomoto.pl/osobowe/toyota/yaris/ii-2005-2011/")
Niestety pierwszy rezultat może być rozczarowujący, nie udało się pobrać niczego (przekroczony czas oczekiwania)
>>> fetch("https://www.otomoto.pl/osobowe/toyota/yaris/ii-2005-2011/")
2020-01-09 18:51:05 [scrapy.core.engine] INFO: Spider opened
2020-01-09 18:54:05 [scrapy.downloadermiddlewares.retry] DEBUG: Retrying <GET https://www.otomoto.pl/osobowe/toyota/yaris/ii-2005-2011/> (failed 1 times): User timeout caused connection failure: Getting https://www.otomoto.pl/osobowe/toyota/yaris/ii-2005-2011/ took longer than 180.0 seconds..
2020-01-09 18:57:05 [scrapy.downloadermiddlewares.retry] DEBUG: Retrying <GET https://www.otomoto.pl/osobowe/toyota/yaris/ii-2005-2011/> (failed 2 times): User timeout caused connection failure: Getting https://www.otomoto.pl/osobowe/toyota/yaris/ii-2005-2011/ took longer than 180.0 seconds..
2020-01-09 19:00:05 [scrapy.downloadermiddlewares.retry] DEBUG: Gave up retrying <GET https://www.otomoto.pl/osobowe/toyota/yaris/ii-2005-2011/> (failed 3 times): User timeout caused connection failure: Getting https://www.otomoto.pl/osobowe/toyota/yaris/ii-2005-2011/ took longer than 180.0 seconds..
Zrobiłem ten test próbując inne adresy URL i w zależności od strony udawało się dane pobrać a raz nie. Okazało się że niektóre witryny bronią się przed dostępem do treści przy pomocy automatów. Rozwiązaniem okazało się podanie user agent
:
scrapy shell https://www.otomoto.pl/osobowe/toyota/yaris/ii-2005-2011/ -s USER_AGENT='Mozilla/5.0 (Macintosh; Intel Mac OS X 10_10_2) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/46.0.2490.80 Safari/537.36'
2020-01-09 19:28:00 [scrapy.core.engine] DEBUG: Crawled (200) <GET https://www.otomoto.pl/osobowe/toyota/yaris/ii-2005-2011/> (referer: None)
[s] Available Scrapy objects:
[s] scrapy scrapy module (contains scrapy.Request, scrapy.Selector, etc)
[s] crawler <scrapy.crawler.Crawler object at 0x03824250>
[s] item {}
[s] request <GET https://www.otomoto.pl/osobowe/toyota/yaris/ii-2005-2011/>
[s] response <200 https://www.otomoto.pl/osobowe/toyota/yaris/ii-2005-2011/>
[s] settings <scrapy.settings.Settings object at 0x043B1A50>
[s] spider <DefaultSpider 'default' at 0x47359f0>
[s] Useful shortcuts:
[s] fetch(url[, redirect=True]) Fetch URL and update local objects (by default, redirects are followed)
[s] fetch(req) Fetch a scrapy.Request and update local objects
[s] shelp() Shell help (print this help)
[s] view(response) View response in a browser
Podgląd pobranych danych w przeglądarce umożliwia komenda view(response)
Jeżeli chcemy zobaczyć kod strony to możemy go wyświetlić przy pomocy print(response.text)
część II
Ponieważ pierwsze próby używania Scrapy wyszły pomyślnie, to zacząłem dostosowywać to narzędzie do swoich potrzeb.
Tworzenie projektu
Wykorzystując komendę:
scrapy startproject scrapy_otomoto
Utworzymy na dysku następującą strukturę plików
>tree /f
D:.
└───scrapy_otomoto
│ scrapy.cfg
│
└───scrapy_otomoto
│ items.py
│ middlewares.py
│ pipelines.py
│ settings.py
│ __init__.py
│
├───spiders
│ │ __init__.py
│ │
│ └───__pycache__
└───__pycache__
Wygenerowanie pająka
Kolejnym krokiem jest wygenerowanie “pająka”:
scrapy genspider otomoto otomoto.pl
Wynikiem tej komendy jest utworzenie nowego pliku w folderze spiders
D:.
│ scrapy.cfg
│
└───scrapy_otomoto
│ items.py
│ middlewares.py
│ pipelines.py
│ settings.py
│ __init__.py
│
├───spiders
│ │ otomoto.py
│ │ __init__.py
│ │
│ └───__pycache__
│ __init__.cpython-37.pyc
│
└───__pycache__
settings.cpython-37.pyc
__init__.cpython-37.pyc
Zawartość pliku otomoto.py
:
# -*- coding: utf-8 -*-
import scrapy
class OtomotoSpider(scrapy.Spider):
name = 'otomoto'
allowed_domains = ['otomoto.pl']
start_urls = ['http://otomoto.pl/']
def parse(self, response):
pass
Plik konfiguracyjny scrapy.cfg
:
# Automatically created by: scrapy startproject
#
# For more information about the [deploy] section see:
# https://scrapyd.readthedocs.io/en/latest/deploy.html
[settings]
default = scrapy_otomoto.settings
shell = ipython
[deploy]
#url = http://localhost:6800/
project = scrapy_otomoto
Uruchomienie projektu
Uruchomianie projektu wykonuje się komenda:
cd .\scrapy_otomoto\scrapy_otomoto\spiders
scrapy runspider otomoto.py
To jest minimalna konfiguracja projektu w scrapy jednak żeby zadziałał on ze stroną otomoto.pl konieczna będzie dalsza konfiguracja. W tym przypadku problemem jest (tak samo jak za poprzednim razem) brak podanego user-agent
.
częsć III
W części II opisałem jak zbudować projekt na dysku, który nie działał. W tej części dokończę opis konfiguracji do etapu “produkcyjnego”. Zacznę od prostego pająka:
import scrapy
class OtomotoSpider(scrapy.Spider):
name = 'otomoto'
allowed_domains = ['otomoto.pl']
start_urls = ['http://otomoto.pl/']
def parse(self, response):
pass
Pierwszym krokiem będzie dodanie user-agent
tak żeby pająk zaczął spełniać swoje zadanie.
# -*- coding: utf-8 -*-
import scrapy
import json
import datetime
class OtomotoSpider(scrapy.Spider):
name = 'otomoto'
custom_settings = {
'USER_AGENT': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_10_2) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/46.0.2490.80 Safari/537.36',
}
allowed_domains = ['otomoto.pl']
start_urls = ['https://www.otomoto.pl/osobowe/toyota/yaris/ii-2005-2011/']
def parse(self, response):
print(response.status)
lastPage = response.xpath("//span[@class='page']//text()").extract()[-1]# extract offer list
#print(lastPage)
offers = response.xpath("//div[@class='offers list']").extract() # extract offer list
#print(type(offers))
data = json.loads(response.xpath('//script[@type="application/ld+json"]//text()').extract_first()) # extract of ld+json from page
#print(type(data))
current_page = response.meta.get("page", 1)
next_page = current_page + 1
if current_page < int(lastPage):
isTruncated = True
else:
isTruncated = False
if isTruncated == True:
yield scrapy.Request(
url="https://www.otomoto.pl/osobowe/toyota/yaris/ii-2005-2011/?page={page}".format(page=next_page),
callback=self.parse,
meta={'page': next_page},
dont_filter=True
)
if isTruncated == False:
print(lastPage)
# with open('page'+str(current_page)+'.html', 'wb') as html_file:
# html_file.write(response.body)
#print("procesing:"+response.url)
now = datetime.datetime.now()
with open('./data/otomoto_'+str(now.date())+'.html', 'a',encoding='utf-8') as f:
for item in offers:
f.write("%s\n" % item)
with open('./data/otomoto_'+str(now.date())+'.json', 'a') as j:
# this would place the entire output on one line
# use json.dump(lista_items, f, indent=4) to "pretty-print" with four spaces per indent
json.dump(data, j, indent=4)
# def parse_item(self, response):
# with open('page.html', 'wb') as html_file:
# html_file.write(response.body)
# #pass
Część IV
Kolejnym krokiem jest sprawienie żeby zadanie pobierania ogłoszeń wykonywało się samo co jakiś czas. W moim przypadku będzie to raz na dobę, w środku nocy. Korzystam przy tym z dobrodziejstwa własnego serwera VPS.
Automatyczne pobieranie stron (cron + Scrapy)
Skrypt bash - Sprawdzenie lokalizacji interpretera Bash
which bash
Utworzenie pliku getdata.sh
#!/bin/bash
source ~/environments/ml_env/bin/activate
PATH=$PATH:~/environments/ml_env/bin/python
export PATH
cd ~/scrapy/
scrapy runspider otomoto.py
Zrobienie z pliku getdata.sh
pliku wykonywalnego:
chmod +x hello_world.sh
Dla pliku python skrypt bash będzie miał postać:
#!/bin/bash
source ~/environments/ml_env/bin/activate
PATH=$PATH:~/environments/ml_env/bin/python
export PATH
python ~/scrapy/carDataParser.py
Bardzo istotną kwestią jest kodowanie znaków końca linii. W systemach unixowych jest to wyłącznie znak LF
.
Jeżeli skrypt nie działa (przykładowy błąd: -bash: ./parsedata.sh: /bin/bash^M: bad interpreter: No such file or directory
) to warto to sprawdzić. Znaki końca linii można poprawić komendą:
sed -i -e 's/\r$//' plotdata.sh
lub z pomocą dos2unix file.txt
Skrypt bash można uruchomić ręcznie dodając ./
przed nazwą pliku wykonywalnego ./plotdata.sh
cron
Zadanie będzie wykonywane codziennie o 4:33 w nocy.
crontab -e
Składnia zadań wygląda następująco:
* * * * * komenda do wykonania
^ ^ ^ ^ ^
| | | | |
| | | | +----- dzień tygodnia (0 - 7) (niedziela=0, poniedziałek=1, wtorek=2, ..., niedziela=7)
| | | |
| | | +------- miesiąc (1 - 12)
| | |
| | +--------- dzień miesiąca (1 - 31)
| |
| +----------- godzina (0 - 23)
|
+------------- minuta (0 - 59)
Zamiast pierwszych pięciu pól, można użyć jednego z ośmiu łańcuchów specjalnych:
łańcuch znaczenie
------- ---------
@reboot uruchamia raz, przy rozruchu;
@yearly uruchamia raz w roku, "0 0 1 1 *";
@annually (to samo co @yearly);
@monthly uruchamia raz w miesiącu, "0 0 1 * *";
@weekly uruchamia raz w tygodniu, "0 0 * * 0";
@daily uruchamia raz na dzień, "0 0 * * *";
@midnight (to samo co @daily);
@hourly uruchamia raz na godzinę, "0 * * * *".
Gotowe zadanie wygląda następująco:
# (...)
# m h dom mon dow command
MAILTO=""
33 4 * * * ~/scrapy/getdata.sh
Ułatwieniem przy definiowaniu powtarzalny reguł w cron może być strona crontab.guru
Weryfikacja działania cron:
sudo grep CRON /var/log/syslog
Jan 19 13:30:01 michal CRON[29749]: (lambda) CMD (cd ~/scrapy/ && getdata.sh)
Jan 19 13:30:01 michal CRON[29748]: (CRON) info (No MTA installed, discarding output)
Jan 19 13:33:01 michal CRON[29825]: (lambda) CMD (cd ~/scrapy/ && getdata.sh)
Jan 19 13:33:01 michal CRON[29824]: (CRON) info (No MTA installed, discarding output)
Jan 19 13:40:01 michal CRON[29992]: (lambda) CMD (cd ~/scrapy/ && getdata.sh >/dev/null 2>&1)
Jan 19 13:50:01 michal CRON[30207]: (lambda) CMD (cd ~/scrapy/ && getdata.sh >> /var/log/somelogfile.log)
Jan 19 13:50:01 michal CRON[30206]: (CRON) info (No MTA installed, discarding output)
Jan 19 14:14:01 michal CRON[30699]: (lambda) CMD (cd ~/scrapy/ && getdata.sh)
Jan 19 14:17:01 michal CRON[30767]: (root) CMD ( cd / && run-parts --report /etc/cron.hourly)
Jan 19 14:24:01 michal CRON[30964]: (lambda) CMD (cd ~/scrapy/getdata.sh)
Jan 19 14:27:01 michal CRON[31037]: (lambda) CMD (~/scrapy/getdata.sh)
Jan 19 14:33:01 michal CRON[31247]: (lambda) CMD (~/scrapy/getdata.sh)
Jan 19 15:17:01 michal CRON[32144]: (root) CMD ( cd / && run-parts --report /etc/cron.hourly)