(一步一步学爬虫(2)基本库的使用之urllib)
2.1 urllib的使用
- Python urllib 库用于操作网页 URL,并对网页的内容进行抓取处理。
- urllib 包 包含以下几个模块:
- urllib.request - 打开和读取 URL。
- urllib.error - 包含 urllib.request 抛出的异常。
- urllib.parse - 解析 URL。
- urllib.robotparser - 解析 robots.txt 文件。
2.1.1 发送请求
(1)urlopen
-
抓取Python官网
import urllib.request response = urllib.request.urlopen("https://www.python.org") print(response.read().decode('utf-8'))
-
再看响应类型。
-
得到响应对象是HTTPResposn类型,包含read、readinto、getheader、getheaders、fileno等方法,以及msg、version、status、reason、debuglevel、closed等属性。
-
得到响应后,这些我们把它赋值给response变量,然后就可以调用上述那些方法和属性,得到返回结果的一系列信息了。如图。
data参数
-
发送到服务器的其他数据对象,默认为 None。
-
所以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://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.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": "www.httpbin.org", "User-Agent": "Python-urllib/3.10", "X-Amzn-Trace-Id": "Root=1-63a585f3-0dd414295d45dfd372a8cccd" }, "json": null, "origin": "39.69.199.58", "url": "https://www.httpbin.org/post" }
-
可以发现我们传递的参数出现在了 form 字段中,这表明是模拟表单提交,以 POST方式传输数据。
timeout参数
- timeout 参数用于设置超时时间,单位为秒,意思是如果请求超出了设置的这个时间,还没有得到响应,就会抛出异常。如果不指定该参数,则会使用全局默认时间。这个参数支持HTTP、HT FTP 请求。
- 下面用实例来看一下:
import urllib.request
response= urllib.request.urlopen('https://www.httpbin.org/get', timeout=0.1)
print(response.read())
- 运行结果可能如下:
During handling of the above exception, another exception occurred:
Traceback(most recent call last): File"/var/py/python/urllibtest.py",line 4, in <module>
response =urllib.request.urlopen('https://www.httpbin.org/get', timeout=0.1)
urllib.error.URLError:<urlopen error_ssl.c:1059: The handshake operation timed out>
- 这里我们设置超时时间为0.1秒。程序运行了 0.1 秒后,服务器依然没有响应,于是抛出了URLEIr异常。该异常属于 urllib.error 模块,错误原因是超时。把timeout改为1秒,就没有问题了。
- 我们经常利用这个功能,对一些程序进行超时处理。
其他参数
- cafile 和 capath:cafile 为 CA 证书, capath 为 CA 证书的路径,使用 HTTPS 需要用到。
- cadefault:已经被弃用。
- context:ssl.SSLContext类型,用来指定 SSL 设置。
(2)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://www.httpbin.org/post' headers = { 'User-Agent':'Mozilla/4.o (compatible; MSIE 5.5; Windows NT)', 'Host':'www.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": "www.httpbin.org", "User-Agent": "Mozilla/4.o (compatible; MSIE 5.5; Windows NT)", "X-Amzn-Trace-Id": "Root=1-63a58c59-4bc7d5744cfb2a21406a70ae" }, "json": null, "origin": "39.69.199.58", "url": "https://www.httpbin.org/post" }
-
观察结果可以发现,我们成功设置了 data、headers 和 method。
-
通过add header 方法添加 headers的方式如下: req= request.Rcquest(url=url, data=data, method='POST') reo.addheader("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用于管理密码,它维护着用户名密码的对照表。
- HTTPBasicthHandler用于管理认证,如果一个链接在打开时需要认证,那么可以用这个类解决认证问题。
- 关于这些类如何使用,现在先不急着了解,后面会用实例演示。
- 另一个比较重要的类是OpenerDirector,我们可以称之为 Opener。我们之前用过的 urlopen方法实际上就是urllib库为我们提供的一个Opener。
- 那么,为什么要引人Opener 呢?因为需要实现更高级的功能。之前使用的Request 类和urlopen类相当于类库已经封装好的极其常用的请求方法,利用这两个类可以完成基本的请求,但是现在我们号要实现更高级的功能,就需要深人一层进行配置,使用更底层的实例来完成操作,所以这里就到了 Opener。
- Opener 类可以提供 open 方法,该方法返回的响应类型和 urlopen 方法如出一辙。那么,Opener类和 Handler类有什么关系呢?简而言之就是,利用 Handler类来构建 Opener类。
- 下面用几个实例来看看 Handler 类和 Opener 类的用法。
验证
在访问某些网站时,例如https://ssr3.scrape.center,可能会弹出这样的认证窗口,如图所示。
- 遇到这种情况,就表示这个网站启用了基本身份认证,英文叫作 HTTP Basic Access Authenticatic这是一种登录验证方式,允许网页浏览器或其他客户端程序在请求网站时提供用户名和口令形式的身份凭证。
- 那么爬虫如何请求这样的页面呢?借助HTTPBasicAuthHandler模块就可以完成,相关代码如下:
from urllib.request import HTTPPasswordMgrWithDefaultRealm, HTTPBasicAuthHandler, build_opener from urllib.error import URLError username = '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 对象authhandler,其参数是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('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方法构建了一个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=65D32A722A65FB2ECC5B84E842AAEDBB:FG=1 BIDUPSID=65D32A722A65FB2EC07E6128E33E8E65 PSTM=1671797515 BD_NOT_HTTPS=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 1703335265 BAIDUID C3968EAB9B8F2190D61F9CF77B1DEFBA:FG=1 .baidu.com TRUE / FALSE 3819282912 BIDUPSID C3968EAB9B8F2190DBA80F856ABC16CD .baidu.com TRUE / FALSE 3819282912 PSTM 1671799266 www.baidu.com FALSE / FALSE 1671799565 BD_NOT_HTTPS 1
-
另外,LWPCookieJar 同样可以读取和保存Cookie,只是Cookie文件的保存格式和MozillaCookieJar不一样,它会保存成 LWP(libwww-perl)格式。
-
要保存 LWP 格式的 Cookie 文件,可以在声明时就进行修改:
cookie = http,cookiejar.LWPCookie]ar(filename)
-
此时生成的内容如下:
#LWP-Cookies-2.0 Set-Cookie3:BAIDUID"1F30EEDA35C7A94320275F991CA5B3A5:FG=1"; path="/"; domain-".baidu.com"; path_spe domain_dot; expires="2021-05-30 16:06:39Z"; comment=bd; versicn=o Set-Cookie3:BIDUPSID=1F30EEDA35C7A9433C97CF6245CBC383;path-"/";domain=".baidu.com"; path_spec; domain_c expiresm"2088-06-17 19:20:462"; version=o Set-Cookie3:HPSPSSID=31626_1440_2112431069_31254_31594_30841_31673_31464_31715_30823; path="/"; domain=".baidu.com"; path_spec; domain dot; discard; version=o Set-Cookic3:PSTM=1590854799;path=“/"; domain=".baidu.com"; path_spec; domain_dot; expires="2088-06-19:20:467"; version-0 Set-Cookie3:BDSVRTM-11;path-"/";domain="wmw.baidu.com"; path_spec; discard; version=o Set-Cookie3:BDHOME=1;path="/";domain="www.baidu.com"; path_spec; discard; version=o
-
由此看来,不同格式的 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.baldu.com') print(response.read().decode('utf-8'))
-
可以看到,这里调用 load 方法来读取本地的 Cookie 文件,获取了 Cookie 的内容。这样做的前提是我们首先生成了LWPCookieJar格式的Cookie,并保存成了文件。读取Cookie之后,使用同样的方法构建Handler类和Opener类即可完成操作。
-
运行结果正常的话,会输出百度网页的源代码。
-
通过上面的方法,我们就可以设置绝大多数请求的功能。
2.1.2 处理异常
- 我们已经了解了如何发送请求,但是在网络不好的情况下,如果出现了异常,该怎么办呢?这时要是不处理这些异常,程序很可能会因为报错而终止运行,所以异常处理还是十分有必要的。
- 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请求错误,例如认证请求失败等。它有如下个属性。
-
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 Server: GitHub.com Content-Type: text/html; charset=utf-8 Access-Control-Allow-Origin: * ETag: "63a11c18-247b" Content-Security-Policy: default-src 'none'; style-src 'unsafe-inline'; img-src data:; connect-src 'self' x-proxy-cache: MISS X-GitHub-Request-Id: 56EC:56CD:2601A1:2B23AD:63A5B1A4 Accept-Ranges: bytes Date: Fri, 23 Dec 2022 13:50:09 GMT Via: 1.1 varnish Age: 108 X-Served-By: cache-hkg17935-HKG X-Cache: HIT X-Cache-Hits: 1 X-Timer: S1671803409.014786,VS0,VE2 Vary: Accept-Encoding X-Fastly-Request-ID: a98b18f5c0d823736468fe9f9878fc395b5f6377 X-Cache-Lookup: Cache Miss Content-Length: 9339 X-NWS-LOG-UUID: 110272853862988641 Connection: close X-Cache-Lookup: Cache Miss
-
依然是打开同样的网址,这里捕获了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 模块的相关用法,通过合理地捕获异常可以做出更准确的异常判断,使程序更加稳健。
2.1.3 解析链接
- 前面说过,urllib 库里还提供了 parse 模块,这个模块定义了处理 URL的标准接口,例如实现UR各部分的抽取、合并以及链接转换。它支持如下协议的 URL处理:ffle、fp、gopher、hdl、http、http imap、mailto、mms、news、nntp、prospero、rsync、rtsp、rtspu、sftp、sip、sips、snews、svn、svn+ssl 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://netioc/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='', netloc='', path='www.baidu.com/index.html', params='user', query='id=5', fragment='comment')
可以发现,这里提供的URL不包含最前面的协议信息,但是通过默认的scheme 参数,返回了结果 https。 假设带上协议信息:result =urlparse('http://www,baidu.com/index.ht er?id-5Hcomment',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 部分为空。
-
下面我们用实例来看一下:
from urllib.parse import urlparse result=urlparse('https://www.baidu.com/index.html;user?idw#comment', allow_fragments=False) print(result)
运行结果如下:
ParseResult(scheme='https', netloc='www.baidu.com', path='/index.html', params='user', query='idw#comment', fragment='')
假设 URL中不包含 params 和query,我们再通过实例看一下:from urllib.parse import urlparse result=urlparse("https://wnw.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 urlparse 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,否则会抛出参数数量不足或者过多的问题。先用一个实例一下:
这里参数 data 用了列表类型。当然,也可以用其他类型,例如元组或者特定的数据结构。 运行结果如下:from urllib.parse import urlunparse data = ['https', 'www.baidu.com', 'index.html', 'user', 'a=6', 'comment'] print(urlunparse(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 的sch netloc 和 path 这3个内容,并对新链接缺失的部分进行补充,最后返回结果。 下面通过几个实例看一下:
from urllib.parse import urljoin print(urljoin('https://www.baidu.com', 'FAO.html')) print(urljoin('https://www.baidu.com', 'https://lijunshan.com/FAQ.html')) print(urljoin('https://www.baidu.com/about.html', 'https://lijunshan.com/FAQ.html')) print(urljoin('https://www.baidu.com/about.html', 'https://lijunshan.com/FAQ.html?question=2')) print(urljoin('https://www.baidu.com?wd=abc', 'https://lijunshan.com/index.php')) print(urljoin('https://www.baidu.com', '?category=2#comment'))
-
可以发现,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_qs 方法,可以将一串 GET 请求参数转回字典,实例如下:
from urllib.parse import parse_qs query='name=germey&age=25' print(parse_qs(query))
-
运行结果如下: {'name': l'germey'], 'age': ['25']} 可以看到,URL 的参数成功转回为字典类型。
parse_qsl
-
parse_qs1 方法用于将参数转化为由元组组成的列表,实例如下:
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%E 7%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的解析和构造,建议熟练掌握。
2.1.4 分析Robots协议
- 利用urllib 库的 robotparser 模块,可以分析网站的 Robots 协议。我们再来简单了解一下这个 的 块的用法。
Robots 协议
-
Robots 协议也称作爬虫协议、机器人协议,全名为网络爬虫排除标准(Robots Exclusion Protoco用来告诉爬虫和搜索引擎哪些页面可以抓取、哪些不可以。它通常是一个叫作 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 | 搜索 |
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 的语法规则来分析这些内容。
- Ocanfetch:该方法有两个参数,第一个是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方法设 obots.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/data/ 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.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 模块的基本用法和实例,利用此模块,我们可以方便地判断哪些页面能抓取、哪些页面不能。