Python中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,那么在源码各个地方都应贯彻这个原则。

如果只是这样就算了,然而在官方教程中也埋了这样的坑: https://doc.scrapy.org/en/latest/intro/tutorial.html

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


Python中Scrapy框架的start_requests方法有哪些常见暗坑?

7 回复

scrapy 的源码质量。。。
你可以 PR 和维护者讨论。
不过我记得自己之前 PR 体验不好,维护者让我改代码去兼容一个几年前的依赖版本,原因是他们的 for profit 产品没升级依赖。。


Scrapy的start_requests方法看着简单,但确实有几个地方容易踩坑。

1. 忘记调用super().start_requests() 如果你重写了__init__方法,特别是自定义了start_urls,一定要记得调用父类方法,不然start_urls就白定义了。

class MySpider(scrapy.Spider):
    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)
        # 你的初始化代码
        self.start_urls = ['http://example.com']
    
    def start_requests(self):
        # 必须调用super()才能用start_urls
        yield from super().start_requests()

2. 生成器没处理好 start_requests必须返回可迭代对象,常用yield。直接return一个列表也行,但用yield更符合Scrapy的异步风格。

# 正确做法
def start_requests(self):
    for url in self.custom_urls:
        yield scrapy.Request(url, callback=self.parse_item)

# 错误:直接返回列表(虽然能运行,但不符合框架设计)
def start_requests(self):
    return [scrapy.Request(url) for url in self.custom_urls]

3. 回调函数绑定错误start_requests里手动创建Request时,如果不指定callback,默认会用self.parse。但如果你重写了parse方法做其他用途,这里就会出错。

def start_requests(self):
    # 明确指定回调函数,避免依赖默认的parse
    yield scrapy.Request(
        url='http://example.com/api',
        callback=self.parse_api,  # 明确指定
        dont_filter=True
    )

4. 请求参数传递问题start_requests传递数据到parse函数,要用meta字典。直接给Request加自定义属性是没用的。

def start_requests(self):
    yield scrapy.Request(
        url='http://example.com',
        meta={'category': 'books'},  # 正确方式
        callback=self.parse
    )

def parse(self, response):
    category = response.meta['category']  # 这里能取到

5. 忽略请求去重 start_requests发出的请求默认会经过去重过滤。如果你需要重复请求同一个URL(比如每次爬取都需要登录),要设置dont_filter=True

def start_requests(self):
    # 登录页需要每次都请求,不去重
    yield scrapy.Request(
        url='http://example.com/login',
        callback=self.login,
        dont_filter=True
    )

6. 异步初始化问题 如果start_requests依赖异步操作的结果(比如从数据库读URL),要确保在start_requests里处理好异步转同步,或者用CrawlSpiderrules配合LinkExtractor

简单说就是:记得调super、用yield、明确回调、数据走meta、注意去重。

想想要是 start_urls = [‘a’, ‘b’ , ‘c’, ‘a’], 然后第二个被 filter 了也蛮爆炸的. 不过你要是实际用这种情况很少啦, 而且反正数据库也要做去重. 多抓一次其实影响不大.

没太看明白这种写法为啥你会触发两次抓取,执行一次之后 spider 应该 close 了,难道你的意思是执行过程中产生了同样的 url 没有正常过滤?
我几个在跑的都是差不多写法,没有发现重复抓取现象

多谢楼主啊 我的几个爬虫经常出这个问题 我还纳闷这是什么情况

赞一个

有日志吗?看看日志不是清楚了

回到顶部