Aún no tienes acceso a esta clase

Crea una cuenta y continúa viendo este curso

Curso de Scrapy

Curso de Scrapy

Facundo García Martoni

Facundo García Martoni

Múltiples callbacks

14/27
Recursos

Aportes 20

Preguntas 5

Ordenar por:

¿Quieres ver más aportes, preguntas y respuestas de la comunidad? Crea una cuenta o inicia sesión.

Hasta el inicio de la clase me guardaba todo bien (haciendo un append de cada cosa) pero he visto 5 veces el vídeo, viendo qué me quedó mal, y sigue igual, simplemente ya no me guarda las quotes, me da el diccionario pero sin los quotes, sólo el título y las top ten tags

Resumen:
■■■■■■■

Este script tiene el propósito de lograr lo siguiente:

  1. Extraer de la primera página de quotestoscrape :
    • Authors
    • Quotes
    • Tags (by Quote)
  2. Visualización en shell de un dataframe de pandas de la data extraída de esta primera página.
  3. Extraer de las demás páginas del sitio solo las Quotes.
import scrapy
import pandas as pd


class Experiment(scrapy.Spider):
    name = 'Experiment_2'
    start_urls = ['...quotes.toscrape.com/page/1']
    custom_settings = {
        'FEEDS': {
            'quotes.json': {
                'format': 'json',
                'encoding': 'utf8',
                'store_empty': False,
                'fields': None,
                'indent': 4,
                'item_export_kwargs': {
                    'export_empty_fields': True,
                },
            },
        },
    }


    def parse_only_quotes(self, response,**kwargs):
        '''
        Allows to append results of scraped pages.

        
        '''
        if kwargs:
            authors = kwargs['Authors']
            quotes = kwargs['Quotes']
            tags = kwargs['Tags']

        authors.extend(response.xpath('//small[@class="author"]/text()').getall())    
        quotes.extend(response.xpath('//span[@class="text"]/text()').getall())
        tags.extend(response.xpath('//div[@class="quote"]/div[@class="tags"]/meta').getall())
        

        
        # Next link
        next_page = response.xpath('//li[@class="next"]/a/@href').get()

        if next_page:
            yield response.follow(next_page, callback= self.parse_only_quotes, cb_kwargs={'Authors': authors, 'Quotes': quotes, 'Tags': tags})
        else:
            yield{
                'Quotes': quotes
            }


    def parse(self,response):
        # Title Web page
        title = response.xpath('//h1/a/text()').get()

        # Quote
        authors = response.xpath('//small[@class="author"]/text()').getall()
        quotes = response.xpath('//span[@class="text"]/text()').getall()

        # Tags for Quote.
        # Scrape the raw meta tag. Since the tags content by quote change every quote use this aproach.
        raw_tags = response.xpath('//div[@class="quote"]/div[@class="tags"]/meta').getall()

        # Transformation to keep the tags for quote.
        tags =[]
        for string in raw_tags:
            tag = string.replace('<meta class="keywords" itemprop="keywords" content="','')
            tag = tag.replace('">','')
            tags.append(tag)

        # ■■■■■■ Visualization on shell the order of th data scraped.
        scraped_topandas = {'Authors': authors, 'Quotes': quotes, 'Tags': tags}
        data = pd.DataFrame(scraped_topandas)
        print(data)
        # ■■■■■■
        yield {'Title': title, 'Authors': authors, 'Quotes': quotes, 'Tags': tags}

        # Next link
        next_page = response.xpath('//li[@class="next"]/a/@href').get()
        if next_page:
            yield response.follow(next_page, callback= self.parse_only_quotes, cb_kwargs={'Authors': authors, 'Quotes': quotes, 'Tags': tags})

<h5>Notas:</h5>
  • Por buenas prácticas los procesos de transformación se pueden realizar en el archivo pipelines.py, es decir, una vez tiene los objetos scrapy los pasa allí para que puedas realizar la limpieza de la data que escrapeaste, la limpieza de los strings, fechas, validación de ausencia de links para manejar errores con objectos nativos tan solo hacer raise del objeto DropItem ej:

# Verifica la existencia de los valores a llenar.
class CheckItemPipeline:
   def process_item(self, article, spider):
       if not article['lastUpdate'] or not article['url'] or not article['title']:
           raise DropItem('Missing something!')
       return article


#Convierte a objeto datetime.
class CleanDatePipeline:
   def process_item(self, article, spider):
       article['lastUpdate'] = article['lastUpdate'].replace(' Esta página se editó por última vez el','').strip()
       article['lastUpdate'] = article['lastUpdate'].replace('a las','').strip()
       article['lastUpdate'] = article['lastUpdate'].replace('.','').strip()
       try:
           article['lastUpdate'] = datetime.strptime(article['lastUpdate'], '%d %B %Y')
       except ValueError as v:
           print(v)
       
       return article


  • En general es bueno mantener separadas las funcionalidades que hace tu código y evitar los acoplamientos, haciendo tu código más fácil de mantener. Es decir la configuración de como se exportará los archivos puede suceder en varias partes, pero para mantener separado el proceso de serialización con el de extracción, puedes configurar todo en settings.py ej:

# Controlo la salida del archivo

FEED_URI ="articles.csv"
FEED_FORMAT="csv"


# Con esto configuro mis procesos de transformación.

ITEM_PIPELINES = {
 'article_scraper.pipelines.CheckItemPipeline': 100,
 'article_scraper.pipelines.CleanDatePipeline': 200,
}

Me ayudo mucho para entender le concepto de callbacks haber tomado los cursos de la escuela de Javascript, nunca se casen con un solo lenguaje, incluso en la ruta de ciencia de datos es muy útil saber al menos lo básico de javascript.

Aqui una breve explicacion de MDN https://developer.mozilla.org/en-US/docs/Glossary/Callback_function

Adjunto mi codigo con el cual la estructura del json quedo

{
    "quotes": [
    ],
    "title": "",
    "top_ten_tags": [
    ]
}
import scrapy

"""
Title = //h1/a/text()
Quotes = //span[@class="text" and @itemprop="text"]/text()
Top Ten Tags = //div[contains(@class, "tags-box")]/span[@class="tag-item"]/a/text()
Next = //ul[@class="pager"]//li[@class="next"]/a/@href
"""


class QuotesSpider(scrapy.Spider):
    name = "quotes"
    start_urls = [
        "http://quotes.toscrape.com/page/1/"
    ]
    custom_settings = {
        "FEED_URI": "quotes.json",
        "FEED_FORMAT": "json"
    }

    def parse_only_quotes(self, response, **kwargs):
        new_quotes = response.xpath(
            '//span[@class="text" and @itemprop="text"]/text()').getall()
        kwargs["quotes"].extend(new_quotes)

        next_page_button_link = response.xpath(
            '//ul[@class="pager"]//li[@class="next"]/a/@href').get()
        if next_page_button_link:
            yield response.follow(
                next_page_button_link,
                callback=self.parse_only_quotes,
                cb_kwargs=kwargs
            )
        else:
            yield kwargs

    def parse(self, response):
        title = response.xpath('//h1/a/text()').get()

        quotes = response.xpath(
            '//span[@class="text" and @itemprop="text"]/text()').getall()

        tags = response.xpath(
            '//div[contains(@class, "tags-box")]/span[@class="tag-item"]/a/text()').getall()

        next_page_button_link = response.xpath(
            '//ul[@class="pager"]//li[@class="next"]/a/@href').get()
        if next_page_button_link:
            yield response.follow(
                next_page_button_link,
                callback=self.parse_only_quotes,
                cb_kwargs={
                    "title": title,
                    "quotes": quotes,
                    "top_ten_tags": tags,
                }
            )

A mi al principio no me funcionaba y era porque tenia la versión 1.6 de Scrapy, luego de actualizar con pip install --upgrade scrapy (ubuntu) funcionó y me trajo las quotes : )

import scrapy

class QuotesSpider(scrapy.Spider):
	name = 'quotes'
	start_urls = [
		'https://quotes.toscrape.com/'
	]

	custom_settings = {
		'FEED_URI': 'quotes.json',
		'FEED_FORMAT': 'json'
	}

	title = '//h1/a/text()'
	quotes = '//span[@class="text" and @itemprop="text"]/text()'
	top_ten_tags = '//div[contains(@class, "tags-box")]//span[@class="tag-item"]/a/text()'
	next_page_btn = '//ul[@class="pager"]//li[@class="next"]/a/@href'

	def parse_only_quotes(self, response, **kwargs):
		if kwargs:
			quotes = kwargs['quotes']

		quotes.extend(response.xpath(self.quotes).getall())

		next_page_btn = response.xpath(self.next_page_btn).get()
		if next_page_btn:
			yield response.follow(next_page_btn, callback=self.parse_only_quotes, cb_kwargs={'quotes': quotes})
		else:
			yield {
				'quotes': quotes
			}

	def parse(self, response):
		print(response.status, response.headers)
		
		title = response.xpath(self.title).get()

		quotes = response.xpath(self.quotes).getall()

		top_ten_tags = response.xpath(self.top_ten_tags).getall()

		yield {
			'title': title,
			'top_ten_tags': top_ten_tags

		}

		next_page_btn = response.xpath(self.next_page_btn).get()
		if next_page_btn:
			yield response.follow(next_page_btn, callback=self.parse_only_quotes, cb_kwargs={'quotes': quotes})


Vi las clase más de 8 veces no logré que me funcionara, solo encontré la solución entre los comentarios de los compañeros 😦

custom_settings = { #para guardar el resultado en un archivo json
’FEEDS’: {
‘items.json’: {
‘format’: ‘json’,
‘encoding’: ‘utf8’,
‘store_empty’: False,
‘fields’: None,
‘indent’: 4,
‘item_export_kwargs’: {
‘export_empty_fields’: True,
}
}
}
}

asi funciona de nuevo

ScrapyDeprecationWarning: The FEED_URI and FEED_FORMAT settings have been deprecated in favor of the FEEDS setting. Please see the FEEDS setting docs for more details
exporter = cls(crawler)

Ya no me guarda el json. En la consola todo bien.

Una de las mayores ventajas de Scrapy es la velocidad . Dado que es asincrónico, las arañas Scrapy no tienen que esperar para realizar solicitudes una a la vez, pero pueden realizar solicitudes en paralelo. Esto aumenta la eficiencia, lo que hace que la memoria Scrapy y la CPU sean eficientes en comparación con las herramientas de web scraping analizadas anteriormente.

XPath son las siglas de XML Path Language. Utiliza una sintaxis no XML para proporcionar una forma flexible de direccionar (señalar) diferentes partes de un documento XML . También se puede utilizar para probar los nodos direccionados dentro de un documento para determinar si coinciden con un patrón o no.

Me funciono a la perfección todo el código, solo tuve que reveer la clase y darme cuenta de unos errores que tenia, dejo adjunto el código por que quizá a alguien le sirve.

import scrapy

# Titulo = //h1/a/text()
# Citas = //span[@class="text" and @itemprop="text"]/text()
# Top ten tags = //div[contains/@class, "tags-box"]//span[@class="tag-item"]a/text()
# Next Page Button = //ul[@class="pager"]//li[@class="next"]/a/@href


class QuotesSpider(scrapy.Spider):
    name = 'quotes'
    start_urls = [
        '//quotes.toscrape.com/page/1/'
    ]
    custom_settings = {
        'FEED_URI': 'quotes.json',
        'FEED_FORMAT': 'json'
    }

    def parse_only_quotes(self, response, **kwargs):
        if kwargs:
            quotes = kwargs['quotes']
        quotes.extend(response.xpath(
            '//span[@class="text" and @itemprop="text"]/text()').getall())

        next_page_button_link = response.xpath(
            '//ul[@class="pager"]//li[@class="next"]/a/@href').get()
        if next_page_button_link:
            yield response.follow(next_page_button_link, callback=self.parse_only_quotes, cb_kwargs={'quotes': quotes})
        else:
            yield{
                'quotes': quotes
            }

    def parse(self, response):
        title = response.xpath('//h1/a/text()').get()
        quotes = response.xpath(
            '//span[@class="text" and @itemprop="text"]/text()').getall()
        top_ten_tags = response.xpath(
            '//span[@class="tag-item"]/a/text()').getall()

        yield {
            'title': title,
            'top_ten_tags': top_ten_tags
        }

        next_page_button_link = response.xpath(
            '//ul[@class="pager"]//li[@class="next"]/a/@href').get()
        if next_page_button_link:
            yield response.follow(next_page_button_link, callback=self.parse_only_quotes, cb_kwargs={'quotes': quotes})

¿A alguien le funcionó? Solo me trae el título y las tags, desinstalé e instalé scrapy como dijeron aquí y tampoco funcionó.

Les comparto mi codigo, hice algunas factorizaciones y corregi un pequeño bug en el codigo del video_ ( Los Quotes se guardaban como otro objeto en lugar de ser una lista del mismo objeto, tenias un objeto con el titulo y las tags, y otro objeto con la lista de quotes )_

    def parse(self, response, **kwargs):
        title = response.css('h1 a::text').get()
        top_tags = response.css('.tag-item .tag::text').getall()

        data = {'title': title,
               'top tags': top_tags,
               'quotes': self.get_quotes(response)
               }
        yield self.next_page(response, data)

    def parse_quotes(self, response, **kwargs):
        kwargs['quotes'].extend(self.get_quotes(response))
        yield self.next_page(response, kwargs)

    def get_quotes(self, response):
        quotes = response.css('.quote')
        quotes_list = []
        for quote in quotes:
            quotes_list.append({
                'text': quote.css('.text::text').get(),
                'author': quote.css('.author::text').get(),
                'tags': quote.css('.tags .tag::text').getall(),
            })
        return quotes_list

    def next_page(self, response, data):
        next_page_link = response.css('.next a::attr(href)').get()
        if next_page_link:
            return response.follow(next_page_link, callback=self.parse_quotes, cb_kwargs=data)
        else:
            return data

La verdad no se si este bien hacer una factorizacion asi, ya que no soy experto usando generadores y mucho menos en como los usa scrapy, pero me funciono

Con base en la respuesta de David Esteban, mejore un poco el código:

Para ejecutar simplemente:

rm quotes.json | scrapy crawl quotes -o quotes.json


Código Python:

import scrapy

class QuotesSpider(scrapy.Spider):
    name = 'quotes'
    start_urls = [
        'http://quotes.toscrape.com'
    ]
    custom_settings = {
        'FEED_URI': 'quotes.json',
        'FEED_FORMAT': 'json'
    }
    
    def parse_only_quotes(self, response, **kwargs):
        if kwargs:
            # Generate new Quotes (Page)
            new_quotes = self.get_all_quotes(response)
            kwargs["quotes"].extend(new_quotes)
            # Get the new link
            next_page = self.get_next_link(response)
            if next_page:
                yield response.follow(
                    next_page,
                    callback=self.parse_only_quotes,
                    cb_kwargs=kwargs
                )
            else:
                yield kwargs
        

    def parse(self, response):
        # Basic Data
        title = response.xpath('//h1/a/text()').get()
        quotes = self.get_all_quotes(response)
        top_quotes = response.xpath('//div[contains(@class, "tags-box")]//span[@class="tag-item"]/a/text()').getall()
        # Get the new link
        next_page = self.get_next_link(response)
        if next_page:
            yield response.follow(next_page, callback=self.parse_only_quotes,
                cb_kwargs={
                    "title": title, 
                    "top_quotes": top_quotes,
                    "quotes": quotes
                }
        )

    # Complements (General)
    def get_all_quotes(self, response):
        quotes = response.xpath('//span[@class="text" and @itemprop="text"]/text()').getall()
        return quotes

    def get_next_link(self, response):
        link = response.xpath('//ul[@class="pager"]//li[@class="next"]/a/@href').get()
        return link

Genial este curso

Aqui un codigo funcional, antes recuerden tener la version de scrapy 2.x

import scrapy

# Titulo = //h1/a/text()
# Citas = //span[@class="text" and @itemprop="text"]/text()
# Top ten tags = //div[contains(@class, "tags-box")]//span[@class="tag-item"]/a/text()
# Next Page Button = //ul[@class="pager"]//li[@class="next"]/a/@href

class QuotesSpider(scrapy.Spider):
    name = 'quotes'             # Estos nombres son los que se invocan y se deben digitar de manera irrepetible dentro del proyecto
    start_urls = [              # La lista de direcciones desde las que comienza el Spider
        'http://quotes.toscrape.com/page/1'
    ]

    custom_settings = {           # Esta configuracion permite ejecutar y guardar automaticamente
        'FEED_URI':'quotes.json',
        'FEED_FORMAT':'json'
    }

    def parse_only_quotes(self, response, **kwargs):
        if kwargs:
            new_quotes = response.xpath(
                '//span[@class="text" and @itemprop="text"]/text()'
            ).getall()
            new_url = response.url
            kwargs["quotes"].extend(new_quotes)
            kwargs["urls"].append(new_url)

        next_page_button_link = response.xpath('//ul[@class="pager"]//li[@class="next"]/a/@href').get()
        if next_page_button_link:
            yield response.follow(next_page_button_link, callback=self.parse_only_quotes, cb_kwargs=kwargs)   #Follow recibe 3 params, el link y el callback que es la funcion que se ejecuta luego de entrar a la pagina, con args que le puedo pasar
        else:
            yield kwargs


    def parse(self, response):  # Este metodo analiza toda la respuesta y trae solo lo que queremos
        
        urls            = list()
        urls.append(response.url)
        title           = response.xpath('//h1/a/text()').get()
        quotes          = response.xpath('//span[@class="text" and @itemprop="text"]/text()').getall()
        topTenTags      = response.xpath('//div[contains(@class, "tags-box")]//span[@class="tag-item"]/a/text()').getall()
        
        next_page_button_link = response.xpath('//ul[@class="pager"]//li[@class="next"]/a/@href').get()
        if next_page_button_link:
            yield response.follow(
                next_page_button_link, 
                callback=self.parse_only_quotes, 
                cb_kwargs={
                    'title': title,
                    'top_ten_tags': topTenTags,
                    'urls':urls,
                    'quotes':quotes
                })   #Follow recibe 3 params, el link y el callback que es la funcion que se ejecuta luego de entrar a la pagina, con args que le puedo pasar

para corregir el error de que por casualidad la primera pagina no tenga varias paginas, entonces solo es de indicarle un else en el metodo parse

        next_page_button_link = response.xpath('//ul[@class="pager"]/li[@class="next"]/a/@href').get()
        if next_page_button_link:
            yield response.follow(next_page_button_link, callback=self.parse_only_quotes, cb_kwargs={'quotes': quotes})
        else:
            yield {
                'quotes': quotes
            }

Qué buena clase! Comparto mi código con algunos comentarios:

import scrapy

#Título = //h1/a/text()
#Citas = //span[@class="text" and @itemprop="text"]/text()
#Top ten tags = //div[contains(@class, "tags-box")]//span[@class="tag-item"]/a/text()
#Next page button = //ul[@class="pager"]/li[@class="next"]/a/@href


class QuotesSpider(scrapy.Spider):
    name = 'quotes' #nombre unico no repetible
    start_urls = [
        'https://quotes.toscrape.com/'
    ]
    custom_settings = {
        'FEED_URI': 'quotes.json',
        'FEED_FORMAT': 'json'
    }   #Para crear archivo json directamente sin necesidad de indicar en consola

    def parse_only_quotes(self, response, **kwargs):  #Para extraer exclusivamente las citas
        if kwargs:
            quotes = kwargs['quotes']
        quotes.extend(response.xpath('//span[@class="text" and @itemprop="text"]/text()').getall())
        #quotes.extend sirve para combiar las citas de la 1era página con los de la 2da página

        #Vamos a la siguiente página
        next_page_button_link = response.xpath(
            '//ul[@class="pager"]/li[@class="next"]/a/@href').get()
        if next_page_button_link:
            yield response.follow(next_page_button_link, callback=self.parse_only_quotes, cb_kwargs={'quotes': quotes})
        else:
            yield{
                'quotes': quotes
            }

    def parse(self, response):      
        title = response.xpath('//h1/a/text()').get()
        quotes = response.xpath(
            '//span[@class="text" and @itemprop="text"]/text()').getall()
        top_ten_tags = response.xpath('//div[contains(@class, "tags-box")]//span[@class="tag-item"]/a/text()').getall()
        
        yield{
            'title': title,
            'top_ten_tags': top_ten_tags
        }

        #Continuando la página, self.parse para que vuelva a ejecutar la función parse
        next_page_button_link = response.xpath('//ul[@class="pager"]/li[@class="next"]/a/@href').get()
        if next_page_button_link:
            yield response.follow(next_page_button_link, callback=self.parse_only_quotes, cb_kwargs={'quotes': quotes})

un poco complejo