澳门新浦京娱乐场网站-www.146.net-新浦京娱乐场官网
做最好的网站

框架初窥,简易爬虫

简易爬虫设计

网络爬虫

网络爬虫技术总结

  对于大数据行业,数据的价值不言而喻,在这个信息爆炸的年代,互联网上有太多的信息数据,对于中小微公司,合理利用爬虫爬取有价值的数据,是弥补自身先天数据短板的不二选择,本文主要从爬虫原理、架构、分类以及反爬虫技术来对爬虫技术进行了总结。

 

1、爬虫技术概述

 

网络爬虫(Web crawler),是一种按照一定的规则,自动地抓取万维网信息的程序或者脚本,它们被广泛用于互联网搜索引擎或其他类似网站,可以自动采集所有其能够访问到的页面内容,以获取或更新这些网站的内容和检索方式。从功能上来讲,爬虫一般分为数据采集,处理,储存三个部分。

 

传统爬虫从一个或若干初始网页的URL开始,获得初始网页上的URL,在抓取网页的过程中,不断从当前页面上抽取新的URL放入队列,直到满足系统的一定停止条件。聚焦爬虫的工作流程较为复杂,需要根据一定的网页分析算法过滤与主题无关的链接,保留有用的链接并将其放入等待抓取的URL队列。然后,它将根据一定的搜索策略从队列中选择下一步要抓取的网页URL,并重复上述过程,直到达到系统的某一条件时停止。另外,所有被爬虫抓取的网页将会被系统存贮,进行一定的分析、过滤,并建立索引,以便之后的查询和检索;对于聚焦爬虫来说,这一过程所得到的分析结果还可能对以后的抓取过程给出反馈和指导。

 

相对于通用网络爬虫,聚焦爬虫还需要解决三个主要问题:

 

(1) 对抓取目标的描述或定义;

(2) 对网页或数据的分析与过滤;

(3) 对URL的搜索策略。

 

图片 1

 

2、爬虫原理

 

2.1 网络爬虫原理

 

Web网络爬虫系统的功能是下载网页数据,为搜索引擎系统提供数据来源。很多大型的网络搜索引擎系统都被称为基于 Web数据采集的搜索引擎系统,比如 Google、Baidu。由此可见Web 网络爬虫系统在搜索引擎中的重要性。网页中除了包含供用户阅读的文字信息外,还包含一些超链接信息。Web网络爬虫系统正是通过网页中的超连接信息不断获得网络上的其它网页。正是因为这种采集过程像一个爬虫或者蜘蛛在网络上漫游,所以它才被称为网络爬虫系统或者网络蜘蛛系统,在英文中称为Spider或者Crawler。

 

图片 2

 

2.2 网络爬虫系统的工作原理

 

在网络爬虫的系统框架中,主过程由控制器,解析器,资源库三部分组成。控制器的主要工作是负责给多线程中的各个爬虫线程分配工作任务。解析器的主要工作是下载网页,进行页面的处理,主要是将一些JS脚本标签、CSS代码内容、空格字符、HTML标签等内容处理掉,爬虫的基本工作是由解析器完成。资源库是用来存放下载到的网页资源,一般都采用大型的数据库存储,如Oracle数据库,并对其建立索引。

 

控制器

 

控制器是网络爬虫的中央控制器,它主要是负责根据系统传过来的URL链接,分配一线程,然后启动线程调用爬虫爬取网页的过程。

 

解析器

 

解析器是负责网络爬虫的主要部分,其负责的工作主要有:下载网页的功能,对网页的文本进行处理,如过滤功能,抽取特殊HTML标签的功能,分析数据功能。

 

资源库

 

主要是用来存储网页中下载下来的数据记录的容器,并提供生成索引的目标源。中大型的数据库产品有:Oracle、Sql Server等。

 

Web网络爬虫系统一般会选择一些比较重要的、出度(网页中链出超链接数)较大的网站的URL作为种子URL集合。网络爬虫系统以这些种子集合作为初始URL,开始数据的抓取。因为网页中含有链接信息,通过已有网页的 URL会得到一些新的 URL,可以把网页之间的指向结构视为一个森林,每个种子URL对应的网页是森林中的一棵树的根节点。

 

这样,Web网络爬虫系统就可以根据广度优先算法或者深度优先算法遍历所有的网页。由于深度优先搜索算法可能会使爬虫系统陷入一个网站内部,不利于搜索比较靠近网站首页的网页信息,因此一般采用广度优先搜索算法采集网页。Web网络爬虫系统首先将种子URL放入下载队列,然后简单地从队首取出一个URL下载其对应的网页。得到网页的内容将其存储后,再经过解析网页中的链接信息可以得到一些新的URL,将这些URL加入下载队列。然后再取出一个URL,对其对应的网页进行下载,然后再解析,如此反复进行,直到遍历了整个网络或者满足某种条件后才会停止下来。

图片 3

 

网络爬虫的基本工作流程如下:

 

1.首先选取一部分精心挑选的种子URL;

2.将这些URL放入待抓取URL队列;

3.从待抓取URL队列中取出待抓取在URL,解析DNS,并且得到主机的ip,并将URL对应的网页下载下来,存储进已下载网页库中。此外,将这些URL放进已抓取URL队列;

4.分析已抓取URL队列中的URL,分析其中的其他URL,并且将URL放入待抓取URL队列,从而进入下一个循环。

图片 4

图片 5

 

2.3 抓取策略

 

在爬虫系统中,待抓取URL队列是很重要的一部分。待抓取URL队列中的URL以什么样的顺序排列也是一个很重要的问题,因为这涉及到先抓取那个页面,后抓取哪个页面。而决定这些URL排列顺序的方法,叫做抓取策略。下面重点介绍几种常见的抓取策略:

 

2.3.1 深度优先遍历策略

 

深度优先遍历策略是指网络爬虫会从起始页开始,一个链接一个链接跟踪下去,处理完这条线路之后再转入下一个起始页,继续跟踪链接。我们以下面的图为例:

 

遍历的路径:A-F-G E-H-I B C D

图片 6

2.3.2 宽度优先遍历策略

 

宽度优先遍历策略的基本思路是,将新下载网页中发现的链接直接插入待抓取URL队列的末尾。也就是指网络爬虫会先抓取起始网页中链接的所有网页,然后再选择其中的一个链接网页,继续抓取在此网页中链接的所有网页。还是以上面的图为例:

 

遍历路径:A-B-C-D-E-F G H I

 

2.3.3 反向链接数策略

 

反向链接数是指一个网页被其他网页链接指向的数量。反向链接数表示的是一个网页的内容受到其他人的推荐的程度。因此,很多时候搜索引擎的抓取系统会使用这个指标来评价网页的重要程度,从而决定不同网页的抓取先后顺序。

 

在真实的网络环境中,由于广告链接、作弊链接的存在,反向链接数不能完全等他我那个也的重要程度。因此,搜索引擎往往考虑一些可靠的反向链接数。

 

2.3.4 Partial PageRank策略

 

Partial PageRank算法借鉴了PageRank算法的思想:对于已经下载的网页,连同待抓取URL队列中的URL,形成网页集合,计算每个页面的PageRank值,计算完之后,将待抓取URL队列中的URL按照PageRank值的大小排列,并按照该顺序抓取页面。

 

如果每次抓取一个页面,就重新计算PageRank值,一种折中方案是:每抓取K个页面后,重新计算一次PageRank值。但是这种情况还会有一个问题:对于已经下载下来的页面中分析出的链接,也就是我们之前提到的未知网页那一部分,暂时是没有PageRank值的。为了解决这个问题,会给这些页面一个临时的PageRank值:将这个网页所有入链传递进来的PageRank值进行汇总,这样就形成了该未知页面的PageRank值,从而参与排序。

 

2.3.5 OPIC策略策略

 

该算法实际上也是对页面进行一个重要性打分。在算法开始前,给所有页面一个相同的初始现金(cash)。当下载了某个页面P之后,将P的现金分摊给所有从P中分析出的链接,并且将P的现金清空。对于待抓取URL队列中的所有页面按照现金数进行排序。

 

2.3.6 大站优先策略

 

对于待抓取URL队列中的所有网页,根据所属的网站进行分类。对于待下载页面数多的网站,优先下载。这个策略也因此叫做大站优先策略。

 

3、爬虫分类

 

开发网络爬虫应该选择Nutch、Crawler4j、WebMagic、scrapy、WebCollector还是其他的?上面说的爬虫,基本可以分3类:

 

(1)分布式爬虫:Nutch

(2)JAVA爬虫:Crawler4j、WebMagic、WebCollector

(3)非JAVA爬虫:scrapy(基于Python语言开发)

 

3.1 分布式爬虫

 

爬虫使用分布式,主要是解决两个问题:

 

1)海量URL管理

2)网速

 

现在比较流行的分布式爬虫,是Apache的Nutch。但是对于大多数用户来说,Nutch是这几类爬虫里,最不好的选择,理由如下:

 

1)Nutch是为搜索引擎设计的爬虫,大多数用户是需要一个做精准数据爬取(精抽取)的爬虫。Nutch运行的一套流程里,有三分之二是为了搜索引擎而设计的。对精抽取没有太大的意义。也就是说,用Nutch做数据抽取,会浪费很多的时间在不必要的计算上。而且如果你试图通过对Nutch进行二次开发,来使得它适用于精抽取的业务,基本上就要破坏Nutch的框架,把Nutch改的面目全非,有修改Nutch的能力,真的不如自己重新写一个分布式爬虫框架了。

 

2)Nutch依赖hadoop运行,hadoop本身会消耗很多的时间。如果集群机器数量较少,爬取速度反而不如单机爬虫快。

 

3)Nutch虽然有一套插件机制,而且作为亮点宣传。可以看到一些开源的Nutch插件,提供精抽取的功能。但是开发过Nutch插件的人都知道,Nutch的插件系统有多蹩脚。利用反射的机制来加载和调用插件,使得程序的编写和调试都变得异常困难,更别说在上面开发一套复杂的精抽取系统了。而且Nutch并没有为精抽取提供相应的插件挂载点。Nutch的插件有只有五六个挂载点,而这五六个挂载点都是为了搜索引擎服务的,并没有为精抽取提供挂载点。大多数Nutch的精抽取插件,都是挂载在“页面解析”(parser)这个挂载点的,这个挂载点其实是为了解析链接(为后续爬取提供URL),以及为搜索引擎提供一些易抽取的网页信息(网页的meta信息、text文本)。

 

4)用Nutch进行爬虫的二次开发,爬虫的编写和调试所需的时间,往往是单机爬虫所需的十倍时间不止。了解Nutch源码的学习成本很高,何况是要让一个团队的人都读懂Nutch源码。调试过程中会出现除程序本身之外的各种问题(hadoop的问题、hbase的问题)。

 

5)很多人说Nutch2有gora,可以持久化数据到avro文件、hbase、mysql等。很多人其实理解错了,这里说的持久化数据,是指将URL信息(URL管理所需要的数据)存放到avro、hbase、mysql。并不是你要抽取的结构化数据。其实对大多数人来说,URL信息存在哪里无所谓。

 

6)Nutch2的版本目前并不适合开发。官方现在稳定的Nutch版本是nutch2.2.1,但是这个版本绑定了gora-0.3。如果想用hbase配合nutch(大多数人用nutch2就是为了用hbase),只能使用0.90版本左右的hbase,相应的就要将hadoop版本降到hadoop 0.2左右。而且nutch2的官方教程比较有误导作用,Nutch2的教程有两个,分别是Nutch1.x和Nutch2.x,这个Nutch2.x官网上写的是可以支持到hbase 0.94。但是实际上,这个Nutch2.x的意思是Nutch2.3之前、Nutch2.2.1之后的一个版本,这个版本在官方的SVN中不断更新。而且非常不稳定(一直在修改)。

 

所以,如果你不是要做搜索引擎,尽量不要选择Nutch作为爬虫。有些团队就喜欢跟风,非要选择Nutch来开发精抽取的爬虫,其实是冲着Nutch的名气,当然最后的结果往往是项目延期完成。

 

如果你是要做搜索引擎,Nutch1.x是一个非常好的选择。Nutch1.x和solr或者es配合,就可以构成一套非常强大的搜索引擎了。如果非要用Nutch2的话,建议等到Nutch2.3发布再看。目前的Nutch2是一个非常不稳定的版本。

图片 7

 

分布式爬虫平台架构图

 

3.2 JAVA爬虫

 

这里把JAVA爬虫单独分为一类,是因为JAVA在网络爬虫这块的生态圈是非常完善的。相关的资料也是最全的。这里可能有争议,我只是随便谈谈。

 

其实开源网络爬虫(框架)的开发非常简单,难问题和复杂的问题都被以前的人解决了(比如DOM树解析和定位、字符集检测、海量URL去重),可以说是毫无技术含量。包括Nutch,其实Nutch的技术难点是开发hadoop,本身代码非常简单。网络爬虫从某种意义来说,类似遍历本机的文件,查找文件中的信息。没有任何难度可言。之所以选择开源爬虫框架,就是为了省事。比如爬虫的URL管理、线程池之类的模块,谁都能做,但是要做稳定也是需要一段时间的调试和修改的。

 

对于爬虫的功能来说。用户比较关心的问题往往是:

 

1)爬虫支持多线程么、爬虫能用代理么、爬虫会爬取重复数据么、爬虫能爬取JS生成的信息么?

 

不支持多线程、不支持代理、不能过滤重复URL的,那都不叫开源爬虫,那叫循环执行http请求。

 

能不能爬js生成的信息和爬虫本身没有太大关系。爬虫主要是负责遍历网站和下载页面。爬js生成的信息和网页信息抽取模块有关,往往需要通过模拟浏览器(htmlunit,selenium)来完成。这些模拟浏览器,往往需要耗费很多的时间来处理一个页面。所以一种策略就是,使用这些爬虫来遍历网站,遇到需要解析的页面,就将网页的相关信息提交给模拟浏览器,来完成JS生成信息的抽取。

 

2)爬虫可以爬取ajax信息么?

 

网页上有一些异步加载的数据,爬取这些数据有两种方法:使用模拟浏览器(问题1中描述过了),或者分析ajax的http请求,自己生成ajax请求的url,获取返回的数据。如果是自己生成ajax请求,使用开源爬虫的意义在哪里?其实是要用开源爬虫的线程池和URL管理功能(比如断点爬取)。

 

如果我已经可以生成我所需要的ajax请求(列表),如何用这些爬虫来对这些请求进行爬取?

 

爬虫往往都是设计成广度遍历或者深度遍历的模式,去遍历静态或者动态页面。爬取ajax信息属于deep web(深网)的范畴,虽然大多数爬虫都不直接支持。但是也可以通过一些方法来完成。比如WebCollector使用广度遍历来遍历网站。爬虫的第一轮爬取就是爬取种子集合(seeds)中的所有url。简单来说,就是将生成的ajax请求作为种子,放入爬虫。用爬虫对这些种子,进行深度为1的广度遍历(默认就是广度遍历)。

 

3)爬虫怎么爬取要登陆的网站?

这些开源爬虫都支持在爬取时指定cookies,模拟登陆主要是靠cookies。至于cookies怎么获取,不是爬虫管的事情。你可以手动获取、用http请求模拟登陆或者用模拟浏览器自动登陆获取cookie。

 

4)爬虫怎么抽取网页的信息?

 

开源爬虫一般都会集成网页抽取工具。主要支持两种规范:CSS SELECTOR和XPATH。至于哪个好,这里不评价。

 

5)爬虫怎么保存网页的信息?

 

有一些爬虫,自带一个模块负责持久化。比如webmagic,有一个模块叫pipeline。通过简单地配置,可以将爬虫抽取到的信息,持久化到文件、数据库等。还有一些爬虫,并没有直接给用户提供数据持久化的模块。比如crawler4j和webcollector。让用户自己在网页处理模块中添加提交数据库的操作。至于使用pipeline这种模块好不好,就和操作数据库使用ORM好不好这个问题类似,取决于你的业务。

 

6)爬虫被网站封了怎么办?

爬虫被网站封了,一般用多代理(随机代理)就可以解决。但是这些开源爬虫一般没有直接支持随机代理的切换。所以用户往往都需要自己将获取的代理,放到一个全局数组中,自己写一个代理随机获取(从数组中)的代码。

 

7)网页可以调用爬虫么?

爬虫的调用是在Web的服务端调用的,平时怎么用就怎么用,这些爬虫都可以使用。

 

8)爬虫速度怎么样?

单机开源爬虫的速度,基本都可以讲本机的网速用到极限。爬虫的速度慢,往往是因为用户把线程数开少了、网速慢,或者在数据持久化时,和数据库的交互速度慢。而这些东西,往往都是用户的机器和二次开发的代码决定的。这些开源爬虫的速度,都很可以。

 

9)明明代码写对了,爬不到数据,是不是爬虫有问题,换个爬虫能解决么?

 

如果代码写对了,又爬不到数据,换其他爬虫也是一样爬不到。遇到这种情况,要么是网站把你封了,要么是你爬的数据是javascript生成的。爬不到数据通过换爬虫是不能解决的。

 

10)哪个爬虫可以判断网站是否爬完、那个爬虫可以根据主题进行爬取?

 

爬虫无法判断网站是否爬完,只能尽可能覆盖。

 

至于根据主题爬取,爬虫之后把内容爬下来才知道是什么主题。所以一般都是整个爬下来,然后再去筛选内容。如果嫌爬的太泛,可以通过限制URL正则等方式,来缩小一下范围。

 

11)哪个爬虫的设计模式和构架比较好?

 

设计模式纯属扯淡。说软件设计模式好的,都是软件开发完,然后总结出几个设计模式。设计模式对软件开发没有指导性作用。用设计模式来设计爬虫,只会使得爬虫的设计更加臃肿。

 

至于构架,开源爬虫目前主要是细节的数据结构的设计,比如爬取线程池、任务队列,这些大家都能控制好。爬虫的业务太简单,谈不上什么构架。

 

所以对于JAVA开源爬虫,我觉得,随便找一个用的顺手的就可以。如果业务复杂,拿哪个爬虫来,都是要经过复杂的二次开发,才可以满足需求。

 

3.3 非JAVA爬虫

 

在非JAVA语言编写的爬虫中,有很多优秀的爬虫。这里单独提取出来作为一类,并不是针对爬虫本身的质量进行讨论,而是针对larbin、scrapy这类爬虫,对开发成本的影响。

 

先说python爬虫,python可以用30行代码,完成JAVA 50行代码干的任务。python写代码的确快,但是在调试代码的阶段,python代码的调试往往会耗费远远多于编码阶段省下的时间。使用python开发,要保证程序的正确性和稳定性,就需要写更多的测试模块。当然如果爬取规模不大、爬取业务不复杂,使用scrapy这种爬虫也是蛮不错的,可以轻松完成爬取任务。

图片 8

 

上图是Scrapy的架构图,绿线是数据流向,首先从初始URL 开始,Scheduler 会将其交给 Downloader 进行下载,下载之后会交给 Spider 进行分析,需要保存的数据则会被送到Item Pipeline,那是对数据进行后期处理。另外,在数据流动的通道里还可以安装各种中间件,进行必要的处理。 因此在开发爬虫的时候,最好也先规划好各种模块。我的做法是单独规划下载模块,爬行模块,调度模块,数据存储模块。

 

对于C 爬虫来说,学习成本会比较大。而且不能只计算一个人的学习成本,如果软件需要团队开发或者交接,那就是很多人的学习成本了。软件的调试也不是那么容易。

 

还有一些ruby、php的爬虫,这里不多评价。的确有一些非常小型的数据采集任务,用ruby或者php很方便。但是选择这些语言的开源爬虫,一方面要调研一下相关的生态圈,还有就是,这些开源爬虫可能会出一些你搜不到的BUG(用的人少、资料也少)

 

4、反爬虫技术

 

因为搜索引擎的流行,网络爬虫已经成了很普及网络技术,除了专门做搜索的Google,Yahoo,微软,百度以外,几乎每个大型门户网站都有自己的搜索引擎,大大小小叫得出来名字得就几十种,还有各种不知名的几千几万种,对于一个内容型驱动的网站来说,受到网络爬虫的光顾是不可避免的。

 

一些智能的搜索引擎爬虫的爬取频率比较合理,对网站资源消耗比较少,但是很多糟糕的网络爬虫,对网页爬取能力很差,经常并发几十上百个请求循环重复抓取,这种爬虫对中小型网站往往是毁灭性打击,特别是一些缺乏爬虫编写经验的程序员写出来的爬虫破坏力极强,造成的网站访问压力会非常大,会导致网站访问速度缓慢,甚至无法访问。

 

一般网站从三个方面反爬虫:用户请求的Headers,用户行为,网站目录和数据加载方式。前两种比较容易遇到,大多数网站都从这些角度来反爬虫。第三种一些应用ajax的网站会采用,这样增大了爬取的难度。

 

4.1 通过Headers反爬虫

 

从用户请求的Headers反爬虫是最常见的反爬虫策略。很多网站都会对Headers的User-Agent进行检测,还有一部分网站会对Referer进行检测(一些资源网站的防盗链就是检测Referer)。如果遇到了这类反爬虫机制,可以直接在爬虫中添加Headers,将浏览器的User-Agent复制到爬虫的Headers中;或者将Referer值修改为目标网站域名。对于检测Headers的反爬虫,在爬虫中修改或者添加Headers就能很好的绕过。

 

[评论:往往容易被忽略,通过对请求的抓包分析,确定referer,在程序中模拟访问请求头中添加]

 

4.2 基于用户行为反爬虫

 

还有一部分网站是通过检测用户行为,例如同一IP短时间内多次访问同一页面,或者同一账户短时间内多次进行相同操作。

 

[评论:这种防爬,需要有足够多的ip来应对]

 

大多数网站都是前一种情况,对于这种情况,使用IP代理就可以解决。可以专门写一个爬虫,爬取网上公开的代理ip,检测后全部保存起来。这样的代理ip爬虫经常会用到,最好自己准备一个。有了大量代理ip后可以每请求几次更换一个ip,这在requests或者urllib2中很容易做到,这样就能很容易的绕过第一种反爬虫。

 

[评论:动态拨号也是一种解决方案]

 

对于第二种情况,可以在每次请求后随机间隔几秒再进行下一次请求。有些有逻辑漏洞的网站,可以通过请求几次,退出登录,重新登录,继续请求来绕过同一账号短时间内不能多次进行相同请求的限制。

 

[评论:对于账户做防爬限制,一般难以应对,随机几秒请求也往往可能被封,如果能有多个账户,切换使用,效果更佳]

 

4.3动态页面的反爬虫

 

上述的几种情况大多都是出现在静态页面,还有一部分网站,我们需要爬取的数据是通过ajax请求得到,或者通过Java生成的。首先用Firebug或者HttpFox对网络请求进行分析。如果能够找到ajax请求,也能分析出具体的参数和响应的具体含义,我们就能采用上面的方法,直接利用requests或者urllib2模拟ajax请求,对响应的json进行分析得到需要的数据。

 

[评论:感觉google的、IE的网络请求分析使用也挺好]

 

能够直接模拟ajax请求获取数据固然是极好的,但是有些网站把ajax请求的所有参数全部加密了。我们根本没办法构造自己所需要的数据的请求。我这几天爬的那个网站就是这样,除了加密ajax参数,它还把一些基本的功能都封装了,全部都是在调用自己的接口,而接口参数都是加密的。遇到这样的网站,我们就不能用上面的方法了,我用的是selenium   phantomJS框架,调用浏览器内核,并利用phantomJS执行js来模拟人为操作以及触发页面中的js脚本。从填写表单到点击按钮再到滚动页面,全部都可以模拟,不考虑具体的请求和响应过程,只是完完整整的把人浏览页面获取数据的过程模拟一遍。

 

[评论:支持phantomJS]

 

用这套框架几乎能绕过大多数的反爬虫,因为它不是在伪装成浏览器来获取数据(上述的通过添加 Headers一定程度上就是为了伪装成浏览器),它本身就是浏览器,phantomJS就是一个没有界面的浏览器,只是操控这个浏览器的不是人。利用 selenium phantomJS能干很多事情,例如识别点触式(12306)或者滑动式的验证码,对页面表单进行暴力破解等等。它在自动化渗透中还 会大展身手,以后还会提到这个。

 

5、参考资料

 

[1] 网络爬虫基本原理

[2] 基于HttpClient4.0的网络爬虫基本框架

[3] 开源Python网络爬虫框架Scrapy

[4] 开源爬虫框架各有什么优缺点

[5] 社会化海量数据采集爬虫框架搭建

[6] 网站常见的反爬虫和应对方法

 

来源:牛顿的肩膀

原文:

版权:本文版权归原作者所有

Scrapy001-框架初窥

@(Spider)[POSTS]

引言

说这是一个爬虫有点说大话了,但这个名字又恰到好处,所以在前面加了”简易“两个字,表明
这是一个阉割的爬虫,简单的使用或者玩玩儿还是可以的。
公司最近有新的业务要去抓取竞品的数据,看了之前的同学写的抓取系统,存在一定的问题,
规则性太强了,无论是扩展性还是通用性发面都稍微弱了点,之前的系统必须要你搞个列表,
然后从这个列表去爬取,没有深度的概念,这对爬虫来说简直是硬伤。因此,我决定搞一个
稍微通用点的爬虫,加入深度的概念,扩展性通用型方面也提升下。

爬虫类型

  • 通用网络爬虫:如搜索引擎,面向关键字,目标是尽可能大的网络覆盖率,侧重广度
  • 聚焦网络爬虫:抓取某一特定主题相关的网络资源
  • 增量式网络爬虫:对已经爬取的网页进行增量式更新,只爬取新产生或发生变化的网页。
  • 深层网络爬虫:不能通过静态链接获取的,隐藏在搜索表单之后的,如需要登录后才可以查看到的资源。

1.Scrapy简介

Scrapy是一个应用于抓取、提取、处理、存储等网站数据的框架(类似Django)。

应用:

  • 数据挖掘
  • 信息处理
  • 存储历史数据
  • 监测
  • 自动化测试
  • 。。。

还有很多,详细参考:

设计

我们这里约定下,要处理的内容(可能是url,用户名之类的)我们都叫他实体(entity)。
考虑到扩展性这里采用了队列的概念,待处理的实体全部存储在队列中,每次处理的时候,
从队列中拿出一个实体,处理完成之后存储,并将新抓取到的实体存入队列中。当然了这里
还需要做存储去重处理,入队去重处理,防止处理程序做无用功。

   --------   -----------   ---------- 
  | entity | |  enqueue  | |  result  |
  |  list  | | uniq list | | uniq list|
  |        | |           | |          |
  |        | |           | |          |
  |        | |           | |          |
  |        | |           | |          |
   --------   -----------   ---------- 

当每个实体进入队列的时候入队排重队列设置入队实体标志为一后边不再入队,当处理完
实体,得到结果数据,处理完结果数据之后将结果诗句标志如结果数据排重list,当然了
,这里你也可以做更新处理,代码中可以做到兼容。

                      ------- 
                     |  开始 |
                      --- --- 
                         |
                         v
                      -------   enqueue deep为1的实体
                     | init  |--------------------------------> 
                      --- ---   set 已经入过队列 flag
                         |    
                         v    
                     ---------  empty queue   ------ 
             ------>| dequeue  ------------->| 结束 |
            |        ---- ----                ------ 
            |            |                           
            |            |                           
            |            |                           
            |            v                           
            |     ---------------   enqueue deep为deep 1的实体             
            |    | handle entity |------------------------------> 
            |     ------- -------   set 已经入过队列 flag             
            |            |                       
            |            |                       
            |            v                       
            |     ---------------   set 已经处理过结果 flag
            |    | handle result |--------------------------> 
            |     ------- -------              
            |            |                     
             ------------                      

urllib

from urllib import request,parse

url = r'http://www.baidu.com'
postdata = parse.urlencode([('wd','china')])  #post提交的参数

#构造请求
req = request.Request(url)
#添加HTTP头来模拟浏览器
req.add_header('User-Agent', 'Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/60.0.3112.78 Safari/537.36')
#当浏览器向web服务器发送请求的时候,一般会带上Referer,告诉服务器我是从哪个页面链接过来的,用于页面统计和资源防盗链
req.add_header('Referer', 'https://passport.weibo.cn/signin/login?entry=mweibo&res=wel&wm=3349&r=http://m.weibo.cn/')
#获得响应
with request.urlopen(req,data=postdata.encode('utf-8')) as f:
    print(f.status)
    for k,v in f.getheaders():
        print('%s %s' %(k,v))
    print(f.read().decode('utf-8'))

2.Scrapy架构

Scrapy使用了Twisted异步网络库来处理网络通讯。结构如下:

图片 9

Scrapy的核心组件:

  • 引擎(Scrapy Engine
    用来处理整个系统的数据流,触发事务(框架核心),负责控制和调度各个组件

  • 调度器(Scheduler
    用来接受引擎发过来的请求,压入队列中,并在引擎再次请求的时候返回,如:要抓取的链接(URL)的优先队列,由它来决定下一个要抓取的URL是什么,并进行去重。

  • 下载器(Downloader
    用于下载网页内容,并将网页内容返回给Spiders(自己编写的虫子)--下载器建立在Twisted模型上

  • 爬虫(Spider
    干活的虫子,从特定的网页中提取自己需要的信息,即:实体Item,用户也可以提取下级链接,继续抓取页面内容。

  • 项目管道(Pipline
    负责处理爬虫从网页中抽取的实体,主要的功能是持久化实体(Item)、验证实体的有效性、清除垃圾信息。当页面被爬虫解析后,解析后内容将会发送到项目管理通道,经过几个特定的次序处理。

  • 下载器中间件(Downloader Middlewares
    位于Scrapy引擎和下载器之间的框架,主要是处理Scrapy引擎和下载器之间的请求与响应。

  • 爬虫中间件(Spider Middlewares
    介于Scrapy引擎和Spider之间的框架,处理爬虫的响应输入和请求输出。

  • 调度中间件(Scheduler Middlewares)
    介于Scrapy引擎和调度之间的中间件,从Scrapy引擎发送到调度的请求和响应。

从上面可以看出,Scrapy的耦合性很低,扩展性更灵活,自定义性好

爬取策略(反作弊应对)

为了爬取某些网站,最怕的就是封ip,封了ip入过没有代理就只能呵呵呵了。因此,爬取
策略还是很重要的。

爬取之前可以先在网上搜搜待爬取网站的相关信息,看看之前有没有前辈爬取过,吸收他
门的经验。然后就是是自己仔细分析网站请求了,看看他们网站请求的时候会不会带上特
定的参数?未登录状态会不会有相关的cookie?最后就是尝试了,制定一个尽可能高的抓
取频率。

如果待爬取网站必须要登录的话,可以注册一批账号,然后模拟登陆成功,轮流去请求,
如果登录需要验证码的话就更麻烦了,可以尝试手动登录,然后保存cookie的方式(当然
,有能力可以试试ocr识别)。当然登陆了还是需要考虑上一段说的问题,不是说登陆了就
万事大吉,有些网站登录之后抓取频率过快会封掉账号。

所以,尽可能还是找个不需要登录的方法,登录被封账号,申请账号、换账号比较麻烦。

requests

import requests

user_agent = 'Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/60.0.3112.78 Safari/537.36'
headers = {'User-Agent':user_agent}
kv = {'wd':'china'}
r = requests.get('http://www.baidu.com/s',params=kv,headers = headers)
print(r.status_code )
if r.status_code ==  200:
    print(r.headers)
    print(r.headers.get('content-type'))
else:
    r.raise_for_status()

#获取cookie  
for cookie in r.cookies.keys():
    print(cookie   ':'   r.cookies.get(cookie))

print(r.url)
print(r.content) #字节形式
print(r.text[-100:]) #文本形式
print(r.encoding) #根据头部信息猜测的编码方式,不一定准确
print(r.apparent_encoding) #根据内容猜测的编码方式
r.encoding = r.apparent_encoding

import requests

url = 'http://www.baidu.com'
s = requests.Session()
#首先访问,服务器会先分配一个cookie
re = s.get(url,allow_redirects=True)

user_agent = 'Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/60.0.3112.78 Safari/537.36'
headers = {'User-Agent':user_agent}
kv = {'wd':'china'}
#在访问就带有cookie了,不会视为非法用户
re = s.get('http://www.baidu.com/s',params=kv,headers = headers)
print(re.status_code )

import requests

url = 'http://github.com'
proxies = {
           "http":"http://user:pass@host/",
           }
r = requests.get(url,timeout = 2,proxies=proxies)
print(r.history)

import requests

cs_url    = 'http://httpbin.org/post'
my_data   = {
    'key1' : 'value1',
    'key2' : 'value2'
}

r = requests.post (cs_url, data = my_data)
print(r.content)

3.Scrapy执行流程

如下图:

图片 10

此图来自大神的文章

按照上图的序号,数据的执行流程如下:

  1. 引擎自定义爬虫中获取初始化请求(种子URL--自定义爬虫文件中的start_urls
  2. 引擎将该请求放入调度器中,同时引擎从调度器获取一个待下载的请求(二者异步执行)
  3. 调度器返回给引擎一个待下载请求
  4. 引擎将待下载请求发送给下载器,中间会经过一系列下载器中间件
  5. 待下载请求经过下载器处理之后,会生成一个响应对象(response),返回给引擎,中间会再次经过一系列下载器中间件
  6. 引擎接收到下载器返回的响应对象(response)后,将其发送给自定义爬虫,执行自定义逻辑,中间会经过一系列爬虫中间件
  7. 响应对象(response)经过内部的选择器框架初窥,简易爬虫。筛选并调用回调函数处理后,完成逻辑,生成结果对象新的请求对象给引擎,再次经过一系列爬虫中间件
  8. 引擎将返回的结果对象交给结果处理器处理,将新的请求对象交给调度器
  9. 新的请求对象重复上述过程,直到没有新的请求处理

新的请求对象:多层URL的筛选中的下一层URL

==> 详细组件关系,待解读完源码后继续跟上。

抓取数据源和深度

初始数据源选择也很重要。我要做的是一个每天抓取一次,所以我找的是带抓取网站每日
更新的地方,这样初始化的动作就可以作为全自动的,基本不用我去管理,爬取会从每日
更新的地方自动进行。

抓取深度也很重要,这个要根据具体的网站、需求、及已经抓取到的内容确定,尽可能全
的将网站的数据抓过来。

BeautifulSoup

from _pytest.capture import unicode
from bs4 import BeautifulSoup

soup = BeautifulSoup(open('index.html'),'lxml')
#print(soup.prettify())
# print(soup.title)
# print(soup.title.name)
# soup.title.name = 'mytitle'
# print(soup.mytitle.name)
# print(soup.a)
# print(soup.p.attrs)
# print(soup.p.get('class'))
# print('-----------------------')
# print(soup.p.string) #NavigableString
# unicode_str = unicode(soup.p.string)
# print(unicode_str)
#子节点
# print(soup.head.contents)
# print(soup.head.contents[0].string) #.string : 如果一个标记里面没有标记了或者只有唯一标记,则会返回最里面的内容,否则包含多个子节点,则tag无法确定,返回None
# for child in soup.head.children:
#     print(child)
# for child in soup.head.descendants:
#     print(child)
for string in soup.strings: #.stripped_strings去掉空格和空行
    print(string)

for parent in soup.a.parents:
    if parent is None:
        print('1'   parent)
    else:
        print(parent.name)
#兄弟节点
print(soup.p.next_sibling)
print(soup.p.prev_sibling)
for sibling in soup.a.next_siblings:
    print(repr(sibling))
#前后节点
for ele in soup.a.next_elements:
    print(ele)

from _pytest.capture import unicode
from bs4 import BeautifulSoup
import re
from pip._vendor.distlib._backport.tarfile import TUREAD

soup = BeautifulSoup(open('index.html'),'lxml')

print(soup.findAll('b')) #寻找所有的<b>标记
print(soup.findAll(re.compile('^b'))) #寻找所有以b开头的标记
print(soup.findAll(['a','b'])) #找到所有的a标记和b标记
print(soup.findAll(True)) #找到所有tag

def hasClass_Id(tag):
    return tag.has_attr('class') and tag.has_attr('id')
print(soup.findAll(hasClass_Id)) #定义过滤器,找到包含class和id属性的元素

#-------------------------------
print(soup.findAll(id='link1'))
print(soup.findAll(href=re.compile("163"))) #查找href属性含有“163”的tag
print(soup.findAll('a',class_='py1')) #用class过滤

data_soup = BeautifulSoup('<div data-foo="value">foo!</div>','lxml')
print(data_soup.find_all(attrs={"data-foo":"value"}) )#定义字典参数来搜索包含特殊属性的tag
print(soup.find_all('a',text=re.compile('^A'))) #text参数用来搜索字符串
print(soup.find_all('title',recursive=False)) #只搜索tag的直接子节点

xpath,css选择器

from _pytest.capture import unicode
from bs4 import BeautifulSoup
import re
from pip._vendor.distlib._backport.tarfile import TUREAD

soup = BeautifulSoup(open('index.html'),'lxml')
#css选择器
print(soup.select("title")) #直接插找title标记

print(soup.select("html head title")) #逐层查找title

print(soup.select("head > title")) #查找head下的title
print(soup.select("p > # link1")) #查找p下id为link1的标记

print(soup.select(".course")) #根据css类名查找
print(soup.select("# link1")) #根据tag的id找
print(soup.select('a[href]')) #根据是否存在某个属性来查找

import json

from bs4 import BeautifulSoup
import requests


url = 'http://seputu.com/'
user_agent = 'Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/60.0.3112.78 Safari/537.36'
headers = {'User-Agent':user_agent}
r = requests.get(url,headers=headers)
r.encoding = r.apparent_encoding
# print(r.text)
soup = BeautifulSoup(r.text,'html.parser',from_encoding='utf-8')
content = []
for mulu in soup.findAll(class_='mulu'):
    h2 = mulu.find('h2')
    if h2 != None:
        h2_title = h2.string
        list = []
        for a in mulu.find(class_='box').find_all('a'):
            href = a.get('href')
            box_title = a.get('title')
            list.append({'href':href,'box_title':box_title})
        content.append({'title':h2_title,'content':list})
with open('qiye.json','w') as fp:
    json.dump(content,fp=fp,indent=4)

import csv
import json
import re

from bs4 import BeautifulSoup
import requests


url = 'http://seputu.com/'
user_agent = 'Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/60.0.3112.78 Safari/537.36'
headers = {'User-Agent':user_agent}
r = requests.get(url,headers=headers)
r.encoding = r.apparent_encoding
# print(r.text)
soup = BeautifulSoup(r.text,'html.parser',from_encoding='utf-8')

pattern = re.compile(r's*[(.*)]s (.*)')
rows = []
for mulu in soup.findAll(class_='mulu'):
    h2 = mulu.find('h2')
    if h2 != None:
        h2_title = h2.string

        for a in mulu.find(class_='box').find_all('a'):
            href = a.get('href')
            box_title = a.get('title')           
            match = pattern.search(str(box_title))
            if match != None:
                date = match.group(1)
                real_title = match.group(2)
                content = (h2_title,real_title,href,date)
                print(content)
                rows.append(content)
result_header = ['title','real_title','href','date']
with open('qiye.csv','w') as f:
    f_csv = csv.writer(f,)
    f_csv.writerow(result_header)
    f_csv.writerows(rows)

爬取图片等多媒体文件 urllib.request.urlretrieve

import csv
import json
import re
import urllib

from bs4 import BeautifulSoup
import requests

def schedue(a,b,c):  
    '''''回调函数 
    @a:已经下载的数据块 
    @b:数据块的大小 
    @c:远程文件的大小 
    '''  
    per=100.0*a*b/c  
    if per>100:  
        per=100  
    print('%.2f%%' % per)

url = 'http://www.ivsky.com/bizhi/fengjing/'
user_agent = 'Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/60.0.3112.78 Safari/537.36'
headers = {'User-Agent':user_agent}
r = requests.get(url,headers=headers)
r.encoding = r.apparent_encoding
# print(r.text)
soup = BeautifulSoup(r.text,'html.parser',from_encoding='utf-8')


for tupian in soup.findAll(class_='left'):
    for img in tupian.findAll('img'):
        urllib.request.urlretrieve(img.get('src'),img.get('alt') '.jpg',schedue)

4.支持特性

上面介绍的仅仅是冰上一角,Scrapy提供了很多强大的特性是爬虫更加高效,例如:

  • HTML, XML源数据 选择及提取 的内置支持
  • 提供了一系列在spider之间共享的可复用的过滤器(即 Item Loaders),对智能处理爬取数据提供了内置支持。
  • 通过 feed导出 提供了多格式(JSON、CSV、XML),多存储后端(FTP、S3、本地文件系统)的内置支持
  • 提供了media pipeline,可以 自动下载 爬取到的数据中的图片(或者其他资源)。
  • 高扩展性。您可以通过使用 signals ,设计好的API(中间件, extensions, pipelines)来定制实现您的功能。
  • 内置的中间件及扩展为下列功能提供了支持:
  1. cookies and session 处理
  2. HTTP 压缩
  3. HTTP 认证
  4. HTTP 缓存
  5. user-agent模拟
  6. robots.txt
  7. 爬取深度限制
  8. 其他
  • 针对非英语语系中不标准或者错误的编码声明, 提供了自动检测以及健壮的编码支持。
  • 支持根据模板生成爬虫。在加速爬虫创建的同时,保持在大型项目中的代码更为一致。详细内容请参阅 genspider 命令。
    针对多爬虫下性能评估、失败检测,提供了可扩展的 状态收集工具 。
  • 提供 交互式shell终端 , 为您测试XPath表达式,编写和调试爬虫提供了极大的方便
  • 提供 System service, 简化在生产环境的部署及运行
  • 内置 Web service, 使您可以监视及控制您的机器
  • 内置 Telnet终端 ,通过在Scrapy进程中钩入Python终端,使您可以查看并且调试爬虫
  • Logging 为您在爬取过程中捕捉错误提供了方便
  • 支持 Sitemaps 爬取
  • 具有缓存的DNS解析器

优化

在生产环境运行之后又改了几个地方。

第一就是队列这里,改为了类似栈的结构。因为之前的队列,deep小的实体总是先执行,
这样会导致队列中内容越来越多,内存占用很大,现在改为栈的结构,递归的先处理完一个
实体的所以深度,然后在处理下一个实体。比如说初始10个实体(deep=1),最大爬取深度
是3,每一个实体下面有10个子实体,然后他们队列最大长度分别是:

    队列(lpush,rpop)              => 1000个
    修改之后的队列(lpush,lpop)   => 28个

上面的两种方式可以达到同样的效果,但是可以看到队列中的长度差了很多,所以改为第二
中方式了。

最大深度限制是在入队的时候处理的,如果超过最大深度,直接丢弃。另外对队列最大长度
也做了限制,让制意外情况出现问题。

基础爬虫框架

五大模块:

  • 爬虫调度器
  • URL管理器:链接去重:1)内存去重,如set; 2)关系数据库去重; 3)缓存数据库去重,大部分成熟爬虫采用。
  • HTML下载器、
  • HTML解析器、
  • 数据存储器

代码

下面就是又长又无聊的代码了,本来想发在github,又觉得项目有点小,想想还是直接贴出来吧,不好的地方还望看朋友们直言不讳,不管是代码还是设计。

abstract class SpiderBase
{
    /**
     * @var 处理队列中数据的休息时间开始区间
     */
    public $startMS = 1000000;

    /**
     * @var 处理队列中数据的休息时间结束区间
     */
    public $endMS = 3000000;

    /**
     * @var 最大爬取深度
     */
    public $maxDeep = 1;

    /**
     * @var 队列最大长度,默认1w
     */
    public $maxQueueLen = 10000;

    /**
     * @desc 给队列中插入一个待处理的实体
     *       插入之前调用 @see isEnqueu 判断是否已经如果队列
     *       直插入没如果队列的
     *
     * @param $deep 插入实体在爬虫中的深度
     * @param $entity 插入的实体内容
     * @return bool 是否插入成功
     */
    abstract public function enqueue($deep, $entity);

    /**
     * @desc 从队列中取出一个待处理的实体
     *      返回值示例,实体内容格式可自行定义
     *      [
     *          "deep" => 3,
     *          "entity" => "balabala"
     *      ]
     *
     * @return array
     */
    abstract public function dequeue();

    /**
     * @desc 获取待处理队列长度
     *
     * @return int 
     */
    abstract public function queueLen();

    /**
     * @desc 判断队列是否可以继续入队
     *
     * @param $params mixed
     * @return bool
     */
    abstract public function canEnqueue($params);

    /**
     * @desc 判断一个待处理实体是否已经进入队列
     * 
     * @param $entity 实体
     * @return bool 是否已经进入队列
     */
    abstract public function isEnqueue($entity);

    /**
     * @desc 设置一个实体已经进入队列标志
     * 
     * @param $entity 实体
     * @return bool 是否插入成功
     */
    abstract public function setEnqueue($entity);

    /**
     * @desc 判断一个唯一的抓取到的信息是否已经保存过
     *
     * @param $entity mixed 用于判断的信息
     * @return bool 是否已经保存过
     */
    abstract public function isSaved($entity);

    /**
     * @desc 设置一个对象已经保存
     *
     * @param $entity mixed 是否保存的一句
     * @return bool 是否设置成功
     */
    abstract public function setSaved($entity);

    /**
     * @desc 保存抓取到的内容
     *       这里保存之前会判断是否保存过,如果保存过就不保存了
     *       如果设置了更新,则会更新
     *
     * @param $uniqInfo mixed 抓取到的要保存的信息
     * @param $update bool 保存过的话是否更新
     * @return bool
     */
    abstract public function save($uniqInfo, $update);

    /**
     * @desc 处理实体的内容
     *       这里会调用enqueue
     *
     * @param $item 实体数组,@see dequeue 的返回值
     * @return 
     */ 
    abstract public function handle($item);

    /**
     * @desc 随机停顿时间
     *
     * @param $startMs 随机区间开始微妙
     * @param $endMs 随机区间结束微妙
     * @return bool
     */
    public function randomSleep($startMS, $endMS)
    {
        $rand = rand($startMS, $endMS);
        usleep($rand);
        return true;
    }

    /**
     * @desc 修改默认停顿时间开始区间值
     *
     * @param $ms int 微妙
     * @return obj $this
     */
    public function setStartMS($ms)
    {
        $this->startMS = $ms;
        return $this;
    }

    /**
     * @desc 修改默认停顿时间结束区间值
     *
     * @param $ms int 微妙
     * @return obj $this
     */
    public function setEndMS($ms)
    {
        $this->endMS = $ms;
        return $this;
    }

    /**
     * @desc 设置队列最长长度,溢出后丢弃
     *
     * @param $len int 队列最大长度
     */
    public function setMaxQueueLen($len)
    {
        $this->maxQueueLen = $len;
        return $this;
    }

    /**
     * @desc 设置爬取最深层级
     *       入队列的时候判断层级,如果超过层级不做入队操作
     *
     * @param $maxDeep 爬取最深层级
     * @return obj
     */
    public function setMaxDeep($maxDeep)
    {   
        $this->maxDeep = $maxDeep;
        return $this;
    }

    public function run()
    {
        while ($this->queueLen()) {
            $item = $this->dequeue();
            if (empty($item))
                continue;
            $item = json_decode($item, true);
            if (empty($item) || empty($item["deep"]) || empty($item["entity"]))
                continue;
            $this->handle($item);
            $this->randomSleep($this->startMS, $this->endMS);
        }
    }

    /**
     * @desc 通过curl获取链接内容
     *  
     * @param $url string 链接地址
     * @param $curlOptions array curl配置信息
     * @return mixed
     */
    public function getContent($url, $curlOptions = [])
    {
        $ch = curl_init();
        curl_setopt_array($ch, $curlOptions);
        curl_setopt($ch, CURLOPT_URL, $url);
        if (!isset($curlOptions[CURLOPT_HEADER]))
            curl_setopt($ch, CURLOPT_HEADER, 0);
        if (!isset($curlOptions[CURLOPT_RETURNTRANSFER]))
            curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1);
        if (!isset($curlOptions[CURLOPT_USERAGENT]))
            curl_setopt($ch, CURLOPT_USERAGENT, "Mozilla/5.0 (Macintosh; Intel Mac");
        $content = curl_exec($ch);
        if ($errorNo = curl_errno($ch)) {
            $errorInfo = curl_error($ch);
            echo "curl error : errorNo[{$errorNo}], errorInfo[{$errorInfo}]n";
            curl_close($ch);
            return false;
        }
        $httpCode = curl_getinfo($ch,CURLINFO_HTTP_CODE);
        curl_close($ch);
        if (200 != $httpCode) {
            echo "http code error : {$httpCode}, $url, [$content]n";
            return false;
        }

        return $content;
    }
}

abstract class RedisDbSpider extends SpiderBase
{
    protected $queueName = "";

    protected $isQueueName = "";

    protected $isSaved = "";

    public function __construct($objRedis = null, $objDb = null, $configs = [])
    {
        $this->objRedis = $objRedis;
        $this->objDb = $objDb;
        foreach ($configs as $name => $value) {
            if (isset($this->$name)) {
                $this->$name = $value;
            }
        }
    }

    public function enqueue($deep, $entities)
    {
        if (!$this->canEnqueue(["deep"=>$deep]))
            return true;
        if (is_string($entities)) {
            if ($this->isEnqueue($entities))
                return true;
            $item = [
                "deep" => $deep,
                "entity" => $entities
            ];
            $this->objRedis->lpush($this->queueName, json_encode($item));
            $this->setEnqueue($entities);
        } else if(is_array($entities)) {
            foreach ($entities as $key => $entity) {
                if ($this->isEnqueue($entity))
                    continue;
                $item = [
                    "deep" => $deep,
                    "entity" => $entity
                ];
                $this->objRedis->lpush($this->queueName, json_encode($item));
                $this->setEnqueue($entity);
            }
        }
        return true;
    }

    public function dequeue()
    {
        $item = $this->objRedis->lpop($this->queueName);
        return $item;
    }

    public function isEnqueue($entity)
    {
        $ret = $this->objRedis->hexists($this->isQueueName, $entity);
        return $ret ? true : false;
    }

    public function canEnqueue($params)
    {
        $deep = $params["deep"];
        if ($deep > $this->maxDeep) {
            return false;
        }
        $len = $this->objRedis->llen($this->queueName);
        return $len < $this->maxQueueLen ? true : false;
    }

    public function setEnqueue($entity)
    {
        $ret = $this->objRedis->hset($this->isQueueName, $entity, 1);
        return $ret ? true : false;
    }

    public function queueLen()
    {
        $ret = $this->objRedis->llen($this->queueName);
        return intval($ret);
    }

    public function isSaved($entity)
    {
        $ret = $this->objRedis->hexists($this->isSaved, $entity);
        return $ret ? true : false;
    }

    public function setSaved($entity)
    {
        $ret = $this->objRedis->hset($this->isSaved, $entity, 1);
        return $ret ? true : false;
    }
}

class Test extends RedisDbSpider
{

    /**
     * @desc 构造函数,设置redis、db实例,以及队列相关参数
     */
    public function __construct($redis, $db)
    {
        $configs = [
            "queueName" => "spider_queue:zhihu",
            "isQueueName" => "spider_is_queue:zhihu",
            "isSaved" => "spider_is_saved:zhihu",
            "maxQueueLen" => 10000
        ];
        parent::__construct($redis, $db, $configs);
    }

    public function handle($item)
    {
        $deep = $item["deep"];
        $entity = $item["entity"];
        echo "开始抓取用户[{$entity}]n";
        echo "数据内容入库n";
        echo "下一层深度如队列n";
        echo "抓取用户[{$entity}]结束n";
    }

    public function save($addUsers, $update)
    {
        echo "保存成功n";
    }
}

动态网站抓取

动态网站数据是局部更新。
两种做法:

  • 直接从JavaScript采集加载的数据
  • 直接采集浏览器中加载好的数据

网页登录POST分析

利用Chrome浏览器F12控制台Network,Serarch进行分析。
如登陆,如果用户名和密码都是用的明文传输,在手动登陆的时候有个小技巧,那就是故意把密码填错,这样可以很容易看到用户名和密码正确的提交路径和提交方式。注意要在打开目标网站登陆界面之前就要打开抓包工具,因为一般加密登陆用户名和密码的js代码在用户登陆之前就被请求了。如果在手动登陆的过程再打开,那么可能就找不到它的JS加密文件了。这种情况一般用于加密提交用户名和密码的时候。

POST中的参数只有三种情况:

  1. 在源代码页面中的
  2. 是通过服务器返回的
  3. 通过运行js生成的 。
    这部分还没能够完全掌握,仍需要好好练习。
    参考:
    微博模拟登录
    百度云盘模拟登录

验证码问题

  1. IP代理
    使用开源项目IPProxyPool代理池
import requests
import json
r = requests.get('http://127.0.0.1:8000/?types=0&count=5&country=国内')
ip_ports = json.loads(r.text)
print(ip_ports)
ip = ip_ports[0][0]
port = ip_ports[0][1]
proxies={
    'http':'http://%s:%s'%(ip,port),
    'https':'http://%s:%s'%(ip,port)
}
r = requests.get('http://ip.chinaz.com/',proxies=proxies)
r.encoding='utf-8'
print(r.text)
  1. Cookie登录
    将cookie信息保存到本地,可以保存一段时间,下次登录直接使用cookie。
  2. 传统验证码识别
    使用图像识别。
  3. 人工打码
    打码兔,QQ超人打码等平台,自动识别 人工识别的组合方式。
  4. 滑动验证打码
    使用selenium:
  • 在浏览器上模拟鼠标拖动
  • 计算图片中缺口偏移
  • 模拟人类拖动鼠标的轨迹
    还可以采取多账号登录后,保存cookie信息,组建cookie池。
    一般爬取难度 :www > m > wap

终端协议分析

当网页抓取困难时,可以考虑PC端或者APP端。
PC端可以使用HTTPAnalyzer分析获取数据api;
app可以在模拟器上使用,然后使用Wireshark抓包分析。

Scrapy

各大组件和功能:

  • Scrapy引擎(Engine):控制数据流在系统的所有组件中流动,并在相应动作发生时触发事件。
  • 调度器(Scheduler):从引擎接收request并让它们入队,一般以后引擎请求request时提供给引擎。
  • 下载器(Downloader)
  • Spider :用户编写用于分析Response并提取item或额外跟进的url的类。相当于Html解析器
  • Item Pipeline:负责处理Spider提取出的item,如清理验证及持久化(如存储在数据库中)。相当于数据存储器。
  • 下载器中间件(Downloader middlewares):引擎与下载器之间的特定钩子,处理下载器传递给引擎的response。
  • Spider中间件(Spider middlewares):引擎与Spider之间的特定钩子,处理Spider的输入(response)和输出(Itme及requests)。

windows下安装

需要

  • pywin32
  • pyOpenSSL:下载完成后运行python setup.py install
  • lxml:pip install lxml' 最后安装Scrapy:pip install Scrapywindows上安装Scrapy确实比较坑,Scrapy依赖Twisted,尤其是在安装Twisted中会出现问题。 在我安装的过程中,直接使用pip install Scrapy` 进行安装时,直接pip install需要在本地进行编译,我电脑上studio 2015各种报错,比如
error: command 'cl.exe' failed: No such file or directory

在网上搜了好多,最终在知乎找到了一种“曲线救国”的解决办法,如下:

Python Extension Packages for Windows
去上面地址下载你对应版本cp35的whl,注意,虽然你系统是64位,但要看清你python版本是32还是64位的,再下载对应的win32或者amd64文件
安装wheel
pip install wheel
进入.whl所在的文件夹,执行命令即可完成安装
pip install 带后缀的完整文件名

然后再使用pip install Scrapy 去安装就ok啦。

本文由澳门新浦京娱乐场网站发布于www.146.net,转载请注明出处:框架初窥,简易爬虫