整合营销服务商

电脑端+手机端+微信端=数据同步管理

免费咨询热线:

R语言爬虫系列|动态数据抓取范例

过前面几期的推送,小编基本上已经将R语言爬虫所需要的基本知识介绍完了。R虽然是以一门统计分析工具出现在大多数人印象中的,但其毕竟本质上是一门编程语言,对于爬虫的支持虽不如Python那样多快好省,但悉心研究一下总能做出一些让你惊喜的效果。

大约很早之前,小编就写过关于R语言爬虫新贵rvest的抓取介绍,之前说rvest+SelectGadgetor是结构化网页抓取的实战利器,大家的溢美之词不断。详情可见推文:

R语言爬虫利器:rvest包+SelectorGadget抓取链家杭州二手房数据

但网络爬虫这个江湖太险恶,单靠一招rvest行走江湖必然凶多吉少,一不小心碰到什么AJAX和动态网页凭仅掌握rvest的各位必定束手无策。本文小编就简单介绍下在用R语言进行实际的网络数据抓取时如何将动态数据给弄到手。

所谓动态网页和异步加载,在之前的系列4的时候小编已通过AJAX介绍过了,简单而言就是我明明在网页中看到了这个数据,但到后台HTML中却找不到了,这通常就是XHR在作祟。这时候我们就不要看原始的HTML数据了,需要进行二次请求,通过web开发者工具找到真实请求的url。下面小编就以两个网页为例,分别通过GET和POST请求拿到动态网页数据,全过程主要使用httr包来实现,httr包可谓是RCurl包的精简版,说其短小精悍也不为过。httr包与RCurl包在主要函数的区别如下所示:

GET请求抓取微信好友列表数据

很早之前圈子里就看到过用Python抓取自己微信好友数据的案例分享,今天便以微信网页版为例,探一探其网页结构。首先登录个人微信网页版,右键打开web开发者工具,下来一大堆请求:

简单找一下发现网页中的微信好友列表信息并没有呈现在HTML 中,大概可以断定微信好友数据是通过动态加载来显示的,所以直接定位到XHR中,经过几番尝试,结合右侧的preview,我们会发现大量整齐划一的数据,所以二次请求的url真的就是它了:

找到真正的url之后,接下来就是获取请求信息了,切换到Headers版块,Header版块下的4个子信息我们都需要关注,它们是我们构造爬虫请求头的关键。

从Header中我们可以看到该信息是通过GET方法请求得到的,General信息下的Request URL,Request Method, Status Code; Response Headers信息下的Connection, Content-Type; Request Headers信息下的Accept, Cookie, Referer, User-Agent以及最后的Query String Parameters都是我们需要重点关注的。

找到相应的信息之后,我们便可以直接利用httr包在R中构建爬虫请求:

#传入微信cookie信息
Cookie <- “我的微信cookie”
#构造请求头
headers <- c('Accept'='application/json',
 'Content-Type'='text/plain', 
 'User-Agent'='Mozilla/5.0 (Windows NT 6.1) AppleWebKit/537. 36 (KHTML, like Gecko) Chrome/49.0.2623.221 Safari/537.36 SE 2.X Met aSr 1.0', 
 'Referer'='https://wx.qq.com/',
 'Connection'='keep-alive',
 'cookie'=Cookie
)

二次请求实际的url:

#实际请求的url
url<-"https://wx.qq.com/cgi-bin/mmwebwx-bin/webwxgetcontact?r=1507597918348&seq=0&skey=@crypt_ee7cd3e3_70091604da65a07600cfdca47b81cfaf"

GET方法单次执行请求:

#执行请求
louwill <- GET(url,add_headers(.headers =headers))

响应结果如下:

-> GET /cgi-bin/mmwebwx-bin/webwxgetcontact?r=1507597918348&seq=0&skey=@crypt_ee7cd3e3_70091604da65a07600cfdca47b81cfaf HTTP/1.1
-> Host: wx.qq.com
-> Accept-Encoding: gzip, deflate
-> Accept: application/json
-> Content-Type: text/plain
-> User-Agent: Mozilla/5.0 (Windows NT 6.1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/49.0.2623.221 Safari/537.36 SE 2.X MetaSr 1.0
-> Referer: https://wx.qq.com/
-> Connection: keep-alive
-> cookie: 我的微信cookie
->
<- HTTP/1.1 200 OK
<- Connection: keep-alive
<- Content-Type: text/plain
<- Content-Encoding: gzip
<- Content-Length: 90977
<-

响应状态码为200,okay。

从响应中提取原始字符内容:

content(louwill)
[1] "{\n\"BaseResponse\": {\n\"Ret\": 0,\n\"ErrMsg\": \"\"\n}\n,\n\"MemberCount\": 658,\n\"MemberList\": [{\n\"Uin\": 0,\n\"UserName\": \"weixin\",\n\"NickName\": \"微信团队\",\n\"HeadImgUrl\": \"/cgi-bin/mmwebwx-bin/webwxgeticon?seq=570002&username=weixin&skey=@crypt_ee7cd3e3_70091604da65a07600cfdca47b81cfaf\",\n\"ContactFlag\": 1,\n\"MemberCount\": 0,\n\"MemberList\": [],\n\"RemarkName\": \"\",\n\"HideInputBarFlag\": 0,\n\"Sex\": 0,\n\"Signature\": \"微信团队官方帐号\",\n\"VerifyFlag\": 56,\n\"OwnerUin\": 0,\n\"PYInitial\": \"WXTD\",\n\"PYQuanPin\": \"weixintuandui\",\n\"RemarkPYInitial\": \"\",\n\"RemarkPYQuanPin\": \"\",\n\"StarFriend\": 0,\n\"AppAccountFlag\": 0,\n\"Statues\": 0,\n\"AttrStatus\": 4,\n\"Province\": \"\",\n\"City\": \"\",\n\"Alias\": \"\",\n\"SnsFlag\": 0,\n\"UniFriend\": 0,\n\"DisplayName\": \"\",\n\"ChatRoomId\": 0,\n\"KeyWord\": \"wei\",\n\"EncryChatRoomId\": \"\",\n\"IsOwner\": 0\n}\n,{\n\"Uin\": 0,\n\"UserName\": \"@34c5cc09db0a616522f7ccc7309b1d29\",\n\"NickName\": \"微信支付... <truncated>

从结果中可以看出,微信好友列表的信息就被抓取下来了,数据信息非常杂乱,需要进一步清洗整理,小编这里重在展示GET请求获取动态网页数据(主要是懒)就不往下整理啦。

POST请求抓取网易云课堂数据

虽说动态网站数据请求也有GET方法的,但小编发现POST方法才是动态网页的主要的请求方式。受杜老师小魔方文章启发,小编也试一下这个网页上的效果。登录网易云课堂账号,右键开发者工具,直接定位到XHR,查找课程数据属于哪个url。通过尝试和preview,可以发现课程信息都被封装在一个studycourse.json的文件中:

跟GET请求方法一样,切换到Header版块后继续关注General等四个子信息,但POST请求下我们需要注意的一点是:POST请求下没有像GET请求一样的Query String Parameters,而是由Request Payload来构造请求头表单参数,这一点和GET方法大不相同。总而言之,在动态网页的HTTP请求中,如果是GET请求,请求头表单参数以name=value&name1=value1的形式直接附在url后面,如果是POST请求,请求头表单参数以相同的形式放在构造的表单体中,所以对于网易云课堂的数据请求在R中构造如下:

#构造请求头
#这里小编没有登录账号,cookie就不要了
headers <- c('Accept'='application/json',
 'Content-Type'='application/json', 
 'User-Agent'='ozilla/5.0 (Windows NT 6.1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/49.0.2623.221 Safari/537.36 SE 2.X MetaSr 1.0', 
 'edu-script-token'= '37aa682d1473455c8a77e6a4476e8f9e',
 'Referer'='http://study.163.com/courses',
 'Connection'='keep-alive'
)
#POST请求需要构造请求头表单参数
payload<-list(
 'pageIndex'=1,
 'pageSize'=50,
 'relativeOffset'=0,
 'frontCategoryId'=-1
)

二次请求实际的url:

url <- "http://study.163.com/p/search/studycourse.json"

POST方法单次执行请求:

louwill2<-POST(url,add_headers(.headers =headers),body =payload, encode="json")

结果如下:

-> POST /p/search/studycourse.json HTTP/1.1
-> Host: study.163.com
-> Accept-Encoding: gzip, deflate
-> Cookie: EDUWEBDEVICE=5d0eadccd2314c4d8bc6e758b8b23d4e; NTESSTUDYSI=d3d36984547a43d6924334ee6a184a08
-> Accept: application/json
-> Content-Type: application/json
-> User-Agent: ozilla/5.0 (Windows NT 6.1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/49.0.2623.221 Safari/537.36 SE 2.X MetaSr 1.0
-> edu-script-token: 83de95a25f5d45eb84bfeff8ec334e15
-> Referer: http://study.163.com/courses
-> Connection: keep-alive
-> cookie: 网易云课堂cookie
-> Content-Length: 69
->
>> {"pageIndex":1,"pageSize":50,"relativeOffset":0,"frontCategoryId":-1}
<- HTTP/1.1 200 OK
<- Server: nginx
<- Date: Tue, 10 Oct 2017 08:29:51 GMT
<- Content-Type: application/json;charset=UTF-8
<- Transfer-Encoding: chunked
<- Connection: keep-alive
<- Vary: Accept-Encoding
<- Server-Host: hzayq-study-platform7
<- Content-Encoding: gzip
<-
Response [http://study.163.com/p/search/studycourse.json]
 Date: 2017-10-10 08:29
 Status: 200
 Content-Type: application/json;charset=UTF-8
 Size: 71 kB

请求状态码200,也okay。

从响应中提取原始字符内容:

head(content(louwill2))
$result$list[[39]]
$result$list[[39]]$productId
[1] 1002971001
$result$list[[39]]$courseId
[1] 1002971001
$result$list[[39]]$productName
[1] "英语知识点解析及小学单词带读"
$result$list[[39]]$productType
[1] 0
$result$list[[39]]$startTime
[1] -1
$result$list[[39]]$endTime
[1] 9.223372e+18
$result$list[[39]]$description
[1] "通过几分钟的微课片段,精讲中小学的英语知识点,让学生通过比较学习,把这些知识点编织成有系统的知识网。"
$result$list[[39]]$provider
[1] "中小学英语语法王"

跟前面一样,后续的数据处理与清洗小编就懒得弄啦。POST方法与GET方法略有区别,就是需要构造请求头表单参数。R语言针对动态网页抓取,使用RCurl/httr包,认真分析网页结构,一般都能搞定。

End.

运行人员:中国统计网小编(微信号:itongjilove)

微博ID:中国统计网

中国统计网,是国内最早的大数据学习网站,公众号:中国统计网

http://www.itongji.cn

为一名毫无开发经验的非计算机出身的数据爱好者,初入此坑时深受爬虫难学之苦,当初未通Python之道,写个scrapy框架就痛苦至极。想想现在大数据技术那么牛逼了,为什么我抓个数据还处处被封,后来又觉得是自己技术不够强大。本文以拉勾网为例给大家介绍一款便捷快速的R语言爬虫方法,通过Rvest包+SelectorGdaget选择器即可轻松实现简单的数据抓取。

01准备工具:Rvest包+SelectorGadget选择器

下载安装Rvest包:

install.packages("Rvest")

library(Rvest)

要想全面了解Rvest包的朋友可以去查官方帮助文档:

help(package="Rvest")

Selectorgadget插件作为一个轻便快捷的CSS选择器,好用程度简直爆炸,鼠标点击几下即可生成你想要抓取的html节点信息。这么一款神器,调用方法也是极其简单,打开任何一款搜索网页,键入Selectorgadget,点击第一个链接,也是Selectorgadget官方链接,拉到页面底端倒数第二个链接,将其拖拽到你的浏览器收藏夹,待下次打开需要爬取的网页时点击即可启用。

需拖拽的链接如图(Or drag this link to your bookmark bar):

下次调用时,打开需要抓取的网页,点击我们拖拽到收藏夹的Selectorgadget会在网页右下角出现一个长方形条框,点击网页中任何我们想抓取的信息,条框内即可生成相应的文本表达式,将这些文本表达式复制到Rvest包对应的爬虫函数中,即可轻松完成抓取。需要注意的是,使用Selectorgadget选择节点信息是一个筛选的过程,其间需要将我们不需要的信息(点击后变红)重复点击以删除,留下需要的信息(绿色和黄色部分)。

02,拉勾网数据抓取

我们选择抓取拉勾网数据分析师岗位信息:

抓取代码如下:

library(stringr)library(xml2)
library(rvest) #加载包
i<-1:30#设定抓取页数
lagou_data<-data.frame()#创建数据框存储数据
#写个循环,对固定网页结构重复抓取
for (i in 1:30){
web<-read_html(str_c("https://www.lagou.com/zhaopin/shujufenxi/",i),encoding="UTF-8")#read_html函数解析网页并规定编码str_c函数对页数循环
job<-web%>%html_nodes("h2")%>%html_text()#"h2"即为Selectorgadget定位节点信息
job[16]<-NA
job<-job[!is.na(job)]#将多余信息设置为NA并剔除
#以此类推,抓取岗位其他信息
company<-web%>%html_nodes(".company_name a")%>%html_text()
inf1<-web%>%html_nodes(".p_bot .li_b_l")%>%html_text()
inf2<-web%>%html_nodes(".industry")%>%html_text()
temptation<-web%>%html_nodes(".li_b_r")%>%html_text()

#存储以上信息

job_inf<-data.frame(job,company,inf1,inf2,temptation)

lagou_data<-rbind(lagou_data,job_inf)

}

write.csv(job_inf,file="D:/Rdata/datasets/job_inf.csv")#写入数据

清洗整理后最终抓取部分数据示例如图:

03,简单小结

用rvest包结合SelectorGadget 选择器能够快速实现R语言下的网络数据抓取,并适当结合stringr包中的字符串处理函数对网页数据进行清洗和整理,抓取过程省时省力,适合R语言和爬虫入门的朋友使用学习。

End.

来源:公众号“R语言中文社区”

运行人员:中国统计网小编(微信号:itongjilove)

微博ID:中国统计网

中国统计网,是国内最早的大数据学习网站,公众号:中国统计网

http://www.itongji.cn

读:本文主要分为两个部分:一部分是网络爬虫的概述,帮助大家详细了解网络爬虫;另一部分是HTTP请求的Python实现,帮助大家了解Python中实现HTTP请求的各种方式,以便具备编写HTTP网络程序的能力。

作者:范传辉

如需转载请联系华章科技

01 网络爬虫概述

接下来从网络爬虫的概念、用处与价值和结构等三个方面,让大家对网络爬虫有一个基本的了解。

1. 网络爬虫及其应用

随着网络的迅速发展,万维网成为大量信息的载体,如何有效地提取并利用这些信息成为一个巨大的挑战,网络爬虫应运而生。网络爬虫(又被称为网页蜘蛛、网络机器人),是一种按照一定的规则,自动地抓取万维网信息的程序或者脚本。下面通过图3-1展示一下网络爬虫在互联网中起到的作用:

▲图3-1 网络爬虫

网络爬虫按照系统结构和实现技术,大致可以分为以下几种类型:通用网络爬虫、聚焦网络爬虫、增量式网络爬虫、深层网络爬虫。实际的网络爬虫系统通常是几种爬虫技术相结合实现的。

搜索引擎(Search Engine),例如传统的通用搜索引擎baidu、Yahoo和Google等,是一种大型复杂的网络爬虫,属于通用性网络爬虫的范畴。但是通用性搜索引擎存在着一定的局限性:

  1. 不同领域、不同背景的用户往往具有不同的检索目的和需求,通用搜索引擎所返回的结果包含大量用户不关心的网页。
  2. 通用搜索引擎的目标是尽可能大的网络覆盖率,有限的搜索引擎服务器资源与无限的网络数据资源之间的矛盾将进一步加深。
  3. 万维网数据形式的丰富和网络技术的不断发展,图片、数据库、音频、视频多媒体等不同数据大量出现,通用搜索引擎往往对这些信息含量密集且具有一定结构的数据无能为力,不能很好地发现和获取。
  4. 通用搜索引擎大多提供基于关键字的检索,难以支持根据语义信息提出的查询。

为了解决上述问题,定向抓取相关网页资源的聚焦爬虫应运而生。

聚焦爬虫是一个自动下载网页的程序,它根据既定的抓取目标,有选择地访问万维网上的网页与相关的链接,获取所需要的信息。与通用爬虫不同,聚焦爬虫并不追求大的覆盖,而将目标定为抓取与某一特定主题内容相关的网页,为面向主题的用户查询准备数据资源。

说完了聚焦爬虫,接下来再说一下增量式网络爬虫。增量式网络爬虫是指对已下载网页采取增量式更新和只爬行新产生的或者已经发生变化网页的爬虫,它能够在一定程度上保证所爬行的页面是尽可能新的页面。

和周期性爬行和刷新页面的网络爬虫相比,增量式爬虫只会在需要的时候爬行新产生或发生更新的页面,并不重新下载没有发生变化的页面,可有效减少数据下载量,及时更新已爬行的网页,减小时间和空间上的耗费,但是增加了爬行算法的复杂度和实现难度。

例如:想获取赶集网的招聘信息,以前爬取过的数据没有必要重复爬取,只需要获取更新的招聘数据,这时候就要用到增量式爬虫。

最后说一下深层网络爬虫。Web页面按存在方式可以分为表层网页和深层网页。表层网页是指传统搜索引擎可以索引的页面,以超链接可以到达的静态网页为主构成的Web页面。深层网络是那些大部分内容不能通过静态链接获取的、隐藏在搜索表单后的,只有用户提交一些关键词才能获得的Web页面。

例如用户登录或者注册才能访问的页面。可以想象这样一个场景:爬取贴吧或者论坛中的数据,必须在用户登录后,有权限的情况下才能获取完整的数据。

2. 网络爬虫结构

下面用一个通用的网络爬虫结构来说明网络爬虫的基本工作流程,如图3-4所示。

▲图3-4 网络爬虫结构

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

  1. 首先选取一部分精心挑选的种子URL。
  2. 将这些URL放入待抓取URL队列。
  3. 从待抓取URL队列中读取待抓取队列的URL,解析DNS,并且得到主机的IP,并将URL对应的网页下载下来,存储进已下载网页库中。此外,将这些URL放进已抓取URL队列。
  4. 分析已抓取URL队列中的URL,从已下载的网页数据中分析出其他URL,并和已抓取的URL进行比较去重,最后将去重过的URL放入待抓取URL队列,从而进入下一个循环。

02 HTTP请求的Python实现

通过上面的网络爬虫结构,我们可以看到读取URL、下载网页是每一个爬虫必备而且关键的功能,这就需要和HTTP请求打交道。接下来讲解Python中实现HTTP请求的三种方式:urllib2/urllib、httplib/urllib以及Requests。

1. urllib2/urllib实现

urllib2和urllib是Python中的两个内置模块,要实现HTTP功能,实现方式是以urllib2为主,urllib为辅。

1.1 首先实现一个完整的请求与响应模型

urllib2提供一个基础函数urlopen,通过向指定的URL发出请求来获取数据。最简单的形式是:

import urllib2
response=urllib2.urlopen('http://www.zhihu.com')
html=response.read()
print html

其实可以将上面对http://www.zhihu.com的请求响应分为两步,一步是请求,一步是响应,形式如下:

import urllib2
# 请求
request=urllib2.Request('http://www.zhihu.com')
# 响应
response = urllib2.urlopen(request)
html=response.read()
print html

上面这两种形式都是GET请求,接下来演示一下POST请求,其实大同小异,只是增加了请求数据,这时候用到了urllib。示例如下:

import urllib
import urllib2
url = 'http://www.xxxxxx.com/login'
postdata = {'username' : 'qiye',
 'password' : 'qiye_pass'}
# info 需要被编码为urllib2能理解的格式,这里用到的是urllib
data = urllib.urlencode(postdata)
req = urllib2.Request(url, data)
response = urllib2.urlopen(req)
html = response.read()

但是有时会出现这种情况:即使POST请求的数据是对的,但是服务器拒绝你的访问。这是为什么呢?问题出在请求中的头信息,服务器会检验请求头,来判断是否是来自浏览器的访问,这也是反爬虫的常用手段。

1.2 请求头headers处理

将上面的例子改写一下,加上请求头信息,设置一下请求头中的User-Agent域和Referer域信息。

import urllib
import urllib2
url = 'http://www.xxxxxx.com/login'
user_agent = 'Mozilla/4.0 (compatible; MSIE 5.5; Windows NT)'
referer='http://www.xxxxxx.com/'
postdata = {'username' : 'qiye',
 'password' : 'qiye_pass'}
# 将user_agent,referer写入头信息
headers={'User-Agent':user_agent,'Referer':referer}
data = urllib.urlencode(postdata)
req = urllib2.Request(url, data,headers)
response = urllib2.urlopen(req)
html = response.read()

也可以这样写,使用add_header来添加请求头信息,修改如下:

import urllib
import urllib2
url = 'http://www.xxxxxx.com/login'
user_agent = 'Mozilla/4.0 (compatible; MSIE 5.5; Windows NT)'
referer='http://www.xxxxxx.com/'
postdata = {'username' : 'qiye',
 'password' : 'qiye_pass'}
data = urllib.urlencode(postdata)
req = urllib2.Request(url)
# 将user_agent,referer写入头信息
req.add_header('User-Agent',user_agent)
req.add_header('Referer',referer)
req.add_data(data)
response = urllib2.urlopen(req)
html = response.read()

对有些header要特别留意,服务器会针对这些header做检查,例如:

  • User-Agent:有些服务器或Proxy会通过该值来判断是否是浏览器发出的请求。
  • Content-Type:在使用REST接口时,服务器会检查该值,用来确定HTTP Body中的内容该怎样解析。在使用服务器提供的RESTful或SOAP服务时,Content-Type设置错误会导致服务器拒绝服务。常见的取值有:application/xml(在XML RPC,如RESTful/SOAP调用时使用)、application/json(在JSON RPC调用时使用)、application/x-www-form-urlencoded(浏览器提交Web表单时使用)。
  • Referer:服务器有时候会检查防盗链。

1.3 Cookie处理

urllib2对Cookie的处理也是自动的,使用CookieJar函数进行Cookie的管理。如果需要得到某个Cookie项的值,可以这么做:

import urllib2
import cookielib
cookie = cookielib.CookieJar()
opener = urllib2.build_opener(urllib2.HTTPCookieProcessor(cookie))
response = opener.open('http://www.zhihu.com')
for item in cookie:
 print item.name+':'+item.value

但是有时候会遇到这种情况,我们不想让urllib2自动处理,我们想自己添加Cookie的内容,可以通过设置请求头中的Cookie域来做:

import urllib2
opener = urllib2.build_opener()
opener.addheaders.append( ( 'Cookie', 'email=' + "xxxxxxx@163.com" ) )
req = urllib2.Request( "http://www.zhihu.com/" )
response = opener.open(req)
print response.headers
retdata = response.read()

1.4 Timeout设置超时

在Python2.6之前的版本,urllib2的API并没有暴露Timeout的设置,要设置Timeout值,只能更改Socket的全局Timeout值。示例如下:

import urllib2
import socket
socket.setdefaulttimeout(10) # 10 秒钟后超时
urllib2.socket.setdefaulttimeout(10) # 另一种方式

在Python2.6及新的版本中,urlopen函数提供了对Timeout的设置,示例如下:

import urllib2
request=urllib2.Request('http://www.zhihu.com')
response = urllib2.urlopen(request,timeout=2)
html=response.read()
print html

1.5 获取HTTP响应码

对于200 OK来说,只要使用urlopen返回的response对象的getcode()方法就可以得到HTTP的返回码。但对其他返回码来说,urlopen会抛出异常。这时候,就要检查异常对象的code属性了,示例如下:

import urllib2
try:
 response = urllib2.urlopen('http://www.google.com')
 print response
except urllib2.HTTPError as e:
 if hasattr(e, 'code'):
 print 'Error code:',e.code

1.6 重定向

urllib2默认情况下会针对HTTP 3XX返回码自动进行重定向动作。要检测是否发生了重定向动作,只要检查一下Response的URL和Request的URL是否一致就可以了,示例如下:

import urllib2
response = urllib2.urlopen('http://www.zhihu.cn')
isRedirected = response.geturl() == 'http://www.zhihu.cn'

如果不想自动重定向,可以自定义HTTPRedirectHandler类,示例如下:

import urllib2
class RedirectHandler(urllib2.HTTPRedirectHandler):
 def http_error_301(self, req, fp, code, msg, headers):
 pass
 def http_error_302(self, req, fp, code, msg, headers):
 result = urllib2.HTTPRedirectHandler.http_error_301(self, req, fp, code, 
 msg, headers)
 result.status = code
 result.newurl = result.geturl()
 return result
opener = urllib2.build_opener(RedirectHandler)
opener.open('http://www.zhihu.cn')

1.7 Proxy的设置

在做爬虫开发中,必不可少地会用到代理。urllib2默认会使用环境变量http_proxy来设置HTTP Proxy。但是我们一般不采用这种方式,而是使用ProxyHandler在程序中动态设置代理,示例代码如下:

import urllib2
proxy = urllib2.ProxyHandler({'http': '127.0.0.1:8087'})
opener = urllib2.build_opener([proxy,])
urllib2.install_opener(opener)
response = urllib2.urlopen('http://www.zhihu.com/')
print response.read()

这里要注意的一个细节,使用urllib2.install_opener()会设置urllib2的全局opener,之后所有的HTTP访问都会使用这个代理。这样使用会很方便,但不能做更细粒度的控制,比如想在程序中使用两个不同的Proxy设置,这种场景在爬虫中很常见。比较好的做法是不使用install_opener去更改全局的设置,而只是直接调用opener的open方法代替全局的urlopen方法,修改如下:

import urllib2
proxy = urllib2.ProxyHandler({'http': '127.0.0.1:8087'})
opener = urllib2.build_opener(proxy,)
response = opener.open("http://www.zhihu.com/")
print response.read()

2. httplib/urllib实现

httplib模块是一个底层基础模块,可以看到建立HTTP请求的每一步,但是实现的功能比较少,正常情况下比较少用到。在Python爬虫开发中基本上用不到,所以在此只是进行一下知识普及。下面介绍一下常用的对象和函数:

  • 创建HTTPConnection对象:
  • class httplib.HTTPConnection(host[, port[, strict[, timeout[, source_address]]]])。
  • 发送请求:
  • HTTPConnection.request(method, url[, body[, headers]])。
  • 获得响应:
  • HTTPConnection.getresponse()。
  • 读取响应信息:
  • HTTPResponse.read([amt])。
  • 获得指定头信息:
  • HTTPResponse.getheader(name[, default])。
  • 获得响应头(header, value)元组的列表:
  • HTTPResponse.getheaders()。
  • 获得底层socket文件描述符:
  • HTTPResponse.fileno()。
  • 获得头内容:
  • HTTPResponse.msg。
  • 获得头http版本:
  • HTTPResponse.version。
  • 获得返回状态码:
  • HTTPResponse.status。
  • 获得返回说明:
  • HTTPResponse.reason。

接下来演示一下GET请求和POST请求的发送,首先是GET请求的示例,如下所示:

import httplib
conn =None
try:
 conn = httplib.HTTPConnection("www.zhihu.com")
 conn.request("GET", "/")
 response = conn.getresponse()
 print response.status, response.reason
 print '-' * 40
 headers = response.getheaders()
 for h in headers:
 print h
 print '-' * 40
 print response.msg
except Exception,e:
 print e
finally:
 if conn:
 conn.close()

POST请求的示例如下:

import httplib, urllib
conn = None
try:
 params = urllib.urlencode({'name': 'qiye', 'age': 22})
 headers = {"Content-type": "application/x-www-form-urlencoded"
 , "Accept": "text/plain"}
 conn = httplib.HTTPConnection("www.zhihu.com", 80, timeout=3)
 conn.request("POST", "/login", params, headers)
 response = conn.getresponse()
 print response.getheaders() # 获取头信息
 print response.status
 print response.read()
except Exception, e:
 print e
 finally:
 if conn:
 conn.close()

3. 更人性化的Requests

Python中Requests实现HTTP请求的方式,是本人极力推荐的,也是在Python爬虫开发中最为常用的方式。Requests实现HTTP请求非常简单,操作更加人性化。

Requests库是第三方模块,需要额外进行安装。Requests是一个开源库,源码位于:

GitHub: https://github.com/kennethreitz/requests

希望大家多多支持作者。

使用Requests库需要先进行安装,一般有两种安装方式:

  • 使用pip进行安装,安装命令为:pip install requests,不过可能不是最新版。
  • 直接到GitHub上下载Requests的源代码,下载链接为:
  • https://github.com/kennethreitz/requests/releases
  • 将源代码压缩包进行解压,然后进入解压后的文件夹,运行setup.py文件即可。

如何验证Requests模块安装是否成功呢?在Python的shell中输入import requests,如果不报错,则是安装成功。如图3-5所示。

▲图3-5 验证Requests安装

3.1 首先还是实现一个完整的请求与响应模型

以GET请求为例,最简单的形式如下:

import requests
r = requests.get('http://www.baidu.com')
print r.content

大家可以看到比urllib2实现方式的代码量少。接下来演示一下POST请求,同样是非常简短,更加具有Python风格。示例如下:

import requests
postdata={'key':'value'}
r = requests.post('http://www.xxxxxx.com/login',data=postdata)
print r.content

HTTP中的其他请求方式也可以用Requests来实现,示例如下:

r = requests.put('http://www.xxxxxx.com/put', data = {'key':'value'})
r = requests.delete('http://www.xxxxxx.com/delete')
r = requests.head('http://www.xxxxxx.com/get')
r = requests.options('http://www.xxxxxx.com/get')

接着讲解一下稍微复杂的方式,大家肯定见过类似这样的URL:

http://zzk.cnblogs.com/s/blogpost?Keywords=blog:qiyeboy&pageindex=1

就是在网址后面紧跟着“?”,“?”后面还有参数。那么这样的GET请求该如何发送呢?肯定有人会说,直接将完整的URL带入即可,不过Requests还提供了其他方式,示例如下:

import requests
 payload = {'Keywords': 'blog:qiyeboy','pageindex':1}
r = requests.get('http://zzk.cnblogs.com/s/blogpost', params=payload)
print r.url

通过打印结果,我们看到最终的URL变成了:

http://zzk.cnblogs.com/s/blogpost?Keywords=blog:qiyeboy&pageindex=1

3.2 响应与编码

还是从代码入手,示例如下:

import requests
r = requests.get('http://www.baidu.com')
print 'content-->'+r.content
print 'text-->'+r.text
print 'encoding-->'+r.encoding
r.encoding='utf-8'
print 'new text-->'+r.text

其中r.content返回的是字节形式,r.text返回的是文本形式,r.encoding返回的是根据HTTP头猜测的网页编码格式。

输出结果中:“text-->”之后的内容在控制台看到的是乱码,“encoding-->”之后的内容是ISO-8859-1(实际上的编码格式是UTF-8),由于Requests猜测编码错误,导致解析文本出现了乱码。Requests提供了解决方案,可以自行设置编码格式,r.encoding='utf-8'设置成UTF-8之后,“new text-->”的内容就不会出现乱码。

但是这种手动的方式略显笨拙,下面提供一种更加简便的方式:chardet,这是一个非常优秀的字符串/文件编码检测模块。安装方式如下:

pip install chardet

安装完成后,使用chardet.detect()返回字典,其中confidence是检测精确度,encoding是编码形式。示例如下:

import requests
r = requests.get('http://www.baidu.com')
print chardet.detect(r.content)
r.encoding = chardet.detect(r.content)['encoding']
print r.text

直接将chardet探测到的编码,赋给r.encoding实现解码,r.text输出就不会有乱码了。

除了上面那种直接获取全部响应的方式,还有一种流模式,示例如下:

import requests
r = requests.get('http://www.baidu.com',stream=True)
print r.raw.read(10)

设置stream=True标志位,使响应以字节流方式进行读取,r.raw.read函数指定读取的字节数。

3.3 请求头headers处理

Requests对headers的处理和urllib2非常相似,在Requests的get函数中添加headers参数即可。示例如下:

import requests
user_agent = 'Mozilla/4.0 (compatible; MSIE 5.5; Windows NT)'
headers={'User-Agent':user_agent}
r = requests.get('http://www.baidu.com',headers=headers)
print r.content

3.4 响应码code和响应头headers处理

获取响应码是使用Requests中的status_code字段,获取响应头使用Requests中的headers字段。示例如下:

import requests
r = requests.get('http://www.baidu.com')
if r.status_code == requests.codes.ok:
 print r.status_code# 响应码
 print r.headers# 响应头
 print r.headers.get('content-type')# 推荐使用这种获取方式,获取其中的某个字段
 print r.headers['content-type']# 不推荐使用这种获取方式
else:
 r.raise_for_status()

上述程序中,r.headers包含所有的响应头信息,可以通过get函数获取其中的某一个字段,也可以通过字典引用的方式获取字典值,但是不推荐,因为如果字段中没有这个字段,第二种方式会抛出异常,第一种方式会返回None。

r.raise_for_status()是用来主动地产生一个异常,当响应码是4XX或5XX时,raise_for_status()函数会抛出异常,而响应码为200时,raise_for_status()函数返回None。

3.5 Cookie处理

如果响应中包含Cookie的值,可以如下方式获取Cookie字段的值,示例如下:

import requests
user_agent = 'Mozilla/4.0 (compatible; MSIE 5.5; Windows NT)'
headers={'User-Agent':user_agent}
r = requests.get('http://www.baidu.com',headers=headers)
# 遍历出所有的cookie字段的值
for cookie in r.cookies.keys():
 print cookie+':'+r.cookies.get(cookie)

如果想自定义Cookie值发送出去,可以使用以下方式,示例如下:

import requests
user_agent = 'Mozilla/4.0 (compatible; MSIE 5.5; Windows NT)'
headers={'User-Agent':user_agent}
cookies = dict(name='qiye',age='10')
r = requests.get('http://www.baidu.com',headers=headers,cookies=cookies)
print r.text

还有一种更加高级,且能自动处理Cookie的方式,有时候我们不需要关心Cookie值是多少,只是希望每次访问的时候,程序自动把Cookie的值带上,像浏览器一样。Requests提供了一个session的概念,在连续访问网页,处理登录跳转时特别方便,不需要关注具体细节。使用方法示例如下:

import Requests
oginUrl = 'http://www.xxxxxxx.com/login'
s = requests.Session()
#首先访问登录界面,作为游客,服务器会先分配一个cookie
r = s.get(loginUrl,allow_redirects=True)
datas={'name':'qiye','passwd':'qiye'}
#向登录链接发送post请求,验证成功,游客权限转为会员权限
r = s.post(loginUrl, data=datas,allow_redirects= True)
print r.text

上面的这段程序,其实是正式做Python开发中遇到的问题,如果没有第一步访问登录的页面,而是直接向登录链接发送Post请求,系统会把你当做非法用户,因为访问登录界面时会分配一个Cookie,需要将这个Cookie在发送Post请求时带上,这种使用Session函数处理Cookie的方式之后会很常用。

3.6 重定向与历史信息

处理重定向只是需要设置一下allow_redirects字段即可,例如:

r=requests.get('http://www.baidu.com',allow_redirects=True)

将allow_redirects设置为True,则是允许重定向;设置为False,则是禁止重定向。如果是允许重定向,可以通过r.history字段查看历史信息,即访问成功之前的所有请求跳转信息。示例如下:

import requests
r = requests.get('http://github.com')
print r.url
print r.status_code
print r.history

打印结果如下:

https://github.com/
200
(<Response [301]>,)

上面的示例代码显示的效果是访问GitHub网址时,会将所有的HTTP请求全部重定向为HTTPS。

3.7 超时设置

超时选项是通过参数timeout来进行设置的,示例如下:

requests.get('http://github.com', timeout=2)

3.8 代理设置

使用代理Proxy,你可以为任意请求方法通过设置proxies参数来配置单个请求:

import requests
proxies = {
 "http": "http://0.10.1.10:3128",
 "https": "http://10.10.1.10:1080",
}
requests.get("http://example.org", proxies=proxies)

也可以通过环境变量HTTP_PROXY和HTTPS_PROXY?来配置代理,但是在爬虫开发中不常用。你的代理需要使用HTTP Basic Auth,可以使用http://user:password@host/语法:

proxies = {
 "http": "http://user:pass@10.10.1.10:3128/",
}

03 小结

本文主要讲解了网络爬虫的结构和应用,以及Python实现HTTP请求的几种方法。希望大家对本文中的网络爬虫工作流程和Requests实现HTTP请求的方式重点吸收消化。

关于作者:范传辉,资深网虫,Python开发者,参与开发了多项网络应用,在实际开发中积累了丰富的实战经验,并善于总结,贡献了多篇技术文章广受好评。研究兴趣是网络安全、爬虫技术、数据分析、驱动开发等技术。

本文摘编自《Python爬虫开发与项目实战》,经出版方授权发布。

延伸阅读《Python爬虫开发与项目实战》

推荐语:零基础学习爬虫技术,从Python和Web前端基础开始讲起,由浅入深,包含大量案例,实用性强。