通过lambda回调在Scrapy爬虫中传递参数

12 投票
4 回答
8249 浏览
提问于 2025-04-16 05:11

你好,

我有一段简单的爬虫代码:

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 个回答

2

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答案。按照那个做就没问题了 :)

38

根据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
11

这些 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))

就可以了。

撰写回答