5.3 HTTP之外的协议互联网上数据传输的协议远远不止HTTP一个。要了解RCurl组件支持协议的概貌我们可以利用如下命令上面列出的所有协议中并不是所有都和网络抓取用途有关。下面我们会重点讲解两个在浏览和抓取网络内容时会经常接触到的协议 HTTPS和FTP。5.3.1 HTTP安全协议严格地说超文本传输协议安全版Hypertext Transfer Protocol SecureHTTPS本身并不是一个协议而是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和Totty2002以及Garfinkel2002的优秀教程。如果你想更深入理解数字密码学Ferguson et al.2010以及Paar和Pelzl2011的这两本书会是很好的选择。不过对本书内容而言值得了解的是客户端和服务器之间的安全通道实际上是如何建立起来的以及在R里如何实现。下面就是一个极其简化的“SSL握手”模式也就是客户端和服务器在实际交换加密的HTTP消息之前关于建立HTTPS连接的协商参见Gourley and Totty 2002pp.322-328。1客户端通过443端口建立到服务器的TCP连接并发送关于SSL版本和加密设置的信息。2服务器发回关于SSL和加密设置的信息。服务器也会通过发送一个证书来证实其身份。该证书包括了关于发证机构、持证者及有效期等信息。由于任何人不用太费力就能自己创建一套证书所以可信的发证机构Certificate AuthorityCA的数字签名是非常重要的。现在有很多商业化的CA但是某些证书提供商也会免费发放证书。3客户端判断是否信任该证书。浏览器和操作系统都会内置有可信发证机构的清单。如果签发服务器证书的机构是可信机构其中之一客户端就认为该服务器是可信的。否则浏览器会询问用户是信任该服务器并继续通信还是应该停止通信。4通过使用该HTTPS服务器的公钥客户端创建了一个只有该服务器能够读取的会话秘钥并发送给服务器。5服务器解密会话秘钥。6现在客户端和服务器都拿到了会话密钥。因此关于该秘钥的知识不再是非对称的而是对称的。这降低了加密所需的计算成本。之后在服务器和客户端之间来回传输的数据就可以通过这个对称的SSL通道进行加密和解密了。请注意很重要的一点SSL保护的是通信的内容。这包括HTTP标头、cookie和正文。不过不受保护的是IP地址也就是和客户端进行通信的网站。我们会在9.1.7节讨论在R里如何建立通过HTTPS的连接以及个别函数背后隐藏的技术细节在R里运用HTTPS一点也不难。5.3.2 FTP文件传输协议File Transfer ProtocolFTP是为文件传输以及文件目录管理制定的文件传输包括从客户端向服务器上传和从服务器向客户端下载。FTP于1971年由Abhay Bhushan1971首次提出它的当前规格说明参见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。基本函数是getURLgetBinaryURL则便于处理二进制的内容而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脚本会检查输入数据并返回“HellonameYou areageyears old.”。在浏览器里我们可以看见表单的URL是http://www.rdatacollection.com/materials/http/GETexample.php?namenameageage它指定由一个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 2014p.270-272以及http://www.w3.org/TR/html401/interact/forms.html。对于application/x-www-form-urlencoded表单内容类型我们必须声明stylepost而对于multipart/form-data表单内容类型则需要声明stylehttppost。这样可以正确格式化表单体里的参数对并加入请求标头字段Content-Typeapplication/x-www-form-urlencoded或Content-Typemultipart/form-data。为了找到合适的POST格式我们可以在form元素里查找一个叫enctype的属性。如果它被声明为enctypeapplication/x-www-form-urlencoded我们就使用stylepost。如果表单里面没有这个属性如前面的例子那么去掉style参数也是可行的。