一. 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 中包含四个模块,分别是 request
、parse
、 error
、 robotparser
,他们各自的主要作用是:
- 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()
:返回 URLgetcode()
:返回状态码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
:指定目标网站的 URLdata
:发送 POST 请求时提交的表单数据,默认为 Noneheaders
:发送请求时附加的请求头部,默认为 {}origin_req_host
:请求方的 host 名称或者 IP 地址,默认为 Noneunverifiable
:请求方的请求无法验证,默认为 Falsemethod
:指定请求方法,默认为 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
提供了三项内容 scheme
、netloc
和 path
。如果这 3 项在新的链接里不存在,就予以补充;如果新的链接存在,就使用新的链接的部分。而 base_url
中的 params
、query
和 fragment
是不起作用的。
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 模块中包含两个重要的类:URLError
和 HTTPError
,这两个类专门用于处理与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对象提供status
、data
和headers
等属性:
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©right=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©right=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)
图片爬取结果: