Scrapy può essere utilizzato per eliminare contenuti dinamici da siti Web che utilizzano AJAX?


145

Recentemente ho imparato Python e sto immergendo la mano nella costruzione di un raschietto per il web. Non è niente di speciale; il suo unico scopo è quello di ottenere i dati da un sito Web di scommesse e farli inserire in Excel.

La maggior parte dei problemi è risolvibile e sto avendo un bel pasticcio in giro. Tuttavia sto colpendo un grosso ostacolo per un problema. Se un sito carica una tabella di cavalli ed elenca i prezzi attuali delle scommesse, questa informazione non è contenuta in nessun file sorgente. L'indizio è che questi dati sono talvolta attivi, con i numeri che vengono ovviamente aggiornati da alcuni server remoti. L'HTML sul mio PC ha semplicemente un buco in cui i loro server stanno trasmettendo tutti i dati interessanti di cui ho bisogno.

Ora la mia esperienza con i contenuti Web dinamici è bassa, quindi questa cosa è qualcosa che non riesco a risolvere.

Penso che Java o Javascript sia una chiave, questo si apre spesso.

Il raschietto è semplicemente un motore di confronto delle quote. Alcuni siti hanno API ma ne ho bisogno per quelli che non lo fanno. Sto usando la libreria scrapy con Python 2.7

Mi scuso se questa domanda è troppo aperta. In breve, la mia domanda è: come posso usare scrapy per raschiare questi dati dinamici in modo che io possa usarli? In modo da poter raccogliere questi dati sulle quote delle scommesse in tempo reale?


1
Come posso ottenere questi dati, i dati che sono dinamici e attivi?
Joseph,

1
Se la tua pagina ha javascript, prova questo
reclosedev

3
Prova alcune Firefoxestensioni come httpFoxo liveHttpHeaderse carica una pagina che utilizza una richiesta Ajax. Scrapy non identifica automaticamente le richieste Ajax, devi cercare manualmente l'URL Ajax appropriato e quindi fare richiesta con quello.
Aamir Adnan,

evviva, darò un brivido alle estensioni di Firefox
Joseph

Esistono numerose soluzioni open source. Ma se stai cercando un modo semplice e veloce per farlo soprattutto per carichi di lavoro di grandi dimensioni, dai un'occhiata a SnapSearch ( snapsearch.io ). È stato creato per siti JS, HTML5 e SPA che richiedono la crawlability dei motori di ricerca. Prova la demo (se il contenuto è vuoto, significa che il sito in realtà non ha restituito alcun contenuto corporeo, il che significa potenzialmente un reindirizzamento 301).
CMCDragonkai,

Risposte:


74

I browser basati su Webkit (come Google Chrome o Safari) hanno strumenti di sviluppo integrati. In Chrome puoi aprirlo Menu->Tools->Developer Tools. La Networkscheda consente di visualizzare tutte le informazioni su ogni richiesta e risposta:

inserisci qui la descrizione dell'immagine

Nella parte inferiore della foto puoi vedere che ho filtrato la richiesta fino a XHR- queste sono richieste fatte dal codice javascript.

Suggerimento: il registro viene cancellato ogni volta che si carica una pagina, nella parte inferiore dell'immagine, il pulsante punto nero conserva il registro.

Dopo aver analizzato le richieste e le risposte, puoi simulare queste richieste dal tuo crawler web ed estrarre dati preziosi. In molti casi sarà più facile ottenere i tuoi dati piuttosto che analizzare HTML, perché quei dati non contengono la logica di presentazione e sono formattati per accedervi tramite codice javascript.

Firefox ha un'estensione simile, si chiama firebug . Alcuni sosterranno che firebug è ancora più potente ma mi piace la semplicità del webkit.


141
Come diavolo può essere una risposta accettata se non contiene nemmeno la parola "trasandato" ??
Toolkit,

Funziona ed è facile analizzare usando il modulo json in Python. È una soluzione! Rispetto a questo, prova a usare il selenio o altre cose che la gente suggerisce, è più mal di testa. Se il metodo alternativo fosse molto più complicato, te lo darei, ma non è il caso qui @Toolkit
Arion_Miles,

1
Questo non è davvero rilevante. La domanda era come usare scarpy per raschiare siti Web dinamici.
E. Erfan,

"Come diavolo può essere una risposta accettata" - Perché l'uso pratico batte la correttezza politica. Gli umani comprendono il CONTESTO.
Espresso

98

Ecco un semplice esempio di scrapycon una richiesta AJAX. Vediamo il sito rubin-kazan.ru .

Tutti i messaggi sono caricati con una richiesta AJAX. Il mio obiettivo è quello di recuperare questi messaggi con tutti i loro attributi (autore, data, ...):

inserisci qui la descrizione dell'immagine

Quando analizzo il codice sorgente della pagina non riesco a vedere tutti questi messaggi perché la pagina web utilizza la tecnologia AJAX. Ma posso con Firebug di Mozilla Firefox (o uno strumento equivalente in altri browser) per analizzare la richiesta HTTP che genera i messaggi sulla pagina Web:

inserisci qui la descrizione dell'immagine

Non ricarica l'intera pagina ma solo le parti della pagina che contengono messaggi. A tal fine, faccio clic su un numero arbitrario di pagina in basso:

inserisci qui la descrizione dell'immagine

E osservo la richiesta HTTP responsabile del corpo del messaggio:

inserisci qui la descrizione dell'immagine

Al termine, analizzo le intestazioni della richiesta (devo citare che questo URL estrarrò dalla pagina di origine dalla sezione var, vedere il codice seguente):

inserisci qui la descrizione dell'immagine

E il contenuto dei dati del modulo della richiesta (il metodo HTTP è "Posta"):

inserisci qui la descrizione dell'immagine

E il contenuto della risposta, che è un file JSON:

inserisci qui la descrizione dell'immagine

Che presenta tutte le informazioni che sto cercando.

Da adesso, devo implementare tutte queste conoscenze in maniera approssimativa. Definiamo il ragno per questo scopo:

class spider(BaseSpider):
    name = 'RubiGuesst'
    start_urls = ['http://www.rubin-kazan.ru/guestbook.html']

    def parse(self, response):
        url_list_gb_messages = re.search(r'url_list_gb_messages="(.*)"', response.body).group(1)
        yield FormRequest('http://www.rubin-kazan.ru' + url_list_gb_messages, callback=self.RubiGuessItem,
                          formdata={'page': str(page + 1), 'uid': ''})

    def RubiGuessItem(self, response):
        json_file = response.body

In parsefunzione ho la risposta per la prima richiesta. In RubiGuessItemho il file JSON con tutte le informazioni.


6
Ciao. Potresti spiegare cos'è "url_list_gb_messages"? Non riesco a capirlo Grazie.
Polarizza il

4
Questo sicuramente è migliore.
1a1a11a

1
@polarise Quel codice utilizza il remodulo (espressioni regolari), cerca la stringa 'url_list_gb_messages="(.*)"'e isola il contenuto tra parentesi nella variabile con lo stesso nome. Questa è una bella introduzione: guru99.com/python-regular-expressions-complete-tutorial.html
MGP

42

Molte volte durante la scansione si verificano problemi in cui il contenuto visualizzato nella pagina viene generato con Javascript e quindi scrapy non è in grado di eseguire la scansione per esso (ad es. Richieste Ajax, follia jQuery).

Tuttavia, se si utilizza Scrapy insieme al Selenium del framework di test Web, è possibile eseguire la scansione di qualsiasi cosa visualizzata in un normale browser Web.

Alcune cose da notare:

  • È necessario che la versione Python di Selenium RC sia installata affinché funzioni e che Selenium sia stato configurato correttamente. Anche questo è solo un modello di crawler. Potresti diventare molto più pazzo e più avanzato con le cose, ma volevo solo mostrare l'idea di base. Allo stato attuale del codice, farai due richieste per ogni URL. Una richiesta è fatta da Scrapy e l'altra è fatta da Selenium. Sono sicuro che ci sono modi per aggirare questo in modo che possiate semplicemente fare in modo che Selenium faccia la sola e unica richiesta, ma non mi sono preoccupato di implementarlo e facendo due richieste è anche possibile eseguire la scansione della pagina con Scrapy.

  • Questo è abbastanza potente perché ora hai l'intero DOM reso disponibile per la scansione e puoi ancora utilizzare tutte le belle funzionalità di scansione in Scrapy. Ciò renderà la scansione più lenta, ovviamente, ma a seconda di quanto è necessario il DOM renderizzato potrebbe valere la pena aspettare.

    from scrapy.contrib.spiders import CrawlSpider, Rule
    from scrapy.contrib.linkextractors.sgml import SgmlLinkExtractor
    from scrapy.selector import HtmlXPathSelector
    from scrapy.http import Request
    
    from selenium import selenium
    
    class SeleniumSpider(CrawlSpider):
        name = "SeleniumSpider"
        start_urls = ["http://www.domain.com"]
    
        rules = (
            Rule(SgmlLinkExtractor(allow=('\.html', )), callback='parse_page',follow=True),
        )
    
        def __init__(self):
            CrawlSpider.__init__(self)
            self.verificationErrors = []
            self.selenium = selenium("localhost", 4444, "*chrome", "http://www.domain.com")
            self.selenium.start()
    
        def __del__(self):
            self.selenium.stop()
            print self.verificationErrors
            CrawlSpider.__del__(self)
    
        def parse_page(self, response):
            item = Item()
    
            hxs = HtmlXPathSelector(response)
            #Do some XPath selection with Scrapy
            hxs.select('//div').extract()
    
            sel = self.selenium
            sel.open(response.url)
    
            #Wait for javscript to load in Selenium
            time.sleep(2.5)
    
            #Do some crawling of javascript created content with Selenium
            sel.get_text("//div")
            yield item
    
    # Snippet imported from snippets.scrapy.org (which no longer works)
    # author: wynbennett
    # date  : Jun 21, 2011

Riferimento: http://snipplr.com/view/66998/


Soluzione ordinata! Hai qualche consiglio su come collegare questo script a Firefox? (Il sistema operativo è Linux Mint). Ricevo "[Errno 111] Connessione rifiutata".
Andrew,

1
Questo codice non funziona più per selenium=3.3.1e python=2.7.10, errore durante l'importazione di selenio dal selenio
benjaminz

1
In quella versione di selenio sua dichiarazione di importazione potrebbe essere: from selenium import webdrivero chromedrivero qualsiasi altra cosa vi capita di utilizzare. Documenti EDIT: aggiungi riferimenti alla documentazione e cambia la mia orribile grammatica!
nulltron

Selenium Remote Control è stato sostituito da Selenium WebDriver, secondo il loro sito web
rainbowsorbet

33

Un'altra soluzione sarebbe quella di implementare un gestore download o un middleware gestore download. (consultare i documenti scrapy per ulteriori informazioni sul middleware downloader) Di seguito è riportata una classe di esempio che utilizza il selenio con il webdriver phantomjs senza testa:

1) Definire la classe all'interno dello middlewares.pyscript.

from selenium import webdriver
from scrapy.http import HtmlResponse

class JsDownload(object):

    @check_spider_middleware
    def process_request(self, request, spider):
        driver = webdriver.PhantomJS(executable_path='D:\phantomjs.exe')
        driver.get(request.url)
        return HtmlResponse(request.url, encoding='utf-8', body=driver.page_source.encode('utf-8'))

2) Aggiungi JsDownload()classe alla variabile DOWNLOADER_MIDDLEWAREall'interno di settings.py:

DOWNLOADER_MIDDLEWARES = {'MyProj.middleware.MiddleWareModule.MiddleWareClass': 500}

3) Integrare l' HTMLResponseinterno your_spider.py. La decodifica del corpo della risposta ti darà l'output desiderato.

class Spider(CrawlSpider):
    # define unique name of spider
    name = "spider"

    start_urls = ["https://www.url.de"] 

    def parse(self, response):
        # initialize items
        item = CrawlerItem()

        # store data as items
        item["js_enabled"] = response.body.decode("utf-8") 

Componente aggiuntivo facoltativo:
volevo la possibilità di dire ai diversi ragni quale middleware usare, quindi ho implementato questo wrapper:

def check_spider_middleware(method):
@functools.wraps(method)
def wrapper(self, request, spider):
    msg = '%%s %s middleware step' % (self.__class__.__name__,)
    if self.__class__ in spider.middleware:
        spider.log(msg % 'executing', level=log.DEBUG)
        return method(self, request, spider)
    else:
        spider.log(msg % 'skipping', level=log.DEBUG)
        return None

return wrapper

affinché il wrapper funzioni tutti i ragni devono avere almeno:

middleware = set([])

per includere un middleware:

middleware = set([MyProj.middleware.ModuleName.ClassName])

Vantaggio:
il vantaggio principale di implementarlo in questo modo piuttosto che nel ragno è che si finisce per fare solo una richiesta. Nella soluzione di AT, ad esempio: il gestore di download elabora la richiesta e quindi passa la risposta allo spider. Il ragno quindi effettua una nuova richiesta nella sua funzione parse_page - Sono due richieste per lo stesso contenuto.


Sono stato un po 'in ritardo a rispondere a questo però>. <
rocktheartsm4l

@ rocktheartsm4l cosa c'è che non va nel solo utilizzo, in process_requests, if spider.name in ['spider1', 'spider2']invece del decoratore
pad

@pad Non c'è niente di sbagliato in questo. Ho appena trovato più chiaro per le mie classi di spider avere un set chiamato middleware. In questo modo ho potuto guardare qualsiasi classe di ragno e vedere esattamente quali middleware sarebbero stati eseguiti per questo. Il mio progetto ha implementato molti middleware, quindi questo ha senso.
rocktheartsm4l,

Questa è una soluzione terribile. Non solo non è legato allo scrapy, ma il codice stesso è estremamente inefficiente, così come l'intero approccio in generale sconfigge l'intero scopo del framework asincrono di scraping web che è scrapy
Granitosaurus,

2
È molto più efficiente di qualsiasi altra soluzione che ho visto su SO poiché l'utilizzo di un middleware per downloader lo rende così una sola richiesta viene fatta per la pagina .. se è così terribile perché non trovi una soluzione migliore e condividi invece di fare affermazioni palesemente unilaterali. "Non legato allo scrapy" stai fumando qualcosa? Oltre all'implementazione di alcune soluzioni complesse, robuste e personalizzate, questo è l'approccio che ho visto usare nella maggior parte delle persone. L'unica differenza è che la maggior parte implementa la parte di selenio nel ragno che causa richieste multiple ...
rocktheartsm4l

10

Stavo usando un middleware per downloader personalizzato, ma non ne ero molto soddisfatto, poiché non riuscivo a far funzionare la cache con esso.

Un approccio migliore era l'implementazione di un gestore di download personalizzato.

C'è un esempio funzionante qui . Sembra così:

# encoding: utf-8
from __future__ import unicode_literals

from scrapy import signals
from scrapy.signalmanager import SignalManager
from scrapy.responsetypes import responsetypes
from scrapy.xlib.pydispatch import dispatcher
from selenium import webdriver
from six.moves import queue
from twisted.internet import defer, threads
from twisted.python.failure import Failure


class PhantomJSDownloadHandler(object):

    def __init__(self, settings):
        self.options = settings.get('PHANTOMJS_OPTIONS', {})

        max_run = settings.get('PHANTOMJS_MAXRUN', 10)
        self.sem = defer.DeferredSemaphore(max_run)
        self.queue = queue.LifoQueue(max_run)

        SignalManager(dispatcher.Any).connect(self._close, signal=signals.spider_closed)

    def download_request(self, request, spider):
        """use semaphore to guard a phantomjs pool"""
        return self.sem.run(self._wait_request, request, spider)

    def _wait_request(self, request, spider):
        try:
            driver = self.queue.get_nowait()
        except queue.Empty:
            driver = webdriver.PhantomJS(**self.options)

        driver.get(request.url)
        # ghostdriver won't response when switch window until page is loaded
        dfd = threads.deferToThread(lambda: driver.switch_to.window(driver.current_window_handle))
        dfd.addCallback(self._response, driver, spider)
        return dfd

    def _response(self, _, driver, spider):
        body = driver.execute_script("return document.documentElement.innerHTML")
        if body.startswith("<head></head>"):  # cannot access response header in Selenium
            body = driver.execute_script("return document.documentElement.textContent")
        url = driver.current_url
        respcls = responsetypes.from_args(url=url, body=body[:100].encode('utf8'))
        resp = respcls(url=url, body=body, encoding="utf-8")

        response_failed = getattr(spider, "response_failed", None)
        if response_failed and callable(response_failed) and response_failed(resp, driver):
            driver.close()
            return defer.fail(Failure())
        else:
            self.queue.put(driver)
            return defer.succeed(resp)

    def _close(self):
        while not self.queue.empty():
            driver = self.queue.get_nowait()
            driver.close()

Supponiamo che il tuo raschietto sia chiamato "raschietto". Se inserisci il codice citato in un file chiamato handlers.py nella radice della cartella "scraper", allora potresti aggiungere a settings.py:

DOWNLOAD_HANDLERS = {
    'http': 'scraper.handlers.PhantomJSDownloadHandler',
    'https': 'scraper.handlers.PhantomJSDownloadHandler',
}

E voilà, il DOM analizzato da JS, con cache scrap, tentativi, ecc.


Mi piace questa soluzione!
rocktheartsm4l,

Bella soluzione. Il driver Selenium è ancora l'unica opzione?
Motheus,

Ottima soluzione Molte grazie.
CrazyGeek,

4

come posso usare scrapy per raschiare questi dati dinamici in modo che io possa usarli?

Mi chiedo perché nessuno abbia pubblicato la soluzione usando solo Scrapy.

Dai un'occhiata al post sul blog del team di Scrapy . L'esempio scarta il sito Web http://spidyquotes.herokuapp.com/scroll che utilizza lo scorrimento infinito.

L'idea è quella di utilizzare gli strumenti di sviluppo del browser e notare le richieste AJAX, quindi in base a tali informazioni creare le richieste per Scrapy .

import json
import scrapy


class SpidyQuotesSpider(scrapy.Spider):
    name = 'spidyquotes'
    quotes_base_url = 'http://spidyquotes.herokuapp.com/api/quotes?page=%s'
    start_urls = [quotes_base_url % 1]
    download_delay = 1.5

    def parse(self, response):
        data = json.loads(response.body)
        for item in data.get('quotes', []):
            yield {
                'text': item.get('text'),
                'author': item.get('author', {}).get('name'),
                'tags': item.get('tags'),
            }
        if data['has_next']:
            next_page = data['page'] + 1
            yield scrapy.Request(self.quotes_base_url % next_page)

Affrontiamo di nuovo lo stesso problema: Scrappy non è fatto per questo scopo ed è qui che ci troviamo di fronte allo stesso problema. Passa a phantomJS o come altri hanno suggerito, crea il tuo middleware per il download
rak007

@ rak007 Driver PhantomJS vs Chrome. Quale consiglieresti?
Chankey Pathak,

2

Sì, Scrapy può eliminare siti Web dinamici, siti Web resi tramite javaScript.

Esistono due approcci per pubblicare questo tipo di siti Web.

Primo,

è possibile utilizzare splashper eseguire il rendering del codice Javascript e quindi analizzare il codice HTML renderizzato. puoi trovare il documento e il progetto qui Scrapy splash, git

Secondo,

Come tutti affermano, monitorando network calls, sì, puoi trovare la chiamata api che recupera i dati e deride quella chiamata nel tuo ragno scrapy potrebbe aiutarti a ottenere i dati desiderati.


1

Gestisco la richiesta Ajax utilizzando Selenium e il driver web di Firefox. Non è così veloce se hai bisogno del crawler come demone, ma molto meglio di qualsiasi soluzione manuale. Ho scritto un breve tutorial qui per riferimento

Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.