Один и тот же элемент, но несколько URL-адресов

#python-3.x #scrapy

#python-3.x #scrapy

Вопрос:

 class MySiteSpider(scrapy.Spider):
    name = 'MySite'
    allowed_domains = ['example.com']
    start_urls = ['http://example.com/']

    def parse(self, response):
        links = LinkExtractor(unique=True).extract_links(response)
        for link in links:
            yield response.follow(link, callback=self.parse)
            pass

        if response.css('.product-page-content'):
            id_ = response.css('#id::text').extract_first()
            item = MyItem()
            item['id'] = id
            item['urls'] = [response.url]
            # Sorting some data
            yield item
  

Проблема в том, что иногда я получаю страницу с другим, url но одинаковым id_ , и в такой ситуации мне нужно добавить этот новый URL к старому элементу с помощью old id_ / Что-то вроде:

 if response.css('.product-page-content'):
    id_ = response.css('#id::text').extract_first()
    if this_id_already_processed:
        old_item['url'].append(response.url)
    else
        item = MyItem()
        item['id'] = id
        item['urls'] = [response.url]
        # Sorting some data
        yield item
  

Ответ №1:

Интересный случай. Есть несколько способов, которыми вы можете это сделать, но основным препятствием в этом случае будет память.

Когда scrapy выдает элемент, он экспортирует его в любой имеющийся у вас результат (стандартный вывод, json, csv и т.д.), И тогда все готово. Если вы хотите объединить свои элементы таким образом, вам нужно либо выполнить постобработку выходных данных, либо сохранить все в памяти.

  1. Вывод после обработки,
    если у вас есть файлы в output.csv , вы могли бы просто запустить скрипт над результатами, чтобы объединить ваши элементы. Смотрите сигнал spider_closed — когда spider закрывается, открывайте ouput.json и корректируйте содержимое

  2. Конвейеры
    При таком подходе вы могли бы хранить все элементы в памяти и обрабатывать их по мере работы сканера:

     import json
    class MyPipeline:
        items = {}
    
        def process_item(self, item):
            if item['id'] in self.items:
                self.items['id']['urls'].append(item['url'])
            else:
                self.items[item['id']] = item
            return item
    
        close_spider(self, spider):
            with open('output.json') as f:
                f.write(json.dumps(self.items))
      
  3. Экспортеры каналов
    Это было бы почти идентично конвейерному подходу — сохраняйте элементы в памяти перед экспортом

Если ваш spider маленький, используйте # 2, в противном случае # 1 — гораздо более эффективный и чистый подход к использованию памяти.

Ответ №2:

Вы не можете сделать это простым способом из-за того, как работает Scrapy. Он обрабатывает запросы асинхронно, выдавая элементы один за другим, сам по себе он не ведет никакой истории. Что вы могли бы сделать, так это использовать некоторый буфер элементов в spider и использовать сигналы для сброса всех элементов в конце обхода.

Смотрите этот фиктивный пример:

 import json
import scrapy
from scrapy import signals


class QuotesSpider(scrapy.Spider):
    name = 'quotes'
    start_urls = ['http://quotes.toscrape.com/page/1/']
    items = []

    @classmethod
    def from_crawler(cls, crawler, *args, **kwargs):
        spider = super(QuotesSpider, cls).from_crawler(crawler, *args, **kwargs)
        crawler.signals.connect(spider.spider_closed, signal=signals.spider_closed)
        return spider

    def parse(self, response):
        for quote in response.xpath('//div[@class="quote"]'):
            item = {
                'text': quote.xpath('normalize-space(./span[@class="text"])').extract_first()
            }
            self.items.append(item)

    def spider_closed(self, spider):
        with open('items.json', 'wt') as f:
            json.dump(self.items, f)