O scrapy pode ser usado para raspar conteúdo dinâmico de sites que usam AJAX?

145

Eu recentemente aprendi Python e estou mergulhando minha mão na construção de um raspador de web. Não é nada extravagante; seu único objetivo é obter os dados de um site de apostas e colocá-los no Excel.

A maioria dos problemas é solucionável e estou tendo uma boa bagunça. No entanto, estou enfrentando um grande obstáculo em relação a um problema. Se um site carrega uma tabela de cavalos e lista os preços atuais das apostas, essas informações não estão em nenhum arquivo de origem. A pista é que esses dados estão ativos algumas vezes, com os números sendo atualizados obviamente a partir de algum servidor remoto. O HTML no meu PC simplesmente tem um buraco no qual seus servidores estão empurrando todos os dados interessantes que eu preciso.

Agora, minha experiência com o conteúdo dinâmico da Web é baixa, então é algo que estou tendo problemas para entender.

Eu acho que Java ou Javascript é uma chave, isso aparece com freqüência.

O raspador é simplesmente um mecanismo de comparação de probabilidades. Alguns sites têm APIs, mas eu preciso disso para aqueles que não têm. Eu estou usando a biblioteca scrapy com Python 2.7

Peço desculpas se esta pergunta é muito aberta. Em resumo, minha pergunta é: como o raspador pode ser usado para raspar esses dados dinâmicos para que eu possa usá-los? Para que eu possa raspar esses dados de probabilidades de apostas em tempo real?

Joseph
fonte
1
Como posso obter esses dados, dinâmicos e ativos?
Joseph
1
Se sua página possui javascript, Tente isto
reclosedev
3
Experimente algumas Firefoxextensões como httpFoxou liveHttpHeaderse carregue uma página que esteja usando solicitação ajax. O Scrapy não identifica automaticamente as solicitações de ajax, você deve procurar manualmente o URL de ajax apropriado e, em seguida, solicitar com isso.
Aamir Adnan
aplausos, eu vou dar as extensões do Firefox um wizz
Joseph
Há várias soluções de código aberto. Mas se você está procurando uma maneira fácil e rápida de fazer isso, especialmente para grandes cargas de trabalho, confira o SnapSearch ( snapsearch.io ). Foi desenvolvido para sites JS, HTML5 e SPA que exigem rastreamento do mecanismo de pesquisa. Experimente a demonstração (se houver conteúdo vazio, isso significa que o site realmente não retornou conteúdo corporal, o que significa potencialmente um redirecionamento 301).
precisa saber é o seguinte

Respostas:

74

Os navegadores baseados no Webkit (como Google Chrome ou Safari) possuem ferramentas de desenvolvedor integradas. No Chrome, você pode abri-lo Menu->Tools->Developer Tools. A Networkguia permite que você veja todas as informações sobre cada solicitação e resposta:

insira a descrição da imagem aqui

Na parte inferior da imagem, você pode ver que eu filtrei a solicitação até XHR- estas são solicitações feitas pelo código javascript.

Dica: o log é limpo toda vez que você carrega uma página, na parte inferior da imagem, o botão de ponto preto preserva o log.

Após analisar solicitações e respostas, você pode simular essas solicitações do seu rastreador da Web e extrair dados valiosos. Em muitos casos, será mais fácil obter seus dados do que analisar o HTML, porque esses dados não contêm lógica de apresentação e estão formatados para serem acessados ​​pelo código javascript.

Firefox tem extensão semelhante, é chamado firebug . Alguns argumentam que o firebug é ainda mais poderoso, mas eu gosto da simplicidade do webkit.

Esqui
fonte
141
Como diabos isso pode ser uma resposta aceita, se ela nem tem a palavra 'scrapy'?
Toolkit
Funciona e é fácil analisar usando o módulo json em python. É uma solução! Comparado a isso, tente usar selênio ou outras coisas que as pessoas estão sugerindo, é mais dor de cabeça. Se o método alternativo foi muito mais complicado, então eu dar a você, mas não é o caso aqui @Toolkit
Arion_Miles
1
Isso não é realmente relevante. A questão era como usar o scarpy para criar sites dinâmicos.
E. Erfan
"Como diabos isso pode ser uma resposta aceita" - Porque o uso prático supera o politicamente correto. Os seres humanos entendem o CONTEXTO.
Espresso
98

Aqui está um exemplo simples de scrapycom uma solicitação AJAX. Vamos ver o site rubin-kazan.ru .

Todas as mensagens são carregadas com uma solicitação AJAX. Meu objetivo é buscar essas mensagens com todos os seus atributos (autor, data, ...):

insira a descrição da imagem aqui

Quando analiso o código-fonte da página, não consigo ver todas essas mensagens porque a página da Web usa a tecnologia AJAX. Mas posso com o Firebug do Mozilla Firefox (ou uma ferramenta equivalente em outros navegadores) analisar a solicitação HTTP que gera as mensagens na página da web:

insira a descrição da imagem aqui

Ele não recarrega a página inteira, mas apenas as partes da página que contêm mensagens. Para isso, clico em um número arbitrário de página na parte inferior:

insira a descrição da imagem aqui

E observo a solicitação HTTP que é responsável pelo corpo da mensagem:

insira a descrição da imagem aqui

Após terminar, analiso os cabeçalhos da solicitação (devo citar que esse URL extrairei da página de origem da seção var, veja o código abaixo):

insira a descrição da imagem aqui

E o conteúdo dos dados do formulário da solicitação (o método HTTP é "Post"):

insira a descrição da imagem aqui

E o conteúdo da resposta, que é um arquivo JSON:

insira a descrição da imagem aqui

Que apresenta todas as informações que estou procurando.

A partir de agora, devo implementar todo esse conhecimento em scrapy. Vamos definir a aranha para esse fim:

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

Na parsefunção, tenho a resposta para o primeiro pedido. Em RubiGuessItemeu tenho o arquivo JSON com todas as informações.

Badarau Petru
fonte
6
Oi. Você poderia explicar o que é 'url_list_gb_messages'? Eu não consigo entender. Obrigado.
polarize
4
Este definitivamente é melhor.
1a1a11a
1
@polarise Esse código está usando o remódulo (expressões regulares), procura a string 'url_list_gb_messages="(.*)"'e isola o conteúdo dos parênteses na variável de mesmo nome. Esta é uma introdução agradável: guru99.com/python-regular-expressions-complete-tutorial.html
MGP
42

Muitas vezes, durante o rastreamento, encontramos problemas em que o conteúdo renderizado na página é gerado com Javascript e, portanto, o scrapy não pode rastrear por ele (por exemplo, solicitações de ajax, loucura do jQuery).

No entanto, se você usar o Scrapy junto com a estrutura de teste da web Selenium, poderemos rastrear qualquer coisa exibida em um navegador da web normal.

Algumas coisas a serem observadas:

  • É necessário ter a versão Python do Selenium RC instalada para que isso funcione e você deve ter configurado o Selenium corretamente. Além disso, este é apenas um rastreador de modelos. Você poderia ficar muito mais louco e mais avançado com as coisas, mas eu só queria mostrar a idéia básica. Como o código está agora, você fará duas solicitações para qualquer URL. Uma solicitação é feita pela Scrapy e a outra pela Selenium. Estou certo de que existem maneiras de contornar isso para que você possa fazer com que o Selenium faça a única solicitação, mas não me importei em implementá-la e, ao fazer duas solicitações, você também pode rastrear a página com o Scrapy.

  • Isso é bastante poderoso, porque agora você tem todo o DOM renderizado disponível para rastreamento e ainda pode usar todos os bons recursos de rastreamento do Scrapy. Isso tornará o rastreamento mais lento, é claro, mas dependendo de quanto você precisar do DOM renderizado, pode valer a pena esperar.

    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

Referência: http://snipplr.com/view/66998/

AT
fonte
Solução pura! Você tem alguma dica sobre como conectar esse script ao Firefox? (O sistema operacional é o Linux Mint). Estou recebendo "[Erro 111] Conexão recusada".
31413 Andrew
1
Este código não funciona para selenium=3.3.1e python=2.7.10, erro ao importar o selênio de selênio
benjaminz
1
Nessa versão de selênio sua declaração de importação seria: from selenium import webdriverou chromedriverou o que acontecer de você estar usando. Docs EDIT: adicione referência de documentação e mude minha gramática horrível!
Nulltron #
Selênio Remote Control foi substituído por selênio WebDriver, de acordo com seu site
rainbowsorbet
33

Outra solução seria implementar um manipulador de downloads ou um middleware de manipulador de downloads. (consulte a documentação do scrapy para obter mais informações sobre o downloader middleware) A seguir, é apresentado um exemplo de classe usando selenium com driver de web phantomjs sem cabeça:

1) Defina a classe dentro do 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) Adicione JsDownload()classe à variável DOWNLOADER_MIDDLEWAREdentro de settings.py:

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

3) Integre o HTMLResponseinterior your_spider.py. Decodificar o corpo da resposta obterá a saída desejada.

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:
eu queria dizer a diferentes aranhas qual middleware usar, então implementei 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

para que o invólucro funcione, todas as aranhas devem ter no mínimo:

middleware = set([])

para incluir um middleware:

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

Vantagem:
A principal vantagem de implementá-lo dessa maneira, e não na aranha, é que você acaba fazendo apenas uma solicitação. Na solução da AT, por exemplo: O manipulador de download processa a solicitação e entrega a resposta à aranha. O spider então faz uma nova solicitação na função parse_page - são duas solicitações para o mesmo conteúdo.

rocktheartsm4l
fonte
Eu era um pouco tarde para responder a esta embora> <.
rocktheartsm4l
@ rocktheartsm4l o que há de errado com apenas usando, em process_requests, if spider.name in ['spider1', 'spider2']em vez do decorador
pad
@pad Não há nada de errado nisso. Acabei de descobrir que minhas aulas de aranha têm um conjunto chamado middleware. Dessa forma, eu podia olhar para qualquer classe de aranha e ver exatamente quais middlewares seriam executados para ela. Meu projeto tinha muito middleware implementado, então isso fazia sentido.
rocktheartsm4l
Esta é uma solução terrível. Não só ele não está relacionado com scrapy mas o código em si é extremamente ineficiente, bem como toda a abordagem em derrotas gerais todo o propósito do quadro raspagem web assíncrona que é scrapy
Granitosaurus
2
É muito mais eficiente do que qualquer outra solução que eu já vi no SO, ao usar um software de download médio, faz com que apenas uma solicitação seja feita para a página ... se é tão terrível por que você não apresenta uma solução melhor e compartilha em vez de fazendo declarações flagrantemente unilaterais. "Não está relacionado ao arranhão" você está fumando alguma coisa? Além de implementar uma solução complexa, robusta e personalizada, essa é a abordagem que eu já vi a maioria das pessoas. A única diferença é que a maioria implementar a parte de selênio na aranha que faz com que várias solicitações a serem feitas ...
rocktheartsm4l
10

Eu estava usando um middleware de download personalizado, mas não estava muito feliz com isso, pois não consegui fazer o cache funcionar com ele.

Uma abordagem melhor foi implementar um manipulador de download personalizado.

Há um exemplo de trabalho aqui . Se parece com isso:

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

Suponha que seu raspador se chame "raspador". Se você colocar o código mencionado dentro de um arquivo chamado handlers.py na raiz da pasta "scraper", poderá adicionar ao seu settings.py:

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

E voilà, o JS analisou o DOM, com cache fragmentado, novas tentativas, etc.

Ivan Chaer
fonte
Eu gosto desta solução!
rocktheartsm4l
Ótima solução. O driver Selenium ainda é a única opção?
Motheus
Ótima solução. Muito obrigado.
CrazyGeek
4

como o scrapy pode ser usado para raspar esses dados dinâmicos para que eu possa usá-los?

Eu me pergunto por que ninguém postou a solução usando apenas o Scrapy.

Confira a postagem do blog da equipe Scrapy SCRAPING INFINITE SCROLLING PAGES . O exemplo retira o site http://spidyquotes.herokuapp.com/scroll que usa rolagem infinita.

A idéia é usar as Ferramentas de desenvolvedor do seu navegador e observar as solicitações AJAX; depois, com base nessas informações, crie as solicitações 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)
Chankey Pathak
fonte
Enfrentamos o mesmo problema novamente: o Scrappy não é feito para esse fim e é aqui que somos confrontados com o mesmo problema. Passar para PhantomJS ou como outros sugeriram, criar seu próprio de download middleware
rak007
@ rak007 PhantomJS vs driver do Chrome. Qual você sugeriria?
Chankey Pathak
2

Sim, o Scrapy pode descartar sites dinâmicos, sites renderizados por meio de javaScript.

Existem duas abordagens para eliminar esse tipo de site.

Primeiro,

você pode usar splashpara renderizar o código Javascript e analisar o HTML renderizado. você pode encontrar o documento e o projeto aqui Scrapy splash, git

Segundo,

Como todo mundo está afirmando, monitorando o network callssim, você pode encontrar a chamada da API que busca os dados e zomba da chamada no seu spider scrapy, que pode ajudá-lo a obter os dados desejados.

ThunderMind
fonte
1

Trato da solicitação do ajax usando o Selenium e o driver da web do Firefox. Não é tão rápido se você precisar do rastreador como um daemon, mas muito melhor do que qualquer solução manual. Eu escrevi um pequeno tutorial aqui para referência

narko
fonte