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

145

Recientemente he estado aprendiendo Python y estoy metiendo mi mano en la construcción de un raspador de web. No es nada lujoso en absoluto; su único propósito es sacar los datos de un sitio web de apuestas y tener estos datos en Excel.

La mayoría de los problemas se pueden resolver y estoy teniendo un buen lío. Sin embargo, estoy llegando a un obstáculo masivo sobre un problema. Si un sitio carga una tabla de caballos y enumera los precios actuales de las apuestas, esta información no se encuentra en ningún archivo fuente. La pista es que estos datos están en vivo a veces, y los números 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, por lo que esto es algo que estoy teniendo problemas para entender.

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

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

Me disculpo 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 poder raspar estos datos de probabilidades de apuestas en tiempo real?

Joseph
fuente
1
¿Cómo puedo obtener estos datos, los datos dinámicos y en vivo?
Joseph
1
Si su página tiene JavaScript, intente esto
reclosedev
3
Pruebe algunas Firefoxextensiones como httpFoxo liveHttpHeadersy cargue una página que esté usando la solicitud ajax. Scrapy no identifica automáticamente las solicitudes de ajax, debe buscar manualmente la URL de ajax adecuada y luego solicitarla con eso.
Aamir Adnan
saludos, le daré un wizz a las extensiones de Firefox
Joseph
Hay una serie de soluciones de código abierto. Pero si está buscando una manera fácil y rápida de hacer esto, especialmente para grandes cargas de trabajo, consulte SnapSearch ( snapsearch.io ). Fue creado para sitios JS, HTML5 y SPA que requieren rastreo de motores de búsqueda. Pruebe la demostración (si hay contenido vacío, esto significa que el sitio en realidad no devolvió contenido corporal, lo que podría significar una redirección 301).
CMCDragonkai

Respuestas:

74

Los navegadores basados ​​en Webkit (como Google Chrome o Safari) tienen herramientas de desarrollador integradas. En Chrome puedes abrirlo Menu->Tools->Developer Tools. La Networkpestaña le permite ver toda la información sobre cada solicitud y respuesta:

ingrese la descripción de la imagen aquí

En la parte inferior de la imagen, puede ver que he filtrado la solicitud a XHR: estas son solicitudes realizadas 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 conservará el 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, ya que esos datos no contienen lógica de presentación y están formateados para acceder a ellos mediante un 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 del webkit.

Esquí
fuente
141
¿Cómo diablos puede ser una respuesta aceptada si ni siquiera tiene la palabra 'scrapy'?
Toolkit
Funciona, y es fácil de analizar usando el módulo json en python. Es una solucion! En comparación con eso, intente usar selenio u otras cosas que la gente sugiera, es más dolor de cabeza. Si el método alternativo fuera mucho más complicado, entonces te lo daría, pero no es el caso aquí @Toolkit
Arion_Miles
1
Esto no es realmente relevante. La pregunta era cómo usar Scarpy para raspar sitios web dinámicos.
E. Erfan
"¿Cómo diablos puede ser una respuesta aceptada?", Porque el uso práctico supera la corrección política. Los humanos entienden el CONTEXTO.
Espresso
98

Aquí hay un ejemplo simple de scrapycon una solicitud AJAX. Deje ver el sitio rubin-kazan.ru .

Todos los mensajes se cargan con una solicitud AJAX. Mi objetivo es obtener estos mensajes con todos sus atributos (autor, fecha, ...):

ingrese 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 Mozilla Firefox (o una herramienta equivalente en otros navegadores) para analizar la solicitud HTTP que genera los mensajes en la página web:

ingrese la descripción de la imagen aquí

No recarga toda la página, sino solo las partes de la página que contienen mensajes. Para este propósito, hago clic en un número arbitrario de página en la parte inferior:

ingrese la descripción de la imagen aquí

Y observo la solicitud HTTP que es responsable del cuerpo del mensaje:

ingrese la descripción de la imagen aquí

Después de terminar, analizo los encabezados de la solicitud (debo citar que esta URL extraeré de la página de origen de la sección var, vea el código a continuación):

ingrese la descripción de la imagen aquí

Y el contenido de los datos del formulario de la solicitud (el método HTTP es "Publicar"):

ingrese la descripción de la imagen aquí

Y el contenido de la respuesta, que es un archivo JSON:

ingrese la descripción de la imagen aquí

Que presenta toda la información que estoy buscando.

A partir de ahora, debo implementar todo este conocimiento en scrapy. Definamos la araña para este propósito:

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 parsefunción tengo la respuesta para la primera solicitud. En RubiGuessItemTengo el archivo JSON con toda la información.

Badarau Petru
fuente
66
Hola. ¿Podría explicar qué es 'url_list_gb_messages'? No puedo entenderlo Gracias.
polarice el
44
Este definitivamente es mejor.
1a1a11a
1
@polarise Ese código está usando el remódulo (expresiones regulares), busca la cadena 'url_list_gb_messages="(.*)"'y aísla el contenido de paréntesis en la variable del mismo nombre. Esta es una buena introducción: guru99.com/python-regular-expressions-complete-tutorial.html
Pop
42

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

Sin embargo, si usa Scrapy junto con el marco de prueba web Selenium, entonces podemos rastrear todo lo que se muestra 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 Selenium correctamente. Además, este es solo un rastreador de plantillas. Podrías ponerte más loco y más avanzado con las cosas, pero solo quería mostrar la idea básica. Tal como está el código ahora, hará dos solicitudes para cualquier URL dada. Scrapy realiza una solicitud y Selenium realiza la otra. Estoy seguro de que hay formas de evitar esto para que puedas hacer que Selenium haga la única solicitud, pero no me molesté en implementar eso y al hacer dos solicitudes, también puedes rastrear la página con Scrapy.

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

    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/

A
fuente
Solución ordenada! ¿Tienes algún consejo para conectar este script a Firefox? (El sistema operativo es Linux Mint). Me aparece "[Errno 111] Conexión rechazada".
Andrew
1
Este código ya no funciona selenium=3.3.1y python=2.7.10, error al importar selenio desde selenio
benjaminz
1
En esa versión de selenio su declaración de importación sería: from selenium import webdrivero chromedrivero lo que sea que se esté utilizando. Docs EDIT: ¡agregue referencias de documentación y cambie mi gramática horrible!
nulltron 01 de
Selenium Remote Control ha sido reemplazado por Selenium WebDriver, según su sitio web
rainbowsorbet
33

Otra solución sería implementar un controlador de descarga o middleware del controlador de descarga. (consulte documentos de Scrapy para obtener más información sobre el middleware del descargador) La siguiente es una clase de ejemplo que utiliza selenio con webdriver phantomjs sin cabeza:

1) Definir clase dentro del 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) Agregar JsDownload()clase a variable DOWNLOADER_MIDDLEWAREdentro de settings.py:

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

3) Integrar el HTMLResponseinterior your_spider.py. Decodificar el cuerpo de respuesta le dará la salida deseada.

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

Complemento opcional:
quería la capacidad de decirle a las diferentes arañas qué middleware usar, así que implementé este contenedor:

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

Para que la envoltura funcione, todas las arañas deben tener como mínimo:

middleware = set([])

para incluir un middleware:

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

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

rocktheartsm4l
fuente
Sin embargo, llegué bastante tarde a responder esto>. <
rocktheartsm4l
@ rocktheartsm4l lo que está mal con sólo el uso, en process_requests, if spider.name in ['spider1', 'spider2']en lugar del decorador
almohadilla
@pad No hay nada de malo en eso. Acabo de encontrar más claro que mis clases de araña tienen un conjunto llamado middleware. De esta manera podría mirar cualquier clase de araña y ver exactamente qué middlewares se ejecutarían para ella. Mi proyecto tenía una gran cantidad de middleware implementado, así que esto tenía sentido.
rocktheartsm4l
Esta es una solución terrible. No solo no está relacionado con el scrapy, sino que el código en sí mismo es extremadamente ineficiente, así como el enfoque general en general derrota el propósito del marco asíncrono de scraping web que es scrapy
Granitosaurus
2
Es mucho más eficiente que cualquier otra solución que haya visto en SO, ya que el uso de un software intermedio de descarga hace que solo se haga una solicitud para la página ... si es tan terrible, ¿por qué no se te ocurre una mejor solución y la compartes en lugar de haciendo afirmaciones descaradamente unilaterales. "No relacionado con scrapy" ¿Estás fumando algo? Además de implementar alguna solución compleja, robusta y personalizada, este es el enfoque que he visto usar a la mayoría de las personas. La única diferencia es que la mayor parte implementar el selenio en la araña que provoca múltiples peticiones que hacer ...
rocktheartsm4l
10

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

Un mejor enfoque fue implementar un controlador de descarga personalizado.

Hay un ejemplo de trabajo aquí . Se parece a esto:

# 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 coloca el código mencionado dentro de un archivo llamado handlers.py en la raíz de la carpeta "scraper", entonces podría agregar a su settings.py:

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

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

Ivan Chaer
fuente
¡Me gusta esta solución!
rocktheartsm4l
Buena solución ¿Selenium driver sigue siendo la única opción?
Motheus
Gran solución Muchas gracias.
CrazyGeek
4

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

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

Echa un vistazo a la publicación del blog del equipo Scrapy SCRAPING INFINITE SCROLLING PAGES . El ejemplo desecha http://spidyquotes.herokuapp.com/scroll sitio web que utiliza desplazamiento infinito.

La idea es utilizar las Herramientas de desarrollo de su navegador y notar las solicitudes de AJAX, luego, basándose en esa información, cree las solicitudes 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)
Chankey Pathak
fuente
Volvemos a enfrentar el mismo problema: Scrappy no está hecho para este propósito y aquí es donde nos enfrentamos al mismo problema. Pase a phantomJS o como otros sugirieron, cree su propio middleware de descarga
rak007
@ rak007 PhantomJS vs controlador de Chrome. ¿Cuál sugerirías?
Chankey Pathak
2

Sí, Scrapy puede eliminar sitios web dinámicos, sitios web que se representan a través de JavaScript.

Hay dos enfoques para eliminar este tipo de sitios web.

Primero,

puede usar splashpara representar el código Javascript y luego analizar el HTML representado. puedes encontrar el documento y proyectar aquí Scrapy splash, git

Segundo,

Como todos dicen, al monitorear network calls , sí, puede encontrar la llamada a la API que obtiene los datos y se burla de esa llamada en su araña temblorosa que podría ayudarlo a obtener los datos deseados.

ThunderMind
fuente
1

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

narko
fuente