Python爬虫框架Scrapy的效率瓶颈分析与优化方案

现在在爬的一个站点,有一个起始链接,后续所有的链接都是通过上一链接返回的 response 来产生的, (可以理解为从返回的 response 里面提取到下一页的链接)

现在的问题是这样写好的爬虫,感觉是不是硬生生把并发搞成了单线程一样 我并发和线程数都调的很大,但是仍感觉速度很慢,大概每秒处理 2-5 个页面,一天也就只能抓 10-15W 的样子 感觉明显有问题

我想问一下怎么样我才能提高我的抓取效率呢?(单机的情况下)

这是我的一些配置 RETRY_ENABLED = 1 RETRY_TIMES = 2 DOWNLOAD_TIMEOUT = 15

DOWNLOAD_DELAY = 0 CONCURRENT_REQUESTS = 100 CONCURRENT_REQUESTS_PER_DOMAIN = 100 CONCURRENT_REQUESTS_PER_IP = 100


Python爬虫框架Scrapy的效率瓶颈分析与优化方案

27 回复

要考虑对方服务器的性能的呀,网站服务器的性能可能还没你的爬虫机器高。。。。


Scrapy的效率瓶颈通常集中在网络I/O、解析逻辑和数据处理这几个环节。

核心瓶颈分析:

  1. 并发与延迟:默认并发数(CONCURRENT_REQUESTS)可能不够,特别是目标服务器延迟高时。调整这个参数和DOWNLOAD_DELAY的平衡是关键。
  2. 解析开销:在parse回调里做复杂的字符串处理或频繁的正则匹配会严重拖慢速度。用lxmlparsel的选择器通常比正则快。
  3. Pipeline阻塞:如果在pipeline里同步写入数据库或调用外部API,整个爬虫都会等它完成。这是最常见的瓶颈之一。
  4. 内存与去重:大量URL去重时,默认的dupefilter可能吃内存。对于超大规模抓取,考虑用布隆过滤器或外部存储(如Redis)做去重。

针对性优化方案:

# settings.py 关键配置调整
CONCURRENT_REQUESTS = 100  # 根据服务器承受能力调整
DOWNLOAD_DELAY = 0.25      # 与并发数配合防封
CONCURRENT_REQUESTS_PER_DOMAIN = 8  # 单域名限流
DOWNLOAD_TIMEOUT = 30      # 避免卡在慢速请求

# 启用异步pipeline示例(需配合异步数据库驱动)
class AsyncMongoPipeline:
    def __init__(self, mongo_uri):
        self.mongo_uri = mongo_uri
    
    @classmethod
    def from_crawler(cls, crawler):
        return cls(crawler.settings.get('MONGO_URI'))
    
    async def open_spider(self, spider):
        self.client = AsyncIOMotorClient(self.mongo_uri)
        self.db = self.client[spider.name]
    
    async def process_item(self, item, spider):
        await self.db.items.insert_one(dict(item))
        return item
    
    async def close_spider(self, spider):
        self.client.close()

# 使用lxml直接解析(在下载器中间件或spider中)
from lxml import html
def fast_parse(response):
    tree = html.fromstring(response.body)
    # 用XPath快速提取,比response.css()有时更快
    return tree.xpath('//h1/text()')

其他立竿见影的技巧:

  • DNS缓存DNSCACHE_ENABLED = True
  • 调大RETRY_TIMES减少重试浪费
  • scrapy-redis做分布式去重和调度
  • 对于JS渲染页面,用scrapy-splashscrapy-playwright但注意它们比直接抓HTML慢

总结建议:先做性能分析再优化,瓶颈往往在意想不到的地方。

我试了调并发数并没有显著的影响到我的抓取速率,而且对方的站是绝对扛得住的(是个大站)。
这里我虽然写的很大,但是其实抓取频率并没有很高(所以才一直没改),而且抓取到的页面都是按顺序的,我觉得是不是我的抓取逻辑有问题,应该怎么样改善呢?

如果别人网站原本的翻页逻辑就是 [要根据上一页内容来得到下一页内容] 的,那你并发再高都没有用,跟 Scrapy 没有关系,如果要并发你只能是从分类之类的地方入手。(不过一般不都是这样爬么,直接计算页数爬的话很容易出现漏数据的情况)

#3 想了想这么说不太准确,“如果要并发” 换成“如果要提高效率”

总共大概有 4000W 页面,如果我找到了这 4000W 页面的列表,我要写在 start_urls 里面才能实现高并发吗?
之前没有接触过 Scrapy-redis,目前的情况是,单机,带宽还可以,IP/Cookie 等所有反爬措施均已解决,可以理解为网站无反爬站措施,这样的话,我该怎么样实现日抓百万呢?

想到的一种可行的方案是,把所有的 url 写入 redis,然后所有的请求从 redis 里面去取 url, 但是单机的情况下,如何实现并发?(就是不是一个请求结束后再去 redis 取下一个,而是多个线程(并发数)同时连接 redis 去取 url,然后这些个线程同时进行抓取)不太清楚 scrapy-redis 有没有解决这个问题。。。

timeout_delay 调小 改成 10s 左右,默认 180s 很多时间浪费可能是代理质量很低造成的。

说错了 看到你已经是 15 了 你没用 redis 的话我觉得还是这里的问题

首先爬虫不必过于追求效率;其次 Scrapy 执行效率是个问题,我现在都是用 Go 框架爬的;最后,Scrapy 你再怎么改也就那样。

LZ 你自己不是很清楚吗,下一页的 URL 是上一个的 response 里面读到的,这个肯定是串行啊。你得改变这种串行获取 url 的方式才行

现在日抓 10-20W 级,有点跟不上需求,需求大概是日抓百万,但是不能分布式。。。不是不能用,是现在的问题是单机的性能远远的浪费了,无论是带宽还是性能,都远远的没有用到。

和我之前差不多,用 Go 之后,每天差不多提升了 10 倍,2M-

改变串行后呢,怎么提高效率,我总不能把 4000W 页面连接都写道 start_urls 里面吧?

其实我现在是有点不太明白 scrapy 是实现并发的原理,网上也没有找到很好的解释文档。按我的理解,它是通过 start_urls 来实现并发的,任何在 parse 里面写的 yield 都会存在上面的串行问题。

我能想到的是把 scrapy 和 redis 对接(单机对接),然后多个线程同时去取 url,然后同时去抓,关键是我不知道 scrapy 支不支持这种操作,也不知道能不能实现或者有没有现成的解决方案,以免重复造轮子或者根本就此路不通。。。

不过好像上面这种想法又回到了 scrapy 是如何实现并发的问题上了。。。。

当然要保证 队列里有足够的 url 够下载器消费啦,你可以 按照某些固定的规则放进去,保证足够的数量就行了
你现在是每次队列里只有 1 个,你 100 个并发下载啥?

谢谢,我觉得问题在这儿,但是这个规则怎么建立没想好,4000W 级别,还要涉及到失效错误链接的处理,请问 scrapy-redis 是不是能解决我的问题?

Scrapy 底层是 Twisted,Twisted 通过事件循环+线程池来实现异步 IO 的效果,LZ 所说的“并发数”,在 Scrapy 中是 CONCURRENT_REQUESTS, 其实只是传给 Twisted 的 Deferred 对象数量。由于 Twisted 只适用于单机环境,如果要增大 LZ 所说的“并发”数,可以调大 CONCURRENT_REQUESTS, 但显然“并发”数不可能无限增大,因为 Twisted 本身也存在限制

一方面 Twisted 本身有 Queue 和线程池,在 Scrapy 中可以通过设置 Twisted 的 REACTOR_THREADPOOL_MAXSIZE 增大线程池线程数。

另外 Twisted 主线程是单线程的,主线程达到瓶颈的话,再扩大线程池也没有意义。

因此你可以认为单机环境下 Scrapy 的瓶颈 == Twisted 主线程处理上限。

关于 4000W url 如何调用 scrapy 爬取的问题,简单说可以将已知的 url 构建为Request, 然后Spider.parse_start_url()yield Request 即可,所有待处理的 Request 会存入 Scheduler,Scheduler 的数据都存在内存,可以提前评估一下内存是否够存放所有的 url。

scrapy-redis 实现的是将 Scheduler 的数据从内存改为 Redis, 一方面 redis 在进程崩溃后数据不会丢失,另一方面可以突破单机的限制,理论上有足够多的机器的话,再多的 URL 也可以同时请求。此时的瓶颈在 url -> Scheduler 生产者的生产速度

非常感谢!您的这一番讲解能让我少走很多弯路,再次感谢!

先把所有的 URL 爬到手,一些能看出规律的就手动构造,然后就多多进程 /多线程 /异步,随便玩

#10 其实,上分布式,可以把 worker 都放在一台机子上的

现在能得到所有的 url 了,我想着怎么能用 scrapy 高效抓取,scrapy 这么多年了 这样一个成熟的框架应该不至于解决不了这种问题。想先单机把 scrapy 性能发挥到极致,了解他的极限和瓶颈在哪里,然后再上分布式再接着进一步优化,计划的学习路线是这样的。

scrapy 的 CrawlSpider 类?

数据是怎么保存的? 用的是同步还是异步调用.在 pipline 用同步阻塞方式去保存数据的话,会阻塞整个抓取调度的.

https://leehodgkinson.com/blog/scrapy-pipelines/

是采用的异步 MySQL 存储的,很多页面是空数据的,所以瓶颈不在存储这一块,下面是主要代码。


def start_requests(self):
url = ‘https://www.xxxx.com/’
longitude, latitude = get_next_coordinate( self.start_longitude, self.start_latitude)
data = get_form(longitude, latitude)
proxy = ‘http://’ + get_proxy()
yield FormRequest(url, method=‘POST’, formdata=data, callback=self.parse, dont_filter=True, meta={‘proxy’:proxy,‘download_timeout’:3,‘longitude’:data[‘longitude’], ‘latitude’:data[‘latitude’]})

def parse(self, response):
info_list = json.loads(response.text)
if info_list[‘Count’]:
for item in info_list[‘list’]:
item_loader = QiyeItemloader(item=QiyeItem())
item_loader.add_value(‘hash’, item[‘Key’])
item_loader.add_value(‘name’, item[‘Name’])
item_loader.add_value(‘longitude’, response.meta[‘longitude’])
item_loader.add_value(‘latitude’, response.meta[‘latitude’])
qiye_item= item_loader.load_item()
yield qiye_item
longitude, latitude = get_next_coordinate(response.meta[‘longitude’], response.meta[‘latitude’])
next_data = get_form(longitude, latitude)
yield FormRequest(response.url, method=‘POST’, formdata = next_data, callback=self.parse, dont_filter=True, meta={‘proxy’:response.meta[‘proxy’],‘download_timeout’:3,‘longitude’:next_data[‘longitude’], ‘latitude’:next_data[‘latitude’]})

我想的一种解决方案是把所有 URL 放在 redis 里面,然后在 start_requests 里面 while True:yield Request()
这样的问题我不知道我这样一直写会不会时间长了我的电脑就崩了。
我如何控制这个被 yield 的 Request 的数量?比如,在队列里面一直有 100 个 Request,每少一个就添一个,始终保持 Start_url 里面有 100 个待爬 URL,这样的情况下,我调 CONCURRENT_REQUESTS 的值,是不是就能真正的控制并发数了?

你得确认瓶颈在什么地方?
假如网页通过代理访问,60 秒才返回一个页面.这样就算你 1000 个并发. 1000 / 60 = 16.6 .这样算每秒最多也就是 16 个而已.

假如网页解析比较费时,这个问题就更加不好解决.因为毕竟这种类似于阻塞的调用.


你可以登录 telnet 用 est() 查看一下状态,分析一下原因 https://docs.scrapy.org/en/latest/topics/telnetconsole.html
可以看一下 engine.scraper.slot.queue 的实现,这里应该可以取到你要的队列大小值.

你还可以尝试用你自己的 redis 这种方案,启用多个进程,看看有没有提升.

谢谢,代理好像不是瓶颈,不加代理提升的速率也非常有限(大概就是去除了代理延迟级别的速度提升) redis 的那种方案确实提高了速率,是我之前写法太蠢了,所有的下一个页面链接都得等我上一个页面请求完毕才能获取,生生的变成了同步。(可是书上和网上都是这样来写的啊,寻找下一页的链接然后 yield ),不知道是我的理解问题,还是这样写本身就存在这种问题,我再多尝试尝试改一改,谢谢啦🙏。

回到顶部