爬虫基础(3)发送请求之urllib库与urllib3库的使用

一. urllib库与urllib3库的简介

python3中有urllib和urllib3两个库,其中urllib几乎是python2.7中urllib和urllib2两个模块的集合,所以我们最常用的是模块urllib模块,而urllib3则作为一个拓展模块使用。

(一)什么是Urllib库

Urllib库是Python中的一个功能强大、用于操作URL,并在做爬虫的时候经常要用到的库。在Python2.x中,分为Urllib库和Urllib2库,Python3.x之后都合并到Urllib库中,使用方法稍有不同。下面介绍的是Python3中的urllib库。

urllib 中包含四个模块,分别是 requestparseerrorrobotparser,他们各自的主要作用是:

  • request:发送请求
  • parse:解析链接
  • error:处理异常
  • robotparser:分析 Robots 协议

以下我们将会分别讲解 Urllib 中各模块的使用方法,但是由于篇幅问题,本文只会涉及模块中比较常用的内容。

在开始讲解前,先给大家提供一个用于测试的网站:发送请求,这个网站可以在页面上返回所发送 请求 的相关信息,十分适合练习使用。

(二)什么是urllib3库

Urllib3是一个功能强大,条理清晰,用于HTTP客户端的Python库,许多Python的原生系统已经开始使用urllib3。Urllib3提供了很多python标准库里所没有的重要特性:

  • 线程安全
  • 连接池
  • 客户端SSL/TLS验证
  • 文件分部编码上传
  • 协助处理重复请求和HTTP重定位
  • 支持压缩编码
  • 支持HTTP和SOCKS代理
  • 100%测试覆盖率

(三)urllib库与urllib3库的关系

我们要明白的是,urllib库和urllib3库没什么关系,这两个库是两个完全不同的包。urllib库并不是在python3.X版本中的urllib3库。

二. urllib库模块详析

本节我们分别详细的讲述一下urllib库的四个重要的模块。关于urllib库的详细内容可参考其官方文档:https://docs.python.org/3/library/urllib.html。下面我们着重说一下前三个urllib模块(最后一个模块应用的频率较少,此处我们只作了解)。

(一)发送请求

request 模块是最基本的 HTTP 请求模块,可以用来模拟发送请求,就像在浏览器里输入网址,然后回车一样,只需要给库方法传入 URL 及额外的参数,就可以模拟实现这个过程了。同时它还带有处理授权验证( authenticaton )、重定向( redirection 、浏览器 Cookies 及其他内容。使用 urllib 的 request 模块,我们可以很方便地实现请求的发送并得到响应。

1. urlopen方法

urlopen方法的作用是快速发送一个简单的网络请求,该方法的调用格式如下:

urllib.request.urlopen(url, data=None, timeout=socket._GLOBAL_DEFAULT_TIMEOUT, cafile=None, capath=None, cadefault=False, context=None)

urlopen 方法无疑是 request 模块中最常用的方法之一,常见的参数说明如下:

  • url:必填,字符串,指定目标网站的 URL。
  • data:指定表单数据,该参数默认为 None,此时 urllib 使用 GET 方法 发送请求;当给参数赋值后,urllib 使用 POST 方法 发送请求,并在该参数中携带表单信息(bytes 类型)。例如下面这个例子:
from urllib import request, parse

data = bytes(parse.urlencode({
    
    'word': 'hello'}), encoding='utf-8')
response = request.urlopen('http://httpbin.org/post', data=data)
print(response.read().decode('utf-8'))

# 运行结果:
{
    
    
  "args": {
    
    }, 
  "data": "", 
  "files": {
    
    }, 
  "form": {
    
    
    "word": "hello"
  }, 
  "headers": {
    
    
    "Accept-Encoding": "identity", 
    "Content-Length": "10", 
    "Content-Type": "application/x-www-form-urlencoded", 
    "Host": "httpbin.org", 
    "User-Agent": "Python-urllib/3.6", 
    "X-Amzn-Trace-Id": "Root=1-5f6a064d-10cc915066a3cda010364b70"
  }, 
  "json": null, 
  "origin": "111.44.171.116", 
  "url": "http://httpbin.org/post"
}

代码解析:我们传递了一个参数word,其值是“hello”。他需要被转码成bytes(字节流)类型。其中转字节流采用bytes()方法,该方法的第一个参数需要是str(字符串)类型,需要用parse模块中的urlencode()方法将参数字典转换为字符串;第二个参数指定编码格式,这里指定为“utf-8”。本次请求的站点是httpbin.org,他可以提供HTTP请求测试。当请求被发送后,我们将返回结果以read()方法打印出来。

  • timeout:可选参数,用来指定等待时间,若超过指定时间还没获得响应,则抛出一个异常。如果不指定该参数,就会使用全局默认时间。它支持HTTP、HTTPS、FTP请求。
from urllib import request

response = request.urlopen('http://httpbin.org/get', timeout=0.1)
print(response.read())

# 运行结果:
Traceback (most recent call last):
  ......
urllib.error.URLError: <urlopen error timed out>

此处我们设置超时时间是0.1秒。程序0.1秒过后,服务器依然没有响应,于是抛出了URLError异常。该异常属于urllib.error模块,错误原因是超时。因此,我们可以通过设置这个超时时间来控制一个网页如果长时间未响应,就跳过它的抓取。一般利用try … except语句来实现。

该方法始终返回一个 HTTPResponse 对象,HTTPResponse 对象常见的属性和方法如下:

  • geturl():返回 URL
  • getcode():返回状态码
  • getheaders():返回全部响应头信息
  • getheader(header):返回指定响应头信息
  • info():返回响应头信息
  • read():返回响应体(bytes 类型),通常需要使用 decode('utf-8') 将其转化为 str 类型
from urllib import request

url = 'http://www.httpbin.org/get'
response = request.urlopen(url)
print(type(response))
print(response.geturl())
print(response.getcode())
print(response.getheaders())
print(response.getheader('Connection'))
print(response.info())
print(response.read().decode('utf-8'))

# 运行结果
>>> <class 'http.client.HTTPResponse'>
>>> http://www.httpbin.org/get
>>> 200
>>> [('Date', 'Tue, 08 Sep 2020 06:24:09 GMT'), ('Content-Type', 'application/json'), ('Content-Length', '283'), ('Connection', 'close'), ('Server', 'gunicorn/19.9.0'), ('Access-Control-Allow-Origin', '*'), ('Access-Control-Allow-Credentials', 'true')]
>>> close
>>> Date: Tue, 08 Sep 2020 10:11:28 GMT
Content-Type: application/json
Content-Length: 283
Connection: close
Server: gunicorn/19.9.0
Access-Control-Allow-Origin: *
Access-Control-Allow-Credentials: true
>>> {
    
    
  "args": {
    
    }, 
  "headers": {
    
    
    "Accept-Encoding": "identity", 
    "Host": "www.httpbin.org", 
    "User-Agent": "Python-urllib/3.6", 
    "X-Amzn-Trace-Id": "Root=1-5f572389-909a186816689b48fdf37700"
  },
  "origin": "221.207.18.109", 
  "url": "http://www.httpbin.org/get"
}

下面是使用urlopen()方法快速抓取百度首页面的代码:

from urllib import request

url = 'http://www.baidu.com'
response = request.urlopen(url)
data = response.read()
with open("1.html", 'wb') as f:
    f.write(data)

代码的逻辑很简单,首先定义一个目标网页URL,然后通过urlopen()方法访问目标网页,该方法模拟浏览器发送请求,返回一个 HTTPResponse 对象;再以read()方法读取响应内容;最后将内容保存到本地文件中。

2. Request对象

实际上,我们还可以给 urllib.request.open() 方法传入一个 Request 对象作为参数。为什么还需要使用 Request 对象呢?因为在上面的参数中我们无法指定 请求头部(headers),而headers在爬取一些设置的反爬的网页数据信息时,可以模拟浏览器访问这些网页,所以他在网络爬虫过程中起着重要的作用。

很多网站可能会首先检查请求头部中的 USER-AGENT 字段来判断该请求是否由网络爬虫程序发起。但是通过修改请求头部中的 USER_AGENT 字段,我们可以将爬虫程序伪装成浏览器,轻松绕过这一层检查。这里提供一个查找常用的 USER-AGENT 的网站:https://techblog.willshouse.com/2012/01/03/most-common-user-agents/

urllib.request.Request(url, data=None, headers={
    
    }, origin_req_host=None, unverifiable=False, method=None)

参数说明如下:

  • url:指定目标网站的 URL
  • data:发送 POST 请求时提交的表单数据,默认为 None
  • headers:发送请求时附加的请求头部,默认为 {}
  • origin_req_host:请求方的 host 名称或者 IP 地址,默认为 None
  • unverifiable:请求方的请求无法验证,默认为 False
  • method:指定请求方法,默认为 None
from urllib import request

url = 'http://www.httpbin.org/headers'
headers = {
    
    
    'User-Agent':'Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/67.0.3396.99 Safari/537.36'
}
req = request.Request(url, headers=headers, method='GET')
res = request.urlopen(req)
html = res.read().decode('utf-8')
print(html)

# 运行结果:
{
    
    
  "headers": {
    
    
    "Accept-Encoding": "identity", 
    "Host": "www.httpbin.org", 
    "User-Agent": "Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/67.0.3396.99 Safari/537.36", 
    "X-Amzn-Trace-Id": "Root=1-5f575683-21240e2a6e35000b2793245b"
  }
}

当我们以POST请求方式发送请求时:

from urllib import request

url = 'http://httpbin.org/post'
headers = {
    
    
    'User-Agent':'Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/67.0.3396.99 Safari/537.36',
    'Host': 'httpbin.org'
}
dict = {
    
    'name': 'Germey'}
data = bytes(parse.urlencode(dict), encoding='utf-8')
req = request.Request(url=url, data=data, headers=headers, method='POST')
response = request.urlopen(req)
print(response.read().decode('utf-8'))

# 运行结果:
{
    
    
  "args": {
    
    }, 
  "data": "", 
  "files": {
    
    }, 
  "form": {
    
    
    "name": "Germey"
  }, 
  "headers": {
    
    
    "Accept-Encoding": "identity", 
    "Content-Length": "11", 
    "Content-Type": "application/x-www-form-urlencoded", 
    "Host": "httpbin.org", 
    "User-Agent": "Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/67.0.3396.99 Safari/537.36", 
    "X-Amzn-Trace-Id": "Root=1-5f6a0d2b-45c37cec3674fbfe1f9487a2"
  }, 
  "json": null, 
  "origin": "111.44.231.223", 
  "url": "http://httpbin.org/post"
}

此外,headers也可以使用add_header()方法来添加:

from urllib import request

url = 'http://httpbin.org/post'
dict = {
    
    'name': 'Germey'}
data = bytes(parse.urlencode(dict), encoding='utf-8')
req = request.Request(url=url, data=data, method='POST')
req.add_header('User-Agent', 'Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/67.0.3396.99 Safari/537.36')
response = request.urlopen(req)
print(response.read().decode('utf-8'))

3. 高级用法

在上面的过程中,我们虽可以构造请求,但对于一些更高级的操作,比如Cookies处理、代理设置等,又该如何处理?

此时,我们就需要用到Handler工具了,它可以被理解为各种处理器,有的专门处理登录验证,有的处理Cookies,有的处理代理设置。总之,利用它们几乎可以做到HTTP请求中所有的事情。

BaseHandler类是urllib.request模块里的一个类,它是所有其他Handler的父类,它提供了最基本的方法,例如default_open()、protocol_request()等。除此之外,还有很多继承了BaseHandler的子类,这些子类列举如下:

  • HTTPDefaultErrorHandler:用于处理HTTP响应错误,错误都会抛出HTTPError类型的异常
  • HTTPRedirectHandler:用于处理重定向
  • HTTPCookieProcessor:用于处理Cookies
  • ProxyHandler:用于设置代理,默认代理为空
  • HTTPPasswordMgr:用于管理密码,它维护了用户名和密码的表
  • HTTPBasicAuthHandler:用于管理认证,如果一个链接打开时需要认证,那么可以使用它来解决认证问题

另外,一个比较重要的类是OpenDirectir,我们可以称之为Opener。前面使用的urlopen()本质上就是一个Opener。Request对象和urlopen方法相当于类库封装好的极为常用的请求方法,利用它们可以完成基本的请求。但是,如果要实现更高级的功能,就需要深入一层进行配置,使用更底层的实例来完成操作,所以需要用到Opener。

那么,Handler和Opener是什么关系呢?简而言之,就是利用Handler来构建Opener。

(1)使用Cookie

什么是Cookie?Cookie 是指某些网站为了辨别用户身份、进行 session 跟踪而储存在用户本地终端上的数据。

  • 如何获取Cookie
from urllib import request
from http import cookiejar

cookie = cookiejar.CookieJar()
cookie_handler = request.HTTPCookieProcessor(cookie)
opener = request.build_opener(cookie_handler)
response_3 = opener.open('http://www.baidu.com')
for item in cookie:
    print(item.name + '=' + item.value)
    
# 运行结果:
BAIDUID=913FEF1E8546D323232AF6CC377A2D59:FG=1
BIDUPSID=913FEF1E8546D323F51A34CCDF860B29
H_PS_PSSID=7509_32606_1447_7579_7551_7630_32691
PSTM=1600073739
BDSVRTM=0
BD_HOME=1
  • 如何使用Cookie
from urllib import request
from http import cookiejar

# 将 Cookie 保存到文件
cookie = cookiejar.MozillaCookieJar('cookie.txt')
cookie_handler = request.HTTPCookieProcessor(cookie)
opener = request.build_opener(cookie_handler)
response_4 = opener.open('http://www.baidu.com')
cookie.save(ignore_discard=True,ignore_expires=True)
# 从文件读取 Cookie 并添加到请求中
cookie = cookiejar.MozillaCookieJar()
cookie = cookie.load('cookie.txt',ignore_discard=True,ignore_expires=True)
cookie_handler = request.HTTPCookieProcessor(cookie)
opener = request.build_opener(cookie_handler)
response_5 = opener.open('http://www.baidu.com')
(2)使用代理

当需要抓取的网站设置了访问限制,这时就需要用到代理来抓取数据。对于某些网站,如果同一个 IP 短时间内发送大量请求,则可能会将该 IP 判定为爬虫,进而对该 IP 进行封禁。所以我们有必要使用随机的 IP 地址来绕开这一层检查,这里提供几个查找免费的 IP 地址的网站:

  • 西刺代理:http://www.xicidaili.com/nn/
  • 云代理:http://www.ip3366.net/free/
  • 快代理:https://www.kuaidaili.com/free/

注意,免费的代理 IP 基本上十分不稳定,而且还可能随时更新,所以最好自己写一个爬虫去维护。

from urllib.error import URLError
from urllib.request import ProxyHandler, build_opener

proxy_handler = ProxyHandler({
    
    
    'http': 'http://127.0.0.1:9743',
    'https': 'https://127.0.0.1:9743'
})
opener = build_opener(proxy_handler)
try:
    response = opener.open('http://www.baidu.com')
    print(response.read().decode('utf-8'))
except URLError as e:
    print(e.reason)

此处我们使用了ProxyHandler,其参数是一个字典,键名是协议类型,键值是代理链接,可以添加多个代理。然后,利用这个Handler及build_opener()方法构造一个Opener,之后发送请求即可。

(二)解析链接

parse 模块是一个工具模块,它提供了许多 URL 处理方法,比如拆分、解析和合并等。

一般来说,URL标准中只会允许一部分ASCII字符,比如数字、字母、部分符号等,二其他的一些字符,比如汉字等,是不符合URL标准的。所以如果我们在URL中使用一些其他不符合标准的字符就会出现问题,此时需要进行URL编码方可解决。该模块中常用的一些方法如下表所示:

方法 功能 返回类型
urlparse() 用于解析 URL,实现URL的识别与分段。该方法返回一个 ParseResult 对象,该对象可以认为是一个六元组,URL 的结构为:scheme://netloc/path;parameters?query#fragment urllib.parse.ParseResult
urlunparse 该方法和 urlparse() 相反,它接受的参数是一个长度为6的可迭代对象,将URL的多个部分组合为一个URL。 str
urlsplit 该方法和 urlparse() 相似,只不过它不再单独解析params部分,只返回5个结果,URL中的params会被合并到path中。该方法返回结果是SplitResult对象。 urllib.parse.SplitResult
urlunsplit 该方法和 urlunparse 相似,它可以将链接各个部分组合成完整链接,传入的参数可以是一个列表、元组等迭代对象,参数长度必须是5。 str
urljoin 该方法通过分析base_url(基础链接)的scheme、netloc和path,然后对新链接缺失的部分进行补充,最后返回结果。 str
quote 该方法可以将内容转化为 URL 编码的格式。URL 中带有中文参数时,有时可能会导致乱码的问题,此时用这个方法可以将中文字符转化为 URL 编码。 str
unquote 该方法和 quote 相反,它可以进行 URL 解码。 str
urlencode 该方法将字典类型数据转化为符合 URL 标准的 str 类型数据,该方法可以很方便地将字典参数转换为 URL 的参数。 str
parse_qs 该方法和 urlencode 相反,它可以把 URL 的参数转换为字典类型数据。 str
parse_qsl 该方法可以把 URL 的参数转换为元组组成的列表。 str

1. 链接分段与合并

通过上述表格的整理,我们可以发现,urlparse()、urlunparse()、urlsplit()、urlunsplit()和urljoin()五个方法可以用于 URL 的分段和合并。如下代码:

# 1、urlparse方法——————————————————————————————————————————————————————————
url = 'http://www.baidu.com/index.html;user?id=5#comment'
result1 = parse.urlparse(url)
print(type(result1))
print(result1)

# 运行结果:
<class 'urllib.parse.ParseResult'>
ParseResult(scheme='http', netloc='www.baidu.com', path='/index.html', params='user', query='id=5', fragment='comment')

# 2、urlunparse方法——————————————————————————————————————————————————————————
data = ['http', 'www.baidu.com', 'index.html', 'user', 'a=6', 'comment']
result2 = parse.urlunparse(data)
print(type(result2))
print(result2)

# 运行结果:
<class 'str'>
http://www.baidu.com/index.html;user?a=6#comment

# 3、urlsplit方法——————————————————————————————————————————————————————————
url = 'http://www.baidu.com/index.html;user?id=5#comment'
result3 = parse.urlsplit(url)
print(type(result3))
print(result3)

# 运行结果:
<class 'urllib.parse.SplitResult'>
SplitResult(scheme='http', netloc='www.baidu.com', path='/index.html;user', query='id=5', fragment='comment')

# 4、urlunsplit方法——————————————————————————————————————————————————————————
data = ['http', 'www.baidu.com', 'index.html', 'a=6', 'comment']
result4 = parse.urlunsplit(data)
print(type(result4))
print(result4)

# 运行结果:
<class 'str'>
http://www.baidu.com/index.html?a=6#comment

# 5、urljoin方法——————————————————————————————————————————————————————————
print(parse.urljoin('http://www.baidu.com', 'FAQ.html'))
print(parse.urljoin('http://www.baidu.com', 'https://cuiqingcai.com/FAQ.html'))
print(parse.urljoin('http://www.baidu.com/about.html', 'https://cuiqingcai.com/FAQ.html'))
print(parse.urljoin('http://www.baidu.com/about.html', 'https://cuiqingcai.com/FAQ.html?question=2'))
print(parse.urljoin('http://www.baidu.com?wd=abc', 'https://cuiqingcai.com/index.php'))
print(parse.urljoin('http://www.baidu.com', '?category=2#comment'))
print(parse.urljoin('www.baidu.com', '?category=2#comment'))
print(parse.urljoin('www.baidu.com#comment', '?category=2'))

# 运行结果:
http://www.baidu.com/FAQ.html
https://cuiqingcai.com/FAQ.html
https://cuiqingcai.com/FAQ.html
https://cuiqingcai.com/FAQ.html?question=2
https://cuiqingcai.com/index.php
http://www.baidu.com?category=2#comment
www.baidu.com?category=2#comment
www.baidu.com?category=2

在上述 urljoin 方法中,我们可以发现,base_url 提供了三项内容 schemenetlocpath。如果这 3 项在新的链接里不存在,就予以补充;如果新的链接存在,就使用新的链接的部分。而 base_url 中的 paramsqueryfragment 是不起作用的。

2. 链接编码与解码

通过上述表格的整理,我们可以发现,quote()、unquote()两个方法可以用于 URL 的编码和解码。如下代码:

# 1、quote方法——————————————————————————————————————————————————————————
params = '爬虫'
url = 'https://www.baidu.com/s?wd=' + parse.quote(params)
print(url)

# 运行结果:
https://www.baidu.com/s?wd=%E7%88%AC%E8%99%AB

# 2、unquote方法——————————————————————————————————————————————————————————
url = parse.unquote('https://www.baidu.com/s?wd=%E7%88%AC%E8%99%AB')
print(url)

# 运行结果:
https://www.baidu.com/s?wd=爬虫

3. 链接参数转换

通过上述表格的整理,我们可以发现,urlencode()、parse_qs()和parse_qsl()两个方法可以用于 URL 的编码和解码。如下代码:

# 1、urlencode方法——————————————————————————————————————————————————————————
params = {
    
    
    'name':'徘徊',
    'password':'123456'
}
base_url = 'http://www.baidu.com?'
url = base_url + parse.urlencode(params)
print(url)

# 运行结果:
http://www.baidu.com?name=%E5%BE%98%E5%BE%8A&password=123456

# 2、parse_qs方法——————————————————————————————————————————————————————————
query='name=%E5%BE%98%E5%BE%8A&password=123456'
data = parse.parse_qs(query)
print(data)

# 运行结果:
{
    
    'name': ['徘徊'], 'password': ['123456']}

# 3、parse_qsl方法——————————————————————————————————————————————————————————
query='name=%E5%BE%98%E5%BE%8A&password=123456'
data = parse.parse_qsl(query)
print(data)

# 运行结果:
[('name', '徘徊'), ('password', '123456')]

(三)处理异常

error 模块是异常处理模块,如果出现请求错误,可以捕获这些异常,然后进行重试或其他操作以保证程序不会意外终止。

程序在执行的过程中,难免会发生异常,发生异常不要紧,关键是要能合理地处理异常。error 模块中包含两个重要的类:URLErrorHTTPError,这两个类专门用于处理与URL相关的异常。进行异常处理时,我们经常使用try…except语句,在try中执行主要代码,在except中捕获异常信息,并进行相应的异常处理。

注意:HTTPError 是 URLError 的子类,所以捕获异常时一般要先处理 HTTPError。

URLError和HTTPError的区别:

  • HTTPError 是对应的 HTTP 请求的返回码错误,如果返回错误码是 400 以上的,则引发 HTTPError
  • URLError 对应的一般是网络出现问题,包括 url 问题

一般来说,产生URLError的原因有如下几种可能:

  • 连接不上服务器
  • 远程URL不存在
  • 无网络
  • 触发了HTTPError

HTTPError子类无法处理产生URLError的前三种原因的异常,既无法处理:连接不上服务器、远程URL不存在、无网络引起的异常。例如,我们构造一个不存在的网址,引发远程URL不存在的异常,此时就只能通过URLError 处理。

1. URLError

URLError类来自urllib库的error模块,它继承自OSError类,是error异常模块的基类,由request模块生成的异常都可以通过捕获这个类来处理。该类有一个属性reason,可以返回错误的原因。reason属性返回的结果有时是一个字符串,有时是一个对象。

2. HTTPError

HTTPError是URLError的子类,专门用来处理HTTP请求错误,比如认证请求失败等。它有如下三个属性:

  • code:返回HTTP状态码,比如404表示网页不存在。
  • reason:同父类一样,用于返回错误的原因。
  • headers:返回请求头。

3. 异常处理优化

在实际处理异常时,我们并不知道使用HTTPError能不能处理异常。如果异常处理中只有HTTPError子类的话,若发生连接不上服务器、远程URL不存在、无网络等异常,那就无法处理。所以我们在使用error模块处理URL引起的异常时,先让其用HTTPError子类进行处理,若无法处理,再让其用URLError进行处理。例如下面代码:

from urllib import request, error
import socket

try:
    response = request.urlopen('http://www.httpbin.org/get', timeout=0.1)
except error.HTTPError as e:
    print("Error Code: ", e.code)
    print("Error Reason: ", e.reason)
except error.URLError as e:
    if isinstance(e.reason, socket.timeout):
        print('Time out')
else:
    print('Request Successfully')
    
# 运行结果:
Time out

上述的优化程序中分别用了HTTPError和URLError进行处理。此外,我们还可以整合一下,只用其中一个类就完成异常处理。

首先,我们要明白,有时不能直接用URLError代替HTTPError,但是改进后可以整合。原因是:如果触发了连接不上服务器、远程URL不存在、无网络等异常中的一个,那就无法输出e.code ,因为此时没有code属性。虽然我们把 e.code 部分去掉就能解决异常问题,但是如果引发的是HTTPError,我们又希望获取对应的状态码,那就顾此失彼了。针对以上的问题,我们对代码重新做一下优化:

from urllib import request, error

try:
    response = request.urlopen('http://www.httpbin.org/get', timeout=0.1)
except error.URLError as e:
    if hasattr(e, 'code'):
        print(e.code)
        print(e.reason)
    if hasattr(e, 'reason'):
        print(e.reason)
else:
    print('Request Successfully')
    
# 运行结果:
timed out

此时,如果发生了HTTPError异常,则判断出有e.code,就会既输出状态码也输出错误原因,若发生了其他异常,则判断没有e.code,所以此时只输出e.reason。

(四)分析Robots协议

robotparser 模块主要是用来识别网站的 robots.txt 文件,然后判断哪些网站可以爬,哪些网站不可以爬,它一般用得比较少。

1. robots.txt文件是什么

Robots协议(也称为爬虫协议、机器人协议等)的全称是“网络爬虫排除标准”(Robots Exclusion Protocol),网站通过Robots协议告诉搜索引擎哪些页面可以抓取,哪些页面不能抓取。

robots.txt文件是一个文本文件,使用任何一个常见的文本编辑器,比如Windows系统自带的Notepad,就可以创建和编辑它。robots.txt是一个协议,而不是一个命令。robots.txt是搜索引擎中访问网站的时候要查看的第一个文件。robots.txt文件告诉蜘蛛程序在服务器上什么文件是可以被查看的。每个站点最好建立一个robots.txt文件,对seo更友好。每当搜索爬虫来寻找并不存在的robots.txt文件时,服务器将在日志中记录一条404错误,所以你应该在网站中添加一个robots.txt(即使这个robots.txt文件只是一个空文件)。

当搜索爬虫访问一个站点时,它首先会检查这个站点根目录下是否存在robots.txt文件。如果存在,搜索爬虫会根据其中定义的爬取范围来爬取;如果没有找到该文件,搜索爬虫便会访问所有可直接访问的页面。

2. robotparser

robotparser 模块提供了一个类RobotFileParser 它可以根据某网站的 robots.txt 文件来判断一个爬虫是否有权限来爬取这个网页。该类用起来很方便,只需要在构造方法里传入 robots.txt 的链接即可。该类的声明如下:

urllib.robotparser.RobotFileParser(url='')

当然了,robots.txt 的链接也可以在使用set_url()时进行设置。下面列出了这个类常用的几个方法:

方法 功能
set_url() 该方法用来设置 robots.txt 文件的链接。如果在创建 RobotFileParser 对象时传入了链接,那么就不需要再使用这个方法设置了。
read() 该方法读取 robots.txt 文件并进行分。这个方法执行一个读取和分析操作,如果不调用这个方法,接下来的判断都会为 False ,所以一定记得调用这个方法。这个方法不会返回任何内容,但是执行了读取操作。
parse() 该方法用来解析 robots. txt 文件,传人的参数是 robots.txt 某些行的内容,它会按照 robots.txt 的语法规则来分析这些内容。
can_fetch() 该方法传入两个参数:第一个是 User-agent ,第二个是要抓取的 URL。返回的内容是该搜索引擎是否可以抓取这个 URL,返回结果是 True 或者 False。
mtime() 该方法返回的是上次抓取和分析 robots.txt 的时间,这对于长时间分析和抓取的搜索爬虫是很有必要的,你可能需要定期检查来抓取最新的 robots.txt。
modified() 该方法同样对长时间分析和抓取的搜索爬虫很有帮助,将当前时间设置为上次抓取和分析 robots.txt 的时间。

下面看看这个示例:

from urllib.robotparser import RobotFileParser

resp = RobotFileParser()
resp.set_url('http://www.jianshu.com/robots.txt')
resp.read()
print(resp.can_fetch('*','http://www.jianshu.com/p/b67554025d7d'))
print(resp.can_fetch('*','http://www.jianshu.com/search?q=python&page=l&type=collections'))

# 运行结果:
False
False

三. urllib3库模块详析

本节我们分别详细的讲述一下urllib3库的几个重要的模块。关于urllib3的详析内容,我们可以在网站:https://urllib3.readthedocs.io/en/latest/进行参考。

(一)安装urllib3库

Urllib3 能通过pip来安装:

$ python -m pip install urllib3
或者
$pip install urllib3

你也可以在github上下载最新的源码,解压之后进行安装:

$git clone git://github.com/shazow/urllib3.git

$python setup.py install

(二)生成请求

我们要生成一个请求,必须要使用request方法,下面我们先来了解一下request方法及其参数。

request方法的主要作用是发送一个完整的网络请求,它的最终返回结果是一个 urllib3.response.HTTPResponse 对象,其调用格式为:

http.request(method, url, fields=None, headers=None, **urlopen_kw)

该方法的参数如下:

  • method:请求方法,例如:GET、POST等
  • url:请求访问的目标网页地址
  • fields:fields参数是字典类型,当请求方式为GET时,它存储url参数;当请求方式为POST时,它存储form表单的数据
  • headers:发送请求时附加的请求头部,其数据类型也是字典
import urllib3

http = urllib3.PoolManager()
headers = {
    
    "header":"python"}
fields = {
    
    "name":"pyvip", "password":"pythonvip"}
response = http.request("GET", "http://httpbin.org/get", fields=fields, headers=headers)
print(type(response))
print("data:{}".format(response.data))

# 运行结果:
<class 'urllib3.response.HTTPResponse'>
data:b'{\n  "args": {\n    "name": "pyvip", \n    "password": "pythonvip"\n  }, \n  "headers": {\n    "Accept-Encoding": "identity", \n    "Header": "python", \n    "Host": "httpbin.org", \n    "X-Amzn-Trace-Id": "Root=1-5f5e2805-06ce5cf31f949e52f00391d7"\n  }, \n  "origin": "111.44.232.242", \n  "url": "http://httpbin.org/get?name=pyvip&password=pythonvip"\n}\n'

正如在上述代码中,如果我们要生成一个请求,那首先需要一个PoolManager实例来生成请求,由该实例对象处理与线程池的连接以及线程池安全的所有细节,不需要任何人为操作。然后,我们通过request()方法创建一个请求。根据代码运行结果,我们可以发现,请求方式为GET,所以fields参数的数据作为URL的参数出现在请求url中。

(三)响应内容

request方式返回的HTTPResponse对象提供statusdataheaders等属性:

import urllib3

http = http = urllib3.PoolManager()
response1 = http.request('GET', 'http://httpbin.org/ip')
print("status:{}".format(response1.status))
print("data:{}".format(response1.data))
print("headers:{}".format(response1.headers))

# 运行结果:
status:200
data:b'{\n  "origin": "111.44.143.123"\n}\n'
headers:HTTPHeaderDict({
    
    'Date': 'Sun, 13 Sep 2020 14:25:34 GMT', 'Content-Type': 'application/json', 'Content-Length': '33', 'Connection': 'keep-alive', 'Server': 'gunicorn/19.9.0', 'Access-Control-Allow-Origin': '*', 'Access-Control-Allow-Credentials': 'true'})

(1)JSON内容

HTTPResponse对象返回的json格式数据可以通过json模块中的load方法转换为字典类型数据:

import json
import urllib3

http = urllib3.PoolManager()
response2 = http.request('GET', 'http://httpbin.org/ip')
print(json.loads(response2.data.decode('utf-8')))

# 运行结果:
{
    
    'origin': '111.44.143.123'}

(2)二进制数据内容

request请求返回的HTTPResponse对象的data属性都是字节字符串类型(如上述代码所示),对于大量的数据我们一般通过stream来处理更合适:

import urllib3

http = urllib3.PoolManager()
response3 = http.request('GET', 'http://httpbin.org/bytes/1024', preload_content = False)
for chunk in response3.stream(32):
    print(chunk)

# 运行结果:
b'\xac\xb1\xe2>\xf8\xbeeMW.\xa6\xca:c\xd0\xfc)\xaf\xa3\x88\x17\xb0\xaa\x91\xbd\xd39\xe2\x0e\xaek_'
b'sZ\x918\xf7D\xbfv}\xc4n\xe3\x83\xe6\x9d\xd4?\xa2\xcd\xd3\xff\xa9\x01\xb6\xf7j`Q\xd1W\xcd\xad'
......

当然,也可以当做一个文件对象来处理:

import urllib3

http = urllib3.PoolManager()
response3 = http.request('GET', 'http://httpbin.org/bytes/1024', preload_content = False)
for line in response3:
    print(line)

# 运行结果:
b'\xd9\xb3\xdaF\xc6\x8c\x04\xb2\xaf\x03\x18nH\x83\x8f\xb1 =\xad\x9b\xefA}\x16\x91\xdd\x12\x81\xb5\xa4\x9d\xfc\x86\x97({\xdal\x19\x97\xe5\xf1l\xc8\xf1\xaa\xecQ\x806\xfb\x9c\x87P\xc3#\xed3^\xeaP\xe4m\xb5+\xc5/\x8d\x16\xec\xd7R\xb3\xd4.\x07H\x87\xee\xe5\x1d\xa8I\xe9?\xa2\x19\x0f\xd7\xf8\xbe\xcc\xc2(j\xadcEd\xc1J\xe5S~\xaf\xb1l\xbd\xb5_\xb7a\xa7\xf0\xdd\xe9d@R\xa7E\xeaX\xf1fK\xb1\xf5T\x10dft@8\xf2\\S4\xd1v\xbd\xf4\x93\xf0!\xbc\x1b\xc0\x03\xcb\xb3\xa9\xb9\xa4\xc6\xab$\xeb\t\xb4Qu\xc0\xd2\xf9\x009\x11W\x8f\xb9n\xec\xf0"\xd1G\xe3\xe4\xf5\x8f\x89\xb5a\x9c\xc7|\x02\x98\xad\xabh\x9a\xb1\xe7a\xd9\x06j\x9f\xd9\xf9dn:FD\xc7H\xe3L6\x95\xb9\x8d\xae1#\xa1f\xf1wA\xa5\x11\xda\xac\xf3i\x8f\n'
b'?\xc6r\xe2[,=2\xd1\xbcp\xdd3\xc3\x06\xf5\x06\xbe\xe5\x85<\x02\xc6vM8rH\x1c\x12-?q\x9d\x8fD(\xaf;P\xd1\xaeO\x96_\xad\r?M\x89\xb3\x1dT\xc4\xf75\xa0\xb0\x9f\x90[\x9f}\x17\xd8\xbc\xd5\x83\x98\xda$f\xd00\x8e\xde\t\xb5\x1d\x897\x96\x86\x1a\'\xd4\x8b\xd0[\x84\xbd\xda1\xa5\xc2\xad\x17\xde\xf5\x89at \x80dw\x14\xae\xd1\xda\x80\xad\xffK \xd6\xac\x98\xfe\xfalVK\xa9\x86zNS\xbe\x91\xa18"\xe0\x99\xc5\x86a\xc8\\/\x07\x88\xf9\x94_\xbe\xb3\xd5\xa9d8\x1aoL\xf1>-\xc3\xd0\xda7\x9a\xc2g\xee\x0c\x06\x81\xc5\x8b~\x07\xa38\x9b\xb3Cw$\x87\x7f\x14\x85}\x7f\x1b\x88\xb7/\xb3\xcf\x0f\xa5c\x9a\xa2\xc6H\x17n\xc9;\xfe\xbd\xfaI\x1f\xef[= \xef\xa7\xa2\xa0WF\xb2g"\x0bF\xa7\x15T\xc98W\x9f\xc6M\xd2.Wd\xc5\xf4\x0fE\xa5\xaf\xb3\x9cM:Z\x8ct"\x81\x1d\xd1\xc8\xcc\x82\xd9\n'
......

(四)请求数据

(1)Headers

在request()方法中,可以定义一个字典类型(dictionary),并作为headers参数传入。

import urllib3

http = urllib3.PoolManager()
response4 = http.request(
    'GET',
    'http://httpbin.org/headers',
    headers={
    
    
        'X-Something': 'value'
    }
)
print(json.loads(response4.data.decode('utf-8'))['headers'])

# 运行结果:
{
    
    'headers': {
    
    'Accept-Encoding': 'identity', 'Host': 'httpbin.org', 'X-Amzn-Trace-Id': 'Root=1-5f5f29c9-47dcafa597b9ebb7a9683270', 'X-Something': 'value'}}

(2)Query Parameters

对于GET、HEAD和DELETE请求,可以简单的通过定义一个字典类型作为fields参数传入即可:

import urllib3

http = urllib3.PoolManager()
response5 = http.request(
    'GET',
    'http://httpbin.org/get',
    fields={
    
    'name': 'pyvip', 'password': 'pythonvip'}
)
print(json.loads(response5.data.decode('utf-8'))['args'])

# 运行结果:
{
    
    'name': 'pyvip', 'password': 'pythonvip'}

对于POST和PUT请求,需要手动对传入数据进行编码,然后加在URL后:

from urllib.parse import urlencode
import urllib3

http = urllib3.PoolManager()
encoded_args = urlencode({
    
    'arg': 'python'})
url = 'http://httpbin.org/post?' + encoded_args
response6 = http.request('POST', url)
print(json.loads(response6.data.decode('utf-8'))['args'])

# 运行结果:
{
    
    'arg': 'python'}

(3)Form Data

对于PUT和POST请求,urllib3会自动将字典类型的field参数编码成表格类型

import urllib3

http = urllib3.PoolManager()
response7 = http.request(
    'POST',
    'http://httpbin.org/post',
    fields={
    
    'field': 'Hello World'}
)
print(json.loads(response7.data.decode('utf-8'))['form'])

# 运行结果:
{
    
    'field': 'Hello World'}

(4)JSON

在发起请求时,可以通过定义body 参数并定义headers的Content-Type参数来发送一个已经过编译的JSON数据:

import json
import urllib3

http = urllib3.PoolManager()
data = {
    
    'attribute': 'value'}
encoded_data = json.dumps(data).encode('utf-8')
response8 = http.request(
    'POST',
    'http://httpbin.org/post',
    body=encoded_data,
    headers={
    
    'Content-Type': 'application/json'}
)
print(json.loads(response8.data.decode('utf-8'))['json'])

# 运行结果
{
    
    'attribute': 'value'}

(5)Files & Binary Data

使用 multipart/form-data 编码方式上传文件,可以使用和传入 Form data 数据一样的方法进行,并将文件定义为一个元组的形式 (file_name,file_data)

>>> with open('example.txt') as fp:
...     file_data = fp.read()
>>> r = http.request(
...     'POST',
...     'http://httpbin.org/post',
...     fields={
    
    
...         'filefield': ('example.txt', file_data),
...     }
... )
>>> json.loads(r.data.decode('utf-8'))['files']
{
    
    'filefield': '...'}

文件名 (filename) 的定义不是严格要求的,但是推荐使用,以使得表现得更像浏览器。同时,还可以向元组中再增加一个数据来定义文件的 MIME 类型:

>>> r = http.request(
...     'POST',
...     'http://httpbin.org/post',
...     fields={
    
    
...         'filefield': ('example.txt', file_data, 'text/plain'),
...     }
... )

如果是发送原始二进制数据,只要将其定义为 body 参数即可。同时,建议对 header 的 Content-Type 参数进行设置:

>>> with open('example.jpg', 'rb') as fp:
...     binary_data = fp.read()
>>> r = http.request(
...     'POST',
...     'http://httpbin.org/post',
...     body=binary_data,
...     headers={
    
    'Content-Type': 'image/jpeg'}
... )
>>> json.loads(r.data.decode('utf-8'))['data']
b'...'

(五)使用Timeouts

使用timeout,可以控制请求的运行时间。在一些简单的应用中,可以将timeout参数设置为一个浮点数:

http.request('GET', 'http://httpbin.org/delay/3', timeout=4.0)

要进行更精细的控制,可以使用 Timeout 实例,将连接的timeout和读的timeout分开设置:

http.request('GET', 'http://httpbin.org/delay/3', timeout=urllib3.Timeout(connect=1.0, read=2.0))

如果想让所有的 request 都遵循一个 timeout,可以将 timeout 参数定义在 PoolManager 中:

http = urllib3.PoolManager(timeout=3.0)

或者

http = urllib3.PoolManager(timeout=urllib3.Timeout(connect=1.0, read=2.0))

当在具体的request中再次定义timeout时,会覆盖PoolManager层面上的timeout。

(六)请求重试

Urllib3 可以自动重试幂等请求,原理和 handles redirect 一样。可以通过设置 retries 参数对重试进行控制。Urllib3 默认进行3次请求重试,并进行3次方向改变。

给 retries 参数定义一个整型来改变请求重试的次数:

http.requests('GET', 'http://httpbin.org/ip', retries=10)

关闭请求重试(retrying request)及重定向(redirect)只要将retries定义为False即可:

http.request('GET', 'http://nxdomain.example.com', retries=False)

关闭重定向(redirect)但保持重试(retrying request),将redirect参数定义为False即可:

http.request('GET', 'http://httpbin.org/redirect/1', redirect=False)

要进行更精细的控制,可以使用retry实例,通过该实例可以对请求的重试进行更精细的控制。例如,进行3次请求重试,但是只进行2次重定向:

http.request(
    'GET',
    'http://httpbin.org/redirect/3',
    retries=urllib3.Retry(3, redirect=2)
)

如果想让所有请求都遵循一个retry策略,可以在PoolManager中定义retry参数:

http = urllib3.PoolManager(retries=False)

或者

http = urllib3.PoolManager(retries=urllib3.Retry(5, redirect=2))

当在具体的request中再次定义retry时,会覆盖 PoolManager层面上的retry。

四. 实例——爬取百度图片

下面我们来爬取百度图片中的壁纸:https://image.baidu.com/search/index?tn=baiduimage&ipn=r&ct=201326592&cl=2&lm=-1&st=-1&fr=&sf=1&fmq=1567133149621_R&pv=&ic=0&nc=1&z=0&hd=0&latest=0&copyright=0&se=1&showtab=0&fb=0&width=&height=&face=0&istype=2&ie=utf-8&sid=&word=壁纸

import urllib3
import re
import os

# 1.找到访问的目标URL
page_url = 'https://image.baidu.com/search/index?tn=baiduimage&ipn=r&ct=201326592&cl=2&lm=-1&st=-1&fr=&sf=1&fmq=1567133149621_R&pv=&ic=0&nc=1&z=0&hd=0&latest=0&copyright=0&se=1&showtab=0&fb=0&width=&height=&face=0&istype=2&ie=utf-8&sid=&word=%E5%A3%81%E7%BA%B8'
# 2.分析请求的流程
http = urllib3.PoolManager()
response = http.request("GET", page_url)
# 3.将响应内容保存成文本格式
text = response.data.decode('utf-8')
print(text)
# 4.获取图片url
img_urls = re.findall('"middleURL":"(.*?)"', text)
# 5.构造请求头
headers = {
    
    
    'User-Agent':'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/83.0.4103.116 Safari/537.36'
}
# 6.遍历图片并保存数据
for index, img_url in enumerate(img_urls):
    img = http.request('GET', img_url, headers=headers)
    # 创建保存图片的文件夹
    if not os.path.exists("wallpaper"):
        os.mkdir("wallpaper")
    img_name = '{}.{}'.format(index, img_url.split('.')[-1])
    img_path = "wallpaper/" + img_name
    with open(img_path, 'wb') as f:
        f.write(img.data)

图片爬取结果:
在这里插入图片描述

猜你喜欢

转载自blog.csdn.net/qq_45617055/article/details/114986893