繁体   English   中英

Scrapy / Python从yield请求中获取项目

[英]Scrapy/Python getting items from yield requests

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

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

怎么能实现这一目标?

梅塔斯没有帮助。 它们输入的不是输出值,我想从一个请求循环中收集值。

对于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这样的语言中它看起来更自然,而它基本上是相同的。

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

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


  • 随机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 }); 
  • 扭曲延迟的风格:

    扭曲的延期
    (来源: https//twistedmatrix.com/documents/16.2.0/core/howto/defer.html#visual-explanation

  • Scrapy蜘蛛回调中的风格:

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

那么Scrapy给我们留下了什么?

随身携带您的数据,不要囤积它;)
这几乎在所有情况下都应该足够了,除非您别无选择,只能从多个页面合并项目信息,但这些请求无法序列化到以下模式中(稍后会详细介绍)。

->- 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数据中的meta数据。 尽管这个名字并不是真正的“元”,但却非常重要。 不要避免它,习惯它。

这样做可能看起来违反直觉,堆积并将所有数据复制到潜在的数千个新生成的请求中; 但是由于Scrapy处理引用的方式,它实际上并不坏,并且Scrapy会早期清理旧对象。 在上面的ASCII技术中,当你的第二代请求全部排队时,第一代响应将被Scrapy从内存中释放,依此类推。 因此,如果使用得当(并且不处理大量文件),这并不是人们可能认为的内存膨胀。

“meta”的另一种可能性是实例变量(全局数据),用于将内容存储在某个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 ,但也要注意缺点(阅读项目自述文件)。

@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}

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

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

这是使用自定义请求优先级的简单方法:

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

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

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实现可能会将项目存储在数据库中,或者对它们执行任意数量的异常处理。

希望这可以帮助。

(并且可以使用更好的文本或示例来编辑它,或者修复可能出现的任何错误。)

如果我正确地理解你想要的是一个while chain

  1. 有一些网址
  2. 抓取所有这些网址以形成一些数据
  3. 使用该数据发出新请求

伪代码:

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

在scrapy中,这有点难看但并不困难:

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_urls,你仍然有异步蜘蛛只有多个同步链:)

暂无
暂无

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM