web-dev-qa-db-fra.com

Scrapy peut-il être utilisé pour extraire du contenu dynamique de sites Web utilisant AJAX?

J'ai récemment appris le python et je plonge la main dans la construction d'un grattoir. Ce n'est rien d'extraordinaire; son seul but est d'extraire les données d'un site de paris et de les placer dans Excel.

La plupart des problèmes sont résolus et j'ai un bon petit bazar. Cependant, je me heurte à un obstacle majeur. Si un site charge un tableau de chevaux et répertorie les prix de paris actuels, cette information ne figure dans aucun fichier source. L'indice est que ces données sont parfois actives, les numéros étant mis à jour à partir d'un serveur distant. Le code HTML sur mon PC est tout simplement troublé par les serveurs de toutes les données intéressantes dont j'ai besoin.

Maintenant, mon expérience avec le contenu Web dynamique est faible, c'est donc quelque chose que j'ai du mal à comprendre. 

Je pense que Java ou Javascript est une clé, cela apparaît souvent. 

Le racleur est simplement un moteur de comparaison de cotes. Certains sites ont des API, mais j'en ai besoin pour ceux qui n'en ont pas. J'utilise la librairie scrapy avec Python 2.7

Je m'excuse si cette question est trop ouverte. En bref, ma question est la suivante: comment utiliser scrapy pour gratter ces données dynamiques afin que je puisse les utiliser? Pour que je puisse récupérer ces données de cotes de paris en temps réel?

124
Joseph

Les navigateurs Webkit (tels que Google Chrome ou Safari) ont des outils de développement intégrés. Dans Chrome, vous pouvez l'ouvrir Menu->Tools->Developer Tools. L'onglet Network vous permet de voir toutes les informations sur chaque demande et réponse:

enter image description here

Dans le bas de l'image, vous pouvez voir que j'ai filtré la demande jusqu'à XHR - il s'agit de demandes effectuées avec du code javascript.

Astuce: le journal est effacé chaque fois que vous chargez une page, en bas de l'image, le bouton en point noir préservera le journal.

Après avoir analysé les demandes et les réponses, vous pouvez simuler ces demandes à partir de votre robot d'exploration Web et extraire des données précieuses. Dans de nombreux cas, il sera plus facile d’obtenir vos données que d’analyser le code HTML, car ces données ne contiennent pas de logique de présentation et sont formatées pour être consultées par du code javascript.

Firefox a une extension similaire, il s'appelle firebug . Certains diront que firebug est encore plus puissant, mais j'aime bien la simplicité de webkit.

74
Ski

Voici un exemple simple d'utilisation de scrapy avec une requête ajax . Voyons le site http://www.rubin-kazan.ru/guestbook.html Tous les messages sont chargés avec une requête ajax. Mon but est de récupérer ces messages avec tous leurs attributs (auteur, date, ...).

enter image description here

Lorsque j'analyse le code source de la page, je ne peux pas voir tous ces messages, car la page Web utilise la technologie ajax. Mais je peux avec Firebug de Mozila Firefox (ou un instrument d’analogie dans un autre navigateur) analyser la requête Http qui génère les messages sur la page Web .enter image description here

À cette fin, je ne recharge pas toutes les pages, mais uniquement la partie de la page contenant des messages. Pour cela, je clique sur un nombre arbitraire de pages en bas enter image description hereet j'observe la requête HTTP responsable du corps du message enter image description here

Après avoir terminé, j’analyse les en-têtes de requête (je dois dire que cette URL sera extraite de la page source de var section, voir le code ci-dessous) .enter image description here

et le contenu des données de formulaire de la demande (la méthode Http est "Post")

enter image description here

et le contenu de la réponse, qui est un fichier Json,

enter image description here

qui présente toutes les informations que je cherche.

À partir de maintenant, je dois mettre en œuvre toutes ces connaissances dans Scrapy. Définissons l'araignée à cet effet.

  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

Dans la fonction d'analyse, j'ai la réponse pour la première demande . Dans RubiGuessItem, j'ai le fichier json avec toutes les informations. 

82
Badarau Petru

Souvent, lors de l'exploration, nous rencontrons des problèmes lorsque le contenu rendu sur la page est généré avec Javascript et que, par conséquent, scrapy est incapable de l'explorer (par exemple, les demandes ajax, la folie jQuery).

Toutefois, si vous utilisez Scrapy avec le framework de test Web Selenium, nous pouvons analyser tout ce qui est affiché dans un navigateur Web normal.

Quelques points à noter:

  • Vous devez avoir installé la version Python de Selenium RC pour que cela fonctionne, et vous devez avoir configuré Selenium correctement. En outre, il ne s'agit que d'un robot d'exploration de modèles. Vous pourriez devenir beaucoup plus fou et plus avancé avec des choses mais je voulais juste montrer l'idée de base. En l'état actuel du code, vous ferez deux demandes pour une URL donnée. Une demande est faite par Scrapy et l'autre par Selenium. Je suis sûr qu'il existe des moyens de contourner ce problème afin de permettre à Selenium de répondre à la demande unique, mais je ne me suis pas soucié de la mettre en œuvre. En faisant deux demandes, vous pouvez également explorer la page avec Scrapy.

  • C'est assez puissant car vous avez maintenant l'intégralité du DOM rendu ainsi disponible pour l'exploration et vous pouvez toujours utiliser toutes les fonctionnalités de Nice dans Scrapy. Bien sûr, cela va ralentir l’exploration, mais cela peut valoir la peine d’attendre en fonction de votre besoin du rendu du DOM.

    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
    

Référence: http://snipplr.com/view/66998/

35
A T

Une autre solution consisterait à implémenter un gestionnaire de téléchargement ou un middleware de gestionnaire de téléchargement. Voici un exemple de middleware utilisant Selenium avec le WebDriver phantomjs sans tête:

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'))

Je voulais pouvoir dire aux différents araignées quel middleware utiliser, alors j'ai implémenté ce 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

settings.py:

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

pour que l'emballage fonctionne, toutes les araignées doivent avoir au minimum:

middleware = set([])

inclure un middleware:

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

Le principal avantage de l'implémenter de cette manière plutôt que dans l'araignée est que vous ne faites qu'une requête. Dans la solution de A T, par exemple: le gestionnaire de téléchargement traite la demande, puis transmet la réponse à l'araignée. L'araignée fait ensuite une nouvelle requête dans sa fonction parse_page - Deux requêtes pour le même contenu.

24
rocktheartsm4l

J'utilisais un middleware de téléchargement personnalisé, mais je n'en étais pas très satisfait, car je ne parvenais pas à faire fonctionner le cache.

Une meilleure approche consistait à implémenter un gestionnaire de téléchargement personnalisé.

Il existe un exemple de travail ici . Cela ressemble à ceci:

# 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()

Supposons que votre racloir s'appelle "racloir". Si vous placez le code mentionné dans un fichier nommé handlers.py à la racine du dossier "scraper", vous pouvez ajouter des éléments à votre fichier settings.py:

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

Et voilà, le DOM analysé par JS, avec cache effacé, tentatives, etc.

6
Ivan Chaer

comment peut-on utiliser scrapy pour gratter ces données dynamiques afin que je puisse utiliser il?

Je me demande pourquoi personne n'a mis en ligne la solution avec Scrapy uniquement. 

Découvrez le billet de blog de l'équipe Scrapy DÉFILAGE DE PAGES DÉFILEMENT INFINITES . L'exemple scraps http://spidyquotes.herokuapp.com/scroll site Web qui utilise le défilement infini. 

L'idée est de utiliser les outils de développement de votre navigateur et de noter les demandes AJAX, puis, sur la base de ces informations, créez les demandes pour 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)
1
Chankey Pathak

Je gère la demande ajax en utilisant Selenium et le pilote Web Firefox. Ce n'est pas si rapide si vous avez besoin du robot d'exploration en tant que démon, mais bien mieux que n'importe quelle solution manuelle. J'ai écrit un court tutoriel ici pour référence

1
narko

oui, Scrapy peut supprimer des sites Web dynamiques, des sites Web rendus au format javaScript.

Il existe deux approches pour gratter ce type de sites Web.

Premier,

vous pouvez utiliser splash pour restituer le code Javascript, puis analyser le code HTML affiché .. ... vous pouvez trouver la doc et le projet ici Scrapy splash, git

Seconde, 

Comme tout le monde le dit, en surveillant le network calls, oui, vous pouvez trouver l'appel de l'API qui va chercher les données et le simulacre de cet appel dans votre Spider Spider pourrait vous aider à obtenir les données souhaitées.

0
ThunderMind