Передача аргументов внутри Scrapy spider через лямбда-обратные вызовы
HI,
У меня есть короткий код паука:
class TestSpider(CrawlSpider):
name = "test"
allowed_domains = ["google.com", "yahoo.com"]
start_urls = [
"http://google.com"
]
def parse2(self, response, i):
print "page2, i: ", i
# traceback.print_stack()
def parse(self, response):
for i in range(5):
print "page1 i : ", i
link = "http://www.google.com/search?q=" + str(i)
yield Request(link, callback=lambda r:self.parse2(r, i))
и я бы ожидал, что вывод будет следующим:
page1 i : 0
page1 i : 1
page1 i : 2
page1 i : 3
page1 i : 4
page2 i : 0
page2 i : 1
page2 i : 2
page2 i : 3
page2 i : 4
однако, фактический вывод таков:
page1 i : 0
page1 i : 1
page1 i : 2
page1 i : 3
page1 i : 4
page2 i : 4
page2 i : 4
page2 i : 4
page2 i : 4
page2 i : 4
поэтому аргумент, который я передаю в callback=lambda r:self.parse2(r, i)
, как-то не так.
Что не так с кодом?
Ответы
Ответ 1
Лямбды получают доступ к i
, который удерживается в закрытии, поэтому все они ссылаются на одно и то же значение (значение i
в функции youre parse
при вызове лямбда). Более простая реконструкция явления:
>>> def do(x):
... for i in range(x):
... yield lambda: i
...
>>> delayed = list(do(3))
>>> for d in delayed:
... print d()
...
2
2
2
Вы можете видеть, что i
в lambdas привязаны к значению i
в функции do
. Они возвратят любое значение, которое оно имеет в настоящее время, и python сохранит эту область действия до тех пор, пока любой из lambdas жив, чтобы сохранить ценность для нее. Это то, что называется закрытием.
Простая, но уродливая работа -
>>> def do(x):
... for i in range(x):
... yield lambda i=i: i
...
>>> delayed = list(do(3))
>>> for d in delayed:
... print d()
...
0
1
2
Это работает, потому что в цикле текущее значение i
привязано к параметру i
лямбда. Альтернативно (и, возможно, немного яснее) lambda r, x=i: (r, x)
. Важная часть состоит в том, что, создавая назначение вне тела лямбды (которое выполняется только позже), вы привязываете переменную к текущему значению i
вместо значения, которое оно принимает в конце цикла. Это делает так, что лямбда не закрываются над i
и могут иметь каждое свое значение.
Итак, все, что вам нужно сделать, это изменить строку
yield Request(link, callback=lambda r:self.parse2(r, i))
к
yield Request(link, callback=lambda r, i=i:self.parse2(r, i))
и ты вишня.
Ответ 2
В соответствии с документацией Scrapy, использующей лямбда, будет запрещена работа рабочих функций библиотек (http://doc.scrapy.org/en/latest/topics/jobs.html).
В запросе() и FormRequest() содержатся словарь с именем meta, который может использоваться для передачи аргументов.
def some_callback(self, response):
somearg = 'test'
yield Request('http://www.example.com',
meta={'somearg': somearg},
callback=self.other_callback)
def other_callback(self, response):
somearg = response.meta['somearg']
print "the argument passed is:", somearg
Ответ 3
lambda r:self.parse2(r, i)
связывает имя переменной i
, а не значение i
. Позже, когда лямбда оценивается текущее значение i
в замыкании, т.е. Используется последнее значение i
. Это легко продемонстрировать.
>>> def make_funcs():
funcs = []
for x in range(5):
funcs.append(lambda: x)
return funcs
>>> f = make_funcs()
>>> f[0]()
4
>>> f[1]()
4
>>>
Здесь make_funcs
- это функция, которая возвращает список функций, каждая из которых привязана к x
. Вы ожидали бы функции при вызове для печати значений от 0 до 4 соответственно. И все же все они возвращают 4
.
Все не потеряно. Существует решение (s?).
>>> def make_f(value):
def _func():
return value
return _func
>>> def make_funcs():
funcs = []
for x in range(5):
funcs.append(make_f(x))
return funcs
>>> f = make_funcs()
>>> f[0]()
0
>>> f[1]()
1
>>> f[4]()
4
>>>
Я использую здесь явную именованную функцию вместо lambda
. В этом случае значение переменной становится привязанным, а не именем. Следовательно, отдельные функции ведут себя так, как ожидалось.
Я вижу, что @Aaron предоставил вам ответ для изменения вашего lambda
. Придерживайтесь этого, и вам будет хорошо идти:)
Ответ 4
class TestSpider(CrawlSpider):
name = "test"
allowed_domains = ["google.com", "yahoo.com"]
start_urls = [
"http://google.com"
]
def parse(self, response):
for i in range(5):
print "page1 i : %s" % i
yield Request("http://www.google.com/search?q=%s" % i, callback=self.next, meta={'i': i})
def next(self, response):
print "page1 i : %s" % response.meta['i']
# traceback.print_stack()