urllib的使用
首先介绍一个 Python 库,叫作 urllib,利用它就可以实现 HTTP 请求的发送,而且不需要关心 HTTP 协议本身甚至更底层的实现,我们要做的是指定请求的 URL、请求头、请求体等信息。此外 urllib 还可以把服务器返回的响应转化为 Python 对象,我们通过该对象便可以方便地获取响应的相关信息,如响应状态码、响应头、响应体等。
|
在 Python2 中,有 |
首先,我们了解一下 urllib 库的使用方法,它是 Python 内置的 HTTP 请求库,也就是说不需要额外安装,可直接使用。urllib 库包含如下 4 个模块。
-
request: 这是最基本的 HTTP 请求模块,可以模拟请求的发送。就像在浏览器里输入网址然后按下回车一样,只需要给库方法传入 URL 以及额外的参数,就可以模拟实现发送请求的过程了。 -
error: 异常处理模块。如果出现请求异常,那么我们可以捕获这些异常,然后进行重试或其他操作以保证程序运行不会意外终止。 -
parse: 一个工具模块。提供了许多 URL 的处理方法,例如拆分、解析、合并等。 -
robotparser: 主要用来识别网站的robots.txt文件,然后判断哪些网站可以爬,哪些网站不可以,它其实用得比较少。
发送请求
使用 urllib 库的 request 模块,可以方便地发送请求并得到响应。我们先来看下它的具体用法。
urlopen
urllib.request 模块提供了最基本的构造 HTTP 请求的方法,利用这个模块可以模拟浏览器的请求发起过程,同时它还具有处理授权验证(Authentication)、重定向(Redirection)、浏览器 Cookie 以及其他一些功能。
下面我们体会一下 request 模块的强大之处。这里以 Python 官网为例,我们把这个网页抓取下来:
import urllib.request
response = urllib.request.urlopen('https://www.python.org')
print(response.read().decode('utf-8'))
|
上面的代码不你能正确执行,修改代码如下:
|
运行结果如图 2-1 所示。
这里我们只用了几行代码,便完成了 Python 官网的抓取,输出了其网页的源代码。得到源代码之后,我们想要的链接、图片地址、文本信息不就都可以提取出来了吗?
接下来,看看返回的响应到底是什么。利用 type 方法输出响应的类型:
import urllib.request
response = urllib.request.urlopen('https://www.python.org')
print(type(response))
输出结果如下:
<class 'http.client.HTTPResponse'>
可以看出,响应是一个 HTTPResponse 类型的对象,主要包含 read、readinto、getheader、getheaders、fileno 等方法,以及 msg、version、status、reason、debuglevel、closed 等属性。
得到响应之后,我们把它赋值给 response 变量,然后就可以调用上述那些方法利属性,得到返回结果的一系列信息了。
例如,调用 read 方法可以得到响应的网页内容、调用 status 属性可以得到响应结果的状态码(200 代表请求成功,404 代表网页未找到等)。
下面再通过一个实例来看看:
import urllib.request
response = urllib.request.urlopen('https://www.python.org')
print(response.status)
print(response.getheaders())
print(response.getheader('Server'))
运行结果如下:
200
[('Server', 'nginx'), ('Content-Type', 'text/html; charset=utf-8'), ('X-Frame-Options', 'DENY'), ('Via', '1.1 vegur'), ('Via', '1.1 varnish'), ('Content-Length', '48775'), ('Accept-Ranges', 'bytes'), ('Date', 'Sun, 15 Mar 2020 13:29:01 GMT'), ('Via', '1.1 varnish'), ('Age', '708'), ('Connection', 'close'), ('X-Served-By', 'cache-bwi5120-BWI, cache-tyo19943-TYO'), ('X-Cache', 'HIT, HIT'), ('X-Cache-Hits', '2, 518'), ('X-Timer', 'S1584278942.717942, VSO,VEO'), ('Vary', 'Cookie'), ('Strict-Transport-Security', 'max-age=63072000; includeSubDomains')]
nginx
其中前两个输出分别是响应的状态码和响应的头信息;最后一个输出是调用 getheader 方法,并传入参数 Server,获取了响应头中 Server 的值,结果是 nginx,意思为服务器是用 Nginx 搭建的。
利用最基本的 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.request data = bytes(urllib.parse.urlencode({'name': 'germey'}), encoding='utf-8') response = urllib.request.urlopen('https://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.httpbin.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": "httpbin.org", "User-Agent": "Python-urllib/3.13", "X-Amzn-Trace-Id": "Root=1-679495bf-599605be3a3642aa625446ce" }, "json": null, "origin": "34.80.45.10", "url": "https://httpbin.org/post" }可以发现我们传递的参数出现在了
form字段中,这表明是模拟表单提交,以 POST 方式传输数据。 -
timeout参数
timeout参数用于设置超时时间,单位为秒,意思是如果请求超出了设置的这个时间,还没有得到响应,就会抛出异常。如果不指定该参数,则会使用全局默认时间。这个参数支持 HTTP、HTTPS、FTP 请求。下面用实例来看—下:
import urllib.request response = urllib.request.urlopen('https://httpbin.org/get', timeout=0.1) print(response.read())运行结果可能如下:
During handling of the above exception, another exception occurred: Traceback (most recent call last): File "D:\BaiduSyncdisk\sourceCode\python\python_learn_roadmap\src\Python3WebScrapyDevelopInAction\UrllibTest\demo5.py", line 3, in <module> ... urllib.error.URLError: <urlopen error _ssl.c:1001: The handshake operation timed out>这里我们设置超时时间为 0.1 秒。程序运行了 0.1 秒后,服务器依然没有响应,于是抛出了
URLError异常。该异常属于urllib.error模块,错误原因是超时。因此可以通过设置这个超时时间,实现当一个网页长时间未响应时,就跳过对它的抓取。此外,利用
try except语句也可以实现,相关代码如下:try: 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参数,该参数必须是ssl.SSLContext类型,用来指定 SSL 的设置。此外,
cafile和capath这两个参数分别用来指定 CA 证书和其路径,这两个在请求 HTTPS 链接时会有用。cadefault参数现在已经弃用了,其默认值为False。至此,我们讲解了
urlopen方法的用法,通过这个最基本的方法,就可以完成简单的请求和网页抓取。 -
Request
利用
urlopen方法可以发起最基本的请求,但它那几个简单的参数并不足以构建一个完整的请求。如果需要往请求中加入Headers等信息,就得利用更强大的Request类来构建请求了。首先,我们用实例感受一下
Request类的用法:import urllib.request request = 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-urllib,我们可以通过修改这个值来伪装浏览器。例如要伪装火狐浏览器,就可以把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, parse url = 'https://httpbin.org/post' headers = { 'User-Agent': 'Mozilla/4.0 (compatible; MSIE 5.5; Windows NT)', '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'))这里我们通过 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": "httpbin.org", "User-Agent": "Mozilla/4.0 (compatible; MSIE 5.5; Windows NT)", "X-Amzn-Trace-Id": "Root=1-67949de7-52e6f15b2e605e767378dfcc" }, "json": null, "origin": "34.80.45.10", "url": "https://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类,接下来举几个子类的例子如下。-
HTTPDefaultErrorHandler用于处理 HTTP 响应错误,所有错误都会抛出HTTPError类型的异常。 -
HTTPRedirectHandler用于处理重定向。 -
HTTPCookieProcessor用于处理Cookie。 -
ProxyHandler用于设置代理,代理默认为空。 -
HTTPPasswordMgr用于管理密码,它维护着用户名密码的对照表。 -
HTTPBasicAuthHandler用于管理认证,如果一个链接在打开时需要认证,那么可以用这个类来解决认证问题。
关于这些类如何使用,现在先不急着了解,后面会用实例演示。
另一个比较重要的类是
OpenerDirector,我们可以称之为Opener。我们之前用过的urlopen方法,实际上就是urllib库为我们提供的一个Opener。那么,为什么要引入
Opener呢? 因为需要实现更高级的功能。之前使用的Request类和urlopen类相当于类库已经封装好的极其常用的请求方法,利用这两个类可以完成基本的请求,但是现在我们需要实现更高级的功能,就需要深入一层进行配置,使用更底层的实例来完成操作,所以这里就用到了Opener。Opener类可以提供open方法,该方法返回的响应类型和urlopen方法如出一辙。那么,Opener类和Handler类有什么关系呢?简而言之就是,利用Handler类来构建Opener类。下面用几个实例来看看
Handler类和Opener类的用法。-
验证
在访问某些网站时,例如 https://ssr3.scrape.center ,可能会弹出这样的认证窗口,如图 2-2 所示。
Figure 2. 图 2-2 认证窗口遇到这种情况,就表示这个网站启用了基本身份认证,英文叫作 HTTP Basic Access Authentication,这是一种登录验证方式,允许网页浏览器或其他客户端程序在请求网站时提供用户名和口令形式的身份凭证。
那么爬虫如何请求这样的页面呢?借助
HTTPBasicAuthHandler模块就可以完成,相关代码如下:from urllib.request import HTTPPasswordMgrWithDefaultRealm, HTTPBasicAuthHandler, build_opener from urllib.error import URLError username = 'admin' password = 'admin' url = 'https://static3.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,其参数是HTTPPasswordMgrWithDefaultRealm对象,它利用add_password方法添加用户名和密码,这样就建立了一个用来处理验证的Handler类。然后将刚建立的
auth_handler类当作参数传入build_opener方法,构建一个Opener,这个Opener在发送请求时就相当于已经验证成功了。最后利用
Opener类中的open方法打开链接,即可完成验证。这里获取的结果就是验证成功后的页面源码内容。-
代理
做爬虫的时候,免不了要使用代理,如果要添加代理,可以这样做:
from urllib.error import URLError from urllib.request import ProxyHandler, build_opener proxy_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('http://www.baidu.com/') print(response.read().decode('utf-8')) except URLError as e: print(e.reason)这里需要我们事先在本地搭建一个 HTTP 代理,并让其运行在 8080 端口上。
上面使用了
proxyHandler,其参数是一个字典,键名是协议类型(例如 HTTP 或者 HTTPS 等)、键值是代理链接,可以添加多个代理。然后利用这个
Handler和build_opener方法构建了一个Opener,之后发送请求即可。-
Cookie
处理
Cookie需要用到相关的Handler。我们先用实例来看看怎样获取网站的
Cookie,相关代码如下:import http.cookiejar, urllib.request cookie = 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)首先,必须声明一个
CookieJar对象。然后需要利用HTTPCookieProcessor构建一个Handler,最后利用build_opener方法构建Opener,执行open函数即可。运行结果如下:
BAIDUID=F9529F238F411BFF08E94199204F1050:FG=1 BIDUPSID=F9529F238F411BFF848961940DEC9115 PSTM=1737795045 BDSVRTM=0 BD_HOME=1可以看到,这里分别输出了每个
Cookie条目的名称和值。既然能输出,那么可不可以输出文件格式的内容呢?我们知道
Cookie实际上也是以文本形式保存的。因此答案当然是肯定的,这里通过下面的实例来看看:import urllib.request, http.cookiejar filename = '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, ignore_expires=True)这时需要将
CookieJar换成MozillaCookieJar,它会在生成文件时用到,是CookieJar的子类,可以用来处理跟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 1769331199 BAIDUID C34B5B19C759F06D59EB26D5C1658A06:FG=1 .baidu.com TRUE / FALSE 3885278846 BIDUPSID C34B5B19C759F06D99ECC787FF44838F .baidu.com TRUE / FALSE 3885278846 PSTM 1737795198 www.baidu.com FALSE / FALSE BDSVRTM 0 www.baidu.com FALSE / FALSE BD_HOME 1另外,
LWPCookieJar同样可以读取和保存Cookie,只是Cookie文件的保存格式和MozillaCookieJar不一样,它会保存成 LWP(libwww-perl)格式。要保存 LWP 格式的
Cookie文件,可以在声明时就进行修改:cookie = http.cookiejar.LWPCookieJar(filename)此时生成的内容如下:
#LWP-Cookies-2.0 Set-Cookie3: BAIDUID=1F30EEDA35C7A94320275F991CA5B3A5:FG=1; path="/"; domain=".baidu.com"; path_spec; domain_dot; expires="2021-05-30 16:06:39Z"; comment=bd; version=0 Set-Cookie3: BIDUPSID=1F30EEDA35C7A9433C97CF6245CBC3B3; path="/"; domain=".baidu.com"; path_spec; domain_dot; expires="2088-06-17 19:20:46Z"; version=0 Set-Cookie3: H_PS_PSSID=31626_1440_21124_31069_31254_31594_30841_31673_31464_31715_30823; path="/"; domain=".baidu.com"; path_spec; domain_dot; discard; version=0 Set-Cookie3: PSTM=1590854799; path="/"; domain=".baidu.com"; path_spec; domain_dot; expires="2088-06-17 19:20:46Z"; version=0 Set-Cookie3: BDSVRTM=11; path="/"; domain="www.baidu.com"; path_spec; discard; version=0 Set-Cookie3: BD_HOME=1; path="/"; domain="www.baidu.com"; path_spec; discard; version=0由此看来,不同格式的
Cookie文件差异还是比较大的。那么,生成
Cookie文件后,怎样从其中读取内容并加以利用呢?下面我们以
LWPCookieJar格式为例来看一下:import urllib.request, http.cookiejar cookie = 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'))可以看到,这里调用
load方法来读取本地的Cookie文件,获取了Cookie的内容。这样做的前提是我们首先生成了LWPCookieJar格式的Cookie并保存成了文件。读取Cookie之后,使用同样的方法构建Handler类和Opener类即可完成操作。运行结果正常的话,会输出百度网页的源代码。
通过上面的方法,我们就可以设置绝大多数请求的功能。
-
处理异常
我们已经了解了如何发送请求,但是在网络不好的情况下,如果出现了异常,该怎么办呢?这时要是不处理这些异常,程序很可能会因为报错而终止运行,所以异常处理还是十分有必要的。
urllib 库中的 error 模块定义了由 request 模块产生的异常。当出现问题时,request 模块便会抛出 error 模块中定义的异常。
-
URLError
URLError 类来自 urllib 库的 error 模块,继承自 OSError ,是 error 异常模块的基类,由 request 模块产生的异常都可以通过捕获这个类来处理。
它具有一个属性 reason,即返回错误的原因。
下面用一个实例来看一下:
from urllib import request, error
try:
response = request.urlopen('https://cuiqingcai.com/404')
except error.URLError as e:
print(e.reason)
我们打开了一个不存在的页面,照理来说应该会报错,但是我们捕获了 URLError 这个异常,运行结果如下:
Not Found
程序没有直接报错,而是输出了错误原因,这样可以避免程序异常终止,同时异常得到了有效处理。
-
HTTPError
HTTPError 是 URLError 的子类,专门用来处理 HTTP 请求错误,例如认证请求失败等。它有如下 3 个属性。
-
Code:返回 HTTP 状态码,例如 404 表示网页不存在,500 表示服务器内部错误等。 -
reason:同父类一样,用于返回错误的原因。 -
headers:返回请求头。
下面我们用几个实例来看看:
from urllib import request, error
try:
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: F142:167F0E:2910984:297EADC:6794AA3B
Accept-Ranges: bytes
Date: Sat, 25 Jan 2025 09:09:16 GMT
Via: 1.1 varnish
Age: 0
X-Served-By: cache-sin-wsss1830052-SIN
X-Cache: MISS
X-Cache-Hits: 0
X-Timer: S1737796156.919862,VS0,VE407
Vary: Accept-Encoding
X-Fastly-Request-ID: c05c4dc2609f924fe50f15c7d7a8226be60ac76a
依然是打开同样的网址,这里捕获了 HTTPError 异常,输出了 reason、code 和 headers 属性。
因为 URLError 是 HTTPError 的父类,所以可以先选择捕获子类的错误,再捕获父类的错误,于是上述代码的更好写法如下:
from urllib import request, error
try:
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.error
try:
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 'TimeoutError'>
TIME OUT
可以发现,reason 属性的结果是 socket.timeout 类。所以这里可以用 isinstance 方法来判断它的类型,做出更详细的异常判断。
本节我们讲述了 error 模块的相关用法,通过合理地捕获异常可以做出更准确的异常判断,使程序更加稳健。
解析链接
前面说过,urllib 库里还提供了 parse 模块,这个模块定义了处理 URL 的标准接口,例如实现 URL 各部分的抽取、合并以及链接转换。它支持如下协议的 URL 处理: file、ftp、gopher、hdl、http、https、imap、mailto、mms、news、nntp、prospero、rsync、rtsp、rtspu、sftp、sip、sips、snews、svn、svn+ssh、telnet 和 wais。
下面我们将介绍 parse 模块中的常用方法,看一下它的便捷之处。
-
urlparse
该方法可以实现 URL 的识别和分段,这里先用一个实例来看一下:
from urllib.parse import urlparse
result = 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 个参数。
-
urlstring: 这是必填项,即待解析的 URL。 -
scheme: 这是默认的协议(例如http或https等)。如果待解析的 URL 没有带协议信息,就会将这个作为默认协议。我们用实例来看一下:from urllib.parse import urlparse result = 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。假设带上协议信息:
result = urlparse('http://www.baidu.com/index.html;user?id=5#comment', scheme='https')则结果如下:
ParseResult(scheme='http', netloc='www.baidu.com', path='/index.html', params='user', query='id=5', fragment='comment')可见,
scheme参数只有在 URL 中不包含协议信息的时候才生效。如果 URL 中有,就会返回解析出的scheme。 -
allow_fragments: 是否忽略
fragment。如果此项被设置为False,那么fragment部分就会被忽略,它会被解析为path、params或者query的一部分,而fragment部分为空。
下面我们用实例来看一下:
result = 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,我们再通过实例看一下:
result = 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 实际上是一个元组,既可以用属性名获取其内容,也可以用索引来顺序获取。实例如下:
result = urlparse('https://www.baidu.com/index.html#comment', allow_fragments=False)
print(result.scheme, result[0], result.netloc, result[1], sep='\n')
这里我们分别用属性名和索引获取了 scheme 和 netloc 运行结果如下:
https
https
www.baidu.com
www.baidu.com
可以发现,两种获取方式都可以成功获取,且结果是一致的。
-
urlunparse
有了 urlparse 方法,相应就会有它的对立方法 urlunparse,用于构造 URL。这个方法接收的参数是一个可迭代对象,其长度必须是 6,否则会抛出参数数量不足或者过多的问题。先用一个实例看下:
from urllib.parse import urlunparse
data = ['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 urlsplit
result = 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 urlsplit
result = 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 urlunsplit
data = ['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 urljoin
print(urljoin('https://www.baidu.com', 'FAQ.html'))
print(urljoin('https://www.baidu.com', 'https://cuiqingcai.com/FAQ.html'))
print(urljoin('https://www.baidu.com/about.html', 'https://cuiqingcai.com/FAQ.html'))
print(urljoin('https://www.baidu.com/about.html', 'https://cuiqingcai.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'))
运行结果如下:
https://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 https://www.baidu.com?category=2#comment www.baidu.com?category=2#comment www.baidu.com?category=2
可以发现,base_url 提供了三项内容:scheme、netloc 和 path。如果新的链接里不存在这三项就予以补充;如果存在,就使用新的链接里面的,base_url 中的是不起作用的。
通过 urljoin 方法,我们可以轻松实现链接的解析、拼合与生成。
urlencode
这里我们再介绍一个常用的方法一一 urlencode,它在构造 GET 请求参数的时候非常有用,实例如下:
from urllib.parse import urlencode
params = {
'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_gs 方法,可以将一串 GET 请求参数转回字典,实例如下:
from urllib.parse import parse_qs
query = 'name=germey&age=25'
print(parse_qs(query))
运行结果如下:
{'name': ['germey'], 'age': ['25']}
可以看到,URL 的参数成功转回为字典类型。
parse_qsl
parse_qsl 方法用于将参数转化为由元组组成的列表,实例如下:
from urllib.parse import parse_qsl
query = 'name=germey&age=25'
print(parse_qsl(query))
运行结果如下:
[('name', 'germey'), ('age', '25')]
可以看到,运行结果是一个列表,该列表中的每一个元素都是一个元组,元组的第一个内容是参数名,第二个内容是参数值。
quote
该方法可以将内容转化为 URL 编码的格式。当 URL 中带有中文参数时,有可能导致乱码问题,此时用 quote 方法可以将中文字符转化为 URL 编码,实例如下:
from urllib.parse import quote
keyword = '壁纸'
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 unquote
url = 'https://www.baidu.com/s?wd=%E5%A3%81%E7%BA%B8'
print(unquote(url))
这里的 url 是上面得到的 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 指定了不允许爬虫爬取的目录,上例设置为 /,代表不允许爬取所有页面。
Allow 一般不会单独使用,会和 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。表 2-1 列出了一些常见搜索爬虫的名称及对应的网站。
| 爬虫名称 | 网站名称 |
|---|---|
BaiduSpider |
百度 |
Googlebot |
谷歌 |
360Spider |
360搜索 |
YoDaoBot |
有道 |
ia_archiver |
Alexa |
Scooter |
altavista |
Bingbot |
必应 |
-
robotparser
了解 Robots 协议之后,就可以使用 robotparser 模块来解析 robots.txt 文件了。该模块提供了一个类 RobotFileParser,它可以根据某网站的 robots.txt 文件判断一个爬取爬虫是否有权限爬取这个网页。
该类用起来非常简单,只需要在构造方法里传入 robots.txt 文件的链接即可。首先看一下它的声明:
urllib.robotparser.RobotFileParser(url='')
当然,也可以不在声明时传入 robots.txt 文件的链接,就让其默认为空,最后再使用 set_url() 方法设置一下也可以。
下面列出了 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.txt文件的时间,这对于长时间分析和抓取robots.txt文件的搜索爬虫很有必要,你可能需要定期检查以抓取最新的robots.txt文件。 -
modified: 它同样对长时间分析和抓取的搜索爬虫很有帮助,可以将当前时间设置为上次抓取和分析robots.txt文件的时间。
下面我们用实例来看一下:
from urllib.robotparser import RobotFileParser
rp = 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 对象 rp,然后通过 set_url 方法设置了 robots.txt 文件的链接。当然,要是不用 set_url 方法,可以在声明对象时直接用如下方法设置:
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/datal
Disallow:/bh
User-agent: Googlebot
Disallow:/baidu
Disallow:/s?
Disallow:/shifen/
Disallow:/homepage/
Disallow:/cpro
Disallow:/ulink?
Disallow:/link?
Disallow:/home/news/data/
Disallow:/bh
不难看出,百度的 robots.txt 文件没有限制 Baiduspider 对百度 homepage 页面的抓取,限制了 Googlebot 对 homepage 页面的抓取。
这里同样可以使用 parse 方法执行对 robots.txt 文件的读取和分析,实例如下:
from urllib.request import urlopen
from urllib.robotparser import RobotFileParser
rp = 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 模块的基本用法和实例,利用此模块,我们可以方便地判断哪些页面能抓取、哪些页面不能。