首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >爬虫的基本框架

爬虫的基本框架

作者头像
爬虫技术学习
发布2023-02-10 20:59:53
3890
发布2023-02-10 20:59:53
举报
文章被收录于专栏:爬虫技术学习爬虫技术学习

最近看过不少讲爬虫的教程[1][2],基本都是一个模式:

  1. 开始先来拿正则、lxml、jquery/pyquery等等教大家从页面上抠出一个一个的值来
  2. 然后深入一些在讲讲http 协议,讲讲怎么拿出 cookie 来模拟登录之类的,讲讲基本的反爬虫和反反爬虫的方法
  3. 最后在上一个 简单地 scrapy 教程,似乎就皆大欢喜了。

具体地采集一个一个的数据的确让人产生成就感,然而这些教程却都忽略了爬虫最核心的逻辑抽象,也就是「爬虫应该采取什么样的策略遍历网页」。其实也很简单,只需要两个队列和一个集合,Scrapy 等框架拆开来看也是如此,本文参照 Scrapy 实现一个最基础的通用爬虫。

万维网是由一个一个的页面构成的,而每个页面和页面之间是由链接来联系的,并且这些链接都是具有方向性的。对应到数据结构的话,我们可以把每一个页面都看作一个节点,而每一个链接都是一个有向边,也就是整个万维网其实是一个巨大的「有向图」[3]。说到这里,可能有的同学已经明白了,可以用广度优先或者深度优先的算法来遍历这个图。当然,这个图是在太巨大了,我们不可能遍历整个图,而是加一些限定条件,只去访问其中很小一部分我们感兴趣的节点,比如某个域名下的网页。

广度优先和深度优先都可以使用递归或者辅助的队列(queue/lifo_queue)来实现。然而如果你的爬虫是用 python 写的话,很遗憾不能使用递归来实现了,原因很简单,我们要访问的网页可能成千上万,如果采用递归来实现,那么爬虫每向前访问一个节点,系统的调用栈就会 +1,而 python 中至今没有尾递归优化,默认的堆栈深度为1000,也就是很可能你访问了1000个网页之后就抛出异常了。所以我们这里使用队列实现对网页的遍历访问。

理论知识说了这么多,下面以一个例子来说明一下如何爬取数据:爬取煎蛋网的妹子图: http://jandan.net/ooxx

首先,我们打开对应的网址,作为起始页面,也就是把这个页面放入待访问的页面的队列。注意,这是我们需要的第一个队列,存放我们的待访问页面。

class MiniSpider(object):   def __init__(self):
        self._request_queue = queue.Queue()  # 带请求页面的队列
        self._request_queue.put('http://jandan.net/ooxx')  # 把第一个待访问页面入队

接着,我们先不考虑具体如何从页面上抽取我们需要的内容,而是考虑如何遍历待访问的页面。我们发现可以通过页面上的翻页按钮找到下一页的链接,这样一页接着一页,就可以遍历所有的页面了。

当然,对这个页面,你可能想到,其实我们只要获取了页面的个数,然后用程序生成一下不就好了吗?比如说第一http://jandan.net/ooxx/page-1,第二页是http://jandan.net/ooxx/page-2。实际上,对这个例子来说是可以的,但是,这种方法又回到了对于每个站点都去寻找站点规律的老路,这并不是一种通用的做法。

在对应的按钮上点击右键,选择审查元素(inspect),可以看到对应 html 元素的代码。我们通过 xpath 来选择对应的节点,来获取下一页的链接。如果你还不了解 xpath,建议你去 Mozilla Developer Network [4] 上学习一个,提高下自身姿势水平。

通过 xpath 表达式 //div[@class=’comments’]//a/@href 我们获得了所有通向上一页下一页的链接。你可以在第二页和第三页上验证一下。

    class MiniSpider(object):       def __init__(self):
       self._request_queue = queue.Queue()  # 带请求页面的队列
       self._request_queue.put('http://jandan.net/ooxx')  # 把第一个待访问页面入队
   def run(self):
       while True:
           url = self._request_queue.get()
           rsp = download(url)
           new_urls = get_xpath(rsp, "//a")  # 新的待访问的页面
           map(self._request_queue.put, new_urls)  # 放入队列

这时候,你可能想到了另一个问题,第一页的下一页和第三页的上一页都是同一个页面——第二页。如果不加处理的话,我们就会重复多次访问一个页面,浪费资源不说,还有可能导致爬虫迷路,在几个页面之间循环访问。这时候我们就需要一个集合,把访问过得页面放入。从而避免重复访问。

class MiniSpider(object):   def __init__(self):
       self._request_queue = queue.Queue()  # 带请求页面的队列
       self._request_queue.put('http://jandan.net/ooxx')  # 把第一个待访问页面入队
       self._dedup_set = set()  # 已经访问过得页面集合   def run(self):
       while True:
           url = self._request_queue.get()
           rsp = download(url)
           self._dedup_set.add(url)  # 访问过了,加入
           new_urls = get_xpath(rsp, "//a")  # 新的待访问的页面
           for new_url in new_urls:
               if new_url not in self._dedup_set:  # 如果还没有访问过
                   self._request_queue.put(new_url)  # 放入队列

好了,既然我们可以遍历需要爬取得页面了,下一步我们开始考虑从页面抽取需要的数据了。我们依然请出我们的老朋友xpath了。在需要的元素上点击右键,编写对应的表达式就可以了。在这个例子里,我们需要获取的是图片,对于图片的下载也是一件很耗时的任务,如果能在另一个线程里进行就好了,所以这里我们引入第二个队列,存放抽取出来的数据。

class MiniSpider(object):   def __init__(self):
       self._request_queue = queue.Queue()  # 带请求页面的队列
       self._request_queue.put('http://jandan.net/ooxx')  # 把第一个待访问页面入队
       self._item_queue = queue.Queue()
       self._dedup_set = set()  # 已经访问过得页面集合   def run_request(self):
       while True:
           url = self._request_queue.get()
           rsp = download(url)
           self._dedup_set.add(url)  # 访问过了,加入
           new_urls = get_xpath(rsp, "//a")  # 新的待访问的页面
           for new_url in new_urls:
               if new_url not in self._dedup_set:  # 如果还没有访问过
                   self._request_queue.put(new_url)  # 放入队列
           items = get_xpath(rsp, "//img/@src")  # 抽取出来的图片地址
           map(self._item_queue, items)   def run_item(self):
       while True:
           image = self._item_queue.get()
           download(image)

把 run_request 和 run_item 两个函数放到不同的线程中,就可以同时遍历网页和下载图片了。

好了,到这里我们的煎蛋妹子图爬虫就写好了,实际上所有的爬虫框架不管多么复杂,使用的异步等等不同的多任务模式也好,本质上都是一样的。 Scrapy 也是采用了类似的方式,不同的地方时,scrapy 才使用的是 Lifo Queue,也就是栈,所以 scrapy 默认是深度优先便利的,而我们上面的爬虫是广度优先遍历的。scrapy 没有采用线程,而是使用了 Twisted 提供的 Actor Model 实现多任务同时运行。

如果再多些几个爬虫之后,可能你就会发现,其实每次需要改动的地方无外乎是查找几个 xpath 表达式,这样我们可以把上面的逻辑抽象成为一个框架,通过编写配置文件来爬取目标数据。相关代码参见: github.com/yifeikong/miniscrapy

比如,上面的代码只需要如下命令:

python miniscrapy.py --spider ooxx.yml

在爬虫运行过程中,会遇到各种各样的封锁,封锁 User-Agent, 封锁 IP,封锁 Cookie,但是这些封锁都是在下载过程中遇到的,和爬虫的整体逻辑是无关的。本文中的逻辑可以一直复用。对于遇到的各种各样的封锁,需要各种灵活多变的方式应对,这时候采用 pipeline 的方式是一个很好的选择,下一篇文章将会介绍。

[1] http://www.jianshu.com/p/11d7da95c3ca [2] https://zhuanlan.zhihu.com/p/25296437 [3] https://zh.wikipedia.org/zh-hans/%E5%9B%BE_(%E6%95%B0%E5%AD%A6)

本文参与 腾讯云自媒体分享计划,分享自微信公众号。
原始发表:2017-04-30,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 爬虫技术学习 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体分享计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档