Scrapy / Python从收益请求中获取项目

时间:2019-01-11 00:19:06

标签: python scrapy

我正在尝试请求多个页面,并将回调中返回的变量存储到列表中,以便以后在以后的请求中使用。

def parse1(self,response):
    items.append(1)

def parse2(self,response):
    items=[]
    urls=['https://www.example1.com','https://www.example2.com']
    for url in urls:
        yield Request(
            url,
            callback=self.parse1,
            dont_filter=True
        )
    print items

如何实现?

元数据无济于事。他们输入的不是输出值,我想从请求循环中收集值。

2 个答案:

答案 0 :(得分:5)

对于刚开始使用Scrapy或异步编程的新手来说,这很可能是最常遇到的问题。 (因此,我将尝试一个更全面的答案。)

您要执行的操作是这样:

Response -> Response -> Response
   | <-----------------------'
   |                \-> Response
   | <-----------------------'
   |                \-> Response
   | <-----------------------'
aggregating         \-> Response
   V 
  Data out 

当您在异步编程中真正要做的是将响应/回调链接起来:

Response -> Response -> Response -> Response ::> Data out to ItemPipeline (Exporters)
        \-> Response -> Response -> Response ::> Data out to ItemPipeline
                    \-> Response -> Response ::> Data out to ItemPipeline
                     \> Response ::> Error

因此,我们需要考虑如何汇总数据的范式转变。

将代码流视为时间轴;您不能时光倒流-或及时返回结果-只能向前。 在安排时间时,您只能获得将来要做的承诺
因此,明智的方法是将自己转发给您将来某个时间点所需的数据。

我认为的主要问题是,在Python中这种感觉和看上去很尴尬,而看起来 在本质上相同的情况下,在JavaScript之类的语言中自然得多。

(我可以这样说,因为我在大约5年的时间里对JavaScript进行了深入而深入的编程,并且了解了它的事件模型, 在接触Python和Scrapy之前,我仍然有一段时间尝试适应Twisted的风格和感觉。而且我永远不会自然而然。)

在Scrapy中更是如此,因为它试图向用户隐藏Twisted deferred的这种复杂性。

但是您应该在以下表示形式中看到一些相似之处:


  • 随机JS示例:

    new Promise(function(resolve, reject) { // code flow
      setTimeout(() => resolve(1), 1000);   //  |
    }).then(function(result) {              //  v
      alert(result);                        //  |
      return result * 2;                    //  |
    }).then(function(result) {              //  |
      alert(result);                        //  |
      return result * 2;                    //  v
    });
    
  • 扭曲的延迟样式:

    Twisted deferreds
    (图片来自https://twistedmatrix.com/documents/16.2.0/core/howto/defer.html#visual-explanation

  • Scrapy Spider回调中的样式:

    scrapy.Request(url,
                   callback=self.parse, # > go to next response callback
                   errback=self.erred)  # > go to custom error callback
    

那Scrapy到哪里去了?

随身携带数据,不要ho积;)
这几乎在每种情况下都足够了,除非您别无选择,只能合并多个页面中的Item信息,但是这些Requests无法序列化为以下模式(稍后会详细介绍)。

->- flow of data ---->---------------------->
Response -> Response
           `-> Data -> Req/Response 
               Data    `-> MoreData -> Yield Item to ItemPipeline (Exporters)
               Data -> Req/Response
                       `-> MoreData -> Yield Item to ItemPipeline
 1. Gen      2. Gen        3. Gen

如何在代码中实现此模型取决于您的用例。

Scrapy在“请求/响应”中提供了meta字段,用于处理数据。 尽管名称不是真的“元”,但相当重要。不要避免,要习惯它。

这样做似乎违反直觉,将所有数据堆积并复制到潜在的数千个新产生的请求中; 但是由于Scrapy处理引用的方式,实际上还不错,而且Scrapy会尽早清除旧对象。 在上述ASCII技术中,当第二代请求全部排队时,Scrapy将第一代响应从内存中释放出来,依此类推。 因此,如果使用正确(并且不处理大量大文件),这并不是真正的内存膨胀问题。

“元”的另一种可能性是实例变量(全局数据),用于将内容存储在某些self.data中 对象或其他对象,并在以后的下一个响应回调中将来对其进行访问。 (从不存在旧版本,因为当时 尚不存在。) 在执行此操作时,请记住始终要记住它是全局共享数据。可能会看到“并行”回调。

最后,有时甚至可以使用外部资源,例如Redis-Queues或套接字在Spider和数据存储区之间通信数据(例如,预填充start_urls)。

这怎么看代码?

您可以编写“递归”解析方法(实际上只是通过相同的回调方法来集中所有响应):

def parse(self, response):
    if response.xpath('//li[@class="next"]/a/@href').extract_first():
        yield scrapy.Request(response.urljoin(next_page_url)) # will "recurse" back to parse()

    if 'some_data' in reponse.body:
        yield { # the simplest item is a dict
            'statuscode': response.body.status,
            'data': response.body,
        }

或者您可以在多个parse方法之间进行拆分,每种方法都处理一种特定类型的页面/响应:

def parse(self, response):
    if response.xpath('//li[@class="next"]/a/@href').extract_first():
        request = scrapy.Request(response.urljoin(next_page_url))
        request.callback = self.parse2 # will go to parse2()
        request.meta['data'] = 'whatever'
        yield request

def parse2(self, response):
    data = response.meta.get('data')
    # add some more data
    data['more_data'] = response.xpath('//whatever/we/@found').extract()
    # yield some more requests
    for url in data['found_links']:
        request = scrapy.Request(url, callback=self.parse3)
        request.meta['data'] = data # and keep on passing it along
        yield request

def parse3(self, response):
    data = response.meta.get('data')
    # ...workworkwork...
    # finally, drop stuff to the item-pipelines
    yield data

甚至可以像这样组合它:

def parse(self, response):
    data = response.meta.get('data', None)
    if not data: # we are on our first request
        if response.xpath('//li[@class="next"]/a/@href').extract_first():
            request = scrapy.Request(response.urljoin(next_page_url))
            request.callback = self.parse # will "recurse" back to parse()
            request.meta['data'] = 'whatever'
            yield request
        return # stop here
    # else: we already got data, continue with something else
    for url in data['found_links']:
        request = scrapy.Request(url, callback=self.parse3)
        request.meta['data'] = data # and keep on passing it along
        yield request

但这对于我的情况而言还不够!

最后,可以考虑使用这些更复杂的方法来处理流控制,这样那些讨厌的异步调用就变得可预测了:

通过更改请求流来强制执行相互依赖的请求的序列化:

def start_requests(self):
    url = 'https://example.com/final'
    request = scrapy.Request(url, callback=self.parse1)
    request.meta['urls'] = [ 
        'https://example.com/page1',
        'https://example.com/page2',
        'https://example.com/page3',
    ]   
    yield request

def parse1(self, response):
    urls = response.meta.get('urls')
    data = response.meta.get('data')
    if not data:
        data = {}
    # process page response somehow
    page = response.xpath('//body').extract()
    # and remember it
    data[response.url] = page

    # keep unrolling urls
    try:
        url = urls.pop()
        request = Request(url, callback=self.parse1) # recurse
        request.meta['urls'] = urls # pass along
        request.meta['data'] = data # to next stage
        return request
    except IndexError: # list is empty
        # aggregate data somehow
        item = {}
        for url, stuff in data.items():
            item[url] = stuff
        return item

scrapy-inline-requests是这方面的另一个选择,但也要注意其缺点(请阅读项目README)。

@inline_requests
def parse(self, response):
    urls = [response.url]
    for i in range(10):
        next_url = response.urljoin('?page=%d' % i)
        try:
            next_resp = yield Request(next_url, meta={'handle_httpstatus_all': True})
            urls.append(next_resp.url)
        except Exception:
            self.logger.info("Failed request %s", i, exc_info=True)

    yield {'urls': urls}

聚集实例存储中的数据(“全局数据”)并通过其中一个或两个来处理流控制

  • 计划程序请求优先级以强制执行命令或响应,因此我们 可以希望在处理上一个请求时,所有较低优先级的操作都已完成。
  • 针对“带外”的自定义pydispatch信号 通知。虽然这些并不是真正的轻量级,但它们是一个完全不同的层 处理事件和通知。

这是使用自定义Request priorities的简单方法:

custom_settings = {
    'CONCURRENT_REQUESTS': 1,
}   
data = {}

def parse1(self, response):
    # prioritize these next requests over everything else
    urls = response.xpath('//a/@href').extract()
    for url in urls:
        yield scrapy.Request(url,
                             priority=900,
                             callback=self.parse2,
                             meta={})
    final_url = 'https://final'
    yield scrapy.Request(final_url, callback=self.parse3)

def parse2(self, response):
    # handle prioritized requests
    data = response.xpath('//what/we[/need]/text()').extract()
    self.data.update({response.url: data})

def parse3(self, response):
    # collect data, other requests will have finished by now
    # IF THE CONCURRENCY IS LIMITED, otherwise no guarantee
    return self.data

以及使用信号的基本示例。
当Spider抓取了所有请求并且坐得很漂亮时,它会侦听内部idle事件,以使用它进行最后一秒的清理(在这种情况下,是汇总我们的数据)。我们可以绝对确定,我们现在不会丢失任何数据。

from scrapy import signals

class SignalsSpider(Spider):

    data = {}

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

    def idle(self, spider):
        if self.ima_done_now:
            return
        self.crawler.engine.schedule(self.finalize_crawl(), spider)
        raise DontCloseSpider

    def finalize_crawl(self):
        self.ima_done_now = True
        # aggregate data and finish
        item = self.data
        return item 

    def parse(self, response):
        if response.xpath('//li[@class="next"]/a/@href').extract_first():
            yield scrapy.Request(response.urljoin(next_page_url), callback=self.parse2)

    def parse2(self, response):
        # handle requests
        data = response.xpath('//what/we[/need]/text()').extract()
        self.data.update({response.url: data})

最后一种可能性是使用外部资源(如消息队列或redis),如前所述,以控制外部的爬虫流量。 这涵盖了我能想到的所有方式。

一旦某个项目产生/返回给引擎,它将被传递给ItemPipeline(可以利用Exporters-请勿与FeedExporters混淆), 您可以继续在Spider之外处理数据。 定制的ItemPipeline实现可以将项目存储在数据库中,或对它们执行任意数量的奇异处理。

希望这会有所帮助。

答案 1 :(得分:0)

如果我对您的理解正确,那么您想要的是while chain

  1. 有一些网址
  2. 检索所有这些网址以形成一些数据
  3. 使用该数据提出新请求

伪代码:

queue = get_queue()
items = []
while queue is not empty:
    items.append(crawl1())
crawl2(items)

这有点难看但并不困难:

default_queue = ['url1', 'url2']
def parse(self, response):
    queue = response.meta.get('queue', self.default_queue)
    items = response.meta.get('items', [])
    if not queue:
        yield Request(make_url_from_items(items), self.parse_items)
        return
    url = queue.pop()
    item = {
        # make item from resposne
    }
    items.append(item)
    yield Request(url, meta={'queue':queue, 'items': items})

这将循环解析,直到queue为空,然后根据结果产生新的请求。应该注意的是,这将成为一个同步链,但是,如果您有多个start_url,您仍然会有一个异步蜘蛛,其中只有多个同步链:)