第3章
CHAPTER 3


静态网页爬取





在网站设计中,纯粹HTML(标准通用标记语言下的一个应用)格式的网页通常被称为“静态网页”。静态网页是相对于动态网页而言的,是指没有后台数据库、不含程序和不可交互的网页。静态网页的更新相对比较麻烦,适用于一般更新较少的展示型网站。容易让人产生误解的是静态页面都是HTML这类页面,实际上静态也不是完全静态,它也可以出现各种动态的效果,如GIF格式的动画、Flash、滚动字幕等。
在网络爬虫中,静态网页的数据比较容易获取,因为所有数据都呈现在网页的HTML代码中。相对而言,使用AJAX动态加载网络的数据不一定会出现在HTML代码中,这就给爬虫增加了困难。
在静态网页中,有一个强大的Requests库能够让我们方便地发送HTTP请求,这个库功能完善,而且操作非常简单。
3.1Requests的安装
在Windows系统下,Requests库可以通过pip安装。打开cmd或terminal,输入: 




pip install requests




即可完成安装,可以输入 import requests 命令来试试是否安装成功,如图31所示即显示安装成功。


图31成功安装Requests


在Requests中,最常用的功能就是获取某个网页内容。现在使用Requests获取个人博客主页的内容。




>>> import requests

>>> r = requests.get('http://www.zhidaow.com')# 发送请求






>>> r.status_code# 返回码

200

>>> r.headers['content-type']  # 返回头部信息

'text/html; charset=utf-8'

>>> r.encoding  # 编码信息

'utf-8'

>>> r.text #内容部分(PS,由于编码问题,建议这里使用r.content)

'\n<!DOCTYPE html>\n<html>\n  <head>\n<script type="text/javascript" src="/fb

…




其中,
(1) r.text是服务器响应的内容,会自动根据响应头部的字符编码进行解码。
(2) r.encoding是服务器的内容所使用的文本编码。




(3) r.status_code用于检测响应的状态码,如果返回200,则表示请求成功; 如果返回的是4xx,则表示客户端错误; 如果返回5xx,则表示服务器错误响应。可以用r.status_code来检测请求是否正确响应。
(4) r.content是字节方式的响应体,会自动解码gzip和deflate编码的响应数据。
3.2获取响应内容
在Python爬虫网络中,可以使用r.encoding获取网页编码。




>>> import requests

>>> r = requests.get('http://www.zhidaow.com')

>>> r.encoding

'utf-8'



在Python中,当发送请求时,Requests会根据HTTP头部来猜测网页编码,当使用r.text时,Requests就会使用这个编码。当然你还可以修改Requests的编码形式。例如: 




>>> r = requests.get('http://www.zhidaow.com')

>>> r.encoding

'utf-8'

>>> r.encoding = 'ISO-8859-1'

>>>



像上面的例子,对encoding修改后就直接会用修改后的编码去获取网页内容。
3.3JSON数据库
JSON全称为JavaScript Object Notation,也就是JavaScript对象标记,它通过对象和数组的组合来表示数据,构造简洁但是结构化程度非常高,是一种轻量级的数据交换格式。下面进行简单的介绍,第7章将对其进行详细介绍。
3.3.1JSON的使用
像urllib1和urllib2,如果用到JSON,就要引入新模块,如JSON和simplejson,但在Requests中已经有了内置的函数——r.json()。以查询IP的API为例: 




>>> r = requests.get('http://ip.taobao.com/service/getIpInfo.php?ip=122.88.60.28')

>>> r.json()['data']['country']

'中国'



3.3.2爬取抽屉网信息
此外,还可以利用Requests和JSON爬取网络信息。
【例31】爬取抽屉网信息(JSON数据)。




import requests 

from fake_useragent import UserAgent 

agent = UserAgent() 

import json #

# # pip search 工具包名字

# # pip install fake_useragent

url = "https://dig.chouti.com/getTopTenLinksOrComments.json?_=1529764992551"

# # 通过浏览器获取的操作一般都是get请求

headers = { "Host":"dig.chouti.com",

"User-Agent":"Mozilla/5.0 (Windows NT 6.1; WOW64; rv:53.0) Gecko/20100101 Firefox/53.0",

"Accept": "application/json,text/javascript,*/*; q=0.01",

"Accept-Language": "zh-CN,zh;q=0.8,en-US;q=0.5,en;q=0.3",

"Accept-Encoding":"gzip,deflate, br",

"X-Requested-With":"XMLHttpRequest",

"Referer":"https://dig.chouti.com/",    "Cookie":"gpsd=0b08b9f5b945fd53eac7868a2e8945a8;JSESSIONID=aaazCSOWV2s7FcALFeHqw;gpid=55eaeb947f15445b82467624c476521f;_pk_id.1.a2d5=dbeb24b52f36519f.1529741245.1.1529741290.1529741245.;_pk_ses.1.a2d5=*",

"Connection":"keep-alive"} 

data = {"_":"1529742010062"} 

res = requests.post(url, headers=headers, data=data) 

rs_js = json.loads(res.content) 

print(rs_js['result']['data'])



运行程序,输出如下: 




raise FakeUserAgentError('Maximum amount of retries reached')

fake_useragent.errors.FakeUserAgentError: Maximum amount of retries reached

[{'action': 1, 'actiontime': 1558879802717000, 'actiontimeStr': '8分钟前', 'closeIp': False, 'commentsCount': 0, 'comm…

…



3.4传递URL参数
为了请求特定的数据,需要在URL的查询字符串中加入某些数据。如果你是自己构建URL,那么数据一般会跟在一个问号后面,并且以键值的形式放在URL中,如http://httpbin.org/get?key1=value1。
在Requests中,可以直接把这些参数保存在字典中,用params构建至URL中。例如,将key1=value1和key2=value2传递到http://httpbin.org/get,可以这样编写: 




import requests

key_dict={'key1':'value1','key2':'value2'}

r=requests.get('http://httpbin.org/get',params=key_dict)






print('URL已经正确编码: ',r.url)

print('字符串方式的响应体: \n',r.text)



通过上述代码的输出结果可以发现URL已经正确编码: 




URL已经正确编码: http://httpbin.org/get?key1=value1&key2=value2



字符串方式的响应体: 




{

"args": {

"key1": "value1",

"key2": "value2"

},

"headers": {

"Accept": "*/*",

"Accept-Encoding": "gzip, deflate",

"Host": "httpbin.org",

"User-Agent": "python-requests/2.19.1"

},

"origin": "14.27.49.210, 14.27.49.210",

"url": "https://httpbin.org/get?key1=value1&key2=value2"

}



3.5获取响应内容
在Requests中,可以通过r.text来获取网页的内容。例如: 




>>> import requests

>>> r = requests.get('https://www.baidu.com')

>>> r.text

'<!DOCTYPE html>\r\n<!--STATUS OK--><html><head><meta http-equiv=content-type content=text/html;charset=utf-8><meta htt…

…



在Requests中,还会自动将内容转码,大多数unicode字体都会无缝转码。此外,还可以通过r.content来获取页面内容。




>>> r = requests.get('https://www.baidu.com')

>>> r.content

b'<!DOCTYPE html>\r\n<!--STATUS OK--><html><head><meta http-equiv=content-type content=text/html;charset=utf-8><meta ht…

…



r.content是以字节的方式去显示,所以在IDLE中以b开头。但在cygwin中用起来并没有,下载网页正好,所以就替代了urllib2的urllib2.urlopen(url).read()功能。
3.6获取网页编码
在Requests中,可以用r.status_code来检查网页的状态码。例如: 




>>> r = requests.get('http://www.mengtiankong.com')

>>> r.status_code






200

>>> r = requests.get('http://www.mengtiankong.com/123123/')

>>> r.status_code

404

>>> r = requests.get('http://www.baidu.com/link?url=QeTRFOS7TuUQRppa0wlTJJr6FfI-YI1DJprJukx4Qy0XnsDO_s9baoO8u1wvjxgqN')

>>> r.url

'http://www.zhidaow.com/'

>>> r.status_code

200



前两个例子很正常,能正常打开的返回200,不能正常打开的返回404。但第三个就有点奇怪了,那个是百度搜索结果中的302跳转地址,但状态码显示是200,接下来用其他方法进行实验: 




>>> r.history

[<Response [302]>]

>>>



这里能看出是使用了302跳转。也许有人认为这样可以通过判断来获取跳转的状态码了,其实还有个更简单的方法: 




>>>r= requests.get('http://www.baidu.com/link?url=QeTRFOS7TuUQRppa0wlTJJr6FfIYI1D-JprJukx4Qy0XnsDO_s9baoO8u1wvjxgqN', allow_redirects = False)

>>> r.status_code

302



只要加上一个参数allow_redirects,禁止了跳转,就会直接出现跳转的状态码了。
3.7定制请求头
请求头Headers提供了关于请求、响应或其他发送实体的信息。对于爬虫而言,请求头十分重要,尽管在上一个例子中并没有制定请求头。如果没有指定请求头或请求的请求头与实际网页不一致,就可能无法返回正确的结果。
Requests并不会基于定制的请求头Headers的具体情况改变自己的行为,只是在最后的请求中,所有的请求头信息都会被传递进去。
在Requests中可以通过r.headers获取响应头内容。例如: 




>>> r.headers

{'Cache-Control': 'private, no-cache, no-store, proxy-revalidate, no-transform', 'Connection': 'Keep-Alive', 'Content-Encoding': 'gzip', 'Content-Type': 'text/html', 'Date': 'Mon, 27 May 2019 04:47:21 GMT', 'Last-Modified': 'Mon, 23 Jan 2017 13:27:57 GMT', 'Pragma': 'no-cache', 'Server': 'bfe/1.0.8.18', 'Set-Cookie': 'BDORZ=27315; max-age=86400; domain=.baidu.com; path=/', 'Transfer-Encoding': 'chunked'}



由结果可以看到是以字典的形式返回了全部内容,也可以访问部分内容。例如: 




>>> r.headers['Content-Type']

'text/html'

>>> r.headers.get('content-type')

'text/html'




而请求头内容可以用r.request.headers来获取。例如: 




>>> r.request.headers

{'User-Agent': 'python-requests/2.18.4', 'Accept-Encoding': 'gzip, deflate', 'Accept': '*/*', 'Connection': 'keep-alive'}



3.8发送POST请求
除了GET请求外,有时还需要发送一些编码为表单形式的数据,如在登录的时候请求就为POST,因为如果用GET请求,密码就会显示在URL中,这是非常不安全的。如果要实现POST请求,那么只需要简单地传递一个字典给Requests中的data参数,这个数据字典就会在发出请求的时候自动编码为表单形式。例如: 




import requests

key_dict={'key1':'value1','key2':'value2'}

r=requests.post('http://httpbin.org/post',data=key_dict)

print(r.text)



运行程序,输出如下: 




{

"args": {},

"data": "",

"files": {},

"form": {

"key1": "value1",

"key2": "value2"

},

"headers": {

"Accept": "*/*",

"Accept-Encoding": "gzip, deflate",

"Content-Length": "23",

"Content-Type": "application/x-www-form-urlencoded",

"Host": "httpbin.org",

"User-Agent": "python-requests/2.19.1"

},

"json": null,

"origin": "14.27.49.210, 14.27.49.210",

"url": "https://httpbin.org/post"

}



可以看到,form变量的值为key_dict输入的值,这样一个POST请求就发送成功了。
3.9设置超时
有时爬虫会遇到服务器长时间不返回,这时爬虫程序就会一直等待,造成爬虫程序没能顺利地执行。因此,可以用Requests在timeout参数设定的秒数结束之后停止等待响应。也就是说,如果服务器在timeout秒内没有应答,就返回异常。
把这个秒数设置为0.001秒,看看会抛出什么异常,这是为了让大家体验timeout异常的效果而设置的值,一般会把这个值设置为20秒。




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

Traceback (most recent call last):

File "C:\Users\ASUS\Anaconda3\lib\site-packages\urllib3\connection.py", line 141, in _new_conn

(self.host, self.port), self.timeout, **extra_kw)

File "C:\Users\ASUS\Anaconda3\lib\site-packages\urllib3\util\connection.py", line 83, in create_connection

raise err

File "C:\Users\ASUS\Anaconda3\lib\site-packages\urllib3\util\connection.py", line 73, in create_connection

sock.connect(sa)

socket.timeout: timed out

During handling of the above exception, another exception occurred:

…



3.10代理访问
采集时为避免被封IP,经常会使用代理。Requests也有相应的proxies属性。例如: 




import requests

proxies = {

"http": "http://10.10.1.10:3128",

"https": "http://10.10.1.10:1080",

}

requests.get("http://www.zhidaow.com", proxies=proxies)



如果代理需要账户和密码,则需如下这样: 




proxies = {

"http": "http://user:pass@10.10.1.10:3128/",

}



3.11自定义请求头部
在Requests中,伪装请求头部是采集时经常用的,可以用如下方法来隐藏: 




import requests

r = requests.get('http://www.zhidaow.com')

print(r.request.headers['User-Agent'])

headers = {'User-Agent': 'alexkh'}

r = requests.get('http://www.zhidaow.com', headers = headers)

print(r.request.headers['User-Agent'])



运行程序,输出如下: 




python-requests/2.19.1

alexkh



3.12Requests爬虫实践
至此,已经介绍了利用爬虫网络对静态网页进行爬取,下面直接通过两个实例来演示爬虫的实践。
3.12.1状态码521网页的爬取
1. 问题发现

在做代理池的时候,发现了一种以前没有见过的反爬虫机制。在用常规的requests.get(url)方法对目标网页进行爬取时,其返回的状态码(status_code)为521,这是一种以前没有见过的状态码。再输出它的爬取内容(text),发现是一些JavaScript代码,如图32所示。下面来探索一下。


图32状态码和爬取内容


2. 分析原理
打开Fiddler,爬取访问网站的包,如图33所示,发现浏览器对于同一网页连续访问了两次,第一次的访问状态码为521,第二次为200(正常访问)。看来网页加了反爬虫机制,需要两次访问才可返回正常网页。


图33Fiddler抓包信息


下面来对比两次请求的区别。521的请求如图34所示; 200的请求如图35所示。


图34521请求



图35200请求



通过对比两次请求头,可发现第二次访问带了新的Cookie值。再考虑上面程序对爬取结果的输出为JavaScript代码,可以考虑其操作过程为: 第一次访问时服务器返回一段可动态生成Cookie值的JavaScript代码; 浏览器运行JavaScript代码生成Cookie值,并带Cookie重新进行访问; 服务器被正常访问,返回页面信息,浏览器渲染加载。
3. 执行流程
弄清楚浏览器的执行过程后,就可以模拟其行为通过Python进行网页爬取。操作步骤如下: 
 用request.get(url)获取JavaScript代码。
 通过正则表达式对代码进行解析,获得JavaScript函数名,JavaScript函数参数和JavaScript函数主体,并将执行函数eval()语句修改为return语句返回Cookie值。
 调用execjs库的executeJS()功能执行JavaScript代码获得Cookie值。
 将Cookie值转化为字典格式,用request.get(url, Cookie = Cookie)方法获取得到正确的网页信息。

4. 实现代码
根据以上流程步骤,实现代码主要表现在: 
(1) 实现程序所需要用到的库。




import re#实现正则表达式

import execjs              #执行JavaScript代码

import requests            #爬取网页



(2) 第一次爬取获得包含JavaScript函数的页面信息后,通过正则表达式对代码进行解析,获得JavaScript函数名、JavaScript函数参数和JavaScript函数主体,并将执行函数eval()语句修改为return语句返回Cookie值。




# js_html为获得的包含JavaScript函数的页面信息

# 提取JavaScript函数名

js_func_name = ''.join(re.findall(r'setTimeout\(\"(\D+)\(\d+\)\"', js_html))

# 提取JavaScript函数参数

js_func_param = ''.join(re.findall(r'setTimeout\(\"\D+\((\d+)\)\"', js_html))

# 提取JavaScript函数主体

js_func = ''.join(re.findall(r'(function .*?)</script>', js_html))



(3) 将执行函数eval()语句修改为return语句返回Cookie值。




# 修改JavaScript函数,返回Cookie值

js_func = js_func.replace('eval("qo=eval;qo(po);")', 'return po')



(4) 调用execjs库的executeJS()功能执行JavaScript代码获得Cookie值。




# 执行JavaScript代码的函数,参数为JavaScript函数主体,JavaScript函数名和JavaScript函数参数

def executeJS(js_func, js_func_name, js_func_param):

 jscontext = execjs.compile(js_func)    #调用execjs.compile()加载JavaScript函数主体内容

 func = jscontext.call(js_func_name,js_func_param)  #使用call()通过函数名和参数执行该函数

 return func

cookie_str = executeJS(js_func, js_func_name, js_func_param)



(5) 将Cookie值转化为字典格式。




#将Cookie值解析为字典格式,方便后面调用

def parseCookie(string):

string = string.replace("document.cookie='", "")

clearance = string.split(';')[0]

return {clearance.split('=')[0]: clearance.split('=')[1]}

cookie = parseCookie(cookie_str)



至此,在获得Cookie后,采用带Cookie的方式重新进行爬取,即可获得我们需要的网页信息。
3.12.2TOP250电影数据
本实践项目的目的是获取豆瓣电影TOP250的所有电影名称,网页地址为: https://movie.douban.com/top250。在此爬虫中,将请求头定制为实际浏览器的请求头。
1. 网站分析
打开豆瓣电影TOP250的网站,右击网页的任意位置,在弹出的快捷菜单中单击“审查元素”命令即可打开该网页的请求头,如图36所示。


图36豆瓣电影TOP250的网站


提取网站中重要的请求头代码为: 




import requests#爬取网页

headers={'user-agent':'Mozilla/5.0(Windows NT 6.1;Win64;x64) AppleWebKit/537.36(KHTML,like Gecko) Chrome/52.0.2743.82 Safari/537.36','Host':'movie.douban.com'}



第一页只有25部电影,如果要获取所有的250页电影,就需要获取总共10页的内容。
通过单击第二页可以发现网页地址变成了: 




https://movie.douban.com/top250?start=25



第三页的地址为https://movie.douban.com/top250?start=50,这很容易理解,每多一页,就给网页地址的start参数加上25。
2. 项目实践
通过以上分析,可以使用requests获取电影网页的代码,并利用for循环翻页。代码为: 




import requests#爬取网页

def get_movies():

headers={'user-agent':'Mozilla/5.0(Windows NT 6.1;Win64;x64) AppleWebKit/537.36(KHTML,like Gecko) Chrome/52.0.2743.82 Safari/537.36','Host':'movie.douban.com'}

for i in range(0,10):

link='http://movie.douban.com/top250?start='+str(i*25)

r=requests.get(link,headers=headers,timeout=10)

print(str(i+1),"页响应状态码: ",r.status_code)

print(r.text)

get_movies()



运行程序,输出如下: 




1 页响应状态码: 200

</p>

<div class="star">

<span class="rating45-t"></span>

<span class="rating_num" property="v:average">8.8</span>

<span property="v:best" content="10.0"></span>

<span>244205人评价</span>

</div>

<p class="quote">

<span class="inq">穷尽一生,我们要学会的,不过是彼此拥抱。</span>

</p>

</div>

</div>

</div>

</li>

<li>

<div class="item">

<div class="pic">

<em class="">117</em>

<a href="https://movie.douban.com/subject/1291990/">

<img width="100" alt="爱在日落黄昏时" src="https://img3.doubanio.com/view/photo/s_ratio_poster/public/p1910924055.jpg" class="">

…



这时,得到的结果只是网页的HTML代码,还需要从中提取需要的电影名称。下面代码实现网页的内容解析: 




import requests

from bs4 import BeautifulSoup 

# 通过find定位标签

# BeautifulSoup文档: https://www.crummy.com/software/BeautifulSoup/bs4/doc/index.zh.html

def bs_parse_movies(html):

movie_list = []

soup = BeautifulSoup(html, "html")

# 查找所有class属性为hd的div标签

div_list = soup.find_all('div', class_='hd')

# 获取每个div中的a中的span(第一个),并获取其文本

for each in div_list:

movie = each.a.span.text.strip()

movie_list.append(movie) 

return movie_list 

# css选择器定位标签

# 更多ccs选择器语法: http://www.w3school.com.cn/cssref/css_selectors.asp

# 注意: BeautifulSoup并不是每个语法都支持

def bs_css_parse_movies(html):

movie_list = []

soup = BeautifulSoup(html, "lxml")

# 查找所有class属性为hd的div标签下的a标签的第一个span标签

div_list = soup.select('div.hd > a > span:nth-of-type(1)')

# 获取每个span的文本

for each in div_list:

movie = each.text.strip()

movie_list.append(movie) 

return movie_list 

# Xpath定位标签

# 更多Xpath语法: https://blog.csdn.net/gongbing798930123/article/details/78955597

def xpath_parse_movies(html):

et_html = etree.HTML(html)

# 查找所有class属性为hd的div标签下的a标签的第一个span标签

urls = et_html.xpath("//div[@class='hd']/a/span[1]") 

movie_list = []

# 获取每个span的文本

for each in urls:

movie = each.text.strip()

movie_list.append(movie) 

return movie_list 

def get_movies():

headers = {

'user-agent': 'Mozilla/5.0 (Windows NT 6.1; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/52.0.2743.82 Safari/537.36',

'Host': 'movie.douban.com'

} 

link = 'https://movie.douban.com/top250'

r = requests.get(link, headers=headers, timeout=10)

print("响应状态码:", r.status_code)

if 200 != r.status_code:

return None 

# 3种定位元素的方式: 

# 普通BeautifulSoup find






return bs_parse_movies(r.text)   

return bs_css_parse_movies(r.text)  

return xpath_parse_movies(r.text) 

movies = get_movies()

print(movies)



运行程序,输出如下: 




响应状态码: 200

to this:

BeautifulSoup(YOUR_MARKUP, "html5lib")

markup_type=markup_type))

['肖申克的救赎', '霸王别姬', '这个杀手不太冷', '阿甘正传', '美丽人生', '泰坦尼克号', '千与千寻', '辛德勒的名单', '盗梦空间', '忠犬八公的故事', '机器人总动员', '三傻大闹宝莱坞', '海上钢琴师', '放牛班的春天', '楚门的世界', '大话西游之大圣娶亲', '星际穿越', '龙猫', '教父', '熔炉', '无间道', '疯狂动物城', '当幸福来敲门', '怦然心动', '触不可及']



3.13习题
1. 请求头Headers提供了关于、或其他的信息。
2. 什么是静态网页?
3. 利用pip安装第三方库wheel。
4. 利用Requests中常用功能获取个人计算机浏览器首页内容。
5. 利用POST发送JSON数据str="loginAccount":"xx","password":"xxx", "userType":"individua"}。