¿Se puede usar scrapy para raspar contenido dinámico de sitios web que utilizan AJAX?


Recientemente he estado aprendiendo Python y estoy sumergiendo mi mano en la construcción de un web-scraper. No es nada lujoso en absoluto; su único propósito es obtener los datos de un sitio web de apuestas y poner estos datos en Excel.

La mayoría de los problemas son solucionables y estoy teniendo un buen pequeño lío alrededor. Sin embargo, estoy golpeando un obstáculo masivo sobre un tema. Si un sitio carga una tabla de caballos y enumera los precios de apuestas actuales, esta información no está en ningún archivo fuente. La pista es que estos datos es en vivo a veces, con los números que se actualizan obviamente desde algún servidor remoto. El HTML en mi PC simplemente tiene un agujero donde sus servidores están empujando a través de todos los datos interesantes que necesito.

Ahora mi experiencia con el contenido web dinámico es baja, así que esto es algo que estoy teniendo problemas para conseguir mi cabeza alrededor.

Creo que Java o Javascript es una clave, esto aparece a menudo.

El raspador es simplemente un motor de comparación de probabilidades. Algunos sitios tienen API, pero necesito esto para aquellos que no lo hacen. Estoy usando la biblioteca scrapy con Python 2.7

Pido disculpas si esta pregunta es demasiado abierta. En resumen, mi pregunta es: ¿cómo se puede usar scrapy para raspar estos datos dinámicos para que pueda usarlos? ¿Para que pueda raspar estos datos de probabilidades de apuestas en tiempo real?

Author: A-B-B, 2011-12-18

7 answers

Los navegadores basados en Webkit (como Google Chrome o Safari) tienen herramientas de desarrollo integradas. En Chrome se puede abrir Menu->Tools->Developer Tools. La pestaña Network le permite ver toda la información sobre cada solicitud y respuesta:

introduzca la descripción de la imagen aquí

En la parte inferior de la imagen se puede ver que he filtrado solicitud hasta XHR - estas son solicitudes hechas por código javascript.

Consejo: el registro se borra cada vez que carga una página, en la parte inferior de la imagen, el botón de punto negro se conservará registro.

Después de analizar las solicitudes y respuestas, puede simular estas solicitudes desde su rastreador web y extraer datos valiosos. En muchos casos será más fácil obtener sus datos que analizar HTML, porque esos datos no contienen lógica de presentación y están formateados para ser accedidos por código javascript.

Firefox tiene una extensión similar, se llama firebug . Algunos argumentarán que firebug es aún más poderoso, pero me gusta la simplicidad de webkit.

 68
Author: Ski,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2014-10-30 09:58:57

Aquí hay un ejemplo simple de usar scrapy con solicitud ajax. Vamos a ver el sitio http://www.rubin-kazan.ru/guestbook.html Todos los mensajes se cargan con una solicitud ajax. Mi objetivo es obtener estos mensajes con todos sus atributos (autor, fecha,...).

introduzca la descripción de la imagen aquí

Cuando analizo el código fuente de la página no puedo ver todos estos mensajes porque la página web utiliza tecnología ajax. Pero puedo con Firebug de Mozila Firefox (o un instrumento de analogía en otro navegador) a analizar la petición Http que genera los mensajes en la página web. introduzca la descripción de la imagen aquí

Para este propósito no recargo toda la página, sino solo la parte de la página que contiene mensajes. Para este propósito, hago clic en un número arbitrario de página en la parte inferior introduzca la descripción de la imagen aquí y observo la solicitud HTTP que es responsable del cuerpo del mensaje introduzca la descripción de la imagen aquí

Después de terminar analizo los encabezados de la solicitud (debo citar que esta url voy a extraer de la página fuente de la sección var, ver el código debajo). introduzca la descripción de la imagen aquí

Y el contenido de los datos del formulario de solicitud (el método Http es "Post")

introduzca la descripción de la imagen aquí

Y el contenido de la respuesta, que es un archivo Json,

introduzca la descripción de la imagen aquí

Que presentan toda la información que estoy buscando.

A partir de ahora debo implementar todo este conocimiento en scrapy. Determinaremos la araña para esto.

  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 la función parse tengo la respuesta para la primera solicitud. En RubiGuessItem tengo el archivo json con toda la información.

 80
Author: Badarau Petru,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2017-07-22 17:57:39

Muchas veces al rastrear nos encontramos con problemas donde el contenido que se renderiza en la página se genera con Javascript y, por lo tanto, scrapy no puede rastrearlo (por ejemplo. peticiones ajax, locura jQuery).

Sin embargo, si usa Scrapy junto con el marco de pruebas web Selenium, entonces podemos rastrear cualquier cosa que se muestre en un navegador web normal.

Algunas cosas a tener en cuenta:

  • Debe tener instalada la versión Python de Selenium RC para que esto funcione, y debe haber configurado el Selenio correctamente. También esto es solo un rastreador de plantillas. Usted podría conseguir mucho más loco y más avanzado con las cosas, pero yo sólo quería mostrar la idea básica. A medida que el código se encuentra ahora, estará haciendo dos solicitudes para cualquier url dada. Una petición es hecha por Scrapy y la otra es hecha por Selenio. Estoy seguro de que hay formas de evitar esto para que pueda hacer que Selenium haga la única solicitud, pero no me molesté en implementar eso y al hacer dos solicitudes puedes rastrear la página con Scrapy también.

  • Esto es bastante poderoso porque ahora tiene todo el DOM renderizado disponible para que pueda rastrear y aún puede usar todas las buenas características de rastreo en Scrapy. Esto hará que el rastreo sea más lento, por supuesto, pero dependiendo de cuánto necesite el DOM renderizado, podría valer la pena la espera.

    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
    

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

 34
Author: A T,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2013-07-17 10:27:39

Otra solución sería implementar un gestor de descargas o un middleware gestor de descargas. El siguiente es un ejemplo de middleware usando selenium con headless phantomjs webdriver:

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

Quería la habilidad de decirle a diferentes arañas qué middleware usar, así que implementé este 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}

Para que wrapper funcione todas las arañas deben tener como mínimo:

middleware = set([])

Para incluir un middleware:

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

La principal ventaja para implementarlo de esta manera en lugar de en la araña es que solo terminas haciendo una solicitud. En una solución de T, por ejemplo: El controlador de descargas procesa la solicitud y luego entrega la respuesta a la araña. La araña luego hace una nueva solicitud en su función parse_page That Que son dos solicitudes para el mismo contenido.

 23
Author: rocktheartsm4l,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2018-10-04 02:59:37

Estaba usando un middleware de descarga personalizado, pero no estaba muy contento con él, ya que no logré hacer que la caché funcionara con él.

Un mejor enfoque fue implementar un manejador de descargas personalizado.

Hay un ejemplo de trabajo aquí. Se ve así:

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

Supongamos que su raspador se llama "raspador". Si pones el código mencionado dentro de un archivo llamado handlers.py en la raíz de la carpeta "raspador", entonces usted podría añadir a su settings.py:

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

Y voilà, el DOM analizado por JS, con caché scrapy, reintentos, etc.

 6
Author: Ivan Chaer,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2016-03-18 22:22:50

Manejo la solicitud ajax usando Selenium y el controlador web de Firefox. No es tan rápido si necesita el rastreador como un demonio, pero mucho mejor que cualquier solución manual. Escribí un breve tutorial aquí para referencia

 1
Author: narko,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2017-04-06 10:49:50

¿Cómo se puede usar scrapy para raspar estos datos dinámicos para que pueda usar ¿eso?

Me pregunto por qué nadie ha publicado la solución usando solo Scrapy.

Echa un vistazo a la entrada del blog de Scrapy team SCRAPING INFINITE SCROLL PAGES . El ejemplo scraps http://spidyquotes.herokuapp.com/scroll sitio web que utiliza desplazamiento infinito.

La idea es utilizar las herramientas de desarrollo de su navegador y observar las solicitudes AJAX, a continuación, basado en eso información crea las solicitudes para 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)
 0
Author: Chankey Pathak,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2017-04-03 08:10:58