基于R语言的自动数据收集:网络抓取和文本挖掘实用指南【2.4】
5.3 HTTP之外的协议
互联网上数据传输的协议远远不止HTTP一个。要了解RCurl组件支持协议的概貌,我们可以利用如下命令:

上面列出的所有协议中,并不是所有都和网络抓取用途有关。下面,我们会重点讲解两个在浏览和抓取网络内容时会经常接触到的协议: HTTPS和FTP。
5.3.1 HTTP安全协议
严格地说,超文本传输协议安全版(Hypertext Transfer Protocol Secure,HTTPS)本身并不是一个协议,而是HTTP协议和SSL/TLS协议(安全套接字层/传输安全层,Secure Sockets Layer/Transport Security Layer)的组合。HTTPS在涉及敏感数据传输的情况下(如银行或在线购物的业务)是不可或缺的。要传输资金或信用卡信息,我们需要确保这些信息对于第三方是不可访问的。HTTPS会把所有客户端—服务器的通信进行加密(参见图5-5)。HTTPS的URL使用https模式,并默认使用443端口。[1]

图5-5 HTTPS协议的原理
HTTPS有两个用途。第一,它帮助客户端确保对话的服务器是可信的(服务器身份验证)。第二,它提供了客户端——服务器通信的加密,从而让用户能够确信其他人无法获取通信过程中交换的内容。
SSL/TLS安全层是作为HTTP运行的应用层的一个子层来运行的。这意味着HTTP消息会在被传输之前进行加密。SSL协议在1994年由Netscape首次定义(参见Freier et al.2011),并在1999年更新为TLS 1.0(参见Dierks and Allen 1999)。当后面使用“SSL”这个术语时,指的是SSL和TLS两者,它们的差异对于我们来说并不重要。
SSL的一个重要特性是公钥(或者叫非对称)加密,它让在不安全网络中进行安全通信成为可能。顾名思义,加密的密钥实际上并不是保密的,而是任何人都可以获得。为了加密一条给特定接收者的消息,就会用到接收者的公钥。而要解密这条消息,就既要用到公钥也要用到私钥,而私钥只有接收者知道。这里的基本思路是,当一个客户端希望向服务器发送一条保密消息时,它知道如何给消息加密,因为服务器的公钥是已知的。不过,在加密之后,除了接收者之外的任何人——甚至包括发送者在内——都无法破译这条消息,因为只有接收者才会兼有公钥和私钥。
要了解HTTPS的用途,我们不必深入钻研公钥加密、密码算法工作原理和它难以破解的原因等细节问题。要想了解SSL背后的密码学知识,我们推荐学习Gourley和Totty(2002)以及Garfinkel(2002)的优秀教程。如果你想更深入理解数字密码学,Ferguson et al.(2010)以及Paar和Pelzl(2011)的这两本书会是很好的选择。不过,对本书内容而言,值得了解的是客户端和服务器之间的安全通道实际上是如何建立起来的,以及在R里如何实现。下面就是一个极其简化的“SSL握手”模式,也就是客户端和服务器在实际交换加密的HTTP消息之前,关于建立HTTPS连接的协商(参见Gourley and Totty 2002,pp.322-328)。
1)客户端通过443端口建立到服务器的TCP连接,并发送关于SSL版本和加密设置的信息。
2)服务器发回关于SSL和加密设置的信息。服务器也会通过发送一个证书来证实其身份。该证书包括了关于发证机构、持证者及有效期等信息。由于任何人不用太费力就能自己创建一套证书,所以可信的发证机构(Certificate Authority,CA)的数字签名是非常重要的。现在有很多商业化的CA,但是某些证书提供商也会免费发放证书。
3)客户端判断是否信任该证书。浏览器和操作系统都会内置有可信发证机构的清单。如果签发服务器证书的机构是可信机构其中之一,客户端就认为该服务器是可信的。否则,浏览器会询问用户是信任该服务器并继续通信,还是应该停止通信。
4)通过使用该HTTPS服务器的公钥,客户端创建了一个只有该服务器能够读取的会话秘钥,并发送给服务器。
5)服务器解密会话秘钥。
6)现在,客户端和服务器都拿到了会话密钥。因此,关于该秘钥的知识不再是非对称的,而是对称的。这降低了加密所需的计算成本。之后在服务器和客户端之间来回传输的数据就可以通过这个对称的SSL通道进行加密和解密了。
请注意很重要的一点,SSL保护的是通信的内容。这包括HTTP标头、cookie和正文。不过,不受保护的是IP地址,也就是和客户端进行通信的网站。
我们会在9.1.7节讨论在R里如何建立通过HTTPS的连接,以及个别函数背后隐藏的技术细节,在R里运用HTTPS一点也不难。
5.3.2 FTP
文件传输协议(File Transfer Protocol,FTP)是为文件传输以及文件目录管理制定的,文件传输包括从客户端向服务器(上传)和从服务器向客户端(下载)。FTP于1971年由Abhay Bhushan(1971)首次提出;它的当前规格说明(参见Postel and Reynolds 1985)也有差不多30年历史了。原则上说,HTTP比FTP有几个优势。它能够支持持久的连接,也就是说,客户端和服务器之间的连接在多次传输过程中可以保持。这对于FTP是不可能的,它的连接必须在每次传输之后重新建立。此外,原生FTP不支持代理和流水线,即在接收到回复之前发出多个同步请求。FTP也有好的一面,它在特定情况下速度更快,因为它不像HTTP那样带有一些标头字段,而是仅仅传输二进制或ASCII文件。
FTP在通信两端各使用两个端口,一个用于数据交换(即“数据端口”,默认是20端口),一个用于命令交换(即“控制端口”,默认是21端口)。和HTTP类似,FTP也有一套命令用来声明传输哪些文件、创建什么目录,以及其他很多操作。[2]FTP连接可以通过两种不同模式建立:主动模式和被动模式。在主动FTP模式下,客户端连接到服务器的命令端口,并请求另一个端口的数据传输。这种模式的问题是,实际数据连接是由服务器建立的。由于客户端的防火墙并不知道客户端在等待数据从特定端口进来,它通常会阻挡服务器试图发送过来的数据。这个问题可以用被动模式解决,在这种模式下,客户端会初始化命令和数据连接。我们会在9.1.2节演示如何在R里访问FTP服务器。
[1] 回顾一下,默认的HTTP端口号是80。
[2 ] 要了解现有命令的概括,请参阅http://www.nsftools.com/tips/RawFTP.htm。
5.4 HTTP实战
我们现在来学习使用R作为HTTP客户端。我们会深入了解两个可用的组件:强大的RCurl组件(Temple Lang 2013a),以及更轻量级但有时也更方便的httr组件(Wickham 2012),它也是基于重量级的Rcurl组件的。
R的基础框架里已有用于下载网络资源的基本功能。download.file()函数能处理很多下载程序,让我们无须对HTTP请求作复杂的修改。此外,还有一套基础函数用于设置和操作网络连接。想了解它们的概要情况,可以在R里输入?connections。不过,使用这些函数并不方便。就拿download.file()来说吧,对于复杂的网络抓取工作,它有两个主要的不足之处。首先,它不是非常灵活。例如,我们没法用它通过HTTPS连接服务器,或额外声明一些标头字段。第二,用download.file()很难做到遵守我们的友好网络抓取标准,因为它缺乏基本的提供身份识别信息的手段。不过,如果我们只是需要下载单个文件,download.file()还是非常好用的。对于更加复杂的任务,我们可以运用RCurl和httr组件里的功能。
5.4.1 libcurl库
我们用R在网上需要做的工作中,很多能利用libcurl库产生神奇的支持作用(Stenberg 2013)。libcurl是一个用C语言编写的外部库。它的开发始于Daniel Stenberg 1996年的cURL项目,自那时起就一直有持续的开发支持。libcurl的用途是为很多平台上的程序提供多种互联网协议的简单接口。随着时间的推移,该库的特性不断增加,现在的版本由大量针对HTTP通信进行设置的可选功能和选项,以及其他一些特性组成。我们可以把它当作一个工具,它知道如何处理下列任务:
·声明HTTP标头。
·解析URL编码。
·处理来自Web服务器的输入数据流。
·建立SSL连接。
·连接代理。
·处理身份验证。
以及很多其他工作。相比之下,R自带的url()和download.file()在处理类似填写表单、身份验证或建立有状态对话等复杂任务时就力不从心了。因此,就有了libcurl的问世,让用户能在他们的普通编程环境下利用这个库。作者Temple Lang在他关于RCurl和libcurl设计理念的文章中指出了libcurl的益处:作为最广泛使用的文件传输库,libcurl经历了大量的测试,并且非常灵活(Temple Lang 2012a)。此外,用C语言编写也让它运行速度很快。要得到libcurl灵活性的初步印象,你可以先去libcurl的接口文档网页http://curl.haxx.se/libcurl/c/curl_easy_setopt.html看一下它的可用选项。或者你也可以在R里输入下面的命令,获得libcurl在RCurl组件里可以使用的“简易”接口选项的完整清单:

这里面目前有174个可用选项。其中有一些是我们之前已经用过的,如useragent或proxy。有时为了方便,我们会称为curl选项而不是libcurl选项。curl是一个命令行工具,同样是由cURL软件项目开发的。在R里我们用到的是libcurl库。[1]
5.4.2 基本请求方法
5.4.2.1 GET方法
为了执行一个基本的GET请求,从Web服务器获取某个资源,RCurl组件提供了一些高级函数:getURL()、getBinaryURL()以及getURLContent()。基本函数是getURL();getBinaryURL()则便于处理二进制的内容;而getURLContent()会尝试通过检查响应标头的Content-Type字段提前确定内容的类型,并进行相应的处理。虽然这样看起来更为可取,但getURLContent()的配置有时更加复杂,所以我们在默认情况下会继续使用getURL(),除非遇到的是二进制内容。
getURL()这个函数会自动确定主机、端口和请求的资源。如果调用成功,也就是说服务器给出了2XX状态码加上正文作为响应,函数就会返回响应的内容。注意,如果一切正常,所有R/libcurl和服务器之间的协商是对我们隐藏的。我们只需把目标URL传递给高级函数即可。例如,如果要从www.r-datacollection.com/materials/http提取helloworld.html,我们就可以输入:

正文会作为字符数据返回。对于二进制内容,我们可以使用getBinaryURL()并获取返回的原始内容。例如,如果要从www.rdatacollection.com/materials/http请求PNG图片文件sky.png,可以输入:

后面的步骤取决于实际上如何处理它。在这个例子里,使用writeBin()函数把该文件保存在本地:

有时候,内容并不是嵌入在静态HTML网页里的,而是在我们提交HTML表单后才返回的。http://www.rdatacollection.com/materials/http/GETexample.html的这个小例子可以让你把名字和年龄作为输入字段进行说明。它的HTML源代码如下所示:

<form>元素表明了输入表单的数据会被发送到一个叫作GETexample.php的文件。[2]从该GET请求接收到这些数据后,文件里的PHP脚本会检查输入数据并返回“Hello<name>!You are<age>years old.”。在浏览器里,我们可以看见表单的URL是http://www.rdatacollection.com/materials/http/GETexample.php?name=<name>&age=<age>,它指定由一个PHP脚本产生输出结果。
在R里,我们如何处理这个以及与之类似的请求呢?有多种方式可以指定一个HTML表单里的参数。首先是利用paste()手工构建URL并将其传递给getURL()函数:

相比使用getURL()并手工构建GET表单请求,一种更简便的方法是使用getForm(),它允许在函数里把参数声明为分开的值。这是我们首选的程序,因为它简化了修改调用的工作,也不需要手工编码URL(见5.1.2节)。为了获得和上面例子里相同的结果,我们可以编写如下语句:

5.4.2.2 POST方法
在使用HTML表单的时候,除了GET方法,我们还经常需要用到POST方法。总体而言,POST能支持更加复杂的请求,因为在这种方法下请求参数不需要插入URL里,而URL的长度是有限制的。使用POST方法意味着参数及其值是在请求正文里而不是在URL中发送的。我们在这里要复制前面的例子,只是现在采用的是一个POST请求。该表单的位置是http://www.rdatacollection.com/materials/http/POSTexample.html。其HTML源代码如下所示:

我们会发现这里的<form>元素和前面例子里的几乎完全一样,除了其中所需的方法不同:现在它是POST。当我们在浏览器里提交这个POST表单时,会看到URL变成了./POSTexample.php,在后面没有像原来的GET查询那样加入了查询参数。要在R里复制该POST查询,我们不必手工构建请求,而是可以使用postForm()函数:

postForm()会自动构建表单体,并把预先声明的参数对填充到表单里。不幸的是,对这些参数对进行格式化会有多种方式,而我们有时必须利用style参数预先显式声明可接受的方式(表单内容类型的详细讲解请参见Nolan和Temple Lang 2014,p.270-272以及http://www.w3.org/TR/html401/interact/forms.html)。对于application/x-www-form-urlencoded表单内容类型,我们必须声明style="post";而对于multipart/form-data表单内容类型,则需要声明style="httppost"。这样可以正确格式化表单体里的参数对,并加入请求标头字段"Content-Type"="application/x-www-form-urlencoded"或"Content-Type"="multipart/form-data"。为了找到合适的POST格式,我们可以在<form>元素里查找一个叫enctype的属性。如果它被声明为enctype='application/x-www-form-urlencoded',我们就使用style="post"。如果表单里面没有这个属性(如前面的例子),那么去掉style参数也是可行的。
更多推荐
所有评论(0)