Wie kann ich in einem Scrapy-Projekt verschiedene Pipelines für verschiedene Spinnen verwenden?


82

Ich habe ein Scrapy-Projekt, das mehrere Spinnen enthält. Kann ich auf irgendeine Weise definieren, welche Pipelines für welche Spinne verwendet werden sollen? Nicht alle von mir definierten Pipelines gelten für jede Spinne.

Vielen Dank


2
Vielen Dank für Ihre sehr gute Frage. Bitte wählen Sie eine Antwort für alle zukünftigen Googler. Die Antwort von mstringer hat bei mir sehr gut funktioniert.
Symbiotech

Antworten:


35

Aufbauend auf der Lösung von Pablo Hoffman können Sie den folgenden Dekorator für die process_itemMethode eines Pipeline-Objekts verwenden, damit das pipelineAttribut Ihrer Spinne überprüft wird, ob es ausgeführt werden soll oder nicht. Beispielsweise:

def check_spider_pipeline(process_item_method):

    @functools.wraps(process_item_method)
    def wrapper(self, item, spider):

        # message template for debugging
        msg = '%%s %s pipeline step' % (self.__class__.__name__,)

        # if class is in the spider's pipeline, then use the
        # process_item method normally.
        if self.__class__ in spider.pipeline:
            spider.log(msg % 'executing', level=log.DEBUG)
            return process_item_method(self, item, spider)

        # otherwise, just return the untouched item (skip this step in
        # the pipeline)
        else:
            spider.log(msg % 'skipping', level=log.DEBUG)
            return item

    return wrapper

Damit dieser Dekorator ordnungsgemäß funktioniert, muss die Spinne über ein Pipeline-Attribut mit einem Container der Pipeline-Objekte verfügen, mit denen Sie das Element verarbeiten möchten. Beispiel:

class MySpider(BaseSpider):

    pipeline = set([
        pipelines.Save,
        pipelines.Validate,
    ])

    def parse(self, response):
        # insert scrapy goodness here
        return item

Und dann in einer pipelines.pyDatei:

class Save(object):

    @check_spider_pipeline
    def process_item(self, item, spider):
        # do saving here
        return item

class Validate(object):

    @check_spider_pipeline
    def process_item(self, item, spider):
        # do validating here
        return item

Alle Pipeline-Objekte sollten weiterhin in den Einstellungen in ITEM_PIPELINES definiert sein (in der richtigen Reihenfolge - wäre schön zu ändern, damit die Reihenfolge auch auf dem Spider angegeben werden kann).


Ich versuche, Ihre Art des Wechsels zwischen Pipelines zu implementieren, erhalte jedoch NameError! Ich bekomme Pipelines ist nicht definiert. Haben Sie diesen Code selbst getestet? könntest du mir helfen?
mehdix_

. @ mehdix_ ja, es funktioniert bei mir. Woher bekommst du einen NameError?
Mstringer

Der Fehler tritt direkt nach dem scrapy crawl <spider name>Befehl auf. Python erkennt die Namen, die ich in der Spider-Klasse festgelegt habe, nicht, damit Pipelines ausgeführt werden können. Ich werde Ihnen Links zu meiner spider.py und Pipeline.py geben damit Sie einen Blick darauf werfen können. Danke
mehdix_

1
Danke für die Klarstellung. Wohin geht das erste Code-Snippet? irgendwo am Ende desspider.py rechts?
mehdix_

1
Ich habe die Bedingung so bearbeitet, dass sie bei bereits definierten Spinnen, für die keine Pipeline festgelegt ist, nicht fehlschlägt. Dadurch werden standardmäßig auch alle Pipelines ausgeführt, sofern nicht anders angegeben. if not hasattr(spider, 'pipeline') or self.__class__ in spider.pipeline:
Nour Wolf

134

Entfernen Sie einfach alle Pipelines aus den Haupteinstellungen und verwenden Sie diese innere Spinne.

Dadurch wird die Pipeline zum Benutzer pro Spinne definiert

class testSpider(InitSpider):
    name = 'test'
    custom_settings = {
        'ITEM_PIPELINES': {
            'app.MyPipeline': 400
        }
    }

3
für den, der sich fragt, was die '400' ist? wie ich - FROM THE DOC - "Die ganzzahligen Werte, die Sie Klassen in dieser Einstellung zuweisen, bestimmen die Reihenfolge, in der sie ausgeführt werden: Elemente werden von Klassen mit niedrigerem Wert zu Klassen mit höherem Wert durchlaufen. Es ist üblich, diese Zahlen im Bereich von 0 bis 1000 zu definieren." - docs.scrapy.org/en/latest/topics/item-pipeline.html
brainLoop

2
Nicht sicher, warum dies nicht die akzeptierte Antwort ist, funktioniert perfekt, viel sauberer und einfacher als akzeptierte Antwort. Genau das habe ich gesucht. Arbeitet immer noch in Scrapy 1.8
Eric F

1
Gerade in Scrapy 1.6 eingecheckt. Es ist nicht erforderlich, die Pipeline-Einstellungen in settings.py zu entfernen. custom_settings in der Spider überschreiben die Pipeline-Einstellungen in settings.py.
Graham Monkman

Funktioniert perfekt für mein Szenario!
Mark Kamyszek

12

Die anderen Lösungen hier gegeben sind gut, aber ich denke , sie langsam sein könnte, weil wir nicht wirklich sind nicht die Pipeline pro Spinne verwenden, anstatt prüfen wir , ob eine Pipeline zurückgegeben jedes Mal ein Element vorhanden ist (und in einigen Fällen könnte dies erreichen Millionen).

Eine gute Möglichkeit, eine Funktion pro Spinne vollständig zu deaktivieren (oder zu aktivieren), ist die Verwendung custom_settingund from_crawlerfür alle Erweiterungen wie diese:

pipelines.py

from scrapy.exceptions import NotConfigured

class SomePipeline(object):
    def __init__(self):
        pass

    @classmethod
    def from_crawler(cls, crawler):
        if not crawler.settings.getbool('SOMEPIPELINE_ENABLED'):
            # if this isn't specified in settings, the pipeline will be completely disabled
            raise NotConfigured
        return cls()

    def process_item(self, item, spider):
        # change my item
        return item

settings.py

ITEM_PIPELINES = {
   'myproject.pipelines.SomePipeline': 300,
}
SOMEPIPELINE_ENABLED = True # you could have the pipeline enabled by default

spider1.py

class Spider1(Spider):

    name = 'spider1'

    start_urls = ["http://example.com"]

    custom_settings = {
        'SOMEPIPELINE_ENABLED': False
    }

Während Sie dies überprüfen, haben wir angegeben, custom_settingsdass die in angegebenen Elemente überschrieben werden settings.py, und wir deaktivierenSOMEPIPELINE_ENABLED diese Spinne.

Wenn Sie diese Spinne ausführen, überprüfen Sie Folgendes:

[scrapy] INFO: Enabled item pipelines: []

Jetzt hat Scrapy die Pipeline vollständig deaktiviert und sich nicht um ihre Existenz für den gesamten Lauf gekümmert. Überprüfen Sie, ob dies auch für Scrapy extensionsund funktioniert middlewares.


11

Ich kann mir mindestens vier Ansätze vorstellen:

  1. Verwenden Sie ein anderes Scrapy-Projekt pro Satz Spinnen + Pipelines (kann angemessen sein, wenn Ihre Spinnen unterschiedlich genug sind, um in verschiedenen Projekten zu sein).
  2. Ändern Sie in der Befehlszeile des Scrapy-Tools die Pipeline-Einstellung scrapy settingszwischen jedem Aufruf Ihrer Spinne
  3. Isolieren Sie Ihre Spinnen in ihre eigenen Scrapy-Tool-Befehle und definieren Sie die default_settings['ITEM_PIPELINES']für Ihre Befehlsklasse in der Pipeline-Liste, die Sie für diesen Befehl benötigen. Siehe Zeile 6 dieses Beispiels .
  4. process_item()Überprüfen Sie in den Pipeline-Klassen selbst, gegen welche Spinne sie ausgeführt wird, und tun Sie nichts, wenn sie für diese Spinne ignoriert werden soll. Sehen Sie sich das Beispiel an, in dem Ressourcen pro Spinne verwendet werden , um den Einstieg zu erleichtern. (Dies scheint eine hässliche Lösung zu sein, da sie Spinnen und Gegenstands-Pipelines eng miteinander verbindet. Sie sollten diese wahrscheinlich nicht verwenden.)

Vielen Dank für Ihre Antwort. Ich habe Methode 1 verwendet, aber ich denke, ein Projekt ist sauberer und ermöglicht mir, Code wiederzuverwenden. Können Sie bitte näher auf Methode 3 eingehen? Wie würde ich Spinnen in ihre eigenen Werkzeugbefehle isolieren?
CodeMonkeyB

Laut dem Link auf einer anderen Antwort können Sie Pipelines nicht überschreiben, also würde Nummer 3 wohl nicht funktionieren.
Daniel Bang


11

Sie können das nameAttribut der Spinne in Ihrer Pipeline verwenden

class CustomPipeline(object)

    def process_item(self, item, spider)
         if spider.name == 'spider1':
             # do something
             return item
         return item

Wenn Sie alle Pipelines auf diese Weise definieren, können Sie das erreichen, was Sie wollen.


4

Sie können die Einstellungen für Elementpipelines in der Spinne einfach wie folgt festlegen:

class CustomSpider(Spider):
    name = 'custom_spider'
    custom_settings = {
        'ITEM_PIPELINES': {
            '__main__.PagePipeline': 400,
            '__main__.ProductPipeline': 300,
        },
        'CONCURRENT_REQUESTS_PER_DOMAIN': 2
    }

Ich kann dann eine Pipeline aufteilen (oder sogar mehrere Pipelines verwenden), indem ich dem Lader / zurückgegebenen Element einen Wert hinzufüge, der angibt, über welchen Teil der Spinne Elemente gesendet wurden. Auf diese Weise erhalte ich keine KeyError-Ausnahmen und weiß, welche Elemente verfügbar sein sollten.

    ...
    def scrape_stuff(self, response):
        pageloader = PageLoader(
                PageItem(), response=response)

        pageloader.add_xpath('entire_page', '/html//text()')
        pageloader.add_value('item_type', 'page')
        yield pageloader.load_item()

        productloader = ProductLoader(
                ProductItem(), response=response)

        productloader.add_xpath('product_name', '//span[contains(text(), "Example")]')
        productloader.add_value('item_type', 'product')
        yield productloader.load_item()

class PagePipeline:
    def process_item(self, item, spider):
        if item['item_type'] == 'product':
            # do product stuff

        if item['item_type'] == 'page':
            # do page stuff

1
Dies sollte die akzeptierte Antwort sein. Flexibler und weniger umständlich
Ben Wilson

1

Einfache aber dennoch nützliche Lösung.

Spinnencode

    def parse(self, response):
        item = {}
        ... do parse stuff
        item['info'] = {'spider': 'Spider2'}

Pipeline-Code

    def process_item(self, item, spider):
        if item['info']['spider'] == 'Spider1':
            logging.error('Spider1 pipeline works')
        elif item['info']['spider'] == 'Spider2':
            logging.error('Spider2 pipeline works')
        elif item['info']['spider'] == 'Spider3':
            logging.error('Spider3 pipeline works')

Hoffe das spart etwas Zeit für jemanden!


0

Ich verwende zwei Pipelines, eine zum Herunterladen von Bildern (MyImagesPipeline) und eine zum Speichern von Daten in Mongodb (MongoPipeline).

Angenommen, wir haben viele Spinnen (Spider1, Spider2, ...........). In meinem Beispiel können Spider1 und Spider5 MyImagesPipeline nicht verwenden

settings.py

ITEM_PIPELINES = {'scrapycrawler.pipelines.MyImagesPipeline' : 1,'scrapycrawler.pipelines.MongoPipeline' : 2}
IMAGES_STORE = '/var/www/scrapycrawler/dowload'

Und unten vollständiger Code der Pipeline

import scrapy
import string
import pymongo
from scrapy.pipelines.images import ImagesPipeline

class MyImagesPipeline(ImagesPipeline):
    def process_item(self, item, spider):
        if spider.name not in ['spider1', 'spider5']:
            return super(ImagesPipeline, self).process_item(item, spider)
        else:
           return item 

    def file_path(self, request, response=None, info=None):
        image_name = string.split(request.url, '/')[-1]
        dir1 = image_name[0]
        dir2 = image_name[1]
        return dir1 + '/' + dir2 + '/' +image_name

class MongoPipeline(object):

    collection_name = 'scrapy_items'
    collection_url='snapdeal_urls'

    def __init__(self, mongo_uri, mongo_db):
        self.mongo_uri = mongo_uri
        self.mongo_db = mongo_db

    @classmethod
    def from_crawler(cls, crawler):
        return cls(
            mongo_uri=crawler.settings.get('MONGO_URI'),
            mongo_db=crawler.settings.get('MONGO_DATABASE', 'scraping')
        )

    def open_spider(self, spider):
        self.client = pymongo.MongoClient(self.mongo_uri)
        self.db = self.client[self.mongo_db]

    def close_spider(self, spider):
        self.client.close()

    def process_item(self, item, spider):
        #self.db[self.collection_name].insert(dict(item))
        collection_name=item.get( 'collection_name', self.collection_name )
        self.db[collection_name].insert(dict(item))
        data = {}
        data['base_id'] = item['base_id']
        self.db[self.collection_url].update({
            'base_id': item['base_id']
        }, {
            '$set': {
            'image_download': 1
            }
        }, upsert=False, multi=True)
        return item

0

Wir können einige Bedingungen in der Pipeline als diese verwenden

    # -*- coding: utf-8 -*-
from scrapy_app.items import x

class SaveItemPipeline(object):
    def process_item(self, item, spider):
        if isinstance(item, x,):
            item.save()
        return item
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.