【爬虫系列】【转】使用爬虫技术实现 Web 页面资源可用性检测

背景

对于电商类型和内容服务类型的网站,常常会出现由于配置错误形成页面连接没法访问的状况(404)。html

显然,要确保网站中的全部连接都具备可访问性,经过人工进行检测确定是不现实的,经常使用的作法是使用爬虫技术按期对网站进行资源爬取,及时发现访问异常的连接。前端

对于网络爬虫,当前市面上已经存在大量的开源项目和技术讨论的文章。不过,感受你们广泛都将焦点集中在爬取效率方面,例如当前就存在大量讨论不一样并发机制哪一个效率更高的文章,而在爬虫的其它特性方面探讨的很少。python

我的认为,爬虫的核心特性除了,还应该包括,而且从重要性的排序来看,应该是从高到低的。git

排在第一位,是由于这是爬虫的基本功能,若爬取的页面不全,就会出现信息遗漏的状况,这种状况确定是不容许的;而排在第二位,是由于爬虫一般都是须要长期稳定运行的,若由于策略处理不当形成爬虫运行过程当中偶尔没法正常访问页面,确定也是没法接受的;最后才是,咱们一般须要爬取的页面连接会很是多,所以效率就很关键,但这也必须创建在的基础上。github

固然,爬虫自己是一个很深的技术领域,我接触的也只是皮毛。本文只针对使用爬虫技术实现 Web 页面资源可用性检测的实际场景,详细剖析下其中涉及到的几个技术点,重点解决以下几个问题:编程

  • 全:如何才能爬取网站全部的页面连接?特别是当前许多网站的页面内容都是要靠前端渲染生成的,爬虫要如何支持这种状况?
  • 稳:不少网站都有访问频率限制,若爬虫策略处理不当,就常出现 403 和 503 的问题,该种问题要怎么解决?
  • 快:如何在保障爬虫功能正常的前提下,尽量地提高爬虫效率?

爬虫实现前端页面渲染

在早些年,基本上绝大多数网站都是经过后端渲染的,即在服务器端组装造成完整的 HTML 页面,而后再将完整页面返回给前端进行展示。而近年来,随着 AJAX 技术的不断普及,以及 AngularJS 这类 SPA 框架的普遍应用,前端渲染的页面愈来愈多。后端

不知你们有没有据说过,前端渲染相比于后端渲染,是不利于进行 SEO 的,由于对爬虫不友好。究其缘由,就是由于前端渲染的页面是须要在浏览器端执行 JavaScript 代码(即 AJAX 请求)才能获取后端数据,而后才能拼装成完整的 HTML 页面。浏览器

针对这类状况,当前也已经有不少解决方案,最经常使用的就是借助 PhantomJS、puppeteer 这类 Headless 浏览器工具,至关于在爬虫中内置一个浏览器内核,对抓取的页面先渲染(执行 Javascript 脚本),而后再对页面内容进行抓取。bash

不过,要使用这类技术,一般都是须要使用 Javascript 来开发爬虫工具,对于我这种写惯了 Python 的人来讲的确有些痛苦。服务器

直到某一天,kennethreitz 大神发布了开源项目 requests-html,看到项目介绍中的那句 Full JavaScript support! 时不由热泪盈眶,就是它了!该项目在 GitHub 上发布后不到三天,star 数就达到 5000 以上,足见其影响力。

requests-html 为啥会这么火?

写过 Python 的人,基本上都会使用 requests 这么一个 HTTP 库,说它是最好的 HTTP 库一点也不夸张(不限编程语言),对于其介绍语 HTTP Requests for Humans 也当之无愧。也是由于这个缘由,Locust 和 HttpRunner 都是基于 requests 来进行开发的。

而 requests-html,则是 kennethreitz 在 requests 的基础上开发的另外一个开源项目,除了能够复用 requests 的所有功能外,还实现了对 HTML 页面的解析,即支持对 Javascript 的执行,以及经过 CSS 和 XPath 对 HTML 页面元素进行提取的功能,这些都是编写爬虫工具很是须要的功能。

在实现 Javascript 执行方面,requests-html 也并无本身造轮子,而是借助了 pyppeteer 这个开源项目。还记得前面提到的 puppeteer 项目么,这是 GoogleChrome 官方实现的 Node API;而 pyppeteer 这个项目,则至关因而使用 Python 语言对 puppeteer 的非官方实现,基本具备 puppeteer 的全部功能。

理清了以上关系后,相信你们对 requests-html 也就有了更好的理解。

在使用方面,requests-html 也十分简单,用法与 requests 基本相同,只是多了 render 功能。

from requests_html import HTMLSession
 
session = HTMLSession()
r = session.get('http://python-requests.org')
r.html.render()

  

爬虫实现访问频率控制

在执行 render() 以后,返回的就是通过渲染后的页面内容。

为了防止流量攻击,不少网站都有访问频率限制,即限制单个 IP 在必定时间段内的访问次数。若超过这个设定的限制,服务器端就会拒绝访问请求,即响应状态码为 403(Forbidden)。

这用来应对外部的流量攻击或者爬虫是能够的,但在这个限定策略下,公司内部的爬虫测试工具一样也没法正常使用了。针对这个问题,经常使用的作法就是在应用系统中开设白名单,将公司内部的爬虫测试服务器 IP 加到白名单中,而后针对白名单中的 IP 不作限制,或者提高限额。但这一样可能会出现问题。由于应用服务器的性能不是无限的,假如爬虫的访问频率超过了应用服务器的处理极限,那么就会形成应用服务器不可用的状况,即响应状态码为 503(Service Unavailable Error)。

基于以上缘由,爬虫的访问频率应该是要与项目组的开发和运维进行统一评估后肯定的;而对于爬虫工具而言,实现对访问频率的控制也就颇有必要了。

那要怎样实现访问频率的控制呢?

咱们能够先回到爬虫自己的实现机制。对于爬虫来讲,无论采用什么实现形式,应该均可以归纳为生产者和消费者模型,即:

  • 消费者:爬取新的页面
  • 生产者:对爬取的页面进行解析,获得须要爬取的页面连接

对于这种模型,最简单的作法是使用一个 FIFO 的队列,用于存储未爬取的连接队列(unvisited_urls_queue)。无论是采用何种并发机制,这个队列均可以在各个 worker 中共享。对于每个 worker 来讲,均可以按照以下作法:

  • 从 unvisited_urls_queue 队首中取出一个连接进行访问;
  • 解析出页面中的连接,遍历全部的连接,找出未访问过的连接;
  • 将未访问过的连接加入到 unvisited_urls_queue 队尾
  • 直到 unvisited_urls_queue 为空时终止任务

而后回到咱们的问题,要限制访问频率,即单位时间内请求的连接数目。显然,worker 之间相互独立,要在执行端层面协同实现总体的频率控制并不容易。但从上面的步骤中能够看出,unvisited_urls_queue 被全部 worker 共享,而且做为源头供给的角色。那么只要咱们能够实现对 unvisited_urls_queue 补充的数量控制,就实现了爬虫总体的访问频率控制。

以上思路是正确的,但在具体实现的时候会存在几个问题:

  • 须要一个用于存储已经访问连接的集合(visited_urls_set),该集合须要在各个 worker 中实现共享;
  • 须要一个全局的计数器,统计到达设定时间间隔(rps即1秒,rpm即1分钟)时已访问的总连接数;

而且在当前的实际场景中,最佳的并发机制是选择多进程(下文会详细说明缘由),每一个 worker 在不一样的进程中,那要实现对集合的共享就不大容易了。同时,若是每一个 worker 都要负责对总请求数进行判断,即将访问频率的控制逻辑放到 worker 中实现,那对于 worker 来讲会是一个负担,逻辑上也会比较复杂。

所以比较好的方式是,除了未访问连接队列(unvisited_urls_queue),另外再新增一个爬取结果的存储队列(fetched_urls_queue),这两个队列都在各个 worker 中共享。那么,接下来逻辑就变得简单了:

  • 在各个 worker 中,只须要从 unvisited_urls_queue 中取数据,解析出结果后通通存储到 fetched_urls_queue,无需关注访问频率的问题;
  • 在主进程中,不断地从 fetched_urls_queue 取数据,将未访问过的连接添加到 unvisited_urls_queue,在添加以前进行访问频率控制。

具体的控制方法也很简单,假设咱们是要实现 RPS 的控制,那么就可使用以下方式(只截取关键片断):

start_timer = time.time()
requests_queued = 0
 
while True:
try:
url = self.fetched_urls_queue.get(timeout=5)
except queue.Empty:
break
 
# visited url will not be crawled twice
if url in self.visited_urls_set:
continue
 
# limit rps or rpm
if requests_queued >= self.requests_limit:
runtime_secs = time.time() - start_timer
if runtime_secs < self.interval_limit:
sleep_secs = self.interval_limit - runtime_secs
# exceed rps limit, sleep
time.sleep(sleep_secs)
 
start_timer = time.time()
requests_queued = 0
 
self.unvisited_urls_queue.put(url)
self.visited_urls_set.add(url)
requests_queued += 1

 


对于提高爬虫效率这部分,当前已经有大量的讨论了,重点都是集中在不一样的并发机制上面,包括多进程、多线程、asyncio等。
提高爬虫效率

不过,他们的并发测试结果对于本文中讨论的爬虫场景并不适用。由于在本文的爬虫场景中,实现前端页面渲染是最核心的一项功能特性,而要实现前端页面渲染,底层都是须要使用浏览器内核的,至关于每一个 worker 在运行时都会跑一个 Chromium 实例。

众所周知,Chromium 对于 CPU 和内存的开销都是比较大的,所以为了不机器资源出现瓶颈,使用多进程机制(multiprocessing)充分调用多处理器的硬件资源无疑是最佳的选择。

另外一个须要注意也是比较被你们忽略的点,就是在页面连接的请求方法上。

请求页面连接,不都是使用 GET 方法么?

的确,使用 GET 请求确定是可行的,但问题在于,GET 请求时会加载页面中的全部资源信息,这自己会是比较耗时的,特别是遇到连接为比较大的图片或者附件的时候。这无疑会耗费不少无谓的时间,毕竟咱们的目的只是为了检测连接资源是否可访问而已。

比较好的的作法是对网站的连接进行分类:

  • 资源型连接,包括图片、CSS、JS、文件、视频、附件等,这类连接只需检测可访问性;
  • 外站连接,这类连接只需检测该连接自己的可访问性,无需进一步检测该连接加载后页面中包含的连接;
  • 本站页面连接,这类连接除了须要检测该连接自己的可访问性,还须要进一步检测该连接加载后页面中包含的连接的可访问性;

在如上分类中,除了第三类是必需要使用 GET 方法获取页面并加载完整内容(render),前两类彻底可使用 HEAD 方法进行代替。一方面,HEAD 方法只会获取状态码和 headers 而不获取 body,比 GET 方法高效不少;另外一方面,前两类连接也无需进行页面渲染,省去了调用 Chromium 进行解析的步骤,执行效率的提升也会很是明显。

总结

本文针对如何使用爬虫技术实现 Web 页面资源可用性检测进行了讲解,重点围绕爬虫如何实现  三个核心特性进行了展开。对于爬虫技术的更多内容,后续有机会咱们再进一步进行探讨。

相关文章
相关标签/搜索