学习爬虫,其基本的操作便是模拟浏览器向服务器发出请求,那么我们需要从哪个地方做起呢?请求需要我们自己构造吗?我们需要关心请求这个数据结构怎么实现吗?需要了解 HTTP、TCP、IP层的网络传输通信吗?需要知道服务器如何响应以及响应的原理吗?
可能你无从下手,不过不用担心,Python的强大之处就是提供了功能齐全的类库来帮助我们实现这些需求。最基础的 HTTP库有 urllib、requests、httpx等。
拿 urllib 这个库来说,有了它,我们只需要关心请求的链接是什么,需要传递的参数是什么,以及如何设置可选的请求头,而无须深人到底层去了解到底是怎样传输和通信的。有了urllib 库,只用两行代码就可以完成一次请求和响应的处理过程,得到网页内容,是不是感觉方便极了?
本篇博客我们先理解urllib的使用。
urllib简介
首先介绍一个 Python 库,叫作 urllib,利用它就可以实现 HTTP 请求的发送,而且不需要关心 HTTP协议本身甚至更底层的实现,我们要做的是指定请求的 URL、请求头、请求体等信息。此外 urllib 还可以把服务器返回的响应转化为Python对象,我们通过该对象便可以方便地获取响应的相关信息,如响应状态码、响应头、响应体等。
在Python2中,有urllib 和urllib2 两个库来实现HTTP请求的发送。而在Python3中,urllib2库已经不存在了,统一为了 urllib。而且现在基本上没有人使用Python2了。
首先,我们了解一下 urllib 库的使用方法,它是 Python 内置的 HTTP 请求库,也就是说不需要额外安装,可直接使用。urllib库包含如下4个模块。
- request:这是最基本的 HTTP 请求模块,可以模拟请求的发送。就像在浏览器里输人网址然后按下回车一样,只需要给库方法传人 URL以及额外的参数,就可以模拟实现发送请求的过程了。
- error:异常处理模块。如果出现请求异常,那么我们可以捕获这些异常,然后进行重试或其他操作以保证程序运行不会意外终止。
- parse:一个工具模块。提供了许多 URL的处理方法,例如拆分、解析、合并等。
- robotparser:主要用来识别网站的 robots.txt 文件,然后判断哪些网站可以爬,哪些网站不可以,它其实用得比较少。
发送请求
使用 urllib 库的 request 模块,可以方便地发送请求并得到响应。我们先来看下它的具体用法。
- urlopen
ur1lib.request 模块提供了最基本的构造 HTTP请求的方法,利用这个模块可以模拟浏览器的请求发起过程,同时它还具有处理授权验证(Authentication)、重定向(Redirection)、浏览器 Cookie 以及其他一些功能。
下面我们体会-下 request 模块的强大之处。这里以百度官网为例,我们把这个网页抓取下来:
import urllib.requestresponse = urllib.request.urlopen('https://www.python.org')
print(response.read().decode('utf-8'))
一运行直接报错了:
Traceback (most recent call last):File "D:\projects\scrapy-demo\main.py", line 4, in <module>print(response.read().decode('utf-8'))
UnicodeDecodeError: 'utf-8' codec can't decode byte 0x8b in position 1: invalid start byte
也就是我们请求回来的格式并不是utf-8,那么我们需要看看它源码中的编码格式究竟是什么:
import urllib.request
response = urllib.request.urlopen('https://www.python.org')
content_encoding = response.getheader('Content-Encoding')
print(content_encoding)
结果是:
gzip
很早之前这个网页返回的就是utf-8,现在格式变了,那么我们只要解压就可以
import urllib.request
import gzipresponse = urllib.request.urlopen('https://www.python.org')
data = gzip.decompress(response.read())
print(data.decode('utf-8'))
运行结果如下:
这里我们只用了几行代码,便完成了 Python 官网的抓取,输出了其网页的源代码。得到源代码之后,我们想要的链接、图片地址、文本信息不就都可以提取出来了吗?
接下来,看看返回的响应到底是什么。利用type 方法输出响应的类型:
import urllib.requestresponse = urllib.request.urlopen('https://www.python.org')
print(type(response))
输出结果如下:
<class 'http.client.HTTPResponse'>
可以看出,响应是一个 HTTPResposne 类型的对象,主要包含read、readinto、getheader、getheaders、fileno 等方法,以及msg、version、status、reason、debuglevel、closed 等属性。
得到响应之后,我们把它赋值给response 变量,然后就可以调用上述那些方法和属性,得到返回结果的一系列信息了。
例如,调用 read 方法可以得到响应的网页内容、调用 status 属性可以得到响应结果的状态码(200代表请求成功,404代表网页未找到等)。
利用最基本的 urlopen 方法,已经可以完成对简单网页的 GET请求抓取。如果想给链接传递一些参数,又该怎么实现呢?首先看一下urlopen 方法的 API:
urllib,request.urlopen(url, data=None, [timeout,]*, cafile=None, capath=None, cadefault=False, context=None)
可以发现,除了第一个参数用于传递 URL之外,我们还可以传递其他内容,例如 data(附加数据)、timeout(超时时间)等。
接下来就详细说明一下 urlopen 方法中几个参数的用法。
- data参数
data参数是可选的。在添加该参数时,需要使用 bytes 方法将参数转化为字节流编码格式的内容,即 bytes 类型。另外,如果传递了这个参数,那么它的请求方式就不再是 GET,而是 POST 了。
下面用实例来看一下:
import urllib.parse
import urllib.requestdata = bytes(urllib.parse.urlencode({'name': 'germey'}), encoding='utf-8')
response = urllib.request.urlopen('https://www.httpbin.org/post', data=data)
print(response.read().decode('utf-8'))
这里我们传递了一个参数 name,值是germey,需要将它转码成 bytes 类型。转码时采用了 bytes 方法该方法的第一个参数得是 str(字符串)类型,因此用 urllib.parse 模块里的 urlencode 方法将字典参数转化为字符串;第二个参数用于指定编码格式,这里指定为 utf-8。
此处我们请求的站点是 www.httpbin.org,它可以提供 HTTP 请求测试。本次我们请求的 URL 为https://www,.htpbin.org/post,这个链接可以用来测试 POST 请求,能够输出请求的一些信息,其中就包含我们传递的 data 参数。
上面实例运行的结果如下:
{"args": {}, "data": "", "files": {}, "form": {"name": "germey"}, "headers": {"Accept-Encoding": "identity", "Content-Length": "11", "Content-Type": "application/x-www-form-urlencoded", "Host": "www.httpbin.org", "User-Agent": "Python-urllib/3.9", "X-Amzn-Trace-Id": "Root=1-6759abf8-40e4809d78505a460c2a2298"}, "json": null, "origin": "36.163.167.171", "url": "https://www.httpbin.org/post"
}
可以发现我们传递的参数出现在了form字段中,这表明是模拟表单提交,以 POST方式传输数据。
- timeout参数
timeout 参数用于设置超时时间,单位为秒,意思是如果请求超出了设置的这个时间,还没有得到响应,就会抛出异常。如果不指定该参数,则会使用全局默认时间。这个参数支持 HTTP、HTTPS.FTP 请求。
下面用实例来看一下:
import urllib.requestresponse = urllib.request.urlopen('https://www.httpbin.org/get', timeout=0.1)
print(response.read())
运行结果可能如下:
...raise URLError(err)
urllib.error.URLError: <urlopen error timed out>
这里我们设置超时时间为0.1秒。程序运行了0.1秒后,服务器依然没有响应,于是抛出了 URLError异常。该异常属于 urllib.error 模块,错误原因是超时。
因此可以通过设置这个超时时间,实现当一个网页长时间未响应时,就跳过对它的抓取。此外,利用 try except 语句也可以实现,相关代码如下:
import socket
import urllib.request
import urllib.errortry:response = urllib.request.urlopen('https://www.httpbin.org/get', timeout=0.1)
except urllib.error.URLError as e:if isinstance(e.reason, socket.timeout):print('TIME OUT')
这里我们请求了 https://www.httpbin.org/get 这个测试链接,设置超时时间为 0.1 秒,然后捕获到URLError 这个异常,并判断异常类型是 socket.timeout,意思是超时异常,因此得出确实是因为超时而报错的结论,最后打印输出了TIME OUT。
运行结果如下:
TIME OUT
按照常理来说,0.1秒几乎不可能得到服务器响应,因此输出了TIME OUT的提示。
通过设置 timeout 参数实现超时处理,有时还是很有用的。
- 其他参数
除了 data参数和 timeout 参数,urlopen方法还有 context参数,该参数必须是 ss1.SSLContext 类型,用来指定 SSL的设置。
此外,cafile 和 capath 这两个参数分别用来指定 CA 证书和其路径,这两个在请求 HTTPS 链接时会有用。
cadefault 参数现在已经弃用了,其默认值为 False。
至此,我们讲解了 urlopen 方法的用法,通过这个最基本的方法,就可以完成简单的请求和网页抓取。
- Request
利用 urlopen 方法可以发起最基本的请求,但它那几个简单的参数并不足以构建一个完整的请求。
如果需要往请求中加入 Headers 等信息,就得利用更强大的 Request 类来构建请求了。
首先,我们用实例感受一下 Request 类的用法:
import urllib.requestrequest = urllib.request.Request('https://python.org')
response = urllib.request.urlopen(request)
print(response.read().decode('utf-8'))
可以发现,我们依然是用urlopen方法来发送请求,只不过这次该方法的参数不再是URL,而是一个 Request 类型的对象。通过构造这个数据结构,一方面可以将请求独立成一个对象,另一方面可更加丰富和灵活地配置参数。
下面我们看一下可以通过怎样的参数来构造 Request类,构造方法如下:
class urllib.request.Request(url,data=None,
headers{},origin_req_host=None,unverifiable=False, method=None)
第一个参数 url 用于请求 URL,这是必传参数,其他的都是可选参数。
第二个参数 data如果要传数据,必须传 bytes 类型的。如果数据是字典,可以先用 urllib.parse模块里的 urlencode 方法进行编码。
第三个参数 headers 是一个字典,这就是请求头,我们在构造请求时,既可以通过 headers 参数直接构造此项,也可以通过调用请求实例的 add header 方法添加。
添加请求头最常见的方法就是通过修改 User-Agent 来伪装浏览器。默认的 User-Agent 是Python-ur1lib,我们可以通过修改这个值来伪装浏览器。例如要伪装火狐浏览器,就可以把 User-Agent
设置为:
Mozilla/5.0(X11;U;Linux i686)Gecko/20071127 Firefox/2.0.0.11
第四个参数 origin req_host 指的是请求方的 host 名称或者 IP 地址。
第五个参数 unverifiable 表示请求是否是无法验证的,默认取值是 False,意思是用户没有足够的权限来接收这个请求的结果。例如,请求一个HTML文档中的图片,但是没有自动抓取图像的权限这时 unverifiable 的值就是 True。
第六个参数 method 是一个字符串,用来指示请求使用的方法,例如 GET、POST 和 PUT 等。
下面我们传人多个参数尝试构建 Request 类:
from urllib import request, parseurl = 'https://www.httpbin.org/post'
headers = {'User-Agent': 'Mozilla/4.0(compatible; MSIE 5.5; Windows NT)','Host': 'ww.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'))
这里我们通过 4个参数构造了一个 Request 类,其中的 url即请求 URL,headers 中指定了User-Agent 和 Host,data用 urlencode 方法和 bytes 方法把字典数据转成字节流格式。另外,指定了请求方式为 POST。
运行结果如下:
{"args": {}, "data": "", "files": {}, "form": {"name": "germey"}, "headers": {"Accept-Encoding": "identity", "Content-Length": "11", "Content-Type": "application/x-www-form-urlencoded", "Host": "ww.httpbin.org", "User-Agent": "Mozilla/4.0(compatible; MSIE 5.5; Windows NT)", "X-Amzn-Trace-Id": "Root=1-675c441e-1f5aeb5b3eccd32e6a610f6f"}, "json": null, "origin": "36.163.154.16", "url": "https://ww.httpbin.org/post"
}
观察结果可以发现,我们成功设置了 data、headers 和 method。通过 add header 方法添加 headers 的方式如下:
req =request.Request(url=url,data=data, method='POST')
req.add header('User-Agent', Mozilla/4.0(compatible; MSIE 5.5; Windows NT)')
有了 Request类,我们就可以更加方便地构建请求,并实现请求的发送啦。
- 高级用法
我们已经可以构建请求了,那么对于一些更高级的操作(例如 Cookie 处理、代理设置等 ),又该怎么实现呢?
此时需要更强大的工具,于是 Handler 登场了。简而言之,Handler 可以理解为各种处理器,有专门处理登录验证的、处理 Cookie 的、处理代理设置的。利用这些Handler,我们几乎可以实现 HTTP请求中所有的功能。
首先介绍一下 urllib.request 模块里的 BaseHandler 类,这是其他所有 Handler 类的父类。它提供了最基本的方法,例如 default open、protocol request 等。
会有各种 Handler 子类继承 BaseHandler类,接下来举几个子类的例子如下。
(1) HTTPDefaultErrorHandler 用于处理 HTTP 响应错误,所有错误都会抛出 HTTPError 类型的异常。
(2) HTTPRedirectHandler 用于处理重定向。
(3) HTTPCookieProcessor 用于处理 Cookie。
(4) ProxyHandler 用于设置代理,代理默认为空。
(5) HTTPPasswordMgr用于管理密码,它维护着用户名密码的对照表。
(6) HTTPBasicAuthHandler 用于管理认证,如果一个链接在打开时需要认证,那么可以用这个类来解决认证问题。
关于这些类如何使用,现在先不急着了解,后面会用实例演示。
另一个比较重要的类是 0penerDirector,我们可以称之为0pener。我们之前用过的 urlopen 方法实际上就是 urllib 库为我们提供的一个 0pener。
那么,为什么要引人 0pener 呢?因为需要实现更高级的功能。之前使用的Request类和 urlopen 类相当于类库已经封装好的极其常用的请求方法,利用这两个类可以完成基本的请求,但是现在我们需要实现更高级的功能,就需要深人一层进行配置,使用更底层的实例来完成操作,所以这里就用到了 Opener。
0pener 类可以提供 open方法,该方法返回的响应类型和 urlopen方法如出一辙。那么,0pener 类和 Handler 类有什么关系呢?简而言之就是,利用 Handler 类来构建 0pener 类。
下面用几个实例来看看 Handler 类和 0pener 类的用法。
- 验证
在访问某些网站时,例如 https://ssr3.scrape.center,可能会弹出这样的认证窗口,如下图所示。
遇到这种情况,就表示这个网站启用了基本身份认证,英文叫作 HTTP Basic Access Authentication.这是一种登录验证方式,允许网页浏览器或其他客户端程序在请求网站时提供用户名和口令形式的身份凭证。
那么爬虫如何请求这样的页面呢?借助 HTTPBasicAuthHandler 模块就可以完成,相关代码如下:
from urllib.request import HTTPBasicAuthHandler, build_opener, HTTPPasswordMgrWithDefaultRealm
from urllib.error import URLErrorusername = 'admin'
password = 'admin'
url = 'https://ssr3.scrape.center/'
p = HTTPPasswordMgrWithDefaultRealm()
p.add_password(None, url, username, password)
auth_handler = HTTPBasicAuthHandler(p)
opener = build_opener(auth_handler)
try:result = opener.open(url)html = result.read().decode('utf-8')print(html)
except URLError as e:print(e.reason)
这里首先实例化了一个 HTTPBasicAuthHandler 对象 auth_handler,其参数是 HTTPPasswordMgr-WithDefaultRealm对象,它利用add_password方法添加用户名和密码,这样就建立了一个用来处理验证的 Handler 类。
然后将刚建立的 auth_handler 类当作参数传人 build_opener 方法,构建一个0pener,这个0pener在发送请求时就相当于已经验证成功了。
最后利用 0pener 类中的 open 方法打开链接,即可完成验证。这里获取的结果就是验证成功后的页面源码内容。
- 代理
做爬虫的时候,免不了要使用代理,如果要添加代理,可以这样做:
from urllib.error import URLError
from urllib.request import ProxyHandler, build_openerproxy_handler = ProxyHandler({"http": 'http://127.0.0.1:8080', 'https': "https://127.0.0.1:8080"})
opener = build_opener(proxy_handler)
try:response = opener.open('https://www.baidu.com')print(response.read().decode('utf-8'))
except URLError as e:print(e.reason)
这里需要我们事先在本地搭建一个 HTTP 代理,并让其运行在 8080端口上。
上面使用了 ProxyHandler,其参数是一个字典,键名是协议类型(例如 HTTP 或者 HTTPS 等)、键值是代理链接,可以添加多个代理。
然后利用这个 Handler 和 build opener 方法构建了一个 0pener,之后发送请求即可。
- Cookie
处理 Cookie 需要用到相关的 Handler。
我们先用实例来看看怎样获取网站的Cookie,相关代码如下:
import http.cookiejar, urllib.requestcookie = http.cookiejar.CookieJar()
handler = urllib.request.HTTPCookieProcessor(cookie)
opener = urllib.request.build_opener(handler)
response = opener.open('https://www.baidu.com')
for item in cookie:print(item.name + "=" + item.value)
首先,必须声明一个 Cookie]ar 对象。然后需要利用 HTTPCookieProcessor 构建一个 Handler,最后利用 build opener 方法构建 0pener,执行 open 函数即可。
运行结果如下:
BAIDUID=077CDC4722F11FC3E3B03E82251C67DD:FG=1
BAIDUID_BFESS=077CDC4722F11FC3E3B03E82251C67DD:FG=1
H_PS_PSSID=61027_60851_61331_61358_61366_61391_61388_61406
PSTM=1734101145
BD_NOT_HTTPS=1
可以看到,这里分别输出了每个Cookie 条目的名称和值。
既然能输出,那么可不可以输出文件格式的内容呢?我们知道 Cookie 实际上也是以文本形式保存的。因此答案当然是肯定的,这里通过下面的实例来看看:
import urllib.request, http.cookiejarfilename = 'cookie.txt'
cookie = http.cookiejar.MozillaCookieJar(filename)
handler = urllib.request.HTTPCookieProcessor(cookie)
opener = urllib.request.build_opener(handler)
response = opener.open('https://www.baidu.com')
cookie.save(ignore_discard=True)
这时需要将 Cookie]ar 换成 MozillaCookie]ar,它会在生成文件时用到,是 (ookie]ar 的子类,可以用来处理跟 Cookie 和文件相关的事件,例如读取和保存 Cookie,可以将 Cookie 保存成 Mozilla 型浏览器的 Cookie 格式。
运行上面的实例之后,会发现生成了一个 cookie.txt 文件,该文件内容如下:
# Netscape HTTP Cookie File
# http://curl.haxx.se/rfc/cookie_spec.html
# This is a generated file! Do not edit..baidu.com TRUE / FALSE 1765637336 BAIDUID 8B9571B632AF777394D83C0368E8AAE7:FG=1
.baidu.com TRUE / TRUE 1765637336 BAIDUID_BFESS 8B9571B632AF777394D83C0368E8AAE7:FG=1
.baidu.com TRUE / FALSE 1765637335 H_PS_PSSID 60277_61027_61099_61219_61245_60853_61367_61390_61388
.baidu.com TRUE / FALSE 3881584983 PSTM 1734101335
www.baidu.com FALSE / FALSE 1734101636 BD_NOT_HTTPS 1
另外,LWPCookie]ar同样可以读取和保存 Cookie,只是 Cookie 文件的保存格式和 MozillaCookie]ar不一样,它会保存成LWP(libwww-perl)格式。
要保存 LWP 格式的 Cookie 文件,可以在声明时就进行修改:
import urllib.request, http.cookiejarfilename = 'cookie.txt'
cookie =http.cookiejar.LWPCookieJar(filename)
handler = urllib.request.HTTPCookieProcessor(cookie)
opener = urllib.request.build_opener(handler)
response = opener.open('https://www.baidu.com')
cookie.save(ignore_discard=True)
此时生成的内容如下:
#LWP-Cookies-2.0
Set-Cookie3: BAIDUID="D8BAD7FD29F9BC81EB78AFCEA14DC816:FG=1"; path="/"; domain=".baidu.com"; path_spec; expires="2025-12-13 14:52:21Z"; version=0
Set-Cookie3: BAIDUID_BFESS="D8BAD7FD29F9BC81EB78AFCEA14DC816:FG=1"; path="/"; domain=".baidu.com"; path_spec; secure; expires="2025-12-13 14:52:21Z"; SameSite=None; version=0
Set-Cookie3: H_PS_PSSID=60275_61027_61098_61218_61243_60853_61325_61360_61372; path="/"; domain=".baidu.com"; path_spec; domain_dot; expires="2025-12-13 14:52:20Z"; version=0
Set-Cookie3: PSTM=1734101540; path="/"; domain=".baidu.com"; path_spec; domain_dot; expires="2092-12-31 18:06:28Z"; version=0
Set-Cookie3: BD_NOT_HTTPS=1; path="/"; domain="www.baidu.com"; path_spec; expires="2024-12-13 14:57:21Z"; version=0
由此看来,不同格式的 Cookie 文件差异还是比较大的。
那么,生成 Cookie 文件后,怎样从其中读取内容并加以利用呢?
下面我们以 LWPCookie]ar 格式为例来看一下:
import urllib.request, http.cookiejarcookie = http.cookiejar.LWPCookieJar()
cookie.load('cookie.txt', ignore_discard=True, ignore_expires=True)
handler = urllib.request.HTTPCookieProcessor(cookie)
opener = urllib.request.build_opener(handler)
response = opener.open('https://www.baidu.com')
print(response.read().decode('utf-8'))
可以看到,这里调用 1oad方法来读取本地的 Cookie 文件,获取了Cookie的内容。这样做的前提是我们首先生成了 LWPCookie]ar 格式的 Cookie,并保存成了文件。读取 Cookie 之后,使用同样的方法构建 Handler 类和 0pener 类即可完成操作。
运行结果正常的话,会输出百度网页的源代码。
通过上面的方法,我们就可以设置绝大多数请求的功能。
处理异常
我们已经了解了如何发送请求,但是在网络不好的情况下,如果出现了异常,该怎么办呢?这时要是不处理这些异常,程序很可能会因为报错而终止运行,所以异常处理还是十分有必要的。
urllib 库中的 error 模块定义了由 request 模块产生的异常。当出现问题时,request 模块便会抛出 error 模块中定义的异常。
- URLError
URLError 类来自 urllib库的error 模块,继承自 0SError类,是error 异常模块的基类,由 request模块产生的异常都可以通过捕获这个类来处理。
它具有一个属性 reason,即返回错误的原因。
下面用一个实例来看一下:
from urllib import request, errortry:response = request.urlopen('https://cuiqingcai.com/404')
except error.URLError as e:print(e.reason)
我们打开了一个不存在的页面,照理来说应该会报错,但是我们捕获了URLError 这个异常,运行结果如下:
Not Found
程序没有直接报错,而是输出了错误原因,这样可以避免程序异常终止,同时异常得到了有效处理。
- HTTPError
HTTPError 是URLError 的子类,专门用来处理HTTP请求错误,例如认证请求失败等。它有如下3个属性。
(1) code:返回 HTTP 状态码,例如 404表示网页不存在,500 表示服务器内部错误等。
(2) reason:同父类一样,用于返回错误的原因。
(3) headers:返回请求头。
下面我们用几个实例来看看:
from urllib import request, errortry:response = request.urlopen('https://cuiqingcai.com/404')
except error.HTTPError as e:print(e.reason, e.code, e.headers, sep='\n')
运行结果如下:
Not Found
404
Connection: close
Content-Length: 9379
Server: GitHub.com
Content-Type: text/html; charset=utf-8
Access-Control-Allow-Origin: *
ETag: "64d39a40-24a3"
Content-Security-Policy: default-src 'none'; style-src 'unsafe-inline'; img-src data:; connect-src 'self'
x-proxy-cache: MISS
X-GitHub-Request-Id: A70E:3CA0C7:3F7B8D:449189:675C4EB2
Accept-Ranges: bytes
Age: 0
Date: Fri, 13 Dec 2024 15:11:54 GMT
Via: 1.1 varnish
X-Served-By: cache-lon420137-LON
X-Cache: MISS
X-Cache-Hits: 0
X-Timer: S1734102715.581747,VS0,VE85
Vary: Accept-Encoding
X-Fastly-Request-ID: aef6c63cad118dac55be2f1269f01bbfaf098acb
依然是打开同样的网址,这里捕获了 HTTPError 异常,输出了reason、code 和 headers 属性。因为URLError 是HTTPError 的父类,所以可以先选择捕获子类的错误,再捕获父类的错误,于是上述代码的更好写法如下:
from urllib import request, errortry:response = request.urlopen('https://cuiqingcai.com/404')
except error.HTTPError as e:print(e.reason, e.code, e.headers, sep='\n')
except error.URLError as e:print(e.reason)
else:print('Request Successfully')
这样就可以做到先捕获 HTTPError,获取它的错误原因、状态码、请求头等信息。如果不是HTTPError 异常,就会捕获URLError 异常,输出错误原因。最后,用else语句来处理正常的逻辑。这是一个较好的异常处理写法。
有时候,reason属性返回的不一定是字符串,也可能是一个对象。再看下面的实例:
import socket
import urllib.request
import urllib.errortry:response = urllib.request.urlopen('https://www.baidu.com', timeout=0.01)
except urllib.error.URLError as e:print(type(e.reason))if isinstance(e.reason, socket.timeout):print('TIME OUT')
这里我们直接设置超时时间来强制抛出 timeout 异常。
运行结果如下:
<class 'socket.timeout'>
TIME OUT
可以发现,reason 属性的结果是 socket.timeout 类。所以这里可以用 isinstance 方法来判断它的类型,做出更详细的异常判断。
本节我们讲述了 error 模块的相关用法,通过合理地捕获异常可以做出更准确的异常判断,使程序更加稳健。
解析链接
前面说过,urllib库里还提供了 parse模块,这个模块定义了处理URL的标准接口,例如实现 URL各部分的抽取、合并以及链接转换。它支持如下协议的URL处理:file、fp、gopher、hdl、htp、https、imap、 mailto、 mms、 news、 nntp、 prospero、 rsync、rtsp、rtspu、sftp、 sip、 sips、 snews、svn、svntssh.telnet 和 wais。
下面我们将介绍 parse 模块中的常用方法,看一下它的便捷之处。
- urlparse
该方法可以实现 URL的识别和分段,这里先用一个实例来看一下:
from urllib.parse import urlparseresult = urlparse('https://www.baidu.com/index.html;user?id=5#comment')
print(type(result))
print(result)
这里我们利用 urlparse 方法对一个URL进行了解析,然后输出了解析结果的类型以及结果本身。运行结果如下:
<class 'urllib.parse.ParseResult'>
ParseResult(scheme='https', netloc='www.baidu.com', path='/index.html', params='user', query='id=5', fragment='comment')
可以看到,解析结果是一个ParseResult类型的对象,包含6部分,分别是scheme、netloc、path、params、query和fragment。
再观察一下上述实例中的 URL:
https://www.baidu.com/index.html;user?id=5#comment
可以发现,urlparse 方法在解析 URL 时有特定的分隔符。例如://前面的内容就是 scheme,代表协议。第一个/符号前面便是netloc,即域名;后面是path,即访问路径。分号;后面是params,代表参数。问号?后面是査询条件 query,一般用作 GET类型的 URL。井号#后面是锚点 fragment,用于直接定位页面内部的下拉位置。
于是可以得出一个标准的链接格式,具体如下:
scheme://netloc/path;params?query#fragment
一个标准的 URL都会符合这个规则,利用 urlparse 方法就可以将它拆分开来。
除了这种最基本的解析方式外,urlparse 方法还有其他配置吗?接下来,看一下它的 API用法:
urllib.parse.urlparse(urlstring,scheme='', allow fragments=True)
可以看到,urlparse 方法有3个参数。
(1) urlstring:这是必填项,即待解析的 URL。
(2) scheme:这是默认的协议(例如 http 或 https 等 )。如果待解析的 URL 没有带协议信息,就会将这个作为默认协议。我们用实例来看一下:
from urllib.parse import urlparseresult = urlparse('www.baidu.com/index.html;user?id=5#comment', scheme='https')
print(result)
运行结果如下:
ParseResult(scheme='https', netloc='', path='www.baidu.com/index.html', params='user', query='id=5', fragment='comment')
可以发现,这里提供的 URL不包含最前面的协议信息,但是通过默认的scheme 参数,返回了结果 https。
假设带上协议信息:
from urllib.parse import urlparseresult = urlparse('http://www.baidu.com/index.html;user?id=5#comment', scheme='https')
print(result)
运行结果如下:
ParseResult(scheme='http', netloc='www.baidu.com', path='/index.html', params='user', query='id=5', fragment='comment')
可见,scheme 参数只有在 URL,中不包含协议信息的时候才生效。如果 URL中有,就会返回解析出的 scheme。
(3)allow fragments:是否忽略 fragment。如果此项被设置为 False,那么 fragment 部分就会被忽略,它会被解析为 path、params 或者 query 的一部分,而 fragment 部分为空。
下面我们用实例来看一下:
from urllib.parse import urlparseresult = urlparse('https://www.baidu.com/index.html;user?id=5#comment', allow_fragments=False)
print(result)
运行结果如下:
ParseResult(scheme='https', netloc='www.baidu.com', path='/index.html', params='user', query='id=5#comment', fragment='')
假设 URL中不包含 params 和 query,我们再通过实例看一下:
from urllib.parse import urlparseresult = urlparse('https://www.baidu.com/index.html#comment', allow_fragments=False)
print(result)
运行结果如下:
ParseResult(scheme='https', netloc='www.baidu.com', path='/index.html#comment', params='', query='', fragment='')
可以发现,此时 fragment 会被解析为 path 的一部分。
返回结果 ParseResult 实际上是一个元组,既可以用属性名获取其内容,也可以用索引来顺序获实例如下:
from urllib.parse import urlparseresult = urlparse('https://ww.baidu.com/index.html#comment', allow_fragments=False)
print(result.scheme, result[0], result.netloc, result[1], sep='\n')
这里我们分别用属性名和索引获取了scheme 和 netloc,运行结果如下:
https
https
ww.baidu.com
ww.baidu.com
可以发现,两种获取方式都可以成功获取,且结果是一致的。
- urlunparse
有了 urlparse 方法,相应就会有它的对立方法 urlunparse,用于构造 URL。这个方法接收的参数是一个可迭代对象,其长度必须是6,否则会抛出参数数量不足或者过多的问题。先用一个实例看一下:
from urllib.parse import urlunparsedata = ['https', 'www.baidu.com', 'index.html', 'user', 'a=6', 'comment']
print(urlunparse(data))
这里参数 data用了列表类型。当然,也可以用其他类型,例如元组或者特定的数据结构。
运行结果如下:
https://www.baidu.com/index.html;user?a=6#comment
这样我们就成功实现了 URL 的构造。
- urlsplit
这个方法和 urlparse 方法非常相似,只不过它不再单独解析 params 这一部分(params 会合并到path中),只返回5个结果。实例如下:
from urllib.parse import urlsplitresult = urlsplit('https://www.baidu.com/index.html;user?id-5#comment')
print(result)
运行结果如下:
SplitResult(scheme='https', netloc='www.baidu.com', path='/index.html;user', query='id-5', fragment='comment')
可以发现,返回结果是SplitResult,这其实也是一个元组,既可以用属性名获取其值,也可以用索引获取。实例如下:
from urllib.parse import urlsplitresult = urlsplit('https://www.baidu.com/index.html;user?id-5#comment')
print(result.scheme, result[0])
运行结果如下:
https https
- urlunsplit
与urlunparse方法类似,这也是将链接各个部分组合成完整链接的方法,传人的参数也是一个可迭代对象,例如列表、元组等,唯一区别是这里参数的长度必须为5。实例如下:
from urllib.parse import urlunsplitdata = ['https', 'www.baidu.com', 'index.html', 'a=6', 'comment']
print(urlunsplit(data))
运行结果如下:
https://www.baidu.com/index.html?a=6#comment
- urljoin
urlunparse 和 urlunsplit方法都可以完成链接的合并,不过前提都是必须有特定长度的对象,链接的每一部分都要清晰分开。
除了这两种方法,还有一种生成链接的方法,是urljoin。我们可以提供一个 base url(基础链接)作为该方法的第一个参数,将新的链接作为第二个参数。urljoin方法会分析 base_url的 scheme.netloc和 path 这3个内容,并对新链接缺失的部分进行补充,最后返回结果。
下面通过几个实例看一下:
from urllib.parse import urljoinprint(urljoin('https://www.baidu.com', 'FAQ.html'))
print(urljoin('https://www.baidu.com', 'https://cuigingcai.com/FA0.html'))
print(urljoin('https://ww,baidu.com/about.html', 'https://cuigingcai.com/FAQ.html'))
print(urljoin('https://ww.baidu.com/about.html', 'https://cuigingcai.com/FAQ.html?question=2'))
print(urljoin('https://www.baidu.com?wd=abc', 'https://cuiqingcai.com/index.php'))
print(urljoin('https://www.baidu.com', '?category=2#comment'))
print(urljoin('www.baidu.com', '?category=2#comment'))
print(urljoin('www.baidu.com#comment', '?category=2'))
可以发现,base_url提供了三项内容:scheme、netloc和 path。如果新的链接里不存在这三项就予以补充;如果存在,就使用新的链接里面的,base_url中的是不起作用的。
通过 urljoin 方法,我们可以轻松实现链接的解析、拼合与生成。
- urlencode
这里我们再介绍一个常用的方法–urlencode,它在构造 GET 请求参数的时候非常有用,实例如下:
from urllib.parse import urlencodeparams = {'name': 'germey', 'age': 25}
base_url = 'https://www.baidu.com?'
url = base_url + urlencode(params)
print(url)
这里首先声明了一个字典 params,用于将参数表示出来,然后调用urlencode方法将 params 序列化为 GET请求的参数。
运行结果如下:
https://www.baidu.com?name=germey&age=25
可以看到,参数已经成功地由字典类型转化为 GET请求参数:
urlencode 方法非常常用。有时为了更加方便地构造参数,我们会事先用字典将参数表示出来然后将字典转化为 URL的参数时,只需要调用该方法即可。
- parse_qs
有了序列化,必然会有反序列化。利用parse_qs 方法,可以将一串 GET请求参数转回字典,实例如下:
from urllib.parse import parse_qsquery = 'name=germey&age=25'
print(parse_qs(query))
运行结果如下:
{'name': ['germey'], 'age': ['25']}
可以看到,URL的参数成功转回为字典类型。
- parse_qsl
parse qsl方法用于将参数转化为由元组组成的列表,实例如下:
from urllib.parse import parse_qslquery = 'name=germey&age=25'
print(parse_qsl(query))
运行结果如下:
[('name', 'germey'), ('age', '25')]
可以看到,运行结果是一个列表,该列表中的每一个元素都是一个元组,元组的第一个内容是参数名,第二个内容是参数值。
- quote
该方法可以将内容转化为 URL编码的格式。当 URL 中带有中文参数时,有可能导致乱码问题此时用 quote 方法可以将中文字符转化为 URL编码,实例如下:
from urllib.parse import quotekeyword = '壁纸'
url = 'https://www.baidu.com/s?wd=' + quote(keyword)
print(url)
这里我们声明了一个中文的搜索文字,然后用 quote 方法对其进行 URL 编码,最后得到的结果如下:
https://www.baidu.com/s?wd=%E5%A3%81%E7%BA%B8
- unquote
有了 quote 方法,当然就有 unquote 方法,它可以进行 URL解码,实例如下:
from urllib.parse import unquoteurl = 'https://www.baidu.com/s?wd=%E5%A3%81%E7%BA%B8'
print(unquote(url))
这里的 ur1 是上面得到的 URL编码结果,利用 unquote 方法将其还原,结果如下:
https://www.baidu.com/s?wd=壁纸
可以看到,利用 unquote 方法可以方便地实现解码。
本节我们介绍了 parse 模块的一些常用 URL处理方法。有了这些方法,我们可以方便地实现 URL的解析和构造,建议熟练掌握。
分析Robots协议
利用 urllib 库的 robotparser 模块,可以分析网站的 Robots 协议。我们再来简单了解一下这个模块的用法。
- Robots协议
Robots协议也称作爬虫协议、机器人协议,全名为网络爬虫排除标准( Robots Exclusion Protocol),用来告诉爬虫和搜索引擎哪些页面可以抓取、哪些不可以。它通常是一个叫作 robots.txt的文本文件,一般放在网站的根目录下。
搜索爬虫在访问一个站点时,首先会检查这个站点根目录下是否存在 robots.txt 文件,如果存在就会根据其中定义的爬取范围来爬取。如果没有找到这个文件,搜索爬虫便会访问所有可直接访问的页面。
下面我们看一个robots.txt 的样例:
User-agent:*
Disallow:/
Allow: /public/
这限定了所有搜索爬虫只能爬取 public 目录。将上述内容保存成robots.txt文件,放在网站的根目录下,和网站的入口文件(例如index.php、index.html和 index.jsp 等)放在一起。
上面样例中的 User-agent 描述了搜索爬虫的名称,这里将其设置为*,代表 Robots 协议对所有爬取爬虫都有效。例如,我们可以这样设置:
User-agent: Baiduspider
这代表设置的规则对百度爬虫是有效的。如果有多条user-agent记录,则意味着有多个爬虫会受到爬取限制,但至少需要指定一条。
Disallow 指定了不允许爬虫爬取的目录,上例设置为/,代表不允许爬取所有页面。
A11ow一般不会单独使用,会和 Disallow一起用,用来排除某些限制。上例中我们设置为 /public/,结合 Disallow 的设置,表示所有页面都不允许爬取,但可以爬取 public 目录。
下面再来看几个例子。禁止所有爬虫访问所有目录的代码如下:
User-agent:*
Disallow:/
允许所有爬虫访问所有目录的代码如下:
User-agent:*
Disallow:
另外,直接把 robots.txt 文件留空也是可以的。
禁止所有爬虫访问网站某些目录的代码如下:
User-agent:*
Disallow: /private/
Disallow:/tmp/
只允许某一个爬虫访问所有目录的代码如下:
User-agent: WebCrawler
Disallow:
User-agent:*
Disallow:/
以上是 robots.txt 的一些常见写法。
- 爬虫名称
大家可能会疑惑,爬虫名是从哪儿来的?为什么叫这个名?其实爬虫是有固定名字的,例如百度的爬虫就叫作 BaiduSpider。下表列出了一些常见搜索爬虫的名称及对应的网站。
- robotparser
了解 Robots 协议之后,就可以使用 robotparser 模块来解析robots.txt文件了。该模块提供了一个类 RobotfileParser,它可以根据某网站的 robots.txt文件判断一个爬取爬虫是否有权限爬取这个网页。
该类用起来非常简单,只需要在构造方法里传人robots.txt文件的链接即可。首先看一下它的声明:
urllib.robotparser.RobotFileParser(url='")
当然,也可以不在声明时传人robots.txt文件的链接,就让其默认为空,最后再使用set ur1()方法设置一下也可以。
下面列出了 RobotFileParser 类的几个常用方法。
- set url:用来设置 robots.txt 文件的链接。如果在创建 RobotFileParser 对象时传人了链接就不需要使用这个方法设置了。
- read:读取 robots.txt 文件并进行分析。注意,这个方法执行读取和分析操作,如果不调用这个方法,接下来的判断都会为False,所以一定记得调用这个方法。这个方法虽不会返回任何内容,但是执行了读取操作。
- parse:用来解析 robots.txt文件,传人其中的参数是robots.txt文件中某些行的内容,它会按照robots.txt 的语法规则来分析这些内容。
- can fetch:该方法有两个参数,第一个是User-Agent,第二个是要抓取的 URL。返回结果是 True或 False,表示 User-Agent 指示的搜索引擎是否可以抓取这个 URL。
- mtime:返回上次抓取和分析 robots.xt文件的时间,这对于长时间分析和抓取robots.txt文件的搜索爬虫很有必要,你可能需要定期检查以抓取最新的 robots.txt 文件。
- modified:它同样对长时间分析和抓取的搜索爬虫很有帮助,可以将当前时间设置为上次抓取和分析 robots.txt 文件的时间。
下面我们用实例来看一下:
from urllib.robotparser import RobotFileParserrp = RobotFileParser()
rp.set_url('https://www.baidu.com/robots.txt')
rp.read()
print(rp.can_fetch('Baiduspider', 'https://www.baidu.com'))
print(rp.can_fetch('Baiduspider', 'https://www.baidu.com/homepage/'))
print(rp.can_fetch('Googlebot', 'https://www.baidu.com/homepage/'))
这里以百度为例,首先创建了一个 RobotFileParser 对象 p,然后通过 set url 方法设置了robots.txt 文件的链接。当然,要是不用 set ur1方法,可以在声明对象时直接用如下方法设置:
rp= RobotFileParser('https://www.baidu.com/robots.txt')
接着利用 can fetch 方法判断了网页是否可以被抓取。
运行结果如下:
True
True
False
可以看到,这里我们利用 Baiduspider 可以抓取百度的首页以及 homepage 页面,但是 Googlebot就不能抓取 homepage 页面。
打开百度的 robots.txt 文件,可以看到如下信息:
User-agent: Baiduspider
Disallow: /baidu
Disallow: /s?
Disallow: /ulink?
Disallow: /link?
Disallow: /home/news/data/
Disallow: /bhUser-agent: Googlebot
Disallow: /baidu
Disallow: /s?
Disallow: /shifen/
Disallow: /homepage/
Disallow: /cpro
Disallow: /ulink?
Disallow: /link?
Disallow: /home/news/data/
Disallow: /bhUser-agent: MSNBot
Disallow: /baidu
Disallow: /s?
Disallow: /shifen/
Disallow: /homepage/
Disallow: /cpro
Disallow: /ulink?
Disallow: /link?
Disallow: /home/news/data/
Disallow: /bhUser-agent: Baiduspider-image
Disallow: /baidu
Disallow: /s?
Disallow: /shifen/
Disallow: /homepage/
Disallow: /cpro
Disallow: /ulink?
Disallow: /link?
Disallow: /home/news/data/
Disallow: /bhUser-agent: YoudaoBot
Disallow: /baidu
Disallow: /s?
Disallow: /shifen/
Disallow: /homepage/
Disallow: /cpro
Disallow: /ulink?
Disallow: /link?
Disallow: /home/news/data/
Disallow: /bhUser-agent: Sogou web spider
Disallow: /baidu
Disallow: /s?
Disallow: /shifen/
Disallow: /homepage/
Disallow: /cpro
Disallow: /ulink?
Disallow: /link?
Disallow: /home/news/data/
Disallow: /bhUser-agent: Sogou inst spider
Disallow: /baidu
Disallow: /s?
Disallow: /shifen/
Disallow: /homepage/
Disallow: /cpro
Disallow: /ulink?
Disallow: /link?
Disallow: /home/news/data/
Disallow: /bhUser-agent: Sogou spider2
Disallow: /baidu
Disallow: /s?
Disallow: /shifen/
Disallow: /homepage/
Disallow: /cpro
Disallow: /ulink?
Disallow: /link?
Disallow: /home/news/data/
Disallow: /bhUser-agent: Sogou blog
Disallow: /baidu
Disallow: /s?
Disallow: /shifen/
Disallow: /homepage/
Disallow: /cpro
Disallow: /ulink?
Disallow: /link?
Disallow: /home/news/data/
Disallow: /bhUser-agent: Sogou News Spider
Disallow: /baidu
Disallow: /s?
Disallow: /shifen/
Disallow: /homepage/
Disallow: /cpro
Disallow: /ulink?
Disallow: /link?
Disallow: /home/news/data/
Disallow: /bhUser-agent: Sogou Orion spider
Disallow: /baidu
Disallow: /s?
Disallow: /shifen/
Disallow: /homepage/
Disallow: /cpro
Disallow: /ulink?
Disallow: /link?
Disallow: /home/news/data/
Disallow: /bhUser-agent: ChinasoSpider
Disallow: /baidu
Disallow: /s?
Disallow: /shifen/
Disallow: /homepage/
Disallow: /cpro
Disallow: /ulink?
Disallow: /link?
Disallow: /home/news/data/
Disallow: /bhUser-agent: Sosospider
Disallow: /baidu
Disallow: /s?
Disallow: /shifen/
Disallow: /homepage/
Disallow: /cpro
Disallow: /ulink?
Disallow: /link?
Disallow: /home/news/data/
Disallow: /bhUser-agent: yisouspider
Disallow: /baidu
Disallow: /s?
Disallow: /shifen/
Disallow: /homepage/
Disallow: /cpro
Disallow: /ulink?
Disallow: /link?
Disallow: /home/news/data/
Disallow: /bhUser-agent: EasouSpider
Disallow: /baidu
Disallow: /s?
Disallow: /shifen/
Disallow: /homepage/
Disallow: /cpro
Disallow: /ulink?
Disallow: /link?
Disallow: /home/news/data/
Disallow: /bhUser-agent: *
Disallow: /
不难看出,百度的 robots.txt 文件没有限制 Baiduspider 对百度 homepage 页面的抓取,限制了Googlebot 对 homepage 页面的抓取。
这里同样可以使用 parse 方法执行对 robots.txt 文件的读取和分析,实例如下:
from urllib.request import urlopen
from urllib.robotparser import RobotFileParserrp = RobotFileParser()
rp.parse(urlopen('https://www.baidu.com/robots.txt').read().decode('utf-8').split('\n'))
print(rp.can_fetch('Baiduspider', 'https://www.baidu.com'))
print(rp.can_fetch('Baiduspider', 'https://www.baidu.com/homepage/'))
print(rp.can_fetch('Googlebot', 'https://www.baidu.com/homepage/'))
运行结果是一样的:
True
True
False
本节介绍了 robotparser 模块的基本用法和实例,利用此模块,我们可以方便地判断哪些页面能抓取、哪些页面不能。
总结
本篇博客内容比较多,我们介绍了 urllib库的request、error、parse、robotparser 模块的基本用法这些是一些基础模块,有一些模块的实用性还是很强的,例如我们可以利用 parse 模块来进行 URL,的各种处理,还是很方便的。