爬虫抓取网页数据(爬虫的核心特性除了快,还应该是从高到低的)
优采云 发布时间: 2022-02-24 14:17爬虫抓取网页数据(爬虫的核心特性除了快,还应该是从高到低的)
背景
对于电商类型和内容服务类型的网站,经常会出现由于配置错误(404).
显然,要保证网站中的所有链接都可以访问,手动检测肯定是不现实的。常见的做法是利用爬虫技术定期爬取网站上的资源,及时发现访问异常的链接。
对于网络爬虫,市场上已经有大量的开源项目和技术讨论文章。不过,似乎大家普遍关注的是爬取的效率。例如,关于不同并发机制中哪种更高效的讨论有很多文章,但关于爬虫的其他特性的讨论并不多。
在我看来,爬虫的核心特性除了快之外,还应该包括full和stable,而从重要性来看,full、stable和fast应该是从高到低。
全部排在第一位,因为这是爬虫的基本功能。如果爬取的页面不完整,就会出现信息遗漏,这是绝对不允许的;第二名是稳定的,因为爬虫通常需要长时间稳定运行。如果在爬虫运行过程中,由于策略处理不当导致爬虫无法正常访问页面,这是绝对不能接受的。最后,它很快。我们通常需要抓取大量的页面链接,所以效率是关键,但也必须建立在完整性和稳定性的基础上。
当然,爬行本身是一个很深的技术领域,我只是在摸索。本文仅针对使用爬虫技术检测网页资源可用性的实际场景,详细分析涉及的几个技术点,重点解决以下问题:
爬虫实现前端页面渲染
早些年,基本上绝大多数网站都是通过后端渲染的,也就是在服务器端组装一个完整的HTML页面,然后将完整的页面返回到前端进行展示。近年来,随着AJAX技术的不断普及和AngularJS等SPA框架的广泛应用,越来越多的页面呈现在前端。
不知道大家有没有听说前端渲染相比后端渲染不利于SEO,因为它对爬虫不友好。原因是前端渲染的页面需要在浏览器端执行JavaScript代码(即AJAX请求)来获取后端数据,然后组装成一个完整的HTML页面。
对于这种情况,已经有很多解决方案了。最常用的是使用 PhantomJS 和 puppeteer 等无头浏览器工具。) 在抓取页面内容之前。
但是,要使用这种技术,通常需要使用 Javascript 来开发爬虫工具,这对于像我这样习惯于编写 Python 的人来说确实有点痛苦。
直到有一天,大神kennethreitz 发布了开源项目requests-html,当我看到了Full JavaScript support!项目在 GitHub 上发布不到三天,star 数就达到了 5000 多,可见其影响力。
为什么 requests-html 如此受欢迎?
写过 Python 的人基本都会用到 requests 之类的 HTTP 库。毫不夸张地说,它是最好的 HTTP 库(不仅限于编程语言),其引入语言 HTTP Requests for Humans 也是当之无愧的。也正因如此,Locust 和 HttpRunner 都是基于请求开发的。
Requests-html 是 kennethreitz 基于 requests 开发的另一个开源项目。除了复用requests的所有功能外,还实现了对HTML页面的解析,即支持Javascript的执行,以及通过CSS和XPath的功能提取HTML页面元素,是编写爬虫非常需要的功能工具。
在实现 Javascript 执行方面,requests-html 并没有自己造轮子,而是依赖于开源项目 pyppeteer。还记得前面提到的 puppeteer 项目,就是 Google Chrome 官方实现的 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)。无论使用哪种并发机制,这个队列都可以在工作人员之间共享。对于每个工作人员,您可以执行以下操作:
那么回到我们的问题,限制访问的频率,也就是单位时间内请求的链接数。显然,工人之间是相互独立的,在执行层面上实现全面的频率控制并不容易。但是从上面的步骤可以看出,unvisited_urls_queue 是所有worker 共享的,起到了source 供应的作用。那么只要我们能实现对unvisited_urls_queue补充的数量控制,就实现了爬虫的整体访问频率控制。
上面的思路是对的,但是具体实现有几个问题:
而在目前的实际场景中,最好的并发机制是选择多个进程(原因后面会详细解释)。每个工人处于不同的进程中,因此共享集合并不容易。同时,如果每个worker都负责判断请求的总数,也就是如果访问频率的控制逻辑在worker中实现的话,对worker来说就是一个负担,逻辑上会比较多复杂的。
因此,更好的方法是在未访问链接队列(unvisited_urls_queue)的基础上,增加一个用于爬取结果的存储队列(fetched_urls_queue),这两者都是worker之间共享的。那么,接下来的逻辑就变得简单了:
具体控制方法也很简单。假设我们要实现对RPS的控制,那么我们可以使用以下方法(只截取关键段):
<p>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