通过lambda回调在Scrapy爬虫中传递参数
你好,
我有一段简单的爬虫代码:
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)
似乎有问题。
这段代码哪里出错了呢?
4 个回答
lambda r:self.parse2(r, i)
这里绑定的是变量名 i
,而不是 i
的值。等到这个 lambda 被执行时,它会使用闭包中 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
。
不过,事情并没有完全糟糕。其实是有解决办法的。
>>> 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
的 答案。按照那个做就没问题了 :)
根据Scrapy的文档,使用lambda会导致库的作业功能无法正常工作(http://doc.scrapy.org/en/latest/topics/jobs.html)。
Request()和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
这些 lambda 表达式在使用 i
时,实际上是引用了一个闭包中的 i
,所以它们都指向同一个值(也就是在你调用 lambda 表达式时,parse
函数里的 i
的值)。我们可以用一个更简单的例子来解释这个现象:
>>> def do(x):
... for i in range(x):
... yield lambda: i
...
>>> delayed = list(do(3))
>>> for d in delayed:
... print d()
...
2
2
2
你可以看到,lambda 表达式中的 i
都绑定到了函数 do
中的 i
的值。它们会返回当前的值,而 Python 会保持这个作用域活着,只要有任何一个 lambda 表达式还在,这样就能保留这个值。这就是所谓的闭包。
有一个简单但不太优雅的解决方法是:
>>> 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
值被绑定到了 lambda 的参数 i
上。或者说得更清楚一点,可以写成 lambda r, x=i: (r, x)
。关键在于,通过在 lambda 的主体 外部 进行赋值(这个赋值会在后面执行),你就把一个变量绑定到了 当前 的 i
值,而不是循环结束时的值。这样一来,lambda 表达式就不会再依赖于同一个 i
,每个 lambda 都可以有自己的值。
所以你只需要把这一行:
yield Request(link, callback=lambda r:self.parse2(r, i))
改成:
yield Request(link, callback=lambda r, i=i:self.parse2(r, i))
就可以了。