Python 异步网络爬虫(2)

上一部分整理了如何利用 aiohttpasyncio 执行异步网络请求,接下来我们将在此基础上实现一个简洁、普适的爬虫框架。

一般网站抓取的流程是这样的:

从入口页面开始提取一组下一级页面的链接,然后递归地执行下去,直到最后一层页面为止。唯一不同的是对每一级页面所要抓取的信息,也就是需要的正则表达式不同,除此之外,请求页面、分析内容、正则匹配的步骤是重复的,因此可以将上面的过程简化为:

其中虚线框中的步骤可以抽象出来,即模拟浏览器行为的 ClientCleaning 方法用于对正则匹配的结果进行清理,并将下一级所需的入口地址返回给 Client,在这一过程中也可能涉及到数据输出到数据库的过程。

这里我参考了 Flask(或 Sanic)框架的设计,即利用 Python 装饰器的语法特性,将不同页面的 Cleaning 方法注册到 Client 中:

这时我们遇到一个比较棘手的问题:由于从当前页面提取数据(extract())的过程是异步的, 而在上一个页面执行完成之前是无法进入下一个页面的,也就是说同一级页面之间是异步的,不同层级页面之间是同步的,那么如何在 asyncio 中安排这种任务?

这其实是一个带有递归属性的生产者/消费者模型,上级页面作为生产者只有在经过网络请求之后才能生产出下级所有入口链接,而下一级的消费者将成为下下一级的生产者……我们可以将事件循环看作是一个“传送带”,一些可能造成阻塞的任务(如extract())会被挂起,等阻塞任务完成后重新进入队列等待被执行:

在上面的问题中,不同层级页面的提取过程可以被封装成 Task 并丢进任务队列,只是不同任务携带不同的页面地址、正则表达式、Cleaning 回调函数等属性,至于这些任务在具体执行时如何调度,就丢给事件循环去操心好了(这也是使用 asyncio 的一条基本原则):

以上就是异步爬虫的基本结构,有一点需要约定好的是所有的 Cleaning 方法必须以列表形式返回清洗之后的结果,且下一级页面入口必须在第一位(最后一页除外)。接下来做一个简单的测试,以豆瓣电影分类页面为入口,进入该类别列表,最后进入电影详情页面,并提取电影时长和评分:

从上面的执行的结果可以看出,正则表达式有时候不能(或不便)直接精确过滤我们所需内容,因此可以在 Cleaning 函数中进行清理(如去掉多余 Tag 或空位符等),另外:

  • 不像 Flask,这里通过 register 注册方法的顺序必须与页面处理顺序保持一致
  • Sanic 一样,注册方法必须也是 Coroutine (async def),同时可以在其中异步执行数据库存储操作;
  • 上级页面信息实际上可以通过扩展 Node 直接传递给下级页面,这在某些相关页面中甚至是必须的;

总结

抽象这一框架的目的主要有以下几点:

  1. 学习使用 asyncio 库及基于协程的异步;
  2. 将网络爬虫的编写过程聚焦到页面关系分析精确正则表达式少量数据清理上;
  3. 简化使用,降低学习成本。

仍有以下内容需要完成:

  1. 错误捕捉与 logging,让调试过程更简单;
  2. 寻找不适应该框架的情况,进行 upgrade;
  3. 性能测试;
  4. 完善浏览器模拟:Headers、proxy、Referer等……

未完待续。

参考

  1. Sanic
  2. Asyncio Doc::Producer/consumer

打赏支持我写出更多好文章,谢谢!

打赏作者

打赏支持我写出更多好文章,谢谢!

1 6 收藏 1 评论

关于作者:Yusheng

关注微信公众号 PyHub! 个人主页 · 我的文章 · 24 ·   

相关文章

可能感兴趣的话题



直接登录
最新评论
跳到底部
返回顶部