Förra gången implementerade vi en grundläggande webbskrapa som laddade ner de senaste frågorna från StackOverflow och lagrade resultaten i MongoDB. I den här artikeln utökar vi vår skrapa så att den genomsöker sidnumreringslänkarna längst ner på varje sida och skrapar frågorna (frågans rubrik och webbadress) från varje sida.
Gratis bonus: Klicka här för att ladda ner ett Python + MongoDB-projektskelett med fullständig källkod som visar hur du kommer åt MongoDB från Python.
Uppdateringar:
- 09/06/2015 - Uppdaterad till den senaste versionen av Scrapy (v1.0.3) och PyMongo (v3.0.3) - hej!
Innan du påbörjar något skrapjobb ska du läsa igenom webbplatsens användarvillkor och respektera robots.txt-filen. Följ också etiska skrapningsmetoder genom att inte översvämma en webbplats med många förfrågningar under en kort tidsperiod. Behandla alla webbplatser du skrapar som om de vore din egen.
Detta är ett samarbete mellan folket på Real Python och György - en Python-entusiast och mjukvaruutvecklare, som för närvarande arbetar på ett big data-företag och samtidigt söker ett nytt jobb. Du kan ställa frågor till honom på twitter - @kissgyorgy.
Komma igång
Det finns två möjliga sätt att fortsätta där vi slutade.
Den första är att utöka vår befintliga Spider genom att extrahera varje nästa sidlänk från svaret i parse_item
metod med ett xpath-uttryck och bara yield
en Request
objekt med en återuppringning till samma parse_item
metod. På så sätt kommer scrapy automatiskt att göra en ny förfrågan till länken vi anger. Du kan hitta mer information om denna metod i Scrapy-dokumentationen.
Det andra, mycket enklare alternativet är att använda en annan typ av spindel - CrawlSpider
(länk). Det är en utökad version av den grundläggande Spider
, designad exakt för vårt användningsfall.
CrawlSpider
Vi kommer att använda samma Scrapy-projekt från den senaste handledningen, så ta tag i koden från repot om du behöver den.
Skapa Boilerplate
Inom "stack"-katalogen börjar du med att skapa spindelplattan från crawl
mall:
$ scrapy genspider stack_crawler stackoverflow.com -t crawl
Created spider 'stack_crawler' using template 'crawl' in module:
stack.spiders.stack_crawler
Scrapy-projektet ska nu se ut så här:
├── scrapy.cfg
└── stack
├── __init__.py
├── items.py
├── pipelines.py
├── settings.py
└── spiders
├── __init__.py
├── stack_crawler.py
└── stack_spider.py
Och stack_crawler.py filen ska se ut så här:
# -*- coding: utf-8 -*-
import scrapy
from scrapy.contrib.linkextractors import LinkExtractor
from scrapy.contrib.spiders import CrawlSpider, Rule
from stack.items import StackItem
class StackCrawlerSpider(CrawlSpider):
name = 'stack_crawler'
allowed_domains = ['stackoverflow.com']
start_urls = ['http://www.stackoverflow.com/']
rules = (
Rule(LinkExtractor(allow=r'Items/'), callback='parse_item', follow=True),
)
def parse_item(self, response):
i = StackItem()
#i['domain_id'] = response.xpath('//input[@id="sid"]/@value').extract()
#i['name'] = response.xpath('//div[@id="name"]').extract()
#i['description'] = response.xpath('//div[@id="description"]').extract()
return i
Vi behöver bara göra några uppdateringar av den här plattan...
Uppdatera start_urls
lista
Lägg först till den första sidan med frågor till start_urls
lista:
start_urls = [
'http://stackoverflow.com/questions?pagesize=50&sort=newest'
]
Uppdatera rules
lista
Därefter måste vi berätta för spindeln var den kan hitta länkarna till nästa sida genom att lägga till ett reguljärt uttryck i rules
attribut:
rules = [
Rule(LinkExtractor(allow=r'questions\?page=[0-9]&sort=newest'),
callback='parse_item', follow=True)
]
Scrapy kommer nu automatiskt att begära nya sidor baserat på dessa länkar och skicka svaret till parse_item
metod för att extrahera frågorna och titlarna.
Om du är mycket uppmärksam, begränsar detta regex genomsökningen till de första 9 sidorna eftersom vi inte vill skrapa alla för denna demo 176 234 sidor!
Uppdatera parse_item
metod
Nu behöver vi bara skriva hur man tolkar sidorna med xpath, vilket vi redan gjorde i den förra handledningen - så bara kopiera den över:
def parse_item(self, response):
questions = response.xpath('//div[@class="summary"]/h3')
for question in questions:
item = StackItem()
item['url'] = question.xpath(
'a[@class="question-hyperlink"]/@href').extract()[0]
item['title'] = question.xpath(
'a[@class="question-hyperlink"]/text()').extract()[0]
yield item
Det är det för spindeln, men gör inte starta det ännu.
Lägg till en nedladdningsfördröjning
Vi måste vara trevliga mot StackOverflow (och vilken webbplats som helst, för den delen) genom att ställa in en nedladdningsfördröjning i settings.py :
DOWNLOAD_DELAY = 5
Detta säger åt Scrapy att vänta minst 5 sekunder mellan varje ny begäran som den gör. Du begränsar i princip dig själv. Om du inte gör detta kommer StackOverflow att begränsa dig. och om du fortsätter att skrapa sajten utan att införa en hastighetsgräns kan din IP-adress förbjudas. Så var snäll - Behandla alla webbplatser du skrapar som om de vore din egen.
Nu är det bara en sak kvar att göra - lagra data.
MongoDB
Förra gången laddade vi bara ner 50 frågor, men eftersom vi plockar mycket mer data den här gången vill vi undvika att lägga till dubbla frågor i databasen. Vi kan göra det genom att använda en MongoDB-upsert, vilket innebär att vi uppdaterar frågetiteln om den redan finns i databasen och infogar på annat sätt.
Ändra MongoDBPipeline
vi definierade tidigare:
class MongoDBPipeline(object):
def __init__(self):
connection = pymongo.MongoClient(
settings['MONGODB_SERVER'],
settings['MONGODB_PORT']
)
db = connection[settings['MONGODB_DB']]
self.collection = db[settings['MONGODB_COLLECTION']]
def process_item(self, item, spider):
for data in item:
if not data:
raise DropItem("Missing data!")
self.collection.update({'url': item['url']}, dict(item), upsert=True)
log.msg("Question added to MongoDB database!",
level=log.DEBUG, spider=spider)
return item
För enkelhetens skull har vi inte optimerat frågan och inte hanterat index eftersom detta inte är en produktionsmiljö.
Testa
Starta spindeln!
$ scrapy crawl stack_crawler
Luta dig nu tillbaka och se din databas fyllas med data!
$ mongo
MongoDB shell version: 3.0.4
> use stackoverflow
switched to db stackoverflow
> db.questions.count()
447
>
Slutsats
Du kan ladda ner hela källkoden från Github-förvaret. Kommentera nedan med frågor. Skål!
Gratis bonus: Klicka här för att ladda ner ett Python + MongoDB-projektskelett med fullständig källkod som visar hur du kommer åt MongoDB från Python.
Letar du efter mer webbskrapning? Se till att kolla in Real Python-kurserna. Vill du anlita en professionell webbskrapa? Kolla in GoScrape.