学习爬虫,最初的操作便是来模拟浏览器向服务器发出一个请求,那么我们需要从哪个地方做起呢?请求需要我们自己来构造吗?我们需要关心请求这个数据结构的实现吗?我们需要了解 HTTP、TCP、IP 层的网络传输通信吗?我们需要知道服务器的响应和应答原理吗?
可能你不知道无从下手,不用担心,Python 的强大之处就是提供了功能齐全的类库来帮助我们完成这些请求,最基础的 HTTP 库有 Urllib、Httplib2、Requests、Treq 等。
拿 Urllib 这个库来说,有了它,我们只需要关心请求的链接是什么,需要传的参数是什么以及可选的请求头设置就好了,不用深入到底层去了解它到底是怎样来传输和通信的。有了它,两行代码就可以完成一个请求和响应的处理过程,得到网页内容,是不是感觉方便极了?
接下来,就让我们从最基础的部分开始了解这些库的使用方法吧。
3.1 使用Urllib
在 Python2 版本中,有 Urllib 和 Urlib2 两个库可以用来实现Request的发送。而在 Python3 中,已经不存在 Urllib2 这个库了,统一为 Urllib,其官方文档链接为:https://docs.python.org/3/library/urllib.html
我们首先了解一下 Urllib 库,它是 Python 内置的 HTTP 请求库,也就是说我们不需要额外安装即可使用,它包含四个模块:
- 第一个模块 request,它是最基本的 HTTP 请求模块,我们可以用它来模拟发送一请求,就像在浏览器里输入网址然后敲击回车一样,只需要给库方法传入 URL 还有额外的参数,就可以模拟实现这个过程了。
- 第二个 error 模块即异常处理模块,如果出现请求错误,我们可以捕获这些异常,然后进行重试或其他操作保证程序不会意外终止。
- 第三个 parse 模块是一个工具模块,提供了许多 URL 处理方法,比如拆分、解析、合并等等的方法。
- 第四个模块是 robotparser,主要是用来识别网站的 robots.txt 文件,然后判断哪些网站可以爬,哪些网站不可以爬的,其实用的比较少。
在这里重点对前三个模块进行下讲解。
3.1.1 发送请求
使用 Urllib 的 request 模块我们可以方便地实现 Request 的发送并得到 Response,我们本节来看下它的具体用法。
1. urlopen()
urllib.request 模块提供了最基本的构造 HTTP 请求的方法,利用它可以模拟浏览器的一个请求发起过程,同时它还带有处理authenticaton(授权验证),redirections(重定向),cookies(浏览器Cookies)以及其它内容。
我们来感受一下它的强大之处,以 Python 官网为例,我们来把这个网页抓下来:
import urllib.request
response = urllib.request.urlopen('https://www.python.org')
print(response.read().decode('utf-8'))
看一下运行结果,如图 3-1 所示:
图 3-1 运行结果
真正的代码只有两行,我们便完成了 Python 官网的抓取,输出了网页的源代码,得到了源代码之后呢?我们想要的链接、图片地址、文本信息不就都可以提取出来了吗?
接下来我们看下它返回的到底是什么,利用 type() 方法输出 Response 的类型。
import urllib.request
response = urllib.request.urlopen('https://www.python.org')
print(type(response))
输出结果如下:
<class 'http.client.HTTPResponse'>
通过输出结果可以发现它是一个 HTTPResposne 类型的对象,它主要包含的方法有 read()、readinto()、getheader(name)、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', 'SAMEORIGIN'), ('X-Clacks-Overhead', 'GNU Terry Pratchett'), ('Content-Length', '47397'), ('Accept-Ranges', 'bytes'), ('Date', 'Mon, 01 Aug 2016 09:57:31 GMT'), ('Via', '1.1 varnish'), ('Age', '2473'), ('Connection', 'close'), ('X-Served-By', 'cache-lcy1125-LCY'), ('X-Cache', 'HIT'), ('X-Cache-Hits', '23'), ('Vary', 'Cookie'), ('Strict-Transport-Security', 'max-age=63072000; includeSubDomains')]
nginx
可见,三个输出分别输出了响应的状态码,响应的头信息,以及通过调用 getheader() 方法并传递一个参数 Server 获取了 headers 中的 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(超时时间)等等。
下面我们详细说明下这几个参数的用法。
data参数
data 参数是可选的,如果要添加 data,它要是字节流编码格式的内容,即 bytes 类型,通过 bytes() 方法可以进行转化,另外如果传递了这个 data 参数,它的请求方式就不再是 GET 方式请求,而是 POST。
下面用一个实例来感受一下:
import urllib.parse
import urllib.request
data = bytes(urllib.parse.urlencode({'word': 'hello'}), encoding='utf8') response = urllib.request.urlopen('http://httpbin.org/post', data=data) print(response.read())
在这里我们传递了一个参数 word,值是 hello。它需要被转码成bytes(字节流)类型。其中转字节流采用了 bytes() 方法,第一个参数需要是 str(字符串)类型,需要用 urllib.parse 模块里的 urlencode() 方法来将参数字典转化为字符串。第二个参数指定编码格式,在这里指定为 utf8。
在这里请求的站点是 httpbin.org,它可以提供 HTTP 请求测试,本次我们请求的 URL 为:http://httpbin.org/post,这个链接可以用来测试 POST 请求,它可以输出 Request 的一些信息,其中就包含我们传递的 data 参数。
运行结果如下:
{
"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.5" }, "json": null, "origin": "123.124.23.253", "url": "http://httpbin.org/post" }
我们传递的参数出现在了 form 字段中,这表明是模拟了表单提交的方式,以 POST 方式传输数据。
timeout参数
timeout 参数可以设置超时时间,单位为秒,意思就是如果请求超出了设置的这个时间还没有得到响应,就会抛出异常,如果不指定,就会使用全局默认时间。它支持 HTTP、HTTPS、FTP 请求。
下面来用一个实例感受一下:
import urllib.request
response = urllib.request.urlopen('http://httpbin.org/get', timeout=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('http://httpbin.org/get', timeout=1)
...
urllib.error.URLError: <urlopen error timed out>
在这里我们设置了超时时间是 1 秒,程序 1 秒过后服务器依然没有响应,于是抛出了 URLError 异常,它属于 urllib.error 模块,错误原因是超时。
因此我们可以通过设置这个超时时间来控制一个网页如果长时间未响应就跳过它的抓取,利用 try except 语句就可以实现这样的操作,代码如下:
import socket
import urllib.request
import urllib.error
try: response = urllib.request.urlopen('http://httpbin.org/get', timeout=0.1) except urllib.error.URLError as e: if isinstance(e.reason, socket.timeout): print('TIME OUT')
在这里我们请求了 http://httpbin.org/get 这个测试链接,设置了超时时间是 0.1 秒,然后捕获了 URLError 这个异常,然后判断异常原因是 socket.timeout 类型,意思就是超时异常,就得出它确实是因为超时而报错,打印输出了 TIME OUT。
运行结果如下:
TIME OUT
常理来说,0.1 秒内基本不可能得到服务器响应,因此输出了 TIME OUT 的提示。
这样,我们可以通过设置 timeout 这个参数来实现超时处理,有时还是很有用的。
其他参数
还有 context 参数,它必须是 ssl.SSLContext 类型,用来指定 SSL 设置。
cafile 和 capath 两个参数是指定 CA 证书和它的路径,这个在请求 HTTPS 链接时会有用。
cadefault 参数现在已经弃用了,默认为 False。
以上讲解了 urlopen() 方法的用法,通过这个最基本的函数可以完成简单的请求和网页抓取,如需更加详细了解,可以参见官方文档:https://docs.python.org/3/library/urllib.request.html。
2. Request
由上我们知道利用 urlopen() 方法可以实现最基本请求的发起,但这几个简单的参数并不足以构建一个完整的请求,如果请求中需要加入 Headers 等信息,我们就可以利用更强大的 Request 类来构建一个请求。
首先我们用一个实例来感受一下 Request 的用法:
import urllib.request
request = urllib.request.Request('https://python.org')
response = urllib.request.urlopen(request) *
response = urllib.request.urlopen('http://httpbin.org/get')
print(response.read().decode('utf-8'))
可以发现,我们依然是用 urlopen() 方法来发送这个请求,只不过这次 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 参数是一个字典,这个就是 Request Headers 了,你可以在构造 Request 时通过 headers 参数直接构造,也可以通过调用 Request 实例的 add_header() 方法来添加。
添加 Request Headers 最常用的用法就是通过修改 User-Agent 来伪装浏览器,默认的 User-Agent 是 Python-urllib,我们可以通过修改它来伪装浏览器,比如要伪装火狐浏览器,你可以把它设置为:
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 = 'http://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='utf8') req = request.Request(url=url, data=data, headers=headers, method='POST') response = request.urlopen(req) print(response.read().decode('utf-8'))
在这里我们通过四个参数构造了一个 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)" }, "json": null, "origin": "219.224.169.11", "url": "http://httpbin.org/post" }
通过观察结果可以发现,我们成功设置了 data,headers 以及 method。
另外 headers 也可以用 add_header() 方法来添加。
req = request.Request(url=url, data=data, method='POST')
req.add_header('User-Agent', 'Mozilla/4.0 (compatible; MSIE 5.5; Windows NT)')
如此一来,我们就可以更加方便地构造一个 Request,实现请求的发送啦。
3. 高级用法
有没有发现,在上面的过程中,我们虽然可以构造 Request,但是一些更高级的操作,比如 Cookies 处理,代理设置等操作我们该怎么办?
接下来就需要更强大的工具 Handler 登场了。
简而言之我们可以把它理解为各种处理器,有专门处理登录验证的,有处理 Cookies 的,有处理代理设置的,利用它们我们几乎可以做到任何 HTTP 请求中所有的事情。
首先介绍下 urllib.request 模块里的 BaseHandler类,它是所有其他 Handler 的父类,它提供了最基本的 Handler 的方法,例如 default_open()、protocol_request() 方法等。
接下来就有各种 Handler 子类继承这个 BaseHandler 类,举例几个如下:
- HTTPDefaultErrorHandler 用于处理 HTTP 响应错误,错误都会抛出 HTTPError 类型的异常。
- HTTPRedirectHandler 用于处理重定向。
- HTTPCookieProcessor 用于处理 Cookies。
- ProxyHandler 用于设置代理,默认代理为空。
- HTTPPasswordMgr 用于管理密码,它维护了用户名密码的表。
- HTTPBasicAuthHandler 用于管理认证,如果一个链接打开时需要认证,那么可以用它来解决认证问题。
- 另外还有其他的 Handler 类,在这不一一列举了,详情可以参考官方文档: https://docs.python.org/3/library/urllib.request.html#urllib.request.BaseHandler
它们怎么来使用,不用着急,下面会有实例为你演示。
另外一个比较重要的类就是 OpenerDirector,我们可以称之为 Opener,我们之前用过 urlopen() 这个方法,实际上它就是 Urllib为我们提供的一个 Opener。
那么为什么要引入 Opener 呢?因为我们需要实现更高级的功能,之前我们使用的 Request、urlopen() 相当于类库为你封装好了极其常用的请求方法,利用它们两个我们就可以完成基本的请求,但是现在不一样了,我们需要实现更高级的功能,所以我们需要深入一层进行配置,使用更底层的实例来完成我们的操作。
所以,在这里我们就用到了比调用 urlopen() 的对象的更普遍的对象,也就是 Opener。
Opener 可以使用 open() 方法,返回的类型和 urlopen() 如出一辙。那么它和 Handler 有什么关系?简而言之,就是利用 Handler 来构建 Opener。
下面我们用几个实例来感受一下他们的用法:
认证
有些网站在打开时它就弹出了一个框,直接提示你输入用户名和密码,认证成功之后才能查看页面,如图 3-2 所示:
图 3-2 认证页面
那么我们如果要请求这样的页面怎么办呢?
借助于 HTTPBasicAuthHandler 就可以完成,代码如下:
from urllib.request import HTTPPasswordMgrWithDefaultRealm, HTTPBasicAuthHandler, build_opener
from urllib.error import URLError username = 'username' password = 'password' url = 'http://localhost:5000/' 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 对象,参数是 HTTPPasswordMgrWithDefaultRealm 对象,它利用 add_password() 添加进去用户名和密码,这样我们就建立了一个处理认证的 Handler。
接下来利用 build_opener() 方法来利用这个 Handler 构建一个 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:9743', 'https': 'https://127.0.0.1:9743' }) 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)
在此本地搭建了一个代理,运行在 9743 端口上。
在这里使用了 ProxyHandler,ProxyHandler 的参数是一个字典,键名是协议类型,比如 HTTP 还是 HTTPS 等,键值是代理链接,可以添加多个代理。
然后利用 build_opener() 方法利用这个 Handler 构造一个 Opener,然后发送请求即可。
Cookies
Cookies 的处理就需要 Cookies 相关的 Handler 了。
我们先用一个实例来感受一下怎样将网站的 Cookies 获取下来,代码如下:
import http.cookiejar, urllib.request
cookie = http.cookiejar.CookieJar()
handler = urllib.request.HTTPCookieProcessor(cookie)
opener = urllib.request.build_opener(handler)
response = opener.open('http://www.baidu.com')
for item in cookie: print(item.name+"="+item.value)
首先我们必须声明一个 CookieJar 对象,接下来我们就需要利用 HTTPCookieProcessor 来构建一个 Handler,最后利用 build_opener() 方法构建出 Opener,执行 open() 函数即可。
运行结果如下:
BAIDUID=2E65A683F8A8BA3DF521469DF8EFF1E1:FG=1
BIDUPSID=2E65A683F8A8BA3DF521469DF8EFF1E1
H_PS_PSSID=20987_1421_18282_17949_21122_17001_21227_21189_21161_20927
PSTM=1474900615
BDSVRTM=0
BD_HOME=0
可以看到输出了每一条 Cookie 的名称还有值。
不过既然能输出,那可不可以输出成文件格式呢?我们知道 Cookies 实际也是以文本形式保存的。
答案当然是肯定的,我们用下面的实例来感受一下:
filename = 'cookies.txt'
cookie = http.cookiejar.MozillaCookieJar(filename)
handler = urllib.request.HTTPCookieProcessor(cookie)
opener = urllib.request.build_opener(handler)
response = opener.open('http://www.baidu.com')
cookie.save(ignore_discard=True, ignore_expires=True)
这时的 CookieJar就需要换成 MozillaCookieJar,生成文件时需要用到它,它是 CookieJar 的子类,可以用来处理 Cookies 和文件相关的事件,读取和保存 Cookies,它可以将 Cookies 保存成 Mozilla 型浏览器的 Cookies 的格式。
运行之后可以发现生成了一个 cookies.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 3622386254 BAIDUID 05AE39B5F56C1DEC474325CDA522D44F:FG=1
.baidu.com TRUE / FALSE 3622386254 BIDUPSID 05AE39B5F56C1DEC474325CDA522D44F
.baidu.com TRUE / FALSE H_PS_PSSID 19638_1453_17710_18240_21091_18560_17001_21191_21161
.baidu.com TRUE / FALSE 3622386254 PSTM 1474902606
www.baidu.com FALSE / FALSE BDSVRTM 0
www.baidu.com FALSE / FALSE BD_HOME 0
另外还有一个 LWPCookieJar,同样可以读取和保存 Cookies,但是保存的格式和 MozillaCookieJar 的不一样,它会保存成与 libwww-perl(LWP) 的 Cookies 文件格式。
要保存成 LWP 格式的 Cookies 文件,可以在声明时就改为:
cookie = http.cookiejar.LWPCookieJar(filename)
生成的内容如下:
#LWP-Cookies-2.0
Set-Cookie3: BAIDUID="0CE9C56F598E69DB375B7C294AE5C591:FG=1"; path="/"; domain=".baidu.com"; path_spec; domain_dot; expires="2084-10-14 18:25:19Z"; version=0
Set-Cookie3: BIDUPSID=0CE9C56F598E69DB375B7C294AE5C591; path="/"; domain=".baidu.com"; path_spec; domain_dot; expires="2084-10-14 18:25:19Z"; version=0
Set-Cookie3: H_PS_PSSID=20048_1448_18240_17944_21089_21192_21161_20929; path="/"; domain=".baidu.com"; path_spec; domain_dot; discard; version=0
Set-Cookie3: PSTM=1474902671; path="/"; domain=".baidu.com"; path_spec; domain_dot; expires="2084-10-14 18:25:19Z"; version=0
Set-Cookie3: BDSVRTM=0; path="/"; domain="www.baidu.com"; path_spec; discard; version=0
Set-Cookie3: BD_HOME=0; path="/"; domain="www.baidu.com"; path_spec; discard; version=0
由此看来生成的格式还是有比较大的差异的。
那么生成了 Cookies 文件,怎样从文件读取并利用呢?
下面我们以 LWPCookieJar 格式为例来感受一下:
cookie = http.cookiejar.LWPCookieJar()
cookie.load('cookies.txt', ignore_discard=True, ignore_expires=True)
handler = urllib.request.HTTPCookieProcessor(cookie)
opener = urllib.request.build_opener(handler)
response = opener.open('http://www.baidu.com') print(response.read().decode('utf-8'))
可以看到我们这里调用了 load() 方法来读取本地的 Coookis 文件,获取到了 Cookies 的内容。不过前提是我们首先利用生成了 LWPCookieJar 格式的 Cookies,获取到 Cookies 之后,后面同样的方法构建 Handler 和 Opener 即可。
运行结果正常输出百度网页的源代码。
好,通过如上用法,我们可以实现绝大多数请求功能的设置了。
4. 结语
以上便是 Urllib 库中 request 模块的基本用法,如果有更多想实现的功能,可以参考官方文档的说明:https://docs.python.org/3/library/urllib.request.html#basehandler-objects。
3.1.2 处理异常
在前面一节我们了解了 Request 的发送过程,但是在网络情况不好的情况下,出现了异常怎么办呢?这时如果我们不处理这些异常,程序很可能报错而终止运行,所以异常处理还是十分有必要的。
Urllib 的 error 模块定义了由 request 模块产生的异常。如果出现了问题,request 模块便会抛出 error 模块中定义的异常,本节会对其进行详细的介绍。
1. URLError
URLError 类来自 Urllib 库的 error 模块,它继承自 OSError 类,是 error 异常模块的基类,由 request 模块生的异常都可以通过捕获这个类来处理。
它具有一个属性 reason,即返回错误的原因。
下面用一个实例来感受一下:
from urllib import request, error
try:
response = request.urlopen('http://cuiqingcai.com/index.htm') except error.URLError as e: print(e.reason)
我们打开一个不存在的页面,照理来说应该会报错,但是这时我们捕获了 URLError 这个异常,运行结果如下:
Not Found
程序没有直接报错,而是输出了如上内容,这样通过如上操作,我们就可以避免程序异常终止,同时异常得到了有效处理。
2. HTTPError
它是 URLError 的子类,专门用来处理 HTTP 请求错误,比如认证请求失败等等。
它有三个属性。
- code,返回 HTTP Status Code,即状态码,比如 404 网页不存在,500 服务器内部错误等等。
- reason,同父类一样,返回错误的原因。
- headers,返回 Request Headers。
下面我们来用几个实例感受一下:
from urllib import request,error
try:
response = request.urlopen('http://cuiqingcai.com/index.htm') except error.HTTPError as e: print(e.reason, e.code, e.headers, seq='\n')
运行结果:
Not Found
404
Server: nginx/1.4.6 (Ubuntu)
Date: Wed, 03 Aug 2016 08:54:22 GMT
Content-Type: text/html; charset=UTF-8
Transfer-Encoding: chunked
Connection: close
X-Powered-By: PHP/5.5.9-1ubuntu4.14
Vary: Cookie
Expires: Wed, 11 Jan 1984 05:00:00 GMT
Cache-Control: no-cache, must-revalidate, max-age=0
Pragma: no-cache
Link: <http://cuiqingcai.com/wp-json/>; rel="https://api.w.org/"
依然是同样的网址,在这里我们捕获了 HTTPError 异常,输出了 reason、code、headers 属性。
因为 URLError 是 HTTPError 的父类,所以我们可以先选择捕获子类的错误,再去捕获父类的错误,所以上述代码更好的写法如下:
from urllib import request, error
try:
response = request.urlopen('http://cuiqingcai.com/index.htm') 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,获取它的错误状态码、原因、Headers 等详细信息。如果非 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 'socket.timeout'>
TIME OUT
可以发现 reason 属性的结果是 socket.timeout 类。所以在这里我们可以用 isinstance() 方法来判断它的类型,做出更详细的异常判断。
3. 结语
本节讲述了 error 模块的相关用法,通过合理地捕获异常可以做出更准确的异常判断,使得程序更佳稳健。
3.1.3 解析链接
Urllib 库里还提供了 parse 这个模块,它定义了处理 URL 的标准接口,例如实现 URL 各部分的抽取,合并以及链接转换。它支持如下协议的 URL 处理:file、ftp、gopher、hdl、http、https、imap、mailto、 mms、news、nntp、prospero、rsync、rtsp、rtspu、sftp、shttp、 sip、sips、snews、svn、svn+ssh、telnet、wais,本节我们介绍一下该模块中常用的方法来感受一下它的便捷之处。
1. urlparse()
urlparse() 方法可以实现 URL 的识别和分段,我们先用一个实例来感受一下:
from urllib.parse import urlparse
result = urlparse('http://www.baidu.com/index.html;user?id=5#comment')
print(type(result), result)
在这里我们利用了 urlparse() 方法进行了一个 URL 的解析,首先输出了解析结果的类型,然后将结果也输出出来。
运行结果:
<class 'urllib.parse.ParseResult'>
ParseResult(scheme='http', netloc='www.baidu.com', path='/index.html', params='user', query='id=5', fragment='comment')
观察可以看到,返回结果是一个 ParseResult 类型的对象,它包含了六个部分,分别是 scheme、netloc、path、params、query、fragment。
观察一下实例的URL:
http://www.baidu.com/index.html;user?id=5#comment
urlparse() 方法将其拆分成了六部分,大体观察可以发现,解析时有特定的分隔符,比如 :// 前面的就是 scheme,代表协议,第一个 / 前面便是 netloc,即域名,分号 ;后面是 params,代表参数。
所以可以得出一个标准的链接格式如下:
scheme://netloc/path;parameters?query#fragment
一个标准的 URL 都会符合这个规则,利用 urlparse() 方法我们可以将它解析拆分开来。
除了这种最基本的解析方式,urlopen() 方法还有其他配置吗?接下来看一下它的 API 用法:
urllib.parse.urlparse(urlstring, scheme='', allow_fragments=True)
可以看到它有三个参数:
-
- urlstring,是必填项,即待解析的 URL。
- scheme,是默认的协议(比如http、https等),假如这个链接没有带协议信息,会将这个作为默认的协议。
我们用一个实例感受一下:
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 信息,但是通过指定默认的 scheme 参数,返回的结果是 https。
假设我们带上了 scheme 呢?
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 中不包含 scheme 信息时才会生效,如果 URL 中有 scheme 信息,那就返回解析出的 scheme。
- allow_fragments,即是否忽略 fragment,如果它被设置为 False,fragment 部分就会被忽略,它会被解析为 path、parameters 或者 query 的一部分,fragment 部分为空。
下面我们用一个实例感受一下:
from urllib.parse import urlparse
result = urlparse('http://www.baidu.com/index.html;user?id=5#comment', allow_fragments=False) print(result)
运行结果:
ParseResult(scheme='http', netloc='www.baidu.com', path='/index.html', params='user', query='id=5#comment', fragment='')
假设 URL 中不包含 parameters 和 query 呢?
再来一个实例看下:
from urllib.parse import urlparse
result = urlparse('http://www.baidu.com/index.html#comment', allow_fragments=False) print(result)
运行结果:
ParseResult(scheme='http', netloc='www.baidu.com', path='/index.html#comment', params='', query='', fragment='')
可以发现当 URL 中不包含 params 和 query 时, fragment 便会被解析为 path 的一部分。
返回结果 ParseResult 实际上是一个元组,我们可以用索引顺序来获取,也可以用属性名称获取,实例如下:
from urllib.parse import urlparse
result = urlparse('http://www.baidu.com/index.html#comment', allow_fragments=False) print(result.scheme, result[0], result.netloc, result[1], sep='\n')
在这里我们分别用索引和属性名获取了 scheme 和 netloc,运行结果如下:
http
http
www.baidu.com
www.baidu.com
可以发现二者结果是一致的,两种方法都可以成功获取。
2. urlunparse()
有了 urlparse() 那相应地就有了它的对立方法 urlunparse()。
它接受的参数是一个可迭代对象,但是它的长度必须是 6,否则会抛出参数数量不足或者过多的问题。
先用一个实例感受一下:
from urllib.parse import urlunparse
data = ['http', 'www.baidu.com', 'index.html', 'user', 'a=6', 'comment'] print(urlunparse(data))
参数 data 用了列表类型,当然你也可以用其他的类型如元组或者特定的数据结构。
运行结果如下:
http://www.baidu.com/index.html;user?a=6#comment
这样我们就成功实现了 URL 的构造。
3. urlsplit()
这个和 urlparse() 方法非常相似,只不过它不会单独解析 parameters 这一部分,只返回五个结果。上面例子中的 parameters 会合并到 path中,用一个实例感受一下:
from urllib.parse import urlsplit
result = urlsplit('http://www.baidu.com/index.html;user?id=5#comment')
print(result)
运行结果:
SplitResult(scheme='http', netloc='www.baidu.com', path='/index.html;user', query='id=5', fragment='comment')
可以发现返回结果是 SplitResult,其实也是一个元组类型,可以用属性获取值也可以用索引来获取,实例如下:
from urllib.parse import urlsplit
result = urlsplit('http://www.baidu.com/index.html;user?id=5#comment')
print(result.scheme, result[0])
运行结果:
http http
4. urlunsplit()
与 urlunparse() 类似,也是将链接的各个部分组合成完整链接的方法,传入的也是一个可迭代对象,例如列表、元组等等,唯一的区别是,长度必须为 5。
用一个实例来感受一下:
from urllib.parse import urlunsplit
data = ['http', 'www.baidu.com', 'index.html', 'a=6', 'comment'] print(urlunsplit(data))
运行结果:
http://www.baidu.com/index.html?a=6#comment
同样可以完成链接的拼接生成。
5. urljoin()
有了 urlunparse() 和 urlunsplit() 方法,我们可以完成链接的合并,不过前提必须要有特定长度的对象,链接的每一部分都要清晰分开。
生成链接还有另一个方法,利用 urljoin() 方法我们可以提供一个 base_url(基础链接),新的链接作为第二个参数,方法会分析 base_url 的 scheme、netloc、path 这三个内容对新链接缺失的部分进行补充,作为结果返回。
我们用几个实例来感受一下:
from urllib.parse import urljoin
print(urljoin('http://www.baidu.com', 'FAQ.html')) print(urljoin('http://www.baidu.com', 'https://cuiqingcai.com/FAQ.html')) print(urljoin('http://www.baidu.com/about.html', 'https://cuiqingcai.com/FAQ.html')) print(urljoin('http://www.baidu.com/about.html', 'https://cuiqingcai.com/FAQ.html?question=2')) print(urljoin('http://www.baidu.com?wd=abc', 'https://cuiqingcai.com/index.php')) print(urljoin('http://www.baidu.com', '?category=2#comment')) print(urljoin('www.baidu.com', '?category=2#comment')) print(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
可以发现,base_url 提供了三项内容,scheme、netloc、path,如果这三项在新的链接里面不存在,那么就予以补充,如果新的链接存在,那么就使用新的链接的部分。base_url 中的 parameters、query、fragments 是不起作用的。
通过如上的函数,我们可以轻松地实现链接的解析,拼合与生成。
6. urlencode()
我们再介绍一个常用的 urlencode() 方法,它在构造 GET 请求参数的时候非常有用,我们用实例感受一下:
from urllib.parse import urlencode
params = {
'name': 'germey', 'age': 22 } base_url = 'http://www.baidu.com?' url = base_url + urlencode(params) print(url)
我们首先声明了一个字典,将参数表示出来,然后调用 urlencode() 方法将其序列化为 URL 标准 GET 请求参数。
运行结果:
http://www.baidu.com?name=germey&age=22
可以看到参数就成功由字典类型转化为 GET 请求参数了。
这个方法非常常用,有时为了更加方便地构造参数,我们会事先用字典来表示,要转化为 URL 的参数时只需要调用该方法即可。
7. parse_qs()
有了序列化必然就有反序列化,如果我们有一串 GET 请求参数,我们利用 parse_qs() 方法就可以将它转回字典,我们用一个实例感受一下:
from urllib.parse import parse_qs
query = 'name=germey&age=22'
print(parse_qs(query))
运行结果:
{'name': ['germey'], 'age': ['22']}
可以看到这样就成功转回为字典类型了。
8. parse_qsl()
另外还有一个 parse_qsl() 方法可以将参数转化为元组组成的列表,实例如下:
from urllib.parse import parse_qsl
query = 'name=germey&age=22'
print(parse_qsl(query))
运行结果:
[('name', 'germey'), ('age', '22')]
可以看到运行结果是一个列表,列表的每一个元素都是一个元组,元组的第一个内容是参数名,第二个内容是参数值。
9. quote()
quote() 方法可以将内容转化为 URL 编码的格式,有时候 URL 中带有中文参数的时候可能导致乱码的问题,所以我们可以用这个方法将中文字符转化为 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
这样我们就可以成功实现URL编码的转换。
10. 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 编码后的结果,我们在这里利用 unquote() 方法进行还原,结果如下:
https://www.baidu.com/s?wd=壁纸
可以看到利用 unquote() 方法可以方便地实现解码。
11. 结语
本节介绍了 parse 模块的一些常用 URL 处理方法,有了这些方法我们可以方便地实现 URL 的解析和构造,建议熟练掌握。
3.1.4 分析Robots协议
利用 Urllib 的 robotparser 模块我们可以实现网站 Robots 协议的分析,本节我们来简单了解一下它的用法。
1. Robots协议
Robots 协议也被称作爬虫协议、机器人协议,它的全名叫做网络爬虫排除标准(Robots Exclusion Protocol),用来告诉爬虫和搜索引擎哪些页面可以抓取,哪些不可以抓取。它通常是一个叫做 robots.txt 的文本文件,放在网站的根目录下。
当搜索爬虫访问一个站点时,它首先会检查下这个站点根目录下是否存在 robots.txt 文件,如果存在,搜索爬虫会根据其中定义的爬取范围来爬取。如果没有找到这个文件,那么搜索爬虫便会访问所有可直接访问的页面。(那是否可用robots.txt来反爬虫?)
下面我们看一个 robots.txt 的样例:
User-agent: *
Disallow: /
Allow: /public/
以上的两行实现了对所有搜索爬虫只允许爬取 public目录的作用。
如上简单的两行,保存成 robots.txt 文件,放在网站的根目录下,和网站的入口文件放在一起。比如 index.php、index.html、index.jsp 等等。
那么上面的 User-agent 就描述了搜索爬虫的名称,在这里将值设置为 *,则代表该协议对任何的爬取爬虫有效。比如我们可以设置:
User-agent: Baiduspider
这就代表我们设置的规则对百度爬虫是有效的。如果有多条 User-agent 记录,则就会有多个爬虫会受到爬取限制,但至少需要指定一条。
Disallow 指定了不允许抓取的目录,比如上述例子中设置为/则代表不允许抓取所有页面。
Allow 一般和 Disallow 一起使用,一般不会单独使用,用来排除某些限制,现在我们设置为 /public/ ,起到的作用是所有页面不允许抓取,但是 public 目录是可以抓取的。
下面我们再来看几个例子感受一下:
禁止所有爬虫访问任何目录
User-agent: *
Disallow: /
允许所有爬虫访问任何目录
User-agent: *
Disallow:
或者直接把 robots.txt 文件留空也是可以的。
禁止所有爬虫访问网站某些目录
User-agent: *
Disallow: /private/
Disallow: /tmp/
只允许某一个爬虫访问
User-agent: WebCrawler
Disallow:
User-agent: *
Disallow: /
以上是 robots.txt 的一些常见写法。
2. 爬虫名称
大家可能会疑惑,爬虫名是哪儿来的?为什么就叫这个名?其实它是有固定名字的了,比如百度的就叫做 BaiduSpider,下面的表格列出了一些常见的搜索爬虫的名称及对应的网站:
爬虫名称 | 名称 | 网站 |
---|---|---|
BaiduSpider | 百度 | www.baidu.com |
Googlebot | 谷歌 | www.google.com |
360Spider | 360搜索 | www.so.com |
YodaoBot | 有道 | www.youdao.com |
ia_archiver | Alexa | www.alexa.cn |
Scooter | altavista | www.altavista.com |
3. robotparser
了解了什么是 Robots 协议之后,我们就可以使用 robotparser 模块来解析 robots.txt 了。
robotparser 模块提供了一个类,叫做 RobotFileParser。它可以根据某网站的 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
rp = RobotFileParser()
rp.set_url('http://www.jianshu.com/robots.txt')
rp.read()
print(rp.can_fetch('*', 'http://www.jianshu.com/p/b67554025d7d')) print(rp.can_fetch('*', "http://www.jianshu.com/search?q=python&page=1&type=collections"))
以简书为例,我们首先创建 RobotFileParser 对象,然后通过 set_url() 方法来设置了 robots.txt 的链接。当然不用这个方法的话,可以在声明时直接用如下方法设置:
rp = RobotFileParser('http://www.jianshu.com/robots.txt')
下一步利用了 can_fetch() 方法来判断了网页是否可以被抓取。
运行结果:
True
False
同样也可以使用 parser() 方法执行读取和分析。
用一个实例感受一下:
from urllib.robotparser import RobotFileParser
from urllib.request import urlopen rp = RobotFileParser() rp.parse(urlopen('http://www.jianshu.com/robots.txt').read().decode('utf-8').split('\n')) print(rp.can_fetch('*', 'http://www.jianshu.com/p/b67554025d7d')) print(rp.can_fetch('*', "http://www.jianshu.com/search?q=python&page=1&type=collections"))
运行结果一样:
True
False
4. 结语
以上介绍了 robotparser 模块的基本用法和实例讲解,利用它我们就可以方便地判断哪些页面可以抓取哪些不可以了。
3.2 使用requests
在前面一节我们了解了 Urllib 的基本用法,但是其中确实有不方便的地方。比如处理网页验证、处理 Cookies 等等,需要写 Opener、Handler 来进行处理。为了更加方便地实现这些操作,在这里就有了更为强大的库 Requests,有了它,Cookies、登录验证、代理设置等等的操作都不是事儿。
那么接下来就让我们来领略一下它的强大之处吧。
3.2.1 基本使用
本节我们首先来了解下 Requests 库的基本使用方法。
1. 准备工作
在本节开始之前请确保已经正确安装好了 Requests 库,如果没有安装可以参考第一章的安装说明。
2. 实例引入
在 Urllib 库中有 urlopen() 的方法,实际上它是以 GET 方式请求了一个网页。
那么在 Requests 中,相应的方法就是 get() 方法,是不是感觉表达更明确一些?
下面我们用一个实例来感受一下:
import requests
r = requests.get('https://www.baidu.com/')
print(type(r))
print(r.status_code)
print(type(r.text))
print(r.text)
print(r.cookies)
运行结果如下:
<class 'requests.models.Response'>
200
<class 'str'>
<html>
<head>
<script>
location.replace(location.href.replace("https://","http://"));
</script>
</head>
<body>
<noscript><meta http-equiv="refresh" content="0;url=http://www.baidu.com/"></noscript>
</body>
</html>
<RequestsCookieJar[<Cookie BIDUPSID=992C3B26F4C4D09505C5E959D5FBC005 for .baidu.com/>, <Cookie PSTM=1472227535 for .baidu.com/>, <Cookie __bsi=15304754498609545148_00_40_N_N_2_0303_C02F_N_N_N_0 for .www.baidu.com/>, <Cookie BD_NOT_HTTPS=1 for www.baidu.com/>]>
上面的例子中我们调用 get() 方法即可实现和 urlopen() 相同的操作,得到一个 Response 对象,然后分别输出了 Response 的类型,Status Code,Response Body 的类型、内容还有 Cookies。
通过上述实例可以发现,它的返回类型是 requests.models.Response,Response Body 的类型是字符串 str,Cookies 的类型是 RequestsCookieJar。
使用了 get() 方法就成功实现了一个 GET 请求,但这倒不算什么,更方便的在于其他的请求类型依然可以用一句话来完成。
用一个实例来感受一下:
r = requests.post('http://httpbin.org/post')
r = requests.put('http://httpbin.org/put')
r = requests.delete('http://httpbin.org/delete')
r = requests.head('http://httpbin.org/get') r = requests.options('http://httpbin.org/get')
在这里分别用 post()、put()、delete() 等方法实现了 POST、PUT、DELETE 等请求,怎么样?是不是比 Urllib 简单太多了?
其实这只是冰山一角,更多的还在后面。
3. GET请求
HTTP 中最常见的请求之一就是 GET 请求,我们首先来详细了解下利用 Requests 来构建 GET 请求的方法以及相关属性方法操作。
基本实例
首先让我们来构建一个最简单的 GET 请求,请求的链接为:http://httpbin.org/get,它会判断如果如果是 GET 请求的话,会返回响应的 Request 信息。
import requests
r = requests.get('http://httpbin.org/get')
print(r.text)
运行结果如下:
{
"args": {},
"headers": {
"Accept": "*/*", "Accept-Encoding": "gzip, deflate", "Host": "httpbin.org", "User-Agent": "python-requests/2.10.0" }, "origin": "122.4.215.33", "url": "http://httpbin.org/get" }
可以发现我们成功发起了 GET 请求,返回的结果中包含了 Request Headers、URL、IP 等信息。
那么 GET 请求,如果要附加额外的信息一般是怎样来添加?比如现在我想添加两个参数,名字 name 是 germey,年龄 age 是 22。构造这个请求链接是不是我们要直接写成:
r = requests.get('http://httpbin.org/get?name=germey&age=22')
可以是可以,但是不觉得很不人性化吗?一般的这种信息数据我们会用字典来存储,那么怎样来构造这个链接呢?
同样很简单,利用 params 这个参数就好了。
实例如下:
import requests
data = {
'name': 'germey',
'age': 22 } r = requests.get("http://httpbin.org/get", params=data) print(r.text)
运行结果如下:
{
"args": {
"age": "22",
"name": "germey" }, "headers": { "Accept": "*/*", "Accept-Encoding": "gzip, deflate", "Host": "httpbin.org", "User-Agent": "python-requests/2.10.0" }, "origin": "122.4.215.33", "url": "http://httpbin.org/get?age=22&name=germey" }
通过返回信息我们可以判断,请求的链接自动被构造成了:http://httpbin.org/get?age=22&name=germey。
另外,网页的返回类型实际上是 str 类型,但是它很特殊,是 Json 的格式,所以如果我们想直接把返回结果解析,得到一个字典格式的话,可以直接调用 json() 方法。
用一个实例来感受一下:
import requests
r = requests.get("http://httpbin.org/get")
print(type(r.text))
print(r.json())
print(type(r.json()))
运行结果如下:
<class 'str'>
{'headers': {'Accept-Encoding': 'gzip, deflate', 'Accept': '*/*', 'Host': 'httpbin.org', 'User-Agent': 'python-requests/2.10.0'}, 'url': 'http://httpbin.org/get', 'args': {}, 'origin': '182.33.248.131'}
<class 'dict'>
可以发现,调用 json() 方法,就可以将返回结果是 Json 格式的字符串转化为字典。
但注意,如果返回结果不是 Json 格式,便会出现解析错误,抛出 json.decoder.JSONDecodeError 的异常。
抓取网页
如上的请求链接返回的是 Json 形式的字符串,那么如果我们请求普通的网页,那么肯定就能获得相应的内容了。
下面我们以知乎-发现页面为例来感受一下:
import requests
import re
headers = {
'User-Agent': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_11_4) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/52.0.2743.116 Safari/537.36' } r = requests.get("https://www.zhihu.com/explore", headers=headers) pattern = re.compile('explore-feed.*?question_link.*?>(.*?)</a>', re.S) titles = re.findall(pattern, r.text) print(titles)
如上代码,我们请求了知乎-发现页面:https://www.zhihu.com/explore,在这里加入了 Headers 信息,其中包含了 User-Agent 字段信息,也就是浏览器标识信息。如果不加这个,知乎会禁止抓取。
在接下来用到了最基础的正则表达式,来匹配出所有的问题内容,关于正则表达式会在后面的章节中详细介绍,在这里作为用到实例来配合讲解。
运行结果如下:
['\n为什么很多人喜欢提及「拉丁语系」这个词?\n', '\n在没有水的情况下水系宝可梦如何战斗?\n', '\n有哪些经验可以送给 Kindle 新人?\n', '\n谷歌的广告业务是如何赚钱的?\n', '\n程序员该学习什么,能在上学期间挣钱?\n', '\n有哪些原本只是一个小消息,但回看发现是个惊天大新闻的例子?\n', '\n如何评价今敏?\n', '\n源氏是怎么把那么长的刀从背后拔出来的?\n', '\n年轻时得了绝症或大病是怎样的感受?\n', '\n年轻时得了绝症或大病是怎样的感受?\n']
发现成功提取出了所有的问题内容。
抓取二进制数据
在上面的例子中,我们抓取的是知乎的一个页面,实际上它返回的是一个 HTML 文档,那么如果我们想抓去图片、音频、视频等文件的话应该怎么办呢?
我们都知道,图片、音频、视频这些文件都是本质上由二进制码组成的,由于有特定的保存格式和对应的解析方式,我们才可以看到这些形形色色的多媒体。所以想要抓取他们,那就需要拿到他们的二进制码。
下面我们以 GitHub 的站点图标为例来感受一下:
import requests
r = requests.get("https://github.com/favicon.ico")
print(r.text)
print(r.content)
抓取的内容是站点图标,也就是在浏览器每一个标签上显示的小图标,如图 3-3 所示:
图 3-3 站点图标
在这里打印了 Response 对象的两个属性,一个是text,另一个是 content。
运行结果如下,由于包含特殊内容,在此放运行结果的图片,如图 3-4 所示:
图 3-4 运行结果
那么前两行便是 r.text 的结果,最后一行是 r.content 的结果。
可以注意到,前者出现了乱码,后者结果前面带有一个 b,代表这是 bytes 类型的数据。由于图片是二进制数据,所以前者在打印时转化为 str 类型,也就是图片直接转化为字符串,理所当然会出现乱码。
两个属性有什么区别?前者返回的是字符串类型,如果返回结果是文本文件,那么用这种方式直接获取其内容即可。如果返回结果是图片、音频、视频等文件,Requests 会为我们自动解码成 bytes 类型,即获取字节流数据。
进一步地,我们可以将刚才提取到的图片保存下来。
import requests
r = requests.get("https://github.com/favicon.ico")
with open('favicon.ico', 'wb') as f: f.write(r.content)
在这里用了 open() 方法,第一个参数是文件名称,第二个参数代表以二进制写的形式打开,可以向文件里写入二进制数据,然后保存。
运行结束之后,可以发现在文件夹中出现了名为 favicon.ico 的图标,如图 3-5所示:
图 3-5 图标
同样的,音频、视频文件也可以用这种方法获取。
添加Headers
如 urllib.request 一样,我们也可以通过 headers 参数来传递头信息。
比如上面的知乎的例子,如果不传递 Headers,就不能正常请求:
import requests
r = requests.get("https://www.zhihu.com/explore")
print(r.text)
运行结果如下:
<html><body><h1>500 Server Error</h1> An internal server error occured. </body></html>
但如果加上 Headers 中加上 User-Agent 信息,那就没问题了:
import requests
headers = {
'User-Agent': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_11_4) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/52.0.2743.116 Safari/537.36'
}
r = requests.get("https://www.zhihu.com/explore", headers=headers) print(r.text)
当然我们可以在 headers 这个参数中任意添加其他的字段信息。
4. POST请求
在前面我们了解了最基本的 GET 请求,另外一种比较常见的请求方式就是 POST 了,就像模拟表单提交一样,将一些数据提交到某个链接。
使用 Request 是实现 POST 请求同样非常简单。
我们先用一个实例来感受一下:
import requests
data = {'name': 'germey', 'age': '22'} r = requests.post("http://httpbin.org/post", data=data) print(r.text)
这里我们还是请求:http://httpbin.org/post,它可以判断如果请求是 POST 方式,就把相关请求信息输出出来。
运行结果如下:
{
"args": {},
"data": "",
"files": {},
"form": {
"age": "22",
"name": "germey"
},
"headers": {
"Accept": "*/*",
"Accept-Encoding": "gzip, deflate",
"Content-Length": "18",
"Content-Type": "application/x-www-form-urlencoded",
"Host": "httpbin.org",
"User-Agent": "python-requests/2.10.0"
},
"json": null,
"origin": "182.33.248.131",
"url": "http://httpbin.org/post"
}
可以发现,成功获得了返回结果,返回结果中的 form 部分就是提交的数据,那么这就证明 POST 请求成功发送了。
5. Response
发送 Request 之后,得到的自然就是 Response,在上面的实例中我们使用了 text 和 content 获取了 Response 内容,不过还有很多属性和方法可以获取其他的信息,比如状态码 Status Code、Headers、Cookies 等信息。
下面用一个实例来感受一下:
import requests
r = requests.get('http://www.jianshu.com')
print(type(r.status_code), r.status_code)
print(type(r.headers), r.headers)
print(type(r.cookies), r.cookies)
print(type(r.url), r.url)
print(type(r.history), r.history)
在这里分别打印输出了 status_code 属性得到状态码, headers 属性得到 Response Headers,cookies 属性得到 Cookies,url 属性得到 URL,history 属性得到请求历史。
运行结果如下:
<class 'int'> 200
<class 'requests.structures.CaseInsensitiveDict'> {'X-Runtime': '0.006363', 'Connection': 'keep-alive', 'Content-Type': 'text/html; charset=utf-8', 'X-Content-Type-Options': 'nosniff', 'Date': 'Sat, 27 Aug 2016 17:18:51 GMT', 'Server': 'nginx', 'X-Frame-Options': 'DENY', 'Content-Encoding': 'gzip', 'Vary': 'Accept-Encoding', 'ETag': 'W/"3abda885e0e123bfde06d9b61e696159"', 'X-XSS-Protection': '1; mode=block', 'X-Request-Id': 'a8a3c4d5-f660-422f-8df9-49719dd9b5d4', 'Transfer-Encoding': 'chunked', 'Set-Cookie': 'read_mode=day; path=/, default_font=font2; path=/, _session_id=xxx; path=/; HttpOnly', 'Cache-Control': 'max-age=0, private, must-revalidate'}
<class 'requests.cookies.RequestsCookieJar'> <RequestsCookieJar[<Cookie _session_id=xxx for www.jianshu.com/>, <Cookie default_font=font2 for www.jianshu.com/>, <Cookie read_mode=day for www.jianshu.com/>]>
<class 'str'> http://www.jianshu.com/
<class 'list'> []
session_id 过长在此简写。可以看到,headers 还有 cookies 这两个属性得到的结果分别是 CaseInsensitiveDict 和 RequestsCookieJar 类型。
在这里 Status Code 常用来判断请求是否成功,Requests 还提供了一个内置的 Status Code 查询对象 requests.codes。
用一个实例来感受一下:
import requests
r = requests.get('http://www.jianshu.com')
exit() if not r.status_code == requests.codes.ok else print('Request Successfully')
在这里,通过比较返回码和内置的成功的返回码是一致的,来保证请求得到了正常响应,输出成功请求的消息,否则程序终止,在这里我们用 requests.codes.ok 得到的是成功的状态码 200。
那么肯定不能只有 ok 这个条件码,下面列出了返回码和相应的查询条件:
# Informational.
100: ('continue',),
101: ('switching_protocols',), 102: ('processing',), 103: ('checkpoint',), 122: ('uri_too_long', 'request_uri_too_long'), 200: ('ok', 'okay', 'all_ok', 'all_okay', 'all_good', '\\o/', '✓'), 201: ('created',), 202: ('accepted',), 203: ('non_authoritative_info', 'non_authoritative_information'), 204: ('no_content',), 205: ('reset_content', 'reset'), 206: ('partial_content', 'partial'), 207: ('multi_status', 'multiple_status', 'multi_stati', 'multiple_stati'), 208: ('already_reported',), 226: ('im_used',), # Redirection. 300: ('multiple_choices',), 301: ('moved_permanently', 'moved', '\\o-'), 302: ('found',), 303: ('see_other', 'other'), 304: ('not_modified',), 305: ('use_proxy',), 306: ('switch_proxy',), 307: ('temporary_redirect', 'temporary_moved', 'temporary'), 308: ('permanent_redirect', 'resume_incomplete', 'resume',), # These 2 to be removed in 3.0 # Client Error. 400: ('bad_request', 'bad'), 401: ('unauthorized',), 402: ('payment_required', 'payment'), 403: ('forbidden',), 404: ('not_found', '-o-'), 405: ('method_not_allowed', 'not_allowed'), 406: ('not_acceptable',), 407: ('proxy_authentication_required', 'proxy_auth', 'proxy_authentication'), 408: ('request_timeout', 'timeout'), 409: ('conflict',), 410: ('gone',), 411: ('length_required',), 412: ('precondition_failed', 'precondition'), 413: ('request_entity_too_large',), 414: ('request_uri_too_large',), 415: ('unsupported_media_type', 'unsupported_media', 'media_type'), 416: ('requested_range_not_satisfiable', 'requested_range', 'range_not_satisfiable'), 417: ('expectation_failed',), 418: ('im_a_teapot', 'teapot', 'i_am_a_teapot'), 421: ('misdirected_request',), 422: ('unprocessable_entity', 'unprocessable'), 423: ('locked',), 424: ('failed_dependency', 'dependency'), 425: ('unordered_collection', 'unordered'), 426: ('upgrade_required', 'upgrade'), 428: ('precondition_required', 'precondition'), 429: ('too_many_requests', 'too_many'), 431: ('header_fields_too_large', 'fields_too_large'), 444: ('no_response', 'none'), 449: ('retry_with', 'retry'), 450: ('blocked_by_windows_parental_controls', 'parental_controls'), 451: ('unavailable_for_legal_reasons', 'legal_reasons'), 499: ('client_closed_request',), # Server Error. 500: ('internal_server_error', 'server_error', '/o\\', '✗'), 501: ('not_implemented',), 502: ('bad_gateway',), 503: ('service_unavailable', 'unavailable'), 504: ('gateway_timeout',), 505: ('http_version_not_supported', 'http_version'), 506: ('variant_also_negotiates',), 507: ('insufficient_storage',), 509: ('bandwidth_limit_exceeded', 'bandwidth'), 510: ('not_extended',), 511: ('network_authentication_required', 'network_auth', 'network_authentication')
比如如果我们想判断结果是不是 404 状态,可以用 requests.codes.not_found 来比对。
6. 结语
本节我们了解了利用 Requests 模拟最基本的 GET 和 POST 请求的过程,关于更多高级的用法,会在下一节进行讲解。
3.2.3 高级用法
在前面一节我们了解了 Requests 的基本用法,如基本的 GET、POST 请求以及 Response 对象的用法,本节我们再来了解下 Requests 的一些高级用法,如文件上传,代理设置,Cookies 设置等等。
1. 文件上传
我们知道 Reqeuests 可以模拟提交一些数据,假如有的网站需要我们上传文件,我们同样可以利用它来上传,实现非常简单,实例如下:
import requests
files = {'file': open('favicon.ico', 'rb')} r = requests.post('http://httpbin.org/post', files=files) print(r.text)
在上面一节中我们下载保存了一个文件叫做 favicon.ico,这次我们用它为例来模拟文件上传的过程。需要注意的是,favicon.ico 这个文件需要和当前脚本在同一目录下。如果有其它文件,当然也可以使用其它文件来上传,更改下名称即可。
运行结果如下:
{
"args": {},
"data": "",
"files": { "file": "data:application/octet-stream;base64,AAAAAA...=" }, "form": {}, "headers": { "Accept": "*/*", "Accept-Encoding": "gzip, deflate", "Content-Length": "6665", "Content-Type": "multipart/form-data; boundary=809f80b1a2974132b133ade1a8e8e058", "Host": "httpbin.org", "User-Agent": "python-requests/2.10.0" }, "json": null, "origin": "60.207.237.16", "url": "http://httpbin.org/post" }
以上部分内容省略,这个网站会返回一个 Response,里面包含 files 这个字段,而 form 是空的,这证明文件上传部分会单独有一个 files 字段来标识。
2. Cookies
在前面我们使用了 Urllib 处理过 Cookies,写法比较复杂,而有了 Requests,获取和设置 Cookies 只需要一步即可完成。
我们先用一个实例感受一下获取 Cookies 的过程:
import requests
r = requests.get('https://www.baidu.com')
print(r.cookies)
for key, value in r.cookies.items(): print(key + '=' + value)
运行结果如下:
<RequestsCookieJar[<Cookie BDORZ=27315 for .baidu.com/>, <Cookie __bsi=13533594356813414194_00_14_N_N_2_0303_C02F_N_N_N_0 for .www.baidu.com/>]> BDORZ=27315 __bsi=13533594356813414194_00_14_N_N_2_0303_C02F_N_N_N_0
首先我们调用了 cookies 属性即可成功得到了 Cookies,可以发现它是一个 RequestCookieJar 类型,然后我们用 items() 方法将其转化为元组组成的列表,遍历输出每一个 Cookie 的名和值,实现 Cookies 的遍历解析。
当然,我们也可以直接用 Cookies 来维持登录状态。
比如我们以知乎为例,直接利用 Cookies 来维持登录状态。
首先登录知乎,将 Headers 中的 Cookies 复制下来,如图 3-6 所示:
图 3-6 Cookies
这里可以替换成你自己的 Cookies,将其设置到 Headers 里面,发送 Request,示例如下:
import requests
headers = {
'Cookie': 'q_c1=31653b264a074fc9a57816d1ea93ed8b|1474273938000|1474273938000; d_c0="AGDAs254kAqPTr6NW1U3XTLFzKhMPQ6H_nc=|1474273938"; __utmv=51854390.100-1|2=registration_date=20130902=1^3=entry_date=20130902=1;a_t="2.0AACAfbwdAAAXAAAAso0QWAAAgH28HQAAAGDAs254kAoXAAAAYQJVTQ4FCVgA360us8BAklzLYNEHUd6kmHtRQX5a6hiZxKCynnycerLQ3gIkoJLOCQ==";z_c0=Mi4wQUFDQWZid2RBQUFBWU1DemJuaVFDaGNBQUFCaEFsVk5EZ1VKV0FEZnJTNnp3RUNTWE10ZzBRZFIzcVNZZTFGQmZn|1474887858|64b4d4234a21de774c42c837fe0b672fdb5763b0',
'Host': 'www.zhihu.com', 'User-Agent': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_11_4) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/53.0.2785.116 Safari/537.36', } r = requests.get('https://www.zhihu.com', headers=headers) print(r.text)
发现结果中包含了登录后的结果,如图 3-7 所示:
图 3-7 运行结果
证明登录成功。
当然也可以通过 cookies 参数来设置,不过这样就需要构造 RequestsCookieJar 对象,而且需要分割一下 Cookies ,相对繁琐,不过效果是相同的,实例如下:
import requests
cookies = 'q_c1=31653b264a074fc9a57816d1ea93ed8b|1474273938000|1474273938000; d_c0="AGDAs254kAqPTr6NW1U3XTLFzKhMPQ6H_nc=|1474273938"; __utmv=51854390.100-1|2=registration_date=20130902=1^3=entry_date=20130902=1;a_t="2.0AACAfbwdAAAXAAAAso0QWAAAgH28HQAAAGDAs254kAoXAAAAYQJVTQ4FCVgA360us8BAklzLYNEHUd6kmHtRQX5a6hiZxKCynnycerLQ3gIkoJLOCQ==";z_c0=Mi4wQUFDQWZid2RBQUFBWU1DemJuaVFDaGNBQUFCaEFsVk5EZ1VKV0FEZnJTNnp3RUNTWE10ZzBRZFIzcVNZZTFGQmZn|1474887858|64b4d4234a21de774c42c837fe0b672fdb5763b0'
jar = requests.cookies.RequestsCookieJar()
headers = {
'Host': 'www.zhihu.com', 'User-Agent': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_11_4) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/53.0.2785.116 Safari/537.36' } for cookie in cookies.split(';'): key, value = cookie.split('=', 1) jar.set(key, value) r = requests.get('http://www.zhihu.com', cookies=jar, headers=headers) print(r.text)
上面我们首先新建了一个 RequestCookieJar 对象,然后将复制下来的 Cookies 利用 split() 方法分割,利用 set() 方法设置好每一个 Cookie 的 key 和 value,然后通过调用 Requests 的 get() 方法并传递给 cookies 参数即可,当然由于知乎本身的限制, headers 参数也不能少,只不过不需要在原来的 headers 参数里面设置 Cookie 字段了。
测试后,发现同样可以正常登录知乎。
3. 会话维持
在 Requests 中,我们如果直接利用 get() 或 post() 等方法的确可以做到模拟网页的请求。但是这实际上是相当于不同的会话,即不同的 Session,也就是说相当于你用了两个浏览器打开了不同的页面。
设想这样一个场景,我们第一个请求利用了 post() 方法登录了某个网站,第二次想获取成功登录后的自己的个人信息,你又用了一次 get() 方法去请求个人信息页面。实际上,这相当于打开了两个浏览器,是两个完全不相关的会话,能成功获取个人信息吗?那当然不能。
有小伙伴可能就说了,我在两次请求的时候都设置好一样的 Cookies 不就行了?可以,但这样做起来还是显得很繁琐,我们还有更简单的解决方法。
其实解决这个问题的主要方法就是维持同一个会话,也就是相当于打开一个新的浏览器选项卡而不是新开一个浏览器。但是我又不想每次设置 Cookies,那该怎么办?这时候就有了新的利器 Session对象。
利用它,我们可以方便地维护一个会话,而且不用担心 Cookies 的问题,它会帮我们自动处理好。
下面用一个实例来感受一下:
import requests
requests.get('http://httpbin.org/cookies/set/number/123456789')
r = requests.get('http://httpbin.org/cookies')
print(r.text)
在实例中我们请求了一个测试网址:http://httpbin.org/cookies/set/number/123456789,请求这个网址我们可以设置一个 Cookie,名称叫做 number,内容是 123456789,随后又请求了http://httpbin.org/cookies,此网址可以获取当前的 Cookies。
这样能成功获取到设置的 Cookies 吗?试试看。
运行结果如下:
{
"cookies": {}
}
并不行。我们再用 Session 试试看:
import requests
s = requests.Session()
s.get('http://httpbin.org/cookies/set/number/123456789')
r = s.get('http://httpbin.org/cookies')
print(r.text)
看下运行结果:
{
"cookies": {
"number": "123456789"
}
}
成功获取!这下能体会到同一个会话和不同会话的区别了吧?
所以,利用 Session 我们可以做到模拟同一个会话,而且不用担心 Cookies 的问题,通常用于模拟登录成功之后再进行下一步的操作。
Session 在平常用到的非常广泛,可以用于模拟在一个浏览器中打开同一站点的不同页面,在后文会有专门的章节来讲解这部分内容。
4. SSL证书验证
Requests 提供了证书验证的功能,当发送 HTTP 请求的时候,它会检查 SSL 证书,我们可以使用 verify 这个参数来控制是否检查此证书,其实如果不加的话默认是 True,会自动验证。
在前面我们提到过 12306 的证书实际上是不被官方认可的,会出现证书验证错误的结果,我们现在访问它都可以看到一个证书问题的页面,如图 3-8 所示:
图 3-8 错误页面
现在我们用 Requests 来测试一下:
import requests
response = requests.get('https://www.12306.cn')
print(response.status_code)
运行结果如下:
requests.exceptions.SSLError: ("bad handshake: Error([('SSL routines', 'tls_process_server_certificate', 'certificate verify failed')],)",)
提示一个错误,叫做 SSLError,证书验证错误。所以如果我们请求一个 HTTPS 站点,但是证书验证错误的页面时,就会报这样的错误,那么如何避免这个错误呢?很简单,把 verify 这个参数设置为 False 即可。
改成如下代码:
import requests
response = requests.get('https://www.12306.cn', verify=False)
print(response.status_code)
这样,就会打印出请求成功的状态码。
/usr/local/lib/python3.6/site-packages/urllib3/connectionpool.py:852: InsecureRequestWarning: Unverified HTTPS request is being made. Adding certificate verification is strongly advised. See: https://urllib3.readthedocs.io/en/latest/advanced-usage.html#ssl-warnings InsecureRequestWarning) 200
不过发现报了一个警告,它提示建议让我们给它指定证书。
我们可以通过设置忽略警告的方式来屏蔽这个警告:
import requests
from requests.packages import urllib3
urllib3.disable_warnings()
response = requests.get('https://www.12306.cn', verify=False) print(response.status_code)
或者通过捕获警告到日志的方式忽略警告:
import logging
import requests
logging.captureWarnings(True)
response = requests.get('https://www.12306.cn', verify=False) print(response.status_code)
当然我们也可以指定一个本地证书用作客户端证书,可以是单个文件(包含密钥和证书)或一个包含两个文件路径的元组。
import requests
response = requests.get('https://www.12306.cn', cert=('/path/server.crt', '/path/key')) print(response.status_code)
当然上面代码是实例,我们需要有 crt 和 key 文件,指定它们的路径。注意本地私有证书的 key 必须要是解密状态,加密状态的 key 是不支持的。
5. 代理设置
对于某些网站,在测试的时候请求几次,能正常获取内容。但是一旦开始大规模爬取,对于大规模且频繁的请求,网站可能会直接登录验证,验证码,甚至直接把IP给封禁掉。
那么为了防止这种情况的发生,我们就需要设置代理来解决这个问题,在 Requests 中需要用到 proxies 这个参数。
可以用这样的方式设置:
import requests
proxies = {
'http': 'http://10.10.1.10:3128',
'https': 'http://10.10.1.10:1080', } requests.get('https://www.taobao.com', proxies=proxies)
当然直接运行这个实例可能不行,因为这个代理可能是无效的,请换成自己的有效代理试验一下。
若代理需要使用 HTTP Basic Auth,可以使用类似 http://user:password@host:port 这样的语法来设置代理。
实例如下:
import requests
proxies = {
'https': 'http://user:password@10.10.1.10:3128/',
}
requests.get('https://www.taobao.com', proxies=proxies)
除了基本的 HTTP 代理,Requests 还支持 SOCKS 协议的代理。
首先需要安装 Socks 这个库,命令如下:
pip3 install "requests[socks]"
然后就可以使用 SOCKS 协议代理了,实例如下:
import requests
proxies = {
'http': 'socks5://user:password@host:port',
'https': 'socks5://user:password@host:port' } requests.get('https://www.taobao.com', proxies=proxies)
6. 超时设置
在本机网络状况不好或者服务器网络响应太慢甚至无响应时,我们可能会等待特别久的时间才可能会收到一个响应,甚至到最后收不到响应而报错。为了防止服务器不能及时响应,我们应该设置一个超时时间,即超过了这个时间还没有得到响应,那就报错。
设置超时时间需要用到 timeout 参数。这个时间的计算是发出 Request 到服务器返回 Response 的时间。
下面用一个实例来感受一下:
import requests
r = requests.get('https://www.taobao.com', timeout=1)
print(r.status_code)
通过这样的方式,我们可以将超时时间设置为 1 秒,如果 1 秒内没有响应,那就抛出异常。
实际上请求分为两个阶段,即 connect(连接)和 read(读取)。
上面的设置 timeout 值将会用作 connect 和 read 二者的 timeout 总和。
如果要分别指定,就可以传入一个元组:
r = requests.get('https://www.taobao.com', timeout=(5, 11))
如果想永久等待,那么我们可以直接将 timeout 设置为 None,或者不设置直接留空,因为默认是 None。这样的话,如果服务器还在运行,但是响应特别慢,那就慢慢等吧,它永远不会返回超时错误的。
用法如下:
r = requests.get('https://www.taobao.com', timeout=None)
或直接不加参数:
r = requests.get('https://www.taobao.com')
7. 身份认证
在访问网站时,我们可能会遇到这样的认证页面,如图 3-9 所示:
图 3-9 认证页面
如果遇到这样的网站验证,可以使用 Requests 自带的身份认证功能,实例如下:
import requests
from requests.auth import HTTPBasicAuth
r = requests.get('http://localhost:5000', auth=HTTPBasicAuth('username', 'password')) print(r.status_code)
如果用户名和密码正确的话,请求时就会自动认证成功,会返回 200 状态码,如果认证失败,则会返回 401 状态码。
当然如果参数都传一个 HTTPBasicAuth 类,就显得有点繁琐了,所以 Requests 提供了一个更简单的写法,可以直接传一个元组,它会默认使用 HTTPBasicAuth 这个类来认证。
所以上面的代码可以直接简写如下:
import requests
r = requests.get('http://localhost:5000', auth=('username', 'password')) print(r.status_code)
运行效果和上面的是一样的。
Requests 还提供了其他的认证方式,如 OAuth 认证,不过需要安装 oauth 包,命令如下:
pip3 install requests_oauthlib
使用 OAuth1 认证的方法如下:
import requests
from requests_oauthlib import OAuth1
url = 'https://api.twitter.com/1.1/account/verify_credentials.json' auth = OAuth1('YOUR_APP_KEY', 'YOUR_APP_SECRET', 'USER_OAUTH_TOKEN', 'USER_OAUTH_TOKEN_SECRET') requests.get(url, auth=auth)
更多详细的功能就可以参考 requests_oauthlib 的官方文档:https://requests-oauthlib.readthedocs.org/,在此就不再赘述了。
8. Prepared Request
在前面介绍 Urllib 时我们可以将 Request 表示为一个数据结构,Request 的各个参数都可以通过一个 Request 对象来表示,在 Requests 里面同样可以做到,这个数据结构就叫 Prepared Request。
我们用一个实例感受一下:
from requests import Request, Session
url = 'http://httpbin.org/post'
data = {
'name': 'germey' } headers = { 'User-Agent': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_11_4) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/53.0.2785.116 Safari/537.36' } s = Session() req = Request('POST', url, data=data, headers=headers) prepped = s.prepare_request(req) r = s.send(prepped) print(r.text)
在这里我们引入了 Request,然后用 url、data、headers 参数构造了一个 Request 对象,这时我们需要再调用 Session 的 prepare_request() 方法将其转换为一个 Prepared Request 对象,然后调用 send() 方法发送即可,运行结果如下:
{
"args": {},
"data": "",
"files": {}, "form": { "name": "germey" }, "headers": { "Accept": "*/*", "Accept-Encoding": "gzip, deflate", "Connection": "close", "Content-Length": "11", "Content-Type": "application/x-www-form-urlencoded", "Host": "httpbin.org", "User-Agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_11_4) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/53.0.2785.116 Safari/537.36" }, "json": null, "origin": "182.32.203.166", "url": "http://httpbin.org/post" }
可以看到我们达到了同样的 POST 请求效果。
有了 Request 这个对象,我们就可以将一个个请求当做一个独立的对象来看待,这样在进行队列调度的时候会非常方便,后面我们会有一节使用它来构造一个 Request 队列。
9. 结语
本节讲解了 Requests 的一些高级用法,这些用法在后面实战部分会经常用到,需要熟练掌握。
更多的用法可以参考 Requests 的官方文档:http://docs.python-requests.org/。
3.3 正则表达式
本节我们看一下正则表达式的相关用法,正则表达式是处理字符串的强大的工具,它有自己特定的语法结构,有了它,实现字符串的检索、替换、匹配验证都不在话下。
当然对于爬虫来说,有了它,我们从 HTML 里面提取我们想要的信息就非常方便了。
1. 实例引入
说了这么多,可能我们对它到底是个什么还是比较模糊,下面我们就用几个实例来感受一下正则表达式的用法。
我们打开开源中国提供的正则表达式测试工具:http://tool.oschina.net/regex/,打开之后我们可以输入待匹配的文本,然后选择常用的正则表达式,就可以从我们输入的文本中得出相应的匹配结果了。
例如我们在这里输入待匹配的文本如下:
Hello, my phone number is 010-86432100 and email is cqc@cuiqingcai.com, and my website is http://cuiqingcai.com.
这段字符串中包含了一个电话号码和一个电子邮件,接下来我们就尝试用正则表达式提取出来,如图 3-10 所示:
图 3-10 运行页面
我们在网页中选择匹配 Email 地址,就可以看到在下方出现了文本中的 Email。如果我们选择了匹配网址 URL,就可以看到在下方出现了文本中的 URL。是不是非常神奇?
其实,在这里就是用了正则表达式(Regex)匹配,也就是用了一定的规则将特定的文本提取出来。比如电子邮件它开头是一段字符串,然后是一个 @ 符号,然后就是某个域名,这是有特定的组成格式的。另外对于 URL,开头是协议类型,然后是冒号加双斜线,然后是域名加路径。
对于 URL 来说,我们就可以用下面的正则表达式匹配:
[a-zA-z]+://[^\s]*
如果我们用这个正则表达式去匹配一个字符串,如果这个字符串中包含类似 URL 的文本,那就会被提取出来。
这个正则表达式看上去是乱糟糟的一团,其实不然,这里面都是有特定的语法规则的。比如 a-z 代表匹配任意的小写字母,\s 表示匹配任意的空白字符,* 就代表匹配前面的字符任意多个,这一长串的正则表达式就是这么多匹配规则的组合,最后实现特定的匹配功能。
写好正则表达式后,我们就可以拿它去一个长字符串里匹配查找了,不论这个字符串里面有什么,只要符合我们写的规则,统统可以找出来。那么对于网页来说,如果我们想找出网页源代码里有多少 URL,就可以用匹配URL的正则表达式去匹配,就可以得到源码中的 URL 了。
在上面我们说了几个匹配规则,那么正则表达式的规则到底有多少?那么在这里把常用的匹配规则总结一下:
模式 | 描述 | |
---|---|---|
\w | 匹配字母数字及下划线 | |
\W | 匹配非字母数字及下划线 | |
\s | 匹配任意空白字符,等价于 [\t\n\r\f]. | |
\S | 匹配任意非空字符 | |
\d | 匹配任意数字,等价于 [0-9] | |
\D | 匹配任意非数字 | |
\A | 匹配字符串开始 | |
\Z | 匹配字符串结束,如果是存在换行,只匹配到换行前的结束字符串 | |
\z | 匹配字符串结束 | |
\G | 匹配最后匹配完成的位置 | |
\n | 匹配一个换行符 | |
\t | 匹配一个制表符 | |
^ | 匹配字符串的开头 | |
$ | 匹配字符串的末尾 | |
. | 匹配任意字符,除了换行符,当 re.DOTALL 标记被指定时,则可以匹配包括换行符的任意字符 | |
[...] | 用来表示一组字符,单独列出:[amk] 匹配 'a','m' 或 'k' | |
[^...] | 不在 [] 中的字符:abc 匹配除了 a,b,c 之外的字符。 | |
* | 匹配 0 个或多个的表达式。 | |
+ | 匹配 1 个或多个的表达式。 | |
? | 匹配 0 个或 1 个由前面的正则表达式定义的片段,非贪婪方式 | |
{n} | 精确匹配 n 个前面表达式。 | |
{n, m} | 匹配 n 到 m 次由前面的正则表达式定义的片段,贪婪方式 | |
`a | b` | 匹配 a 或 b |
( ) | 匹配括号内的表达式,也表示一个组 |
可能看完了之后就有点晕晕的了把,不用担心,下面我们会详细讲解下一些常见的规则的用法。怎么用它来从网页中提取我们想要的信息。
2. 了解 re 库
其实正则表达式不是 Python 独有的,它在其他编程语言中也可以使用,但是 Python 的 re 库提供了整个正则表达式的实现,利用 re 库我们就可以在 Python 中使用正则表达式了,在 Python 中写正则表达式几乎都是用的这个库,下面我们就来了解下它的一些常用方法。
3. match()
在这里首先介绍第一个常用的匹配方法,match() 方法,我们向这个方法传入要匹配的字符串以及正则表达式,就可以来检测这个正则表达式是否该匹配字符串了。
match() 方法会尝试从字符串的起始位置匹配正则表达式,如果匹配,就返回匹配成功的结果,如果不匹配,那就返回 None。
我们用一个实例来感受一下:
import re
content = 'Hello 123 4567 World_This is a Regex Demo'
print(len(content))
result = re.match('^Hello\s\d\d\d\s\d{4}\s\w{10}', content)
print(result)
print(result.group())
print(result.span())
运行结果:
41
<_sre.SRE_Match object; span=(0, 25), match='Hello 123 4567 World_This'> Hello 123 4567 World_This (0, 25)
在这里我们首先声明了一个字符串,包含英文字母、空白字符、数字等等内容,接下来我们写了一个正则表达式:
^Hello\s\d\d\d\s\d{4}\s\w{10}
用它来匹配这个长字符串。开头的 ^ 是匹配字符串的开头,也就是以 Hello 开头,然后 \s 匹配空白字符,用来匹配目标字符串的空格,\d 匹配数字,3 个 \d 匹配 123,然后再写 1 个 \s 匹配空格,后面还有 4567,我们其实可以依然用 4 个 \d 来匹配,但是这么写起来比较繁琐,所以在后面可以跟 {4} 代表匹配前面的规则 4 次,也就是匹配 4 个数字,这样也可以完成匹配,然后后面再紧接 1 个空白字符,然后 \w{10} 匹配 10 个字母及下划线,正则表达式到此为止就结束了,我们注意到其实并没有把目标字符串匹配完,不过这样依然可以进行匹配,只不过匹配结果短一点而已。
我们调用 match() 方法,第一个参数传入了正则表达式,第二个参数传入了要匹配的字符串。
打印输出一下结果,可以看到结果是 SRE_Match 对象,证明成功匹配,它有两个方法,group() 方法可以输出匹配到的内容,结果是 Hello 123 4567 World_This,这恰好是我们正则表达式规则所匹配的内容,span() 方法可以输出匹配的范围,结果是 (0, 25),这个就是匹配到的结果字符串在原字符串中的位置范围。
通过上面的例子我们可以基本了解怎样在 Python 中怎样使用正则表达式来匹配一段文字。
匹配目标
刚才我们用了 match() 方法可以得到匹配到的字符串内容,但是如果我们想从字符串中提取一部分内容怎么办呢?就像最前面的实例一样,从一段文本中提取出邮件或电话号等内容。
在这里可以使用 () 括号来将我们想提取的子字符串括起来,() 实际上就是标记了一个子表达式的开始和结束位置,被标记的每个子表达式会依次对应每一个分组,我们可以调用 group() 方法传入分组的索引即可获取提取的结果。
下面我们用一个实例感受一下:
import re
content = 'Hello 1234567 World_This is a Regex Demo'
result = re.match('^Hello\s(\d+)\sWorld', content)
print(result)
print(result.group())
print(result.group(1)) print(result.span())
依然是前面的字符串,在这里我们想匹配这个字符串并且把其中的 1234567 提取出来,在这里我们将数字部分的正则表达式用 () 括起来,然后接下来调用了group(1) 获取匹配结果。
运行结果如下:
<_sre.SRE_Match object; span=(0, 19), match='Hello 1234567 World'>
Hello 1234567 World 1234567 (0, 19)
可以看到在结果中成功得到了 1234567,我们获取用的是group(1),与 group() 有所不同,group() 会输出完整的匹配结果,而 group(1) 会输出第一个被 () 包围的匹配结果,假如正则表达式后面还有 () 包括的内容,那么我们可以依次用 group(2)、group(3) 等来依次获取。
通用匹配
刚才我们写的正则表达式其实比较复杂,出现空白字符我们就写 \s 匹配空白字符,出现数字我们就写 \d 匹配数字,工作量非常大,其实完全没必要这么做,还有一个万能匹配可以用,也就是 .* (点星),.(点)可以匹配任意字符(除换行符),*(星) 又代表匹配前面的字符无限次,所以它们组合在一起就可以匹配任意的字符了,有了它我们就不用挨个字符地匹配了。
所以接着上面的例子,我们可以改写一下正则表达式。
import re
content = 'Hello 123 4567 World_This is a Regex Demo'
result = re.match('^Hello.*Demo$', content)
print(result)
print(result.group())
print(result.span())
在这里我们将中间的部分直接省略,全部用 .* 来代替,最后加一个结尾字符串就好了,运行结果如下:
<_sre.SRE_Match object; span=(0, 41), match='Hello 123 4567 World_This is a Regex Demo'>
Hello 123 4567 World_This is a Regex Demo (0, 41)
可以看到 group() 方法输出了匹配的全部字符串,也就是说我们写的正则表达式匹配到了目标字符串的全部内容,span() 方法输出 (0, 41),是整个字符串的长度。
因此,我们可以在使用 .* 来简化正则表达式的书写。
贪婪与非贪婪
在使用上面的通用匹配 .* 的时候可能我们有时候匹配到的并不是想要的结果,我们看下面的例子:
import re
content = 'Hello 1234567 World_This is a Regex Demo'
result = re.match('^He.*(\d+).*Demo$', content)
print(result)
print(result.group(1))
在这里我们依然是想获取中间的数字,所以中间我们依然写的是 (\d+),数字两侧由于内容比较杂乱,所以两侧我们想省略来写,都写 .*,最后组成 ^He.*(\d+).*Demo$,看样子并没有什么问题,我们看下运行结果:
<_sre.SRE_Match object; span=(0, 40), match='Hello 1234567 World_This is a Regex Demo'>
7
奇怪的事情发生了,我们只得到了 7 这个数字,这是怎么回事?
这里就涉及一个贪婪匹配与非贪婪匹配的原因了,贪婪匹配下,.* 会匹配尽可能多的字符,我们的正则表达式中 .* 后面是 \d+,也就是至少一个数字,并没有指定具体多少个数字,所以 .* 就尽可能匹配多的字符,所以它把 123456 也匹配了,给 \d+ 留下一个可满足条件的数字 7,所以 \d+ 得到的内容就只有数字 7 了。
但这样很明显会给我们的匹配带来很大的不便,有时候匹配结果会莫名其妙少了一部分内容。其实这里我们只需要使用非贪婪匹配匹配就好了,非贪婪匹配的写法是 .*?,多了一个 ?,那么它可以达到怎样的效果?我们再用一个实例感受一下:
import re
content = 'Hello 1234567 World_This is a Regex Demo'
result = re.match('^He.*?(\d+).*Demo$', content)
print(result)
print(result.group(1))
在这里我们只是将第一个 .* 改成了 .*?,转变为非贪婪匹配。结果如下:
<_sre.SRE_Match object; span=(0, 40), match='Hello 1234567 World_This is a Regex Demo'>
1234567
这下我们就可以成功获取 1234567 了。原因可想而知,贪婪匹配是尽可能匹配多的字符,非贪婪匹配就是尽可能匹配少的字符,.*? 之后是 \d+ 用来匹配数字,当 .*? 匹配到 Hello 后面的空白字符的时候,再往后的字符就是数字了,而 \d+ 恰好可以匹配,那么这里 .*? 就不再进行匹配,交给 \d+ 去匹配后面的数字。所以这样,.*? 匹配了尽可能少的字符,\d+ 的结果就是 1234567 了。
所以说,在做匹配的时候,字符串中间我们可以尽量使用非贪婪匹配来匹配,也就是用 .*? 来代替 .*,以免出现匹配结果缺失的情况。
但这里注意,如果匹配的结果在字符串结尾,.*? 就有可能匹配不到任何内容了,因为它会匹配尽可能少的字符,例如:
import re
content = 'http://weibo.com/comment/kEraCN'
result1 = re.match('http.*?comment/(.*?)', content)
result2 = re.match('http.*?comment/(.*)', content) print('result1', result1.group(1)) print('result2', result2.group(1))
运行结果:
result1
result2 kEraCN
观察到 .*? 没有匹配到任何结果,而 .* 则尽量匹配多的内容,成功得到了匹配结果。
所以在这里好好体会一下贪婪匹配和非贪婪匹配的原理,对后面写正则表达式非常有帮助。
修饰符
正则表达式可以包含一些可选标志修饰符来控制匹配的模式。修饰符被指定为一个可选的标志。
我们用一个实例先来感受一下:
import re
content = '''Hello 1234567 World_This
is a Regex Demo
'''
result = re.match('^He.*?(\d+).*?Demo$', content)
print(result.group(1))
和上面的例子相仿,我们在字符串中加了个换行符,正则表达式也是一样的来匹配其中的数字,看一下运行结果:
AttributeError Traceback (most recent call last)
<ipython-input-18-c7d232b39645> in <module>()
5 ''' 6 result = re.match('^He.*?(\d+).*?Demo$', content) ----> 7 print(result.group(1)) AttributeError: 'NoneType' object has no attribute 'group'
运行直接报错,也就是说正则表达式没有匹配到这个字符串,返回结果为 None,而我们又调用了 group() 方法所以导致AttributeError。
那我们加了一个换行符为什么就匹配不到了呢?是因为 . 匹配的是除换行符之外的任意字符,当遇到换行符时,.*? 就不能匹配了,所以导致匹配失败。
那么在这里我们只需要加一个修饰符 re.S,即可修正这个错误。
result = re.match('^He.*?(\d+).*?Demo$', content, re.S)
在 match() 方法的第三个参数传入 re.S,它的作用是使 . 匹配包括换行符在内的所有字符。
运行结果:
1234567
这个 re.S 在网页匹配中会经常用到,因为 HTML 节点经常会有换行,加上它我们就可以匹配节点与节点之间的换行了。
另外还有一些修饰符,在必要的情况下也可以使用:
修饰符 | 描述 |
---|---|
re.I | 使匹配对大小写不敏感 |
re.L | 做本地化识别(locale-aware)匹配 |
re.M | 多行匹配,影响 ^ 和 $ |
re.S | 使 . 匹配包括换行在内的所有字符 |
re.U | 根据Unicode字符集解析字符。这个标志影响 \w, \W, \b, \B. |
re.X | 该标志通过给予你更灵活的格式以便你将正则表达式写得更易于理解。 |
在网页匹配中较为常用的为 re.S、re.I。
转义匹配
我们知道正则表达式定义了许多匹配模式,如 . 匹配除换行符以外的任意字符,但是如果目标字符串里面它就包含 . 我们改怎么匹配?
那么这里就需要用到转义匹配了,我们用一个实例来感受一下:
import re
content = '(百度)www.baidu.com'
result = re.match('\(百度\)www\.baidu\.com', content)
print(result)
当遇到用于正则匹配模式的特殊字符时,我们在前面加反斜线来转义一下就可以匹配了。例如 . 我们就可以用 \. 来匹配,运行结果:
<_sre.SRE_Match object; span=(0, 17), match='(百度)www.baidu.com'>
可以看到成功匹配到了原字符串。
以上是写正则表达式常用的几个知识点,熟练掌握上面的知识点对后面我们写正则表达式匹配非常有帮助。
4. search()
我们在前面提到过 match() 方法是从字符串的开头开始匹配,一旦开头不匹配,那么整个匹配就失败了。
我们看下面的例子:
import re
content = 'Extra stings Hello 1234567 World_This is a Regex Demo Extra stings'
result = re.match('Hello.*?(\d+).*?Demo', content)
print(result)
在这里我们有一个字符串,它是以 Extra 开头的,但是正则表达式我们是以 Hello 开头的,整个正则表达式是字符串的一部分,但是这样匹配是失败的,也就是说只要第一个字符不匹配整个匹配就不能成功,运行结果如下:
None
所以 match() 方法在我们在使用的时候需要考虑到开头的内容,所以在做匹配的时候并不那么方便,它适合来检测某个字符串是否符合某个正则表达式的规则。
所以在这里就有另外一个方法 search(),它在匹配时会扫描整个字符串,然后返回第一个成功匹配的结果,也就是说,正则表达式可以是字符串的一部分,在匹配时,search() 方法会依次扫描字符串,直到找到第一个符合规则的字符串,然后返回匹配内容,如果搜索完了还没有找到,那就返回 None。
我们把上面的代码中的 match() 方法修改成 search(),再看下运行结果:
<_sre.SRE_Match object; span=(13, 53), match='Hello 1234567 World_This is a Regex Demo'>
1234567
这样就得到了匹配结果。
所以说,为了匹配方便,我们可以尽量使用 search() 方法。
下面我们再用几个实例来感受一下 search() 方法的用法。
首先这里有一段待匹配的 HTML 文本,我们接下来写几个正则表达式实例来实现相应信息的提取。
html = '''<div id="songs-list"> <h2 class="title">经典老歌</h2> <p class="introduction"> 经典老歌列表 </p> <ul id="list" class="list-group"> <li data-view="2">一路上有你</li> <li data-view="7"> <a href="/2.mp3" singer="任贤齐">沧海一声笑</a> </li> <li data-view="4" class="active"> <a href="/3.mp3" singer="齐秦">往事随风</a> </li> <li data-view="6"><a href="/4.mp3" singer="beyond">光辉岁月</a></li> <li data-view="5"><a href="/5.mp3" singer="陈慧琳">记事本</a></li> <li data-view="5"> <a href="/6.mp3" singer="邓丽君"><i class="fa fa-user"></i>但愿人长久</a> </li> </ul> </div>'''
观察到 ul 节点里面有许多 li 节点,其中 li 节点有的包含 a 节点,有的不包含 a 节点,a 节点还有一些相应的属性,超链接和歌手名。
首先我们尝试提取 class 为 active的 li 节点内部的超链接包含的歌手名和歌名。
所以我们需要提取第三个 li 节点下的 a 节点的 singer 属性和文本。
所以正则表达式可以以 li 开头,然后接下来寻找一个标志符 active,中间的部分可以用 .*? 来匹配,然后接下来我们要提取 singer 这个属性值,所以还需要写入singer="(.*?)" ,我们需要提取的部分用小括号括起来,以便于用 group() 方法提取出来,它的两侧边界是双引号,然后接下来还需要匹配 a 节点的文本,那么它的左边界是 >,右边界是 \</a>,所以我们指定一下左右边界,然后目标内容依然用 (.*?) 来匹配,所以最后的正则表达式就变成了:
<li.*?active.*?singer="(.*?)">(.*?)</a>
然后我们再调用 search() 方法,它便会搜索整个 HTML 文本,找到符合正则表达式的第一个内容返回。 另外由于代码有换行,所以这里第三个参数需要传入 re.S。
所以整个匹配代码如下:
result = re.search('<li.*?active.*?singer="(.*?)">(.*?)</a>', html, re.S)
if result:
print(result.group(1), result.group(2))
由于我们需要获取的歌手和歌名都已经用了小括号包围,所以可以用 group() 方法获取,序号依次对应 group() 的参数。
运行结果:
齐秦 往事随风
可以看到这个正是我们想提取的 class 为 active 的 li 节点内部的超链接包含的歌手名和歌名。
那么正则表达式不加 active 会怎样呢?也就是匹配不带 class 为 active 的节点内容,我们将正则表达式中的 active 去掉,代码改写如下:
result = re.search('<li.*?singer="(.*?)">(.*?)</a>', html, re.S)
if result:
print(result.group(1), result.group(2))
由于 search() 方法会返回第一个符合条件的匹配目标,那在这里结果就变了。
运行结果如下:
任贤齐 沧海一声笑
因为我们把 active 标签去掉之后,从字符串开头开始搜索,符合条件的节点就变成了第二个 li 节点,后面的就不再进行匹配,所以运行结果自然就变成了第二个 li 节点中的内容。
注意在上面两次匹配中,search() 方法的第三个参数我们都加了 re.S,使得 .*? 可以匹配换行,所以含有换行的 li 节点被匹配到了,如果我们将其去掉,结果会是什么?
result = re.search('<li.*?singer="(.*?)">(.*?)</a>', html)
if result:
print(result.group(1), result.group(2))
运行结果:
beyond 光辉岁月
可以看到结果就变成了第四个 li 节点的内容,这是因为第二个和第三个 li 节点都包含了换行符,去掉 re.S 之后,.*? 已经不能匹配换行符,所以正则表达式不会匹配到第二个和第三个 li 节点,而第四个 li 节点中不包含换行符,所以成功匹配。
由于绝大部分的 HTML 文本都包含了换行符,所以通过上面的例子,我们尽量都需要加上 re.S 修饰符,以免出现匹配不到的问题。
5. findall()
在前面我们说了 search() 方法的用法,它可以返回匹配正则表达式的第一个内容,但是如果我们想要获取匹配正则表达式的所有内容的话怎么办?这时就需要借助于 findall() 方法了。
findall() 方法会搜索整个字符串然后返回匹配正则表达式的所有内容。
还是上面的 HTML 文本,如果我们想获取所有 a 节点的超链接、歌手和歌名,就可以将 search() 方法换成 findall() 方法。如果有返回结果的话就是列表类型,所以我们需要遍历一下来获依次获取每组内容。
results = re.findall('<li.*?href="(.*?)".*?singer="(.*?)">(.*?)</a>', html, re.S)
print(results)
print(type(results))
for result in results:
print(result)
print(result[0], result[1], result[2])
运行结果:
[('/2.mp3', '任贤齐', '沧海一声笑'), ('/3.mp3', '齐秦', '往事随风'), ('/4.mp3', 'beyond', '光辉岁月'), ('/5.mp3', '陈慧琳', '记事本'), ('/6.mp3', '邓丽君', '但愿人长久')]
<class 'list'>
('/2.mp3', '任贤齐', '沧海一声笑')
/2.mp3 任贤齐 沧海一声笑
('/3.mp3', '齐秦', '往事随风')
/3.mp3 齐秦 往事随风
('/4.mp3', 'beyond', '光辉岁月')
/4.mp3 beyond 光辉岁月
('/5.mp3', '陈慧琳', '记事本')
/5.mp3 陈慧琳 记事本
('/6.mp3', '邓丽君', '但愿人长久')
/6.mp3 邓丽君 但愿人长久
可以看到,返回的列表的每个元素都是元组类型,我们用对应的索引依次取出即可。
所以,如果只是获取第一个内容,可以用 search() 方法,当需要提取多个内容时,就可以用 findall() 方法。
6. sub()
正则表达式除了提取信息,我们有时候还需要借助于它来修改文本,比如我们想要把一串文本中的所有数字都去掉,如果我们只用字符串的 replace() 方法那就太繁琐了,在这里我们就可以借助于 sub() 方法。
我们用一个实例来感受一下:
import re
content = '54aK54yr5oiR54ix5L2g'
content = re.sub('\d+', '', content) print(content)
运行结果:
aKyroiRixLg
在这里我们只需要在第一个参数传入 \d+ 来匹配所有的数字,然后第二个参数是替换成的字符串,要去掉的话就可以赋值为空,第三个参数就是原字符串。
得到的结果就是替换修改之后的内容。
那么在上面的 HTML 文本中,如果我们想正则获取所有 li 节点的歌名,如果直接用正则表达式来提取可能比较繁琐,比如可以写成这样子:
<li data-view="6"><a href="/4.mp3" singer="beyond">光辉岁月</a></li>
results = re.findall('<li.*?>\s*?(<a.*?>)?(\w+)(</a>)?\s*?</li>', html, re.S)
for result in results:
print(result[1])
运行结果:
一路上有你
沧海一声笑
往事随风
光辉岁月
记事本
但愿人长久
但如果我们借助于 sub() 方法就比较简单了,我们可以先用sub() 方法将 a 节点去掉,只留下文本,然后再利用findall() 提取就好了。
html = re.sub('<a.*?>|</a>', '', html)
print(html)
results = re.findall('<li.*?>(.*?)</li>', html, re.S)
for result in results: print(result.strip())
运行结果:
<div id="songs-list">
<h2 class="title">经典老歌</h2>
<p class="introduction">
经典老歌列表
</p>
<ul id="list" class="list-group">
<li data-view="2">一路上有你</li>
<li data-view="7">
沧海一声笑
</li>
<li data-view="4" class="active">
往事随风
</li>
<li data-view="6">光辉岁月</li>
<li data-view="5">记事本</li>
<li data-view="5">
但愿人长久
</li>
</ul>
</div>
一路上有你
沧海一声笑
往事随风
光辉岁月
记事本
但愿人长久
可以到 a 节点在经过 sub() 方法处理后都没有了,然后再 findall() 直接提取即可。所以在适当的时候我们可以借助于 sub() 方法做一些相应处理可以事半功倍。
7. compile()
前面我们所讲的方法都是用来处理字符串的方法,最后再介绍一个 compile() 方法,这个方法可以将正则字符串编译成正则表达式对象,以便于在后面的匹配中复用。
import re
content1 = '2016-12-15 12:00'
content2 = '2016-12-17 12:55'
content3 = '2016-12-22 13:21' pattern = re.compile('\d{2}:\d{2}') result1 = re.sub(pattern, '', content1) result2 = re.sub(pattern, '', content2) result3 = re.sub(pattern, '', content3) print(result1, result2, result3)
例如这里有三个日期,我们想分别将三个日期中的时间去掉,所以在这里我们可以借助于 sub() 方法,sub() 方法的第一个参数是正则表达式,但是这里我们没有必要重复写三个同样的正则表达式,所以可以借助于 compile() 方法将正则表达式编译成一个正则表达式对象,以便复用。
运行结果:
2016-12-15 2016-12-17 2016-12-22
另外 compile() 还可以传入修饰符,例如 re.S 等修饰符,这样在 search()、findall() 等方法中就不需要额外传了。所以 compile() 方法可以说是给正则表达式做了一层封装,以便于我们更好地复用。
8. 结语
到此为止,正则表达式的基本用法就介绍完毕了,后面我们会有实战来讲解正则表达式的使用。
3.4 Requests与正则表达式抓取猫眼电影排行
import requests
import re
import json
from requests.exceptions import RequestException
import time
def get_one_page(url):
try:
headers = {
'User-Agent': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_11_4) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/53.0.2785.116 Safari/537.36'
}
response = requests.get(url,headers=headers)
if response.status_code == 200:
return response.text
return None
except RequestException:
return None
def parse_one_page(html):
pattern = re.compile(
'<dd>.*?board-index.*?>(.*?)</i>.*?data-src="(.*?)".*?name.*?a.*?>(.*?)</a>.*?star.*?>(.*?)</p>.*?releasetime.*?>(.*?)</p>.*?integer.*?>(.*?)</i>.*?fraction.*?>(.*?)</i>.*?</dd>', re.S)
items = re.findall(pattern, html)
for item in items:
yield {
'index': item[0],
'image': item[1],
'title': item[2].strip(),
'actor': item[3].strip()[3:] if len(item[3]) > 3 else '',
'time': item[4].strip()[5:] if len(item[4]) > 5 else '',
'score': item[5].strip() + item[6].strip()
}
def write_to_file(content):
with open('result.txt', 'a', encoding='utf-8') as f:
f.write(json.dumps(content, ensure_ascii=False) + '\n')
def main(offset):
url = 'http://maoyan.com/board/4?offset=' + str(offset)
html = get_one_page(url)
for item in parse_one_page(html):
print(item)
write_to_file(item)
if __name__ == '__main__':
for i in range(10):
main(offset=i * 10)
time.sleep(1)