Estoy usando Scrapy para raspar y descargar imágenes. Me gustaría guardar los archivos en Amazon S3 además del sistema de archivos.

No tengo problemas para configurar ninguno de ellos, pero ¿alguien sabe una forma de configurar ambos al mismo tiempo para que los archivos se guarden en una carpeta local y AWS S3 en Scrapy?

-2
mierzwin 26 jun. 2020 a las 01:00

2 respuestas

Se me ocurrió la siguiente solución para guardar el mismo archivo dos veces desde una solicitud GET. En settings.py utilicé las siguientes entradas:

ITEM_PIPELINES = {
    'salvage.pipelines.MyItemsPipeline': 300,
    'salvage.pipelines.MyDualImagesPipeline': 310,
}

IMAGES_STORE = 's3://xxxxxxxxxxxxxxxx/'
AWS_ENDPOINT_URL = 'https://xxx.xxxxxxxxxx.xxxxxxxx.com'
AWS_ACCESS_KEY_ID = 'xxxxxxxxxxxx'
AWS_SECRET_ACCESS_KEY = 'xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx'

IMAGES_STORE_SECONDARY = '/some/path/to/folder/'

Entonces es mi Pipeline personalizado dentro de pipelines.py sobrescribí un método:

from scrapy.utils.project import get_project_settings
from scrapy.pipelines.images import ImagesPipeline
from scrapy.utils.misc import md5sum


project_settings = get_project_settings()


class DualSaveImagesPipeline(ImagesPipeline):

    def __init__(self, store_uri, download_func=None, settings=None):
        super().__init__(store_uri, settings=settings,
                         download_func=download_func)
        self.store_secondary = self._get_store(
            project_settings.get('IMAGES_STORE_SECONDARY'))

    def image_downloaded(self, response, request, info):
        checksum = None
        for path, image, buf in self.get_images(response, request, info):
            if checksum is None:
                buf.seek(0)
                checksum = md5sum(buf)
            width, height = image.size
            self.store.persist_file(
                path, buf, info,
                meta={'width': width, 'height': height},
                headers={'Content-Type': 'image/jpeg'})
            self.store_secondary.persist_file(
                path, buf, info,
                meta={'width': width, 'height': height},
                headers={'Content-Type': 'image/jpeg'})
        return checksum

Esto hizo un truco para mí. Compartir si alguien encuentra los mismos requisitos en su proyecto.

1
mierzwin 26 jun. 2020 a las 14:49

Lo probé solo con dos carpetas locales first, second pero debería funcionar con otros lugares.

Normalmente, puede establecer un solo valor en IMAGES_STORE, así que creé dos Pipeline s con diferentes configuraciones directamente en clase. Reemplazo store_uri en __init__ para enviar a un lugar diferente.

class FirstImagesPipeline(ImagesPipeline):
    def __init__(self, store_uri, download_func=None, settings=None):
        store_uri = 'first'   # local folder which has to exists
        super().__init__(store_uri, download_func, settings)

class SecondImagesPipeline(ImagesPipeline):
    def __init__(self, store_uri, download_func=None, settings=None):
        store_uri = 'second'  # local folder which has to exists
        #store_uri = 's3://bucket/images'
        super().__init__(store_uri, download_func, settings)

Y usarlas ambas

'ITEM_PIPELINES': {
    'FirstImagesPipeline': 1,
    'SecondImagesPipeline': 2,
}

Y guardan la misma imagen en dos carpetas locales first/full y second/full.


Por cierto: en la documentación en ejemplo de uso descubrí que puedo establecer diferentes configuraciones para diferentes Pipelines usando el nombre de la canalización como prefijo FIRSTIMAGESPIPELINE_ y SECONDIMAGESPIPELINE_

FIRSTIMAGESPIPELINE_IMAGES_URLS_FIELD = ...
SECONDIMAGESPIPELINE_IMAGES_URLS_FIELD = ...

FIRSTIMAGESPIPELINE_FILES_EXPIRES = ...
SECONDIMAGESPIPELINE_FILES_EXPIRES = ...

Pero parece que no funciona para IMAGES_STORE


Código de trabajo mínimo que puede poner en un archivo y ejecutar python script.py

Descarga imágenes de http://books.toscrape.com/ que fue creado por los autores de Scrapy como lugar para Aprendizaje de raspado.

import scrapy
from scrapy.pipelines.images import ImagesPipeline

class MySpider(scrapy.Spider):

    name = 'myspider'

    # see page created for scraping: http://toscrape.com/
    start_urls = ['http://books.toscrape.com/'] #'http://quotes.toscrape.com']

    def parse(self, response):
        print('url:', response.url)

        # download images and convert to JPG (even if it is already JPG)
        for url in response.css('img::attr(src)').extract():
            url = response.urljoin(url)
            yield {'image_urls': [url], 'session_path': 'hello_world'}

class FirstImagesPipeline(ImagesPipeline):
    def __init__(self, store_uri, download_func=None, settings=None):
        #print('FirstImagesPipeline:', store_uri)
        print('FirstImagesPipeline:', settings)
        store_uri = 'first'
        super().__init__(store_uri, download_func, settings)

class SecondImagesPipeline(ImagesPipeline):
    def __init__(self, store_uri, download_func=None, settings=None):
        #print('SecondImagesPipeline:', store_uri)
        store_uri = 'second'
        store_uri = 's3://bucket/images'
        super().__init__(store_uri, download_func, settings)

# --- run without project and save in `output.csv` ---

from scrapy.crawler import CrawlerProcess

c = CrawlerProcess({
    'USER_AGENT': 'Mozilla/5.0',

    # download images to `IMAGES_STORE/full` (standard folder) and convert to JPG (even if it is already JPG)
    # it needs `yield {'image_urls': [url]}` in `parse()` and both ITEM_PIPELINES and IMAGES_STORE to work
    'ITEM_PIPELINES': {
        '__main__.FirstImagesPipeline': 1,
        '__main__.SecondImagesPipeline': 2,
    },            # used Pipeline create in current file (needs __main___)
    
#    'IMAGES_STORE': 'test',  # normally you use this folder has to exist before downloading
})

c.crawl(MySpider)
c.start() 

EDITAR: siempre puede usar el estándar ImagePipeline en lugar de uno modificado.

'ITEM_PIPELINES': {
    'ImagesPipeline': 1,        # standard ImagePipeline
    'SecondImagesPipeline': 2,  # modified ImagePipeline
}

IMAGE_STORE = 'first'  # setting for standard ImagePipeline
0
furas 26 jun. 2020 a las 09:55