Czy scrapy może służyć do pobierania dynamicznej zawartości z witryn korzystających z technologii AJAX?


145

Niedawno uczyłem się Pythona i wkładam rękę w tworzenie skrobaka internetowego. To nic nadzwyczajnego; jej jedynym celem jest pobranie danych z witryny bukmacherskiej i umieszczenie ich w programie Excel.

Większość problemów jest do rozwiązania i mam niezły bałagan. Jednak napotykam ogromną przeszkodę w jednym problemie. Jeśli witryna ładuje tabelę koni i podaje aktualne ceny zakładów, informacji tych nie ma w żadnym pliku źródłowym. Wskazówka jest taka, że ​​te dane są czasami na żywo, a liczby są oczywiście aktualizowane z jakiegoś zdalnego serwera. HTML na moim komputerze ma po prostu dziurę, w której ich serwery przepychają wszystkie interesujące dane, których potrzebuję.

Teraz moje doświadczenie z dynamiczną zawartością sieciową jest niewielkie, więc mam problem ze zrozumieniem.

Myślę, że kluczem jest Java lub Javascript, często się to pojawia.

Skrobak to po prostu porównywarka kursów. Niektóre witryny mają interfejsy API, ale potrzebuję tego dla tych, które ich nie mają. Używam biblioteki scrapy w Pythonie 2.7

Przepraszam, jeśli to pytanie jest zbyt otwarte. Krótko mówiąc, moje pytanie brzmi: w jaki sposób można wykorzystać scrapy do zeskrobania tych dynamicznych danych, aby móc ich użyć? Więc mogę zeskrobać dane dotyczące kursów bukmacherskich w czasie rzeczywistym?


1
Jak mogę uzyskać te dane, które są dynamiczne i na żywo?
Joseph,

1
Jeśli twoja strona ma javascript, spróbuj tego
reclosedev

3
Wypróbuj niektóre Firefoxrozszerzenia, takie jak httpFoxlub, liveHttpHeadersi załaduj stronę, która używa żądania AJAX. Scrapy nie identyfikuje automatycznie żądań Ajax, musisz ręcznie wyszukać odpowiedni adres URL Ajax, a następnie wysłać żądanie z tym żądaniem.
Aamir Adnan

Pozdrawiam, dam rozszerzeniom Firefoksa kreatora
Joseph

Istnieje wiele rozwiązań typu open source. Ale jeśli szukasz łatwego i szybkiego sposobu, aby to zrobić, szczególnie w przypadku dużych obciążeń, sprawdź SnapSearch ( snapsearch.io ). Został zbudowany dla witryn JS, HTML5 i SPA wymagających indeksowania przez wyszukiwarki. Wypróbuj wersję demonstracyjną (jeśli jest pusta treść, oznacza to, że witryna w rzeczywistości nie zwróciła treści, co potencjalnie oznacza przekierowanie 301).
CMCDragonkai

Odpowiedzi:


74

Przeglądarki oparte na Webkit (takie jak Google Chrome czy Safari) mają wbudowane narzędzia programistyczne. W Chrome możesz go otworzyć Menu->Tools->Developer Tools. NetworkZakładka pozwala zobaczyć wszystkie informacje na temat każdego żądania i odpowiedzi:

wprowadź opis obrazu tutaj

Na dole obrazka widać, że przefiltrowałem żądania do XHR- są to żądania wysyłane przez kod javascript.

Wskazówka: dziennik jest czyszczony za każdym razem, gdy wczytujesz stronę, u dołu obrazu przycisk z czarną kropką zachowuje dziennik.

Po przeanalizowaniu żądań i odpowiedzi możesz zasymulować te żądania ze swojego robota internetowego i wyodrębnić cenne dane. W wielu przypadkach łatwiej będzie uzyskać dane niż parsowanie HTML, ponieważ dane te nie zawierają logiki prezentacji i są sformatowane tak, aby można było uzyskać do nich dostęp za pomocą kodu JavaScript.

Firefox ma podobne rozszerzenie, nazywa się firebug . Niektórzy twierdzą, że firebug jest jeszcze potężniejszy, ale podoba mi się prostota webkita.


141
Jak do cholery może to być akceptowana odpowiedź, skoro nie ma w niej nawet słowa „scrapy”?
Zestaw narzędzi

To działa i jest łatwe do przeanalizowania za pomocą modułu json w Pythonie. To rozwiązanie! W porównaniu z tym spróbuj użyć selenu lub innych rzeczy, które ludzie sugerują, to bardziej boli głowa. Gdyby alternatywna metoda była znacznie bardziej zawiła, dałbym ci ją, ale tak nie jest w tym przypadku @Toolkit
Arion_Miles

1
To nie jest naprawdę istotne. Pytanie brzmiało, jak używać scarpy do pobierania dynamicznych witryn internetowych.
E. Erfan

„Jak do cholery może to być akceptowana odpowiedź” - ponieważ praktyczne zastosowanie jest ważniejsze od politycznej poprawności. Ludzie rozumieją KONTEKST.
Espresso

98

Oto prosty przykład scrapyz żądaniem AJAX. Zobaczmy stronę rubin-kazan.ru .

Wszystkie wiadomości są ładowane żądaniem AJAX. Moim celem jest pobranie tych wiadomości ze wszystkimi ich atrybutami (autor, data, ...):

wprowadź opis obrazu tutaj

Kiedy analizuję kod źródłowy strony, nie widzę wszystkich tych komunikatów, ponieważ strona korzysta z technologii AJAX. Ale mogę za pomocą Firebuga z Mozilla Firefox (lub równoważnego narzędzia w innych przeglądarkach) przeanalizować żądanie HTTP, które generuje komunikaty na stronie internetowej:

wprowadź opis obrazu tutaj

Nie ładuje ponownie całej strony, ale tylko części strony zawierające wiadomości. W tym celu klikam dowolną liczbę stron na dole:

wprowadź opis obrazu tutaj

I obserwuję żądanie HTTP odpowiedzialne za treść wiadomości:

wprowadź opis obrazu tutaj

Po zakończeniu analizuję nagłówki żądania (muszę zacytować, że ten adres URL wyodrębnię ze strony źródłowej z sekcji var, zobacz kod poniżej):

wprowadź opis obrazu tutaj

Oraz zawartość danych formularza żądania (metoda HTTP to „Post”):

wprowadź opis obrazu tutaj

Oraz treść odpowiedzi, czyli plik JSON:

wprowadź opis obrazu tutaj

Który zawiera wszystkie informacje, których szukam.

Od teraz całą tę wiedzę muszę wdrożyć w scrapy. W tym celu zdefiniujmy pająka:

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

W parsefunkcji mam odpowiedź na pierwsze żądanie. W RubiGuessItemmam plik JSON ze wszystkimi informacjami.


6
Cześć. Czy mógłbyś wyjaśnić, co to jest „url_list_gb_messages”? Nie mogę tego zrozumieć. Dzięki.
polaryzuj

4
Ten zdecydowanie jest lepszy.
1a1a11a

1
@polarise Ten kod używa remodułu (wyrażenia regularne), wyszukuje ciąg znaków 'url_list_gb_messages="(.*)"'i izoluje zawartość nawiasów w zmiennej o tej samej nazwie. To jest fajne wprowadzenie: guru99.com/python-regular-expressions-complete-tutorial.html
MGP

42

Podczas indeksowania często napotykamy problemy, w których treść renderowana na stronie jest generowana za pomocą JavaScript, przez co scrapy nie może jej zaindeksować (np. Żądania ajax, szaleństwo jQuery).

Jeśli jednak używasz Scrapy wraz z platformą do testowania sieci Selenium, jesteśmy w stanie zaindeksować wszystko, co jest wyświetlane w normalnej przeglądarce internetowej.

Kilka uwag:

  • Aby to działało, musisz mieć zainstalowaną wersję Selenium RC w języku Python i poprawnie skonfigurować Selenium. To jest tylko robot szablonów. Możesz stać się bardziej szalony i bardziej zaawansowany, ale chciałem tylko pokazać podstawową ideę. W obecnym stanie kodu będziesz wykonywać dwa żądania dla dowolnego adresu URL. Jedna prośba jest składana przez Scrapy, a druga przez Selenium. Jestem pewien, że istnieją sposoby obejścia tego problemu, abyś mógł po prostu zmusić Selenium do wykonania jednej i jedynej prośby, ale nie zawracałem sobie głowy implementacją tego, a wykonując dwa żądania, możesz również zaindeksować stronę za pomocą Scrapy.

  • Jest to dość potężne, ponieważ teraz masz cały wyrenderowany model DOM do przeszukania i nadal możesz używać wszystkich fajnych funkcji indeksowania w Scrapy. Spowoduje to oczywiście wolniejsze indeksowanie, ale w zależności od tego, jak bardzo potrzebujesz renderowanego DOM, warto poczekać.

    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ódła: http://snipplr.com/view/66998/


Zgrabne rozwiązanie! Czy masz jakieś wskazówki dotyczące podłączenia tego skryptu do przeglądarki Firefox? (System operacyjny to Linux Mint). Otrzymuję komunikat „[Errno 111] Odmowa połączenia”.
Andrew,

1
Ten kod nie działa dla selenium=3.3.1i python=2.7.10, błąd podczas importowania z selen selen
benjaminz

1
W tej wersji selenu swoje oświadczenie import będzie: from selenium import webdriveralbo chromedriveralbo cokolwiek zdarzy ci się być używany. Dokumenty EDIT: Add Reference dokumentacji i zmienić mój straszny gramatyki!
nulltron

Selenium Remote Control został zastąpiony przez Selenium WebDriver, zgodnie z ich stroną internetową
rainbowsorbet

33

Innym rozwiązaniem byłoby zaimplementowanie modułu obsługi pobierania lub oprogramowania pośredniego modułu obsługi pobierania. (zobacz dokumentację scrapy, aby uzyskać więcej informacji o oprogramowaniu pośredniczącym do pobierania) Poniżej znajduje się przykładowa klasa używająca selenu z bezgłowym sterownikiem sieciowym phantomjs:

1) Zdefiniuj klasę w middlewares.pyskrypcie.

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) Dodaj JsDownload()klasę do zmiennej DOWNLOADER_MIDDLEWAREw settings.py:

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

3) Zintegruj HTMLResponsewnętrze your_spider.py. Dekodowanie treści odpowiedzi zapewni pożądane wyjście.

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

Opcjonalny dodatek:
Chciałem mieć możliwość informowania różnych robotów pośredniczących, którego oprogramowania pośredniczącego użyć, więc zaimplementowałem ten 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

aby owijarka działała, wszystkie pająki muszą mieć co najmniej:

middleware = set([])

aby dołączyć oprogramowanie pośredniczące:

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

Zaleta:
Główną zaletą implementacji w ten sposób, a nie w pająku, jest to, że kończy się tylko jedno żądanie. Na przykład w rozwiązaniu AT: program obsługi pobierania przetwarza żądanie, a następnie przekazuje odpowiedź pająkowi. Następnie pająk wysyła zupełnie nowe żądanie w swojej funkcji parse_page - to dwa żądania dotyczące tej samej treści.


Jednak spóźniłem się trochę z odpowiedzią>. <
rocktheartsm4l

@ rocktheartsm4l co w tym złego tuż przy użyciu, w process_requests, if spider.name in ['spider1', 'spider2']zamiast dekoratora
pad

@pad Nie ma w tym nic złego. Po prostu stwierdziłem, że bardziej jasne jest, że moje klasy pająków mają zestaw o nazwie oprogramowanie pośredniczące. W ten sposób mogłem spojrzeć na dowolną klasę pająka i zobaczyć, które dokładnie oprogramowanie pośredniczące zostanie dla niej wykonane. W moim projekcie zaimplementowano dużo oprogramowania pośredniego, więc miało to sens.
rocktheartsm4l

To straszne rozwiązanie. Nie tylko nie jest to związane ze scrapy, ale sam kod jest wyjątkowo nieefektywny, a całe podejście w ogólności pokonuje cały cel asynchronicznego środowiska do skrobania sieci, którym jest scrapy
Granitosaurus

2
Jest znacznie bardziej wydajne niż jakiekolwiek inne rozwiązanie, które widziałem w SO, ponieważ użycie oprogramowania pośredniego do pobierania powoduje, że tylko jedno żądanie jest wysyłane do strony ... jeśli to takie straszne, dlaczego nie wymyślić lepszego rozwiązania i nie udostępniać zamiast rażąco jednostronne twierdzenia. „Nie ma związku ze złomowaniem” palisz coś? Poza wdrożeniem jakiegoś szalenie złożonego, solidnego i niestandardowego rozwiązania, jest to podejście, z którego korzysta większość ludzi. Jedyną różnicą jest to, że większość realizować część selenu w pająka, który powoduje wiele żądań być wykonane ...
rocktheartsm4l

10

Używałem niestandardowego oprogramowania pośredniczącego do pobierania, ale nie byłem z niego zadowolony, ponieważ nie udało mi się zmusić pamięci podręcznej do pracy z nim.

Lepszym podejściem było zaimplementowanie niestandardowego modułu obsługi pobierania.

Jest przykładem pracy tutaj . To wygląda tak:

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

Załóżmy, że twój skrobak nazywa się „skrobakiem”. Jeśli umieścisz wspomniany kod w pliku o nazwie handlers.py w katalogu głównym folderu "scraper", możesz dodać do swojego settings.py:

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

I voilà, JS przeanalizował DOM, z pamięcią podręczną scrapy, ponownymi próbami itp.


Podoba mi się to rozwiązanie!
rocktheartsm4l

Niezłe rozwiązanie. Czy sterownik Selenium nadal jest jedyną opcją?
Motheus

Świetne rozwiązanie. Wielkie dzięki.
CrazyGeek

4

jak można użyć scrapy do zeskrobania tych dynamicznych danych, aby móc ich użyć?

Zastanawiam się, dlaczego nikt nie opublikował rozwiązania wykorzystującego tylko Scrapy.

Przeczytaj wpis na blogu zespołu Scrapy SCRAPING INFINITE SCROLLING PAGES . Przykładowa skrawka http://spidyquotes.herokuapp.com/scroll strona internetowa, która używa nieskończonego przewijania.

Chodzi o to, aby użyć narzędzi deweloperskich swojej przeglądarki i zwrócić uwagę na żądania AJAX, a następnie na podstawie tych informacji utworzyć żądania dla 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)

Znowu stajemy przed tym samym problemem: Scrappy nie jest stworzony do tego celu i tutaj mamy do czynienia z tym samym problemem. Przejdź do phantomJS lub, jak sugerowali inni, utwórz własne oprogramowanie pośredniczące do pobierania
rak007

@ rak007 Sterownik PhantomJS vs Chrome. Który byś zaproponował?
Chankey Pathak

2

tak, Scrapy może usuwać dynamiczne strony internetowe, które są renderowane za pomocą javaScript.

Istnieją dwa podejścia do usuwania tego rodzaju witryn internetowych.

Pierwszy,

można użyć splashdo renderowania kodu JavaScript, a następnie przeanalizować renderowany kod HTML. dokument i projekt można znaleźć tutaj Scrapy splash, git

Druga,

Jak wszyscy twierdzą, monitorując network calls, tak, możesz znaleźć wywołanie interfejsu API, które pobiera dane i udaje, że wywołanie w twoim pająku scrapy może pomóc ci uzyskać pożądane dane.


1

Obsługuję żądanie Ajax za pomocą Selenium i sterownika internetowego Firefox. Nie jest tak szybki, jeśli potrzebujesz robota jako demona, ale znacznie lepszy niż jakiekolwiek rozwiązanie ręczne. Napisałem krótki samouczek tutaj jako odniesienie

Korzystając z naszej strony potwierdzasz, że przeczytałeś(-aś) i rozumiesz nasze zasady używania plików cookie i zasady ochrony prywatności.
Licensed under cc by-sa 3.0 with attribution required.