$ ls ~yifei/notes/

为什么不使用 scrapy,而是从头编写爬虫系统?

Posted on:

Last modified:

最近面了几家公司,每当我提到头条的爬虫都是自己写的时候,对方一个下意识的问题就是:“为什么 不使用开源的 scrapy?”。实际上我在头条的 lead 就是 scrapy 的 contributor,而他自己也不用 自己的框架,显然说明 scrapy 不适合大型项目。那么具体问题在哪儿呢?今天终于有时间了,详细 写写这个问题。

个人不喜欢 scrapy 原因一言以蔽之:高不成,低不就,弊大于利。总的来说,需要使用代码来 爬一些数据的大概分为两类人:

  1. 非程序员,需要爬一些数据来做毕业设计、市场调研等等,他们可能连 Python 都不是很熟;
  2. 程序员,需要大规模、分布式的爬虫系统,对他们来说,语言都无所谓,更别说用不用框架了。

为什么不适合初学者?

对于初学者来说用不上 scrapy 的原因很简单:

  1. scrapy 太复杂了;
  2. scrapy 采用异步模式带来的高性能和在反爬面前实际上没有任何卵用;
  3. scrapy 项目冗余的代码结构对初学者完全是过度设计。

对于一个任何一个已经入门的程序员来说,Python 都算不上一个很复杂的语言,除了不用大括号可能 让一些人感觉有些不适应之外,基本上看看语法上手就能写了。但是恰恰是因为我们都是老司机了, 所以不能体会到每一行代码对于外行来说可能『比登天还难』。如果不用 scrapy,可能我只需要这样:

# 以下代码未经测试,可能有些许 bug
import requests

def main():
    for i in range(100):
        rsp = requests.get(f"http://www.example.com/{i}.html")
        with open("example-{i}.html", "w") as f:
            print(f"saving {i}")
            f.write(rsp.text)

if __name__ == "__main__":
    main()

就写好了一个简单的爬虫。而使用 scrapy 呢,大概需要这样吧:

# 以下代码未经测试,可能有些许 bug
import scrapy

class QuotesSpider(scrapy.Spider):
    name = "quotes"

    def start_requests(self):
        for i in range(100):
            yield scrapy.Request(url=f"http://www.example.com/{i}.html", callback=self.parse)

    def parse(self, response):
        page = response.url.split("/")[-2]
        with open("example-%s.html" % page, "wb") as f:
            f.write(response.body)
        self.log("Save file %s" % page)

先不说代码行数增长了不少,初学者会问到这些问题:“什么是 class?为什么类还有参数?啊, 什么是继承?yield 又是什么鬼,那个 scrapy.Request 又是啥?”这些都是心智负担。那么 scrapy 这些心智负担又给我们带来了什么好处呢?好处是性能和稍微统一的代码结构,但是其实这两个对 初学者并没有什么卵用啊……

Scrapy 采用了 Twisted 作为基础,实现了基于协程的高并发。协程看着虽然挺好,但是对于非程序员 来说,他们往往就想对一个站点做定向爬取,你说你蹭蹭蹭把并发涨上去了,无非两个后果:

  1. 对方承受不住你爬,挂掉了,你拿不到数据;
  2. 对方把你封禁了,疯狂弹验证码,你拿不到数据。

所以,对于非程序员做的一些定向爬取来说,速度是没有意义的,甚至往往是越慢越好。Scrapy out。

那么统一的代码结构有什么卵用吗?答案依然是没有。在 web 开发领域,稍微有点规模的项目还是 要使用框架的,哪怕是 flask 这种微框架。这是因为,在 web 开发领域,有经典的 MVC 模式,我们 需要路由、模板、ORM 这些固定的组件,所以主循环是由框架和 web server 来控制的。而对于爬虫呢? 其实没有什么固定的模式,scrapy 也仅仅是定义了几个钩子函数而已,反倒因为我们没有了主循环, 在编写一些特定逻辑的时候非常受到掣肘。

另外,scrapy 提供的一些其他功能,比如说抓取的队列或者去重等等,个人感觉有过度封装的味道, 而且也都是在内存里,在反爬导致爬虫挂掉这种故障面前没有什么卵用,不二次开发的话还是得重爬。 对于小白来说,也不用想 redis 这些幺蛾子,其实可以用 Google 最开始使用的一个很简单的方法, 就把每个新抓到的 url 写到一个 txt 文件就好了,爬虫每次重启的时候首先读取这个 txt 就好了, 网上乱七八糟的教程大多是炫技的。

为什么不适合大型爬虫系统?

前面说到,scrapy 基于 twisted。twisted 是 Python 的一个异步框架,最大的问题就是太难懂了, 而且现在官方也支持了 asyncio,所以 twisted 的未来堪忧,比起 twisted 来说,我更愿意使用 官方的 asnycio。第二点就是 scrapy 控制了主循环,所以二次开发相当于只能在他的框架内做一些 修修补补,并且还要兼容 twisted。

scrapy 依赖 twisted 这一点导致 scrapy 深入后曲线非常地陡峭,要想了解一些内部的机理,必须 对 twisted 比较明了。而 twisted 正如它的名字一样,是非常扭曲的一些概念,虽然性能非常好, 但是要理解起来是要花上不少时间的。

Web 服务器是一个爬虫可以抽象出来的是各种组件。而 scrapy 太简陋了,比如说去重,直接用的是 内存中的一个集合。如果要依赖 scrapy 写一个大型的爬虫,几乎每个组件都要自己实现,那有何必 用 scrapy 呢?

既然要开发大型爬虫系统,那么其中很重要的一部分就是爬虫的调度了。一种比较简单的模式是 scheduler 作为 master,全局调度。Scrapy 作为一个框架,实际上只是实现了下载部分而已,很难 称得上一个框架。

对于大型爬虫系统,最重要的并不是你使用不使用 Scrapy,而是你不能为每一站点去单独写一个爬虫 的脚本。代码的灵活度实在太大了,对于没有足够经验的工程师来说,写出来的脚本可能很难维护。 重点是要把主循环掌握在爬虫平台的手中,而不是让每一个脚本都各行其是。

显然 scheduler 这部分是不能再用一个爬虫框架来实现的,连主循环都没有怎么写逻辑呢?我们可能 还要实现增量爬取,或者消费业务方发来的爬取请求等各种业务,这块显然是在 scheduler 里面的, 那么这个爬虫系统无非是 scheduler 分发任务给各个 worker 来抓取。worker 还可以使用 scrapy 实现,但是呢,这个 worker 其实已经弱化为一层薄薄的 downloader 了,那我要他干嘛呢?scrapy 的核心逻辑也不过是个深度或者广度优先的遍历而已,少一个依赖不好么……

总结一下,爬虫的工作量要么在反爬,要么在调度等业务逻辑,本身只是一个 requests.get 而已, scrapy 提供的种种抽象对于初学者太复杂,大型系统又用不上,所以个人不推荐使用包括但不限于 scrapy 在内的所有爬虫框架

建议所有认为学习框架会使自己变强的人读读:Stop learning frameworks 和评论,中文翻译

以上仅代表个人观点,欢迎讨论,不要人身攻击。

2023 更新

  1. 爬虫面对的是一个不确定的问题,所以不能像 Web 框架一样,要多留出空间来对付反爬虫
  2. Scrapy 的概念搞得太混乱了,像是什么 item_pipeline 之类,scrapy.cfg 等等东西很多余
  3. pipeline/middleware 对 web 框架或者机器学习来说是一个好的设计模式,但是爬虫真用不到。 对于真正需要采集数据的用户来说,重点是解决怎么下载,而不是处理数据,scrapy 显然头重脚轻了

参考

  1. scrapy 源码解读
WeChat Qr Code

© 2016-2022 Yifei Kong. Powered by ynotes

All contents are under the CC-BY-NC-SA license, if not otherwise specified.

Opinions expressed here are solely my own and do not express the views or opinions of my employer.

友情链接: MySQL 教程站