Kann Scrapy verwendet werden, um dynamische Inhalte von Websites zu entfernen, die AJAX verwenden?


145

Ich habe kürzlich Python gelernt und tauche meine Hand in den Aufbau eines Web-Scraper. Es ist überhaupt nichts Besonderes; Der einzige Zweck besteht darin, die Daten von einer Wettwebsite zu entfernen und diese Daten in Excel zu speichern.

Die meisten Probleme sind lösbar und ich habe ein gutes kleines Durcheinander. Ich stoße jedoch in einem Punkt auf eine massive Hürde. Wenn eine Site eine Tabelle mit Pferden lädt und die aktuellen Wettpreise auflistet, befinden sich diese Informationen in keiner Quelldatei. Der Hinweis ist, dass diese Daten manchmal live sind und die Nummern offensichtlich von einem Remote-Server aktualisiert werden. Der HTML-Code auf meinem PC hat einfach eine Lücke, in der die Server alle interessanten Daten durchsuchen, die ich benötige.

Jetzt sind meine Erfahrungen mit dynamischen Webinhalten gering, daher habe ich Probleme, diesen Kopf herumzukriegen.

Ich denke, Java oder Javascript ist ein Schlüssel, der oft auftaucht.

Der Schaber ist einfach eine Quotenvergleichsmaschine. Einige Websites haben APIs, aber ich brauche diese für diejenigen, die dies nicht tun. Ich verwende die Scrapy-Bibliothek mit Python 2.7

Ich entschuldige mich, wenn diese Frage zu offen ist. Kurz gesagt, meine Frage lautet: Wie kann Scrapy verwendet werden, um diese dynamischen Daten zu kratzen, damit ich sie verwenden kann? Damit ich diese Wettquoten-Daten in Echtzeit kratzen kann?


1
Wie kann ich diese Daten erhalten, die dynamisch und lebendig sind?
Joseph

1
Wenn Ihre Seite Javascript hat, versuchen Sie dies
reclosedev

3
Probieren Sie einige FirefoxErweiterungen wie httpFoxoder aus liveHttpHeadersund laden Sie eine Seite, die eine Ajax-Anfrage verwendet. Scrapy identifiziert die Ajax-Anforderungen nicht automatisch. Sie müssen manuell nach der entsprechenden Ajax-URL suchen und diese dann anfordern.
Aamir Adnan

Prost, ich werde den Firefox-Erweiterungen einen Wizz geben
Joseph

Es gibt eine Reihe von Open Source-Lösungen. Wenn Sie jedoch nach einer einfachen und schnellen Möglichkeit suchen, dies insbesondere für große Workloads zu tun, lesen Sie SnapSearch ( snapsearch.io ). Es wurde für JS-, HTML5- und SPA-Sites entwickelt, die eine Crawling-Fähigkeit für Suchmaschinen erfordern. Probieren Sie die Demo aus (wenn leerer Inhalt vorhanden ist, bedeutet dies, dass die Site tatsächlich keinen Textinhalt zurückgegeben hat, was möglicherweise eine 301-Weiterleitung bedeutet).
CMCDragonkai

Antworten:


74

Webkit-basierte Browser (wie Google Chrome oder Safari) verfügen über integrierte Entwicklertools. In Chrome können Sie es öffnen Menu->Tools->Developer Tools. Auf der NetworkRegisterkarte können Sie alle Informationen zu jeder Anfrage und Antwort anzeigen:

Geben Sie hier die Bildbeschreibung ein

Im unteren Bereich des Bildes sehen Sie, dass ich die Anfrage bis auf gefiltert habe XHR- dies sind Anfragen, die per Javascript-Code gestellt wurden.

Tipp: Das Protokoll wird jedes Mal gelöscht, wenn Sie eine Seite laden. Am unteren Rand des Bildes behält die schwarze Punktschaltfläche das Protokoll bei.

Nach der Analyse von Anforderungen und Antworten können Sie diese Anforderungen von Ihrem Webcrawler simulieren und wertvolle Daten extrahieren. In vielen Fällen ist es einfacher, Ihre Daten abzurufen, als HTML zu analysieren, da diese Daten keine Präsentationslogik enthalten und für den Zugriff mit Javascript-Code formatiert sind.

Firefox hat eine ähnliche Erweiterung, heißt es Firebug . Einige werden argumentieren, dass Firebug noch mächtiger ist, aber ich mag die Einfachheit des Webkits.


141
Wie zum Teufel kann dies eine akzeptierte Antwort sein, wenn es nicht einmal das Wort "Scrapy" enthält?
Toolkit

Es funktioniert und es ist einfach, mit dem json-Modul in Python zu analysieren. Es ist eine Lösung! Versuchen Sie im Vergleich dazu, Selen oder andere Dinge zu verwenden, die von den Leuten vorgeschlagen werden. Es sind mehr Kopfschmerzen. Wenn die alternative Methode viel komplizierter wäre, würde ich sie Ihnen geben, aber hier ist es nicht der Fall @Toolkit
Arion_Miles

1
Das ist nicht wirklich relevant. Die Frage war, wie man Scarpy verwendet, um dynamische Websites zu kratzen.
E. Erfan

"Wie zum Teufel kann das eine akzeptierte Antwort sein?" - Weil der praktische Gebrauch die politische Korrektheit übertrifft. Menschen verstehen KONTEXT.
Espresso

98

Hier ist ein einfaches Beispiel für scrapyeine AJAX-Anfrage. Schauen wir uns die Seite rubin-kazan.ru an .

Alle Nachrichten werden mit einer AJAX-Anfrage geladen. Mein Ziel ist es, diese Nachrichten mit all ihren Attributen (Autor, Datum, ...) abzurufen:

Geben Sie hier die Bildbeschreibung ein

Wenn ich den Quellcode der Seite analysiere, werden nicht alle diese Nachrichten angezeigt, da die Webseite die AJAX-Technologie verwendet. Aber ich kann mit Firebug von Mozilla Firefox (oder einem gleichwertigen Tool in anderen Browsern) die HTTP-Anforderung analysieren, die die Nachrichten auf der Webseite generiert:

Geben Sie hier die Bildbeschreibung ein

Es wird nicht die gesamte Seite neu geladen, sondern nur die Teile der Seite, die Nachrichten enthalten. Zu diesem Zweck klicke ich unten auf eine beliebige Anzahl von Seiten:

Geben Sie hier die Bildbeschreibung ein

Und ich beobachte die HTTP-Anfrage, die für den Nachrichtentext verantwortlich ist:

Geben Sie hier die Bildbeschreibung ein

Nach Abschluss analysiere ich die Header der Anfrage (ich muss zitieren, dass diese URL von der Quellseite aus dem Abschnitt var extrahiert wird, siehe den folgenden Code):

Geben Sie hier die Bildbeschreibung ein

Und der Formulardateninhalt der Anfrage (die HTTP-Methode ist "Post"):

Geben Sie hier die Bildbeschreibung ein

Und der Inhalt der Antwort, bei der es sich um eine JSON-Datei handelt:

Geben Sie hier die Bildbeschreibung ein

Welches alle Informationen präsentiert, die ich suche.

Von jetzt an muss ich all dieses Wissen in Scrapy umsetzen. Definieren wir die Spinne für diesen Zweck:

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

In parseFunktion habe ich die Antwort auf die erste Anfrage. In habe RubiGuessItemich die JSON-Datei mit allen Informationen.


6
Hallo. Könnten Sie bitte erklären, was 'url_list_gb_messages' ist? Ich kann es nicht verstehen Vielen Dank.
polarisieren

4
Dieser ist definitiv besser.
1a1a11a

1
@polarise Dieser Code verwendet das reModul (reguläre Ausdrücke), sucht nach der Zeichenfolge 'url_list_gb_messages="(.*)"'und isoliert den Inhalt von Klammern in der gleichnamigen Variablen. Dies ist eine nette Einführung: guru99.com/python-regular-expressions-complete-tutorial.html
MGP

42

Beim Crawlen treten häufig Probleme auf, bei denen Inhalte, die auf der Seite gerendert werden, mit Javascript generiert werden und Scrapy daher nicht in der Lage ist, danach zu crawlen (z. B. Ajax-Anforderungen, jQuery-Verrücktheit).

Wenn Sie jedoch Scrapy zusammen mit dem Webtest-Framework Selenium verwenden, können wir alles crawlen, was in einem normalen Webbrowser angezeigt wird.

Einige Dinge zu beachten:

  • Sie müssen die Python-Version von Selenium RC installiert haben, damit dies funktioniert, und Sie müssen Selenium ordnungsgemäß eingerichtet haben. Auch dies ist nur ein Template-Crawler. Man könnte mit Dingen viel verrückter und fortgeschrittener werden, aber ich wollte nur die Grundidee zeigen. Nach dem derzeitigen Stand des Codes werden Sie zwei Anfragen für eine bestimmte URL stellen. Eine Anfrage wird von Scrapy gestellt und die andere von Selen. Ich bin mir sicher, dass es Möglichkeiten gibt, dies zu umgehen, damit Sie Selenium möglicherweise nur dazu bringen können, die einzige Anfrage zu stellen, aber ich habe mich nicht darum gekümmert, dies zu implementieren, und wenn Sie zwei Anfragen ausführen, können Sie die Seite auch mit Scrapy crawlen.

  • Dies ist sehr leistungsfähig, da Sie jetzt das gesamte gerenderte DOM zum Crawlen zur Verfügung haben und weiterhin alle nützlichen Crawling-Funktionen in Scrapy verwenden können. Dies führt natürlich zu einem langsameren Crawlen, aber je nachdem, wie viel Sie für das gerenderte DOM benötigen, lohnt sich das Warten möglicherweise.

    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

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


Ordentliche Lösung! Haben Sie Tipps zum Verbinden dieses Skripts mit Firefox? (Betriebssystem ist Linux Mint). Ich erhalte die Meldung "[Errno 111] Verbindung abgelehnt".
Andrew

Dieser Code funktioniert nicht mehr für selenium=3.3.1und python=2.7.10, Fehler beim Importieren von Selen aus Selen
Benjamin

In dieser Version von Selen würde Ihre Import - Anweisung sein: from selenium import webdriveroder chromedriveroder was auch immer passieren Sie verwenden werden. Docs EDIT: Dokumentationsreferenz hinzufügen und meine schreckliche Grammatik ändern!
Nulltron

Selenium Remote Control wurde von Selen WebDriver nach ersetzt ihrer Website
rainbowsorbet

33

Eine andere Lösung wäre die Implementierung eines Download-Handlers oder einer Download-Handler-Middleware. (siehe scrapy docs für weitere Informationen zu Download - Middleware) Nachstehend ist ein Beispiel - Klasse Selen mit Maden PhantomJS WebDriver:

1) Definieren Sie die Klasse im middlewares.pySkript.

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) In der JsDownload()Klasse der Variablen DOWNLOADER_MIDDLEWAREinnerhalb settings.py:

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

3) Integriere das HTMLResponseInnere your_spider.py. Durch Dekodieren des Antwortkörpers erhalten Sie die gewünschte Ausgabe.

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

Optionales Addon:
Ich wollte die Möglichkeit haben, verschiedenen Spinnen mitzuteilen, welche Middleware verwendet werden soll, also habe ich diesen Wrapper implementiert:

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

Damit der Wrapper funktioniert, müssen alle Spinnen mindestens Folgendes haben:

middleware = set([])

um eine Middleware einzuschließen:

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

Vorteil:
Der Hauptvorteil der Implementierung auf diese Weise und nicht in der Spinne besteht darin, dass Sie am Ende nur eine Anfrage stellen. In der Lösung von AT zum Beispiel: Der Download-Handler verarbeitet die Anforderung und gibt dann die Antwort an die Spinne weiter. Die Spinne stellt dann in ihrer Funktion parse_page eine brandneue Anfrage - das sind zwei Anfragen für denselben Inhalt.


Ich war ziemlich spät
dran

@ rocktheartsm4l was ist falsch daran, nur in process_requests, if spider.name in ['spider1', 'spider2']anstelle des Dekorateurs zu verwenden
Pad

@pad Daran ist nichts auszusetzen. Ich fand es gerade klarer für meine Spinnenklassen, ein Set namens Middleware zu haben. Auf diese Weise konnte ich mir jede Spinnenklasse ansehen und genau sehen, welche Middlewares dafür ausgeführt würden. In meinem Projekt war viel Middleware implementiert, daher machte dies Sinn.
Rocktheartsm4l

Dies ist eine schreckliche Lösung. Es ist nicht nur nicht mit Scrapy verbunden, sondern der Code selbst ist äußerst ineffizient, und der gesamte Ansatz macht im Allgemeinen den gesamten Zweck des
Scrapy-

2
Es ist viel effizienter als jede andere Lösung, die ich bei SO gesehen habe, da die Verwendung einer Downloader-Middleware dazu führt, dass nur eine Anfrage für die Seite gestellt wird. Wenn es so schrecklich ist, warum finden Sie keine bessere Lösung und teilen sie stattdessen eklatant einseitige Behauptungen aufstellen. "Nicht verwandt mit Scrapy" rauchst du etwas? Abgesehen von der Implementierung einer verrückten komplexen, robusten und benutzerdefinierten Lösung ist dies der Ansatz, den die meisten Leute verwendet haben. Der einzige Unterschied ist, dass die meisten den Selen-Teil in der Spinne implementieren, was dazu führt, dass mehrere Anfragen gestellt werden ...
rocktheartsm4l

10

Ich habe eine benutzerdefinierte Downloader-Middleware verwendet, war aber nicht sehr zufrieden damit, da ich es nicht geschafft habe, den Cache damit zum Laufen zu bringen.

Ein besserer Ansatz war die Implementierung eines benutzerdefinierten Download-Handlers.

Es ist ein funktionierendes Beispiel hier . Es sieht aus wie das:

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

Angenommen, Ihr Schaber heißt "Schaber". Wenn Sie den genannten Code in eine Datei mit dem Namen handlers.py im Stammverzeichnis des Ordners "scraper" einfügen, können Sie Folgendes zu Ihrer settings.py hinzufügen:

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

Und voilà, das JS analysierte DOM mit Scrapy-Cache, Wiederholungsversuchen usw.


Ich mag diese Lösung!
Rocktheartsm4l

Schöne Lösung. Ist der Selenium-Treiber immer noch die einzige Option?
Motheus

Tolle Lösung. Vielen Dank.
CrazyGeek

4

Wie kann Scrapy verwendet werden, um diese dynamischen Daten zu kratzen, damit ich sie verwenden kann?

Ich frage mich, warum niemand die Lösung nur mit Scrapy veröffentlicht hat.

Schauen Sie sich den Blog-Beitrag des Scrapy-Teams SCRAPING INFINITE SCROLLING PAGES an . Das Beispiel verschrottet die Website http://spidyquotes.herokuapp.com/scroll, die unendliches Scrollen verwendet.

Die Idee ist, die Entwicklertools Ihres Browsers zu verwenden und die AJAX-Anforderungen zu beachten. Erstellen Sie dann basierend auf diesen Informationen die Anforderungen für 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)

Wir stehen wieder vor dem gleichen Problem: Scrappy wird nicht für diesen Zweck hergestellt, und hier werden wir mit demselben Problem konfrontiert. Fahren Sie mit phantomJS fort oder erstellen Sie, wie von anderen vorgeschlagen, Ihre eigene Download-Middleware
rak007

@ rak007 PhantomJS vs Chrome Treiber. Welches würdest du vorschlagen?
Chankey Pathak

2

Ja, Scrapy kann dynamische Websites verschrotten, Websites, die über JavaScript gerendert werden.

Es gibt zwei Ansätze, um diese Art von Websites zu verschrotten.

Zuerst,

Sie können splashJavascript-Code rendern und dann den gerenderten HTML-Code analysieren. Das Dokument und das Projekt finden Sie hier Scrapy Splash, Git

Zweite,

Wie jeder sagt, können Sie durch Überwachen des network calls, ja, den API-Aufruf finden, der die Daten abruft, und den Spott, den der Aufruf in Ihrer Scrapy-Spinne verspottet, um die gewünschten Daten zu erhalten.


1

Ich bearbeite die Ajax-Anfrage mit Selenium und dem Firefox-Webtreiber. Es ist nicht so schnell, wenn Sie den Crawler als Daemon benötigen, aber viel besser als jede manuelle Lösung. Ich schrieb ein kurzes Tutorial hier als Referenz

Durch die Nutzung unserer Website bestätigen Sie, dass Sie unsere Cookie-Richtlinie und Datenschutzrichtlinie gelesen und verstanden haben.
Licensed under cc by-sa 3.0 with attribution required.