我正在尝试请求多个页面,并将回调中返回的变量存储到列表中,以便以后在以后的请求中使用。
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
如何实现?
元数据无济于事。他们输入的不是输出值,我想从请求循环中收集值。
答案 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
});
扭曲的延迟样式:
(图片来自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
伪代码:
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,您仍然会有一个异步蜘蛛,其中只有多个同步链:)