1、浏览器手工爬取过程
既然爬虫是一种自动化的爬取程序,那么在认识爬虫之前,我们不妨先来看看,通过浏览器手工非自动化爬取是什么样的过程。
1)打开浏览器,在地址栏中输入一个网址,如www.qq.com。
2)按回车键,这时浏览器窗口就会显示出一个页面。
3)在这个页面中寻找可被单击的超链接。
4)依次单击这些超链接,就可以获取到新的页面,同时记录下新页面的URL。
5)如此循环反复,直至获取到网站中的所有URL,这样就完成了手工爬取。
在这个过程中,浏览器隐藏了太多的细节,使我们无法看到本质。因此,我们需要拨开浏览器这层“云雾”,来看它在这中间究竟做了哪些工作。
浏览器与服务端的完整交互过程,如下图:
1)当用户在浏览器地址栏中输入URL:www.qq.com,然后按回车键,这时浏览器的主线程会创建一个子进程,并同时将 URL 传递给这个子线程,让它去完成后续的具体交互工作。
2)由于互联网上的资源都是以IP进行寻址的,因此,子线程首先会与DNS服务器进行交互,发送DNS查询请求,查询www.qq.com对应的IP地址信息。
3)在获取IP地址后,浏览器就会与Web服务器建立连接,并发送HTTP请求,而Web服务器在收到请求后,也会向浏览器返回HTTP响应信息。
4)浏览器在接收到HTTP响应信息后,便会对其中的响应内容进行页面解析、Cookie处理和页面渲染等操作,并将最终的页面内容呈现给用户,从而完成整个交互工作。
浏览器背后的工作远远不止这些,这里只是选取了其中几个关键的部分进行介绍,并通过这些内容来抽象描述爬虫所需要满足的基础模型;同时以浏览器的视角来看爬虫的爬行过程,这样有助于我们对爬虫的工作细节有一个直观的了解。
2、URL
URL,又名统一资源定位符,它是对互联网上资源的位置和访问方法的一种简洁的表示,是互联网上标准资源的地址。它可以理解为是互联网上资源的索引,通过它爬虫就可以找到新的资源,获取新的URL。
URL的完整格式(其中带方括号[]的为可选项):
protocol://hostname[:port]/path/[;parameters][?query]#fragment.
举个例子:
http://www.qq.com:80/index.php?id=1#target.
上面例子中对应URL格式的内容如下:
protocol(协议)——http
hostname(主机名)——www.qq.com
port(端口)——80
path(路径)——index.php
query(查询参数)——id=1
fragment(信息片段)——target
在HTML中,URL主要会在下列常见标签中产生,我们可以通过其相关的属性值来获取,如下:
3、超级链接
超级链接简称为超链接,它的作用是将各个独立的页面链接起来,从而形成一个相对完整的Web网站或应用。网页的超链接一般分为三种:一种是绝对URL的超链接,它链接的是网络上的一个站点、网页或其他资源;第二种是相对 URL 的超链接,它链接的是同一网站的资源;还有一种,它链接的是同一网页的资源,这种超链接又叫书签。爬虫在爬取的过程中,需要能够识别这些超链接内容,并通过它们来获取新的页面和新的URL。
在HTML文件中,超链接主要用标签a来表示和标记,它的href属性则是链接的目标。
4、HTTP协议(Request/Response)
HTTP协议主要用于Web应用层传输,是Web架构的核心基础。它于1990年提出,经过长达三十年的使用和发展,得到不断完善和扩展,由于其简捷、快速的特点,得到大家的青睐。现在,互联网大部分Web应用都是基于HTTP协议来实现的。HTTP协议可以看作与Web应用沟通的语言,而理解语言的意思则是任何后续交互的基础。因为只有清楚地明白交互双方的意思,才能更好地进行交互。爬虫也需要与Web应用进行交互,所以自然也就需要理解它,这样爬虫才能更加有效地进行爬取。下面来看看HTTP协议的内容。
HTTP协议目前有三个主要版本:HTTP/0.9、HTTP/1.0和HTTP/1.1。HTTP/0.9是最早的版本,它只定义了最基本的简单请求和简单回答;HTTP/1.0是一个比较完善的版本;HTTP/1.1在继承了 HTTP/1.0 优点的基础上,增加了大量的报头域来改进和扩充其性能,比如,增加Connection请求头来保持持久性链接,并默认开启;增加Host请求头字段来访问同一Web服务器上不同的Web站点等。因此,现在大部分Web应用都是基于HTTP/1.1进行通信的。
HTTP协议由两个重要部分组成,HTTP请求(Request)和HTTP响应(Response)。
(1)HTTP请求
HTTP请求由三部分组成,分别是:请求行、请求报头和请求正文。
1)请求行
请求行的格式如下:
它是以一个方法符号开头,空格分开,后面跟着请求的URI和协议的版本。
2)请求报头
请求报头由key/value形式成对组成,每行一对,key和value用英文冒号“:”分隔。
3)请求正文
请求正文是提交到Web服务器上的数据,当请求方法为POST时,通常会包含请求正文,它会用请求报头中的Content-Type和Content-Length来指定数据类型和数据长度。
一个标准的HTTP请求格式如下:
举例说明如下:
(2)HTTP响应
Web服务器在收到HTTP请求消息后,会返回一个HTTP响应消息。HTTP响应也是由三个部分组成,分别是:响应行、响应头、响应正文。
1)响应行
响应行的格式如下:
其中,HTTP-Version表示服务器HTTP协议的版本;Status-Code表示服务器发回的响应状态代码,由三位数字组成;Reason-Phrase表示状态代码的文本描述。
2)响应头
响应头也是由key/value形式成对组成的,每行一对,key和value用英文冒号“:”分隔。
3)响应正文
响应正文就是服务器返回的资源内容。
一个标准的HTTP响应如下:
举例说明如下:
5、HTTP认证
爬虫在爬取资源的过程中,有时候会遇到HTTP认证的情况,也就是说,Web服务器会对客户端的权限进行认证,只有认证通过才允许其访问服务端的资源。爬虫需要通过HTTP认证才能进一步去爬取,HTTP认证的方式主要有两种,下面分别进行介绍。
(1)Basic认证(基本认证)
Basic认证是HTTP常用的一种认证方式,由于HTTP协议是无状态的,所以客户端每次访问Web应用时,都要在请求的头部携带认证信息,一般是用户名和密码,如果验证不通过,则会提示如下:
Basic认证的请求和响应,抓包如下:
其中,HTTP 请求中的 Authorization 字段包含着用户名和密码信息,Basic 后面的一串字符“YWRtaW46c2VjcmV0”即为用户名和密码的Base64编码,解码后的内容为:admin:secret。从上面的描述中,我们可以看到,Basic 认证的缺点很明显,它是按照明文信息进行传递的,因此很容易被中间人劫持获取。
(2)Digest认证(摘要认证)
Digest认证其实是一种基于挑战-应答模式的认证模型,它比Basic更安全。为了防止重放攻击,客户端在发送第一个请求后,会收到一个状态码为401的响应,响应内容包含一个唯一的字符串:nonce,而且每次请求返回的内容都不一样。摘要式认证过程需要两次交互来完成。
1)第一次交互
客户端在向服务端发送请求后,服务端会返回401 UNAUTHORIZED,同时在响应头中的WWW-Authenticate字段说明认证方式是Digest,其他信息还有realm域信息、nonce随机字符串、opaque透传字段(客户端会原样返回)等,如下:
2)第二次交互
此时客户端会将用户名、密码、nonce、HTTP Method和URI作为校验值进行md5散列计算,然后通过请求头再次发送给服务端,服务端认证成功后就会返回如下的正常内容。
其中客户端请求头Authorization字段中的response值为加密后的密码,服务端通过该值来完成认证,它的生成方式分三步计算:
其实,基本认证和摘要认证都是比较脆弱的认证方式,它们都无法阻止监听和劫持攻击。
6、HEAD方法
HTTP协议中有很多请求方法,这里主要说一下HEAD方法。HEAD方法与GET方法相同,只不过服务器响应时不会返回消息体,只有消息头。一个HEAD请求的响应中,HTTP头包含的元信息应该和一个 GET 请求的响应消息相同。这种方法可以用来获取请求中隐含的元信息,而不用传输实体本身,所以传输效率高,它可以用来检测链接或目录的有效性。下面我们用curl命令发送一个HEAD请求,举例如下:
在HTTP响应中,我们可以看到,虽然响应头有Content-Length字段,但服务端并没有返回响应中的正文内容。
7、Cookie机制
Cookie 由 W3C 组织提出,是最早由 Netscape 社区发展起来的一种会话跟踪机制。目前Cookie已经成为标准,所有的主流浏览器,如IE、FireFox、Chrome等都支持Cookie。我们知道,HTTP是一种无状态的协议,服务端仅从网络连接无法知道客户端的身份,所以Cookie被用来作为会话识别的标识。Cookie实际上是存在客户端的一小段文本信息,当客户端向服务端发送请求时,服务端会通过Response向客户端浏览器发出一个Set-Cookie来设置Cookie信息,这时浏览器会把该 Cookie 信息保存在本地。当浏览器再次请求该网站时,浏览器则会连同该Cookie一同提交给服务端。因此,在遇到存在Cookie的场景时,爬虫也需要遵循该处理原则。举例子说明,我们在浏览器中访问http://192.168.126.141,第一次请求的抓包如下:
刷新浏览器后,抓包来看第二次请求,新设置的Cookie已经被加上了,如下:
8、DNS本地缓存
浏览器在与Web服务器进行交互时,会向DNS服务器发送DNS查询,请求查找域名对应的IP地址。在对一个域名进行爬取时,如果每次都要对域名进行DNS查询解析,就会浪费很多不必要的查询时间,这时DNS缓存的作用就突显出来,它可以将域名与IP对应的关系存储下来。当再次去访问这个域名时,浏览器就会从 DNS 缓存中把 IP 信息取出来,不再去进行DNS查询,从而提高了页面的访问速度。
DNS本地缓存有两种形式:一种是浏览器缓存;另一种是系统缓存。在浏览器中访问域名时,它会优先访问浏览器缓存。一旦未命中,则会访问系统缓存。既然是缓存,那么就会涉及有效时间。系统缓存的DNS记目有一个TTL值(time to live),单位是秒,意思是这个缓存记目的最大有效时间。而浏览器缓存的有效时间,则是由各自厂商单独设置的,不同种类的浏览器,缓存时间不尽相同,比如:chrome浏览器的缓存时间大约为1分钟。
(1)浏览器缓存
这里以chrome浏览器为例,查看其缓存,在地址栏输入如下命令:
执行结果如下:
(2)系统缓存
在Windows平台下,可以通过在命令行窗口输入ipconfig/displaydns查看DNS缓存。由于缓存的生存时间较短,可以在访问的同时,打开命令行窗口进行查看,如下:
在Linux平台下,可以通过NSCD(Name Service Cache Daemon,名称服务缓存守护进程)查看缓存,如果没有安装该服务,对于Ubuntu/Debian的发行版,可以简单通过下述命令进行安装:
可以看到系统缓存的信息,如下:
9、页面解析
这里说的页面解析,主要是指对HTTP请求后的响应内容进行页面分析,并从中提取URL的过程。我们知道,HTTP响应分为响应头和响应体,响应头的内容比较固定,解析也相对简单;而响应体则不一样,它的内容类型多种多样,不同内容的解析方式也不同,因此需要根据响应体的内容类型来区别对待。响应体的内容类型则是由响应头中的“Content-Type”字段来指定的,它主要用于定义网络文件的类型和网页的编码,常见的内容类型如下:
由于我们的目的是获取新的URL,因此只需要关注含有URL信息的内容类型即可,比如HTML文件。虽然文本文件、PDF文件、Flash文件和图片资源文件也可以包含URL信息,但主流的方式仍然还是通过HTML文件来获取URL,所以这里我们主要以HTML文件的解析为例进行介绍。
10、爬虫策略
爬虫在爬取的过程中,会涉及非常多的页面,因此需要讲究一些爬行策略,才能避免页面的重复爬取。通常爬虫策略可以分为三种:广度优先策略、深度优先策略和最佳优先策略。
(1)广度优先策略
广度优先策略是指在爬取过程中,在完成当前层次的爬取后,才进行下一层次的爬取。这句话怎么理解呢?举例说明一下,假设网站的入口页面为A,爬虫在页面A上发现了A1、A2、A3三个页面,当爬取完A1页面后,会把A1页面中的A4和A5放入待抓取的URL列表中,并不会继续爬取A4或A5页面,而是按照同样的规则去爬取A2页面,一直到A3页面爬取结束后,才去爬取第二层的A4和A5页面。
入口:A
第一层:A1、A2、A3
第二层:A4、A5、A6、A7、A8、A9
(2)深度优先策略
深度优先策略是爬虫从起始页的页面开始,沿着一个页面一直爬取下去,直到当前页面没有新的页面时,才会爬取下一个页面。同样,假设网站的入口页面为A,爬虫在页面A上发现了A1、A2、A3三个页面,当爬取完A1页面后,发现了A4和A5两个页面,这时并不会去爬取A2页面,而是继续爬取A4页面,以及A5页面,直到此页面中没有新的页面后,再去爬取A2页面。
入口:A
第一层:A1、A4、A5
第二层:A2、A6、A7
第三层:A3、A8、A9
(3)最佳优先策略(聚焦爬虫策略)
最佳优先策略,是一种启发式的爬行策略。它其实是广度优先策略的一种改进,在广度优先策略的基础上,用一定的网页分析算法,对将要遍历的页面进行评估和筛选,然后选择评估最优的一个或多个页面进行遍历,直至遍历所有的页面为止。
11、页面跳转
在访问某个URL页面,有时该页面会出现跳转的现象,跳转后则会显示新的URL页面内容,对于爬虫来说,它通常需要获取这个新的 URL 信息。因此,这里我们有必要了解一下页面跳转的概念。页面跳转通常有两种形式,一种是客户端跳转,另一种是服务端跳转。
(1)客户端跳转
客户端跳转通常也分为两种:一种是301跳转,301代表永久性转移(Permanently Moved);另一种是302跳转,302代表临时性跳转(Temporarily Moved)。其实301跳转流程与302跳转流程一样,只不过状态码不同而已。当客户端向服务端发送一个请求时,服务端会返回一个301或302的跳转响应,客户端浏览器在接收到这个响应后就会发生页面跳转,它会根据这个响应头中“Location”字段所包含的地址,再次自动向服务端发送一个 HTTP 请求来完成跳转过程。
对于客户端跳转这种情况,我们可以通过WireShark抓包看到完整的跳转过程,如下。
服务端代码:
在客户端访问链接http://192.168.126.137/book/tiaozhuan/301.php,如下图:
这时,我们来抓取网络数据包看下,其实客户端发送了两次请求,如下:
1)第一次HTTP请求
2)第二次请求
2)服务端跳转
服务端在收到客户端的HTTP请求后,由于请求的页面和实际处理请求的页面不同,因此服务端会在内部进行页面跳转,我们称为服务端跳转。在这个过程中,其实服务端只收到客户端的一个HTTP请求,它对客户端来说是透明的,因此客户端看到的仍然是原始的URL,响应的状态码也为200。
我们可以在Nginx中增加下面内容:
其中,abcd.html是客户端发起的请求,而实际服务端处理和响应的是test.html这个页面,如下图:
服务端跳转时,客户端只发送一次请求,浏览器的地址栏不会显示目标地址的URL;而客户端跳转时,由于是两次请求,这时地址栏中会显示目标资源的URL。
12、识别404错误页面
当用户访问网站上不存在或已删除的页面时,服务端就会返回404错误页面,由于其状态码为404,故又称为“404页面”。404错误页面就是告诉我们当前这个URL所对应的资源是不存在的,在爬行的过程中,爬虫也需要识别404错误页面,并根据它来标记当前所爬行的URL是否有效或存在,这样就可以避免无效爬取,提高爬虫效率。通常管理员在设置404错误页面时有下面两种情况:
1)直接在Web容器中设置404错误页面,此时服务端返回404状态码。
2)将404错误页面指向一个新的页面,在页面中使用301或302的方式重定向跳转到这个页面,此时服务器返回301或302状态码。
所以从理论上而前,404错误页面一般返回的状态码为:301、302或404;但也不排除有的管理员设置特殊,直接返回状态码为200的错误页面。所以,对于404错误页面的识别,不能简单根据状态码信息来判断。
13、URL重复/URL相似/URL包含
这三个概念主要用于爬虫对URL列表进行过滤,过滤掉一些对扫描器没有意义的URL,减少重复爬取的时间,提高扫描器整体的效率。
(1)URL重复
URL重复,是指两个URL完全一样。具体来说,就是协议、主机名、端口、路径、参数名和参数值都相同。
(2)URL相似
URL相似,是指两个URL的协议、主机名、端口、路径、参数名和参数个数都相同。
(3)URL包含
URL包含,是指两个URL,将它们分别记为A和B,它们的协议、主机名、端口和路径都相同。若A的参数个数大于或等于B,那么B的参数名列表与A的参数名列表存在包含关系,其实URL相似可以看作URL包含的一种特例,A和B的参数相同。
举例说明如下:
14、区分URL的意义
重复的URL很好理解,因为它们已经被爬行过了,重复的爬取并不会发现新的URL链接,只会浪费计算资源和延长爬行时间。因此,需要找出这类URL,并将其过滤,减少重复爬取。
那么相似的URL和包含的URL呢?
这里我们结合扫描器的场景来看,扫描器获取这些 URL 的目的主要是对它们进行安全漏洞审计,而安全漏洞审计的主要方式是对URL中的参数进行模糊测试(Fuzz testing)。对于相似的 URL 检测,其实就是检查服务端上同一个文件的相同参数。从漏洞检测的角度来看,如果其中一个URL存在漏洞,那么相似的URL一定也会存在漏洞,因此,就没有必要对相似的URL都进行检测。包含的URL也是同样的道理,对于服务端上的同一个文件,我们只需要检测不同的参数。对于相同的参数,可以直接过滤,这样可以避免重复检测,提高扫描器的效率。
15、URL去重
重复的URL会大大降低爬虫效率,因此,我们需要对URL进行去重处理。那么如何进行URL去重呢?常见的方式有两种:布隆过滤器和哈希表去重。
(1)布隆过滤器(Bloom Filter)
布隆过滤器(Bloom-Filter),是由布隆(Burton Howard Bloom)在1970年提出的。它实际上是由一个很长的二进制向量和一系列随机映射函数组成的,可以用于检索一个元素是否在一个集合中。它的优点是空间和查询时间都远远超过一般的算法,缺点是有一定的误识别率和删除困难。因此,Bloom Filter 不适合那些“零错误”的应用场合。而在能容忍低错误率的应用场合下,Bloom Filter比其他常见的算法(如Hash、折半查找)极大地节省了空间。
下面讲一下布隆过滤器的原理。
布隆过滤器首先需要的是一个位数组和k个映射函数(与Hash表类似),在初始状态时,对于长度为m的位数组array,它的所有位置都被置为0。
对于有n个元素的集合S={s1,s2……sn},通过k个映射函数{f1,f2……fk},将集合S中的每个元素 sj(1<=j<=n)映射为 k 个值{g1,g2……gk},然后再将位数组 array 中相对应的array[g1],array[g2]……array[gk]置为1。
如果要查找某个元素 item 是否在 S 中,则通过映射函数{f1,f2……fk}得到 k 个值{g1,g2……gk},然后再判断 array[g1],array[g2]……array[gk]是否都为 1,若全为 1,那么 item在S中,否则item不在S中。
(2)哈希表去重
哈希表去重的做法比较简单,它通过建立一个哈希表,然后将种子 URL 放进去。对于任何一个新的URL,首先它需要在哈希表中进行查找,如果哈希表中不存在,那么就将新的URL插入哈希表中,直至遍历完所有的URL,最后哈希表中的内容就是去重后的URL。这种方式去重效果精确,不会漏掉一个重复的URL,但对空间的消耗也相应较大。根据哈希表存放的位置,可以将其分为两种方式:一种是基于内存的Hash表去重;另一种是基于硬盘的Hash表去重。
1)基于内存的Hash表去重
这种方式直接在内存中对URL进行操作和去重,随着URL的增长,它消耗的内存空间也越来越多,然而内存大小是有瓶颈的,因此,它无法完成对大型网站的全站爬取。但由于数据操作是直接在内存中执行的,所以,它的处理速度很快。
在真实的爬取中,由于URL是字符串形式,占用的字节数较多,按照保守估计,每个URL平均的长度为 20,当然,URL 越长占用的空间也就越大。这种情况下我们可以进行简单的优化,对URL进行压缩存储。
以md5哈希算法为例,md5运算后的结果是128bit,也就是16字节的长度,而且每个URL的长度都可以控制在16字节,这样就可以极大地减少存储空间的开销。
具体的操作方式为:对 URL 进行哈希运算,然后放到这个哈希表中,如果哈希值不存在于哈希表中,就将该 URL 插入结果列表,同时将哈希值插入哈希表,直至遍历结束,此时结果列表中就是去重后的URL。
2)基于硬盘的Hash表去重
它将URL存储在硬盘上,并在硬盘上对其进行去重。这样在处理海量URL的时候,就不用担心内存溢出的问题。这种方式有个成熟的解决方案,就是利用Berkeley DB进行基于硬盘的URL去重。
Berkeley DB是一个开源的文件数据库,介于关系数据库与内存数据库之间,使用方式与内存数据库类似,它提供的是一系列直接访问数据库的函数,是一个高性能的嵌入式数据库编程库(引擎),可以用来保存任意类型的键/值对(Key/Value),而且可以为一个健保存多个数据。它支持数千个并发线程同时操作数据库,支持最大256TB的数据。同时提供诸如C语言、C++、Java、Perl、Python等多种编程语言的API,并且广泛支持大多数类Unix操作系统、Windows操作系统,以及实时操作系统(如:VxWorks)。
Berkeley DB实际是一个在硬盘上的Hash表,我们可以使用压缩后的URL字符串作为Key,而对于Value可以使用Boolean,一个字节;实际上,Value是一个状态标识,减少Value占用存储空间,然后直接向Berkeley DB添加URL即可。当遇到重复的URL时,它就会通过返回值告知我们。
16、页面相似算法
在一些情况中,比如 SQL 注入检测,我们通常需要比较两个页面内容的关系,看看它们是否相似或相同,然后利用它们的差异性来判断输入对后端应用的影响。页面相似算法有很多,这里主要介绍其中常用的两种:编辑距离和Simhash。
(1)编辑距离(Levenshtein Distance)
它是指两个字符串之间,由一个转成另一个所需的最少编辑操作次数。许可的编辑操作包括将一个字符替换成另一个字符,插入一个字符,删除一个字符。编辑距离的算法由俄国科学家Levenshtein提出,所以叫Levenshtein Distance。一般来说,编辑距离越小,两个串的相似度越大。
(2)Simhash
Simhash是Google用来处理海量文本去重的算法,它会为每一个Web文档通过Hash的方式生成一个64位的字节指纹,暂且称之为“特征字”,判断相似度时,只需判断特征字的海明距离是不是小于n(根据经验值,n一般取值为3),就可判断两个文档是否相似。
那么,什么叫海明距离呢?在信息编码中,两个合法代码对应位上编码不同的位数称为码距,又称海明距离。
举例如下:
10101和00110从第一位开始依次有第一位,第四位和第五位不同,则海明距离为3。
17、断连重试
在爬虫的爬行过程中,为了保证爬虫的稳定和健壮,必须要考虑网络抖动的因素。因此,我们需要增加断连重试机制。当连接断开时,爬虫需要尝试去重新建立新的连接,只有当连接断开的次数超过阀值时,才会认定当前的网络不可用。
18、动态链接与静态链接
这里所说的动态链接和静态链接,主要是针对URL而前的,它们可以通过URL的扩展名来区分。
静态链接主要是指静态资源文件,扩展名主要为:rar、zip、ttf、png、gif等。因为它们对获取新的 URL 并没有做出太多贡献,而且这类链接的数量又非常大,因此,我们需要在新一轮爬取前过滤这些无意义的静态链接,这样就可以极大地提高爬行效率。
动态链接与静态链接是相反的,它所代表的页面中包含新的URL,我们需要对其进行页面解析和URL提取操作,这类链接的扩展名主要为:html、shtml、do、asp、aspx、php、jsp等。
全部评论