Scrapy 暗坑之 start_requests

众所周知,Scrapy 默认会过滤重复的 URL,不会重复抓取相同的 URL,除非显式指定。

于是随便写了一个爬图片地址的小虫,然而不知道为什么总会爬两次 baidu 首页,你能看出错在哪里吗?

class ImageSpider(scrapy.Spider):
    name = "images"
    allowed_domains = ["www.baidu.com"]
    start_urls = ['https://www.baidu.com/']

    def parse(self, response):
        images = response.xpath('//img/@src').extract()
        for image in images:
            image_item = ImageItem()
            image_item['img_url'] = response.urljoin(image.strip())
            yield image_item

        urls = response.xpath('//a/@href').extract()
        for url in urls:
            next_url = response.urljoin(url.strip())
            yield Request(next_url)

我想了半天都不明白为什么,以为是过滤器的问题,查了半天资料仍没解决。 后来偶然看了 Spider 源码,才发现坑爹之处。

原来源码的 start_requests 是这样写的(已忽略无关代码)

def start_requests(self):
    for url in self.start_urls:
        yield Request(url, dont_filter=True)

也就是说,因显式指定了 dont_filter=True,start_urls 中的 URL 在首次请求时不会加入过滤列表中,这样相同的 URL 第二次请求时由于不存在于过滤列表中,导致了二次抓取。

我实在不明白为什么会有这种矛盾,既然默认过滤重复 URL,那么在源码各个地方都应贯彻这个原则。

如果只是这样就算了,然而在官方教程中也埋了这样的坑: Scrapy Tutorial (备份)

在 Our first Spider 这一节中,是这样写 start_requests 的

def start_requests(self):
    urls = [
        'http://quotes.toscrape.com/page/1/',
        'http://quotes.toscrape.com/page/2/',
    ]
    for url in urls:
        yield scrapy.Request(url=url, callback=self.parse)

然后在 A shortcut to the start_requests method 一节中表示

Instead of implementing a start_requests() method that generates scrapy.Request objects from URLs, you can just define a start_urls class attribute with a list of URLs. This list will then be used by the default implementation of start_requests() to create the initial requests for your spider

可以把 urls 提取到 start_urls 然后直接 use the default implementation of start_requests()。让人误以为 start_requests 默认实现也没有设置 dont_filter=True。简直就是把全世界所有新手都坑了一遍……