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


145

J'ai récemment appris Python et je me suis lancé dans la construction d'un web-grattoir. Il n'y a rien d'extraordinaire du tout; son seul objectif est de récupérer les données d'un site de paris et de les mettre dans Excel.

La plupart des problèmes peuvent être résolus et j'ai un bon petit désordre. Cependant, je rencontre un énorme obstacle sur un problème. Si un site charge une table de chevaux et répertorie les prix actuels des paris, cette information ne se trouve dans aucun fichier source. L'indice est que ces données sont parfois en direct, les chiffres étant évidemment mis à jour à partir d'un serveur distant. Le HTML sur mon PC a simplement un trou où leurs serveurs transmettent toutes les données intéressantes dont j'ai besoin.

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

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

Le grattoir 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 bibliothèque scrapy avec Python 2.7

Je m'excuse si cette question est trop ouverte. En bref, ma question est la suivante: comment le scrapy peut-il être utilisé 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?


1
Comment puis-je obtenir ces données, les données dynamiques et vivantes?
Joseph

1
Si votre page contient du javascript, essayez ceci
reclosedev

3
Essayez certaines Firefoxextensions comme httpFoxou liveHttpHeaderset chargez une page qui utilise la requête ajax. Scrapy n'identifie pas automatiquement les requêtes ajax, vous devez rechercher manuellement l'URL ajax appropriée, puis faire la demande avec cela.
Aamir Adnan

bravo, je vais donner un wizz aux extensions Firefox
Joseph

Il existe un certain nombre de solutions open source. Mais si vous recherchez un moyen simple et rapide de le faire, en particulier pour les charges de travail importantes, consultez SnapSearch ( snapsearch.io ). Il a été conçu pour les sites JS, HTML5 et SPA nécessitant l'exploration des moteurs de recherche. Essayez la démo (s'il y a du contenu vide, cela signifie que le site n'a renvoyé aucun contenu de corps, ce qui signifie potentiellement une redirection 301).
CMCDragonkai

Réponses:


74

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

entrez la description de l'image ici

En bas de l'image, vous pouvez voir que j'ai filtré la demande jusqu'à XHR- ce sont des demandes faites par code javascript.

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

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

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


141
Comment diable cela peut-il être une réponse acceptée si elle ne contient même pas le mot «scrapy»?
Boîte à outils du

Cela fonctionne et il est facile d'analyser en utilisant le module json en python. C'est une solution! Par rapport à cela, essayez d'utiliser du sélénium ou d'autres choses que les gens suggèrent, c'est plus un mal de tête. Si la méthode alternative était beaucoup plus compliquée, je vous la donnerais, mais ce n'est pas le cas ici @Toolkit
Arion_Miles

1
Ce n'est pas vraiment pertinent. La question était de savoir comment utiliser Scarpy pour gratter des sites Web dynamiques.
E. Erfan

"Comment diable cela peut-il être une réponse acceptée" - Parce que l'utilisation pratique l'emporte sur le politiquement correct. Les humains comprennent le CONTEXTE.
Espresso le

98

Voici un exemple simple de scrapyavec une requête AJAX. Voyons le site rubin-kazan.ru .

Tous les messages sont chargés avec une requête AJAX. Mon objectif est de récupérer ces messages avec tous leurs attributs (auteur, date, ...):

entrez la description de l'image ici

Lorsque j'analyse le code source de la page, je ne vois pas tous ces messages car la page Web utilise la technologie AJAX. Mais je peux avec Firebug de Mozilla Firefox (ou un outil équivalent dans d'autres navigateurs) pour analyser la requête HTTP qui génère les messages sur la page Web:

entrez la description de l'image ici

Il ne recharge pas la page entière mais seulement les parties de la page qui contiennent des messages. Pour cela, je clique sur un nombre arbitraire de page en bas:

entrez la description de l'image ici

Et j'observe la requête HTTP qui est responsable du corps du message:

entrez la description de l'image ici

Après avoir terminé, j'analyse les en-têtes de la requête (je dois citer que cette URL que je vais extraire de la page source de la section var, voir le code ci-dessous):

entrez la description de l'image ici

Et le contenu des données du formulaire de la requête (la méthode HTTP est "Post"):

entrez la description de l'image ici

Et le contenu de la réponse, qui est un fichier JSON:

entrez la description de l'image ici

Qui présente toutes les informations que je recherche.

Désormais, je dois mettre en œuvre toutes ces connaissances dans la 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

En parsefonction j'ai la réponse pour la première demande. Dans RubiGuessItemj'ai le fichier JSON avec toutes les informations.


6
Salut. Pouvez-vous expliquer ce qu'est "url_list_gb_messages"? Je ne peux pas comprendre. Merci.
polariser le

4
Celui-ci est définitivement meilleur.
1a1a11a

1
@polarise Ce code utilise le remodule (expressions régulières), il recherche la chaîne 'url_list_gb_messages="(.*)"'et isole le contenu des parenthèses dans la variable de même nom. Voici une belle intro: guru99.com/python-regular-expressions-complete-tutorial.html
MGP

42

Plusieurs fois, lors de l'exploration, nous rencontrons des problèmes où le contenu rendu sur la page est généré avec Javascript et donc scrapy est incapable de l'explorer (par exemple, requêtes ajax, folie jQuery).

Cependant, si vous utilisez Scrapy avec le framework de test Web Selenium, nous sommes en mesure d'explorer tout ce qui est affiché dans un navigateur Web normal.

Quelques points à noter:

  • Vous devez avoir la version Python de Selenium RC installée pour que cela fonctionne, et vous devez avoir configuré Selenium correctement. Il ne s'agit également que d'un robot d'exploration de modèles. Vous pourriez devenir beaucoup plus fou et plus avancé avec les choses, mais je voulais juste montrer l'idée de base. Dans l'état actuel du code, vous ferez deux requêtes 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 que vous puissiez simplement demander à Selenium de faire la seule et unique demande, mais je n'ai pas pris la peine de l'implémenter et en faisant deux demandes, vous pouvez également explorer la page avec Scrapy.

  • C'est assez puissant car vous avez maintenant tout le DOM rendu disponible pour que vous puissiez l'explorer et vous pouvez toujours utiliser toutes les fonctionnalités d'exploration intéressantes de Scrapy. Cela ralentira bien sûr l'exploration, mais en fonction de combien vous avez besoin du DOM rendu, cela peut valoir la peine d'attendre.

    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/


Solution soignée! Avez-vous des conseils pour connecter ce script à Firefox? (Le système d'exploitation est Linux Mint). J'obtiens "[Errno 111] Connexion refusée".
Andrew

1
Ce code ne fonctionne plus pour selenium=3.3.1et python=2.7.10, erreur lors de l'importation de sélénium à partir de sélénium
benjaminz

1
Dans cette version de sélénium, votre déclaration d'importation serait: from selenium import webdriverou chromedriverou tout ce que vous utilisez. Docs EDIT: Ajoutez une référence à la documentation et changez mon horrible grammaire!
nulltron

La télécommande Selenium a été remplacée par Selenium WebDriver, selon leur site Web
rainbowsorbet

33

Une autre solution consisterait à implémenter un gestionnaire de téléchargement ou un middleware de gestionnaire de téléchargement. (voir la documentation de scrapy pour plus d'informations sur le middleware de téléchargement) Ce qui suit est un exemple de classe utilisant le sélénium avec le pilote Web phantomjs sans tête:

1) Définissez la classe dans le 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) Ajouter une JsDownload()classe à une variable DOWNLOADER_MIDDLEWAREdans settings.py:

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

3) Intégrez l' HTMLResponseintérieur your_spider.py. Le décodage du corps de la réponse vous donnera le résultat souhaité.

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

Addon facultatif:
je voulais la possibilité de dire à différents spiders quel middleware utiliser, j'ai donc 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

pour que le wrapper fonctionne, toutes les araignées doivent avoir au minimum:

middleware = set([])

pour inclure un middleware:

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

Avantage:
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 seule demande. Dans la solution d'AT, 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 toute nouvelle demande dans sa fonction parse_page - C'est deux demandes pour le même contenu.


J'étais un peu en retard pour répondre à cela cependant>. <
rocktheartsm4l

@ rocktheartsm4l quel est le problème avec le simple fait d'utiliser, dans process_requests, if spider.name in ['spider1', 'spider2']au lieu du décorateur
pad

@pad Il n'y a rien de mal à cela. Je viens de trouver plus clair pour mes classes d'araignées d'avoir un ensemble nommé middleware. De cette façon, je pourrais regarder n'importe quelle classe d'araignée et voir exactement quels middlewares seraient exécutés pour elle. Mon projet avait beaucoup de middleware implémenté, donc cela avait du sens.
rocktheartsm4l

C'est une solution terrible. Non seulement ce n'est pas lié à la scrapy, mais le code lui-même est extrêmement inefficace et toute l'approche en général va à l'encontre de tout le but du framework de scraping web asynchrone qui est scrapy
Granitosaurus

2
C'est beaucoup plus efficace que toute autre solution que j'ai vue sur SO, car l'utilisation d'un middleware de téléchargement le rend ainsi une seule demande est faite pour la page .. si c'est si terrible, pourquoi ne pas trouver une meilleure solution et partager au lieu de faire des revendications manifestement unilatérales. "Pas lié à la tremblante", tu fumes quelque chose? Outre la mise en œuvre d'une solution complexe, robuste et personnalisée, c'est l'approche que la plupart des gens utilisent. La seule différence est que la plupart implémentent la partie sélénium dans l'araignée, ce qui entraîne plusieurs demandes ...
Rocktheartsm4l

10

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

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

Il y a un exemple fonctionnel 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 grattoir s'appelle "grattoir". Si vous mettez le code mentionné dans un fichier appelé handlers.py à la racine du dossier "scraper", vous pouvez ajouter à votre settings.py:

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

Et voilà, le DOM analysé par JS, avec cache scrapy, réessais, etc.


J'aime cette solution!
rocktheartsm4l

Belle solution. Le pilote Selenium est-il toujours la seule option?
Motheus

Excellente solution. Merci beaucoup.
CrazyGeek

4

comment scrapy peut-il être utilisé pour gratter ces données dynamiques afin que je puisse les utiliser?

Je me demande pourquoi personne n'a publié la solution en utilisant uniquement Scrapy.

Consultez le billet de blog de l'équipe Scrapy SCRAPING INFINITE SCROLLING PAGES . L'exemple supprime http://spidyquotes.herokuapp.com/scroll site Web qui utilise le défilement infini.

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

Nous sommes à nouveau confrontés au même problème: Scrappy n'est pas fait dans ce but et c'est là que nous sommes confrontés au même problème. Passez à phantomJS ou comme d'autres l'ont suggéré, créez votre propre middleware de téléchargement
rak007

@ rak007 Pilote PhantomJS vs Chrome. Lequel suggéreriez-vous?
Chankey Pathak

2

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

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

Première,

vous pouvez utiliser splashpour rendre le code Javascript, puis analyser le HTML rendu. vous pouvez trouver le document 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 d'API qui récupère les données et simuler cet appel dans votre araignée tremblante pourrait vous aider à obtenir les données souhaitées.


1

Je gère la requête ajax en utilisant Selenium et le pilote Web Firefox. Ce n'est pas si rapide si vous avez besoin du robot 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

En utilisant notre site, vous reconnaissez avoir lu et compris notre politique liée aux cookies et notre politique de confidentialité.
Licensed under cc by-sa 3.0 with attribution required.