前言
在源码剖析 - 公众号采集阅读器 Liuli 一文中提到了 ruia,这篇文章就简单记录一下 ruia。
为啥要看?主要是在阅读 Liuli 的过程中,顺手看了一下 ruia 的仓库,发现代码量很少,其宣传中又强调除爬虫核心功能外的所有功能都通过插件的方式实现,我便对其插件系统的实现感到好奇,是像 Flask 那种动态引入呢?还是其他我不知道的方式?
老规矩,看前先理一下兴趣点,不然一头扎入细节,最后啥也带不走,我主要对以下 2 点感兴趣:
1.ruia 的设计架构是怎么样的?
2.ruia 如何使用插件系统?
观前提示,ruia 并没有实现插件系统,而是利用中间件的形式来实现所谓的插件,跟我自己理解的插件有所差异。
ruia 设计架构
如果你熟悉 Scrapy,那么 ruia 的使用方式和架构你会非常熟悉,因为我在自己开设的进阶爬虫课里手把手带领大家剖析过 Scrapy 框架的代码,所以我比较熟悉Scrapy,如果你不熟悉,本文你可能会比较懵逼。
ruia 相比于 Scrapy 轻量很多,它没有调度器相关的逻辑,而是直接通过 Spider 完成完整的爬取逻辑,先以一个 Demo 为例,看看 ruia 的基本使用,部分代码如下:
class DoubanSpider(Spider):
# 爬虫名称
name = "DoubanSpider"
# 入口url
start_urls = ["https://movie.douban.com/top250"]
# 爬虫相关配置
request_config = {"RETRIES": 3, "DELAY": 0, "TIMEOUT": 20}
concurrency = 10
# aiohttp config
aiohttp_kwargs = {}
# 异步方法
async def parse(self, response):
# 异步阻塞等待
html = await response.text()
etree = response.html_etree(html=html)
pages = ["?start=0&filter="] + [
# 通过css选择器,获得需要进一步爬虫的url
i.get("href") for i in etree.cssselect(".paginator>a")
]
for index, page in enumerate(pages):
url = self.start_urls[0] + page
# 构建新的请求
# 回调方法为parse_item
yield self.request(
url=url, metadata={"index": index}, callback=self.parse_item
)
async def parse_item(self, response):
async for item in DoubanItem.get_items(html=await response.text()):
yield item
async def process_item(self, item: DoubanItem):
self.logger.info(item)
if __name__ == "__main__":
# 启动爬虫
DoubanSpider.start()
从上述代码中,通过 Spider 的 start 方法启动爬虫,该方法代码如下:
@classmethod
def start(
cls,
middleware: typing.Union[typing.Iterable, Middleware] = None,
loop=None,
after_start=None,
before_stop=None,
close_event_loop=True,
**spider_kwargs,
):
# 获取事件循环
loop = loop or asyncio.new_event_loop()
# 实例化当前类,即类方法中的spider_ins变量为当前类的实例 - Scrapy也是这样做的
spider_ins = cls(middleware=middleware, loop=loop, **spider_kwargs)
# 启动事件循环,执行爬虫实例的_start方法
spider_ins.loop.run_until_complete(
spider_ins._start(after_start=after_start, before_stop=before_stop)
)
# 关闭事件循环中的异步生成器对象(asynchronous generator)
spider_ins.loop.run_until_complete(spider_ins.loop.shutdown_asyncgens())
if close_event_loop:
# 关闭事件循环
spider_ins.loop.close()
return spider_ins
从上述代码可知,爬虫的完全流程为: