爬虫编程基础

Urlib

首先我们介绍一个 Python 库,叫做 urllib,利用它我们可以实现 HTTP 请求的发送,而不用去关心 HTTP 协议本身甚至更低层的实现。我们只需要指定请求的 URL、请求头、请求体等信息即可实现 HTTP 请求的发送,同时 urllib 还可以把服务器返回的响应转化为 Python 对象,通过该对象我们便可以方便地获取响应的相关信息了,如响应状态码、响应头、响应体等等。

注意:在 Python 2 中,有 urllib 和 urllib2 两个库来实现请求的发送。而在 Python 3 中,已经不存在 urllib2 这个库了,统一为 urllib,其官方文档链接为:https://docs.python.org/3/library/urllib.html

首先,我们来了解一下 urllib 库的使用方法,它是 Python 内置的 HTTP 请求库,也就是说不需要额外安装即可使用。它包含如下 4 个模块。

  • request:它是最基本的 HTTP 请求模块,可以用来模拟发送请求。就像在浏览器里输入网址然后回车一样,只需要给库方法传入 URL 以及额外的参数,就可以模拟实现这个过程了。
  • error:异常处理模块,如果出现请求错误,我们可以捕获这些异常,然后进行重试或其他操作以保证程序不会意外终止。
  • parse:一个工具模块,提供了许多 URL 处理方法,比如拆分、解析和合并等。
  • robotparser:主要用来识别网站的 robots.txt 文件,然后判断哪些网站可以爬,哪些网站不可以爬,它其实用得比较少。

1. 发送请求

使用 urllib 的 request 模块,我们可以方便地实现请求的发送并得到响应。我们先来看下它的具体用法。

urlopen

urllib.request 模块提供了最基本的构造 HTTP 请求的方法,利用它可以模拟浏览器的一个请求发起过程,同时它还带有处理授权验证(Authentication)、重定向(Redirection)、浏览器 Cookie 以及其他内容。

下面我们来看一下它的强大之处。这里以 Python 官网为例,我们来把这个网页抓下来:

import urllib.request

response = urllib.request.urlopen(‘https://www.python.org‘)
print(response.read().decode(‘utf-8’))

运行结果如图所示。

image-20200315212839610

图 运行结果

这里我们只用了两行代码,便完成了 Python 官网的抓取,输出了网页的源代码。得到源代码之后呢?我们想要的链接、图片地址、文本信息不就都可以提取出来了吗?

接下来,看看它返回的到底是什么。利用 type 方法输出响应的类型:

import urllib.request

response = urllib.request.urlopen(‘https://www.python.org‘)
print(type(response))

输出结果如下:

<class ‘http.client.HTTPResponse’>

可以发现,它是一个 HTTPResposne 类型的对象,主要包含 readreadintogetheadergetheadersfileno 等方法,以及 msgversionstatusreasondebuglevelclosed 等属性。

得到这个对象之后,我们把它赋值为 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,VS0,VE0’), (‘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(超时时间)等。

下面我们详细说明这几个参数的用法。

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’))

这里我们传递了一个参数 word,值是 hello。它需要被转码成 bytes(字节流)类型。其中转字节流采用了 bytes 方法,该方法的第一个参数需要是 str(字符串)类型,需要用 urllib.parse 模块里的 urlencode 方法来将参数字典转化为字符串;第二个参数指定编码格式,这里指定为 utf-8

这里请求的站点是 httpbin.org,它可以提供 HTTP 请求测试。本次我们请求的 URL 为 https://httpbin.org/post,这个链接可以用来测试 POST 请求,它可以输出 Request 的一些信息,其中就包含我们传递的 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.7”,
“X-Amzn-Trace-Id”: “Root=1-5ed27e43-9eee361fec88b7d3ce9be9db”
},
“json”: null,
“origin”: “17.220.233.154”,
“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 “/var/py/python/urllibtest.py”, line 4, in response =
urllib.request.urlopen(‘https://httpbin.org/get‘, timeout=0.1)

urllib.error.URLError:

这里我们设置的超时时间是 1 秒。程序运行 1 秒过后,服务器依然没有响应,于是抛出了 URLError 异常。该异常属于 urllib.error 模块,错误原因是超时。

因此,可以通过设置这个超时时间来控制一个网页如果长时间未响应,就跳过它的抓取。这可以利用 try…except 语句来实现,相关代码如下:

import socket
import urllib.request
import urllib.error

try:
response = urllib.request.urlopen(‘https://httpbin.org/get‘, timeout=0.1)
except urllib.error.URLError as e:
if isinstance(e.reason, socket.timeout):
print(‘TIME OUT’)

这里我们请求了 https://httpbin.org/get 这个测试链接,设置的超时时间是 0.1 秒,然后捕获了 URLError 这个异常,然后判断异常类型是 socket.timeout,意思就是超时异常。因此,得出它确实是因为超时而报错,打印输出了 TIME OUT

运行结果如下:

TIME OUT

按照常理来说,0.1 秒内基本不可能得到服务器响应,因此输出了 TIME OUT 的提示。

通过设置 timeout 这个参数来实现超时处理,有时还是很有用的。

其他参数

除了 data 参数和 timeout 参数外,还有 context 参数,它必须是 ssl.SSLContext 类型,用来指定 SSL 设置。

此外,cafile 和 capath 这两个参数分别指定 CA 证书和它的路径,这个在请求 HTTPS 链接时会有用。

cadefault 参数现在已经弃用了,其默认值为 False

前面讲解了 urlopen 方法的用法,通过这个最基本的方法,我们可以完成简单的请求和网页抓取。若需更加详细的信息,可以参见官方文档:https://docs.python.org/3/library/urllib.request.html

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,我们可以通过修改它来伪装浏览器。比如要伪装火狐浏览器,你可以把它设置为:

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 等。

下面我们传入多个参数来构建请求:

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 个参数构造了一个请求,其中 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-5ed27f77-884f503a2aa6760df7679f05”
},
“json”: null,
“origin”: “17.220.233.154”,
“url”: “https://httpbin.org/post
}

观察结果可以发现,我们成功设置了 dataheaders 和 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)’)

如此一来,我们就可以更加方便地构造请求,实现请求的发送啦。

高级用法

在上面的过程中,我们虽然可以构造请求,但是对于一些更高级的操作(比如 Cookies 处理、代理设置等),该怎么办呢?

接下来,就需要更强大的工具 Handler 登场了。简而言之,我们可以把它理解为各种处理器,有专门处理登录验证的,有处理 Cookie 的,有处理代理设置的。利用它们,我们几乎可以做到 HTTP 请求中所有的事情。

首先,介绍一下 urllib.request 模块里的 BaseHandler 类,它是所有其他 Handler 的父类,它提供了最基本的方法,例如 default_openprotocol_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 相当于类库为你封装好了极其常用的请求方法,利用它们可以完成基本的请求,但是现在不一样了,我们需要实现更高级的功能,所以需要深入一层进行配置,使用更底层的实例来完成操作,所以这里就用到了 Opener。

Opener 可以使用 open 方法,返回的类型和 urlopen 如出一辙。那么,它和 Handler 有什么关系呢?简而言之,就是利用 Handler 来构建 Opener。

下面用几个实例来看看它们的用法。

验证

在访问某些设置了身份认证的网站时,例如 https://ssr3.scrape.center/,我们可能会遇到这样的认证窗口,如图 2- 所示:

image-20210704202140395

图 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://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 对象,其参数是 HTTPPasswordMgrWithDefaultRealm 对象,它利用 add_password 方法添加进去用户名和密码,这样就建立了一个处理验证的 Handler。

接下来,利用这个 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 的处理就需要相关的 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=A09E6C4E38753531B9FB4C60CE9FDFCB:FG=1
BIDUPSID=A09E6C4E387535312F8AA46280C6C502
H_PS_PSSID=31358_1452_31325_21088_31110_31253_31605_31271_31463_30823
PSTM=1590854698
BDSVRTM=10
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 1622390755 BAIDUID 0B4A68D74B0C0E53E5B82AFD9BF9178F:FG=1
.baidu.com TRUE / FALSE 3738338402 BIDUPSID 0B4A68D74B0C0E53471FA6329280FA58
.baidu.com TRUE / FALSE H_PS_PSSID 31262_1438_31325_21127_31110_31596_31673_31464_30823_26350
.baidu.com TRUE / FALSE 3738338402 PSTM 1590854754
www.baidu.com FALSE / FALSE BDSVRTM 0
www.baidu.com FALSE / FALSE BD_HOME 1

另外,LWPCookieJar 同样可以读取和保存 Cookie,但是保存的格式和 MozillaCookieJar 不一样,它会保存成 libwww-perl(LWP)格式的 Cookie 文件。

要保存成 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=1F30EEDA35C7A9433C97CF6245CBC383; 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 文件后,怎样从文件中读取并利用呢?

下面我们以 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 库中 request 模块的基本用法,如果想实现更多的功能,可以参考官方文档的说明:https://docs.python.org/3/library/urllib.request.html#basehandler-objects

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

它是 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
Server: nginx/1.10.3 (Ubuntu)
Date: Sat, 30 May 2020 16:08:42 GMT
Content-Type: text/html; charset=UTF-8
Transfer-Encoding: chunked
Connection: close
Set-Cookie: PHPSESSID=kp1a1b0o3a0pcf688kt73gc780; path=/
Pragma: no-cache
Vary: Cookie
Expires: Wed, 11 Jan 1984 05:00:00 GMT
Cache-Control: no-cache, must-revalidate, max-age=0
Link: https://cuiqingcai.com/wp-json/; rel=”https://api.w.org/

依然是同样的网址,这里捕获了 HTTPError 异常,输出了 reasoncode 和 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,获取它的错误原因、状态码、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 方法来判断它的类型,作出更详细的异常判断。

本节中,我们讲述了 error 模块的相关用法,通过合理地捕获异常可以做出更准确的异常判断,使程序更加稳健。

3. 解析链接

前面说过,urllib 库里还提供了 parse 模块,它定义了处理 URL 的标准接口,例如实现 URL 各部分的抽取、合并以及链接转换。它支持如下协议的 URL 处理:fileftpgopherhdlhttphttpsimapmailtommsnewsnntpprosperorsyncrtsprtspusftpsipsipssnewssvnsvn+sshtelnet 和 wais。本节中,我们介绍一下该模块中常用的方法来看一下它的便捷之处。

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 个部分,分别是 schemenetlocpathparamsquery 和 fragment

观察一下该实例的 URL:

https://www.baidu.com/index.html;user?id=5#comment

可以发现,urlparse 方法将其拆分成了 6 个部分。大体观察可以发现,解析时有特定的分隔符。比如,:// 前面的就是 scheme,代表协议;第一个 / 符号前面便是 netloc,即域名,后面是 path,即访问路径;分号 ; 后面是 params,代表参数;问号 ? 后面是查询条件 query,一般用作 GET 类型的 URL;井号 # 后面是锚点,用于直接定位页面内部的下拉位置。

所以,可以得出一个标准的链接格式,具体如下:

scheme://netloc/path;params?query#fragment

一个标准的 URL 都会符合这个规则,利用 urlparse 方法可以将它拆分开来。

除了这种最基本的解析方式外,urlparse 方法还有其他配置吗?接下来,看一下它的 API 用法:

urllib.parse.urlparse(urlstring, scheme=’’, allow_fragments=True)

可以看到,它有 3 个参数。

  • 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。如果它被设置为 Falsefragment 部分就会被忽略,它会被解析为 pathparameters 或者 query 的一部分,而 fragment 部分为空。

下面我们用实例来看一下:

from urllib.parse import urlparse

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,我们再通过实例看一下:

from urllib.parse import urlparse

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=’’)

可以发现,当 URL 中不包含 params 和 query 时,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。它接收的参数是一个可迭代对象,但是它的长度必须是 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 这一部分,只返回 5 个结果。上面例子中的 params 会合并到 path 中。示例如下:

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(基础链接)作为第一个参数,将新的链接作为第二个参数,该方法会分析 base_url的 schemenetloc 和 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 提供了三项内容 schemenetloc 和 path。如果这 3 项在新的链接里不存在,就予以补充;如果新的链接存在,就使用新的链接的部分。而 base_url 中的 paramsquery 和 fragment 是不起作用的。

通过 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)

这里首先声明一个字典来将参数表示出来,然后调用 urlencode 方法将其序列化为 GET 请求参数。

运行结果如下:

https://www.baidu.com?name=germey&age=25

可以看到,参数成功地由字典类型转化为 GET 请求参数了。

这个方法非常常用。有时为了更加方便地构造参数,我们会事先用字典来表示。要转化为 URL 的参数时,只需要调用该方法即可。

parse_qs

有了序列化,必然就有反序列化。如果我们有一串 GET 请求参数,利用 parse_qs 方法,就可以将它转回字典,示例如下:

from urllib.parse import parse_qs

query = ‘name=germey&age=25’
print(parse_qs(query))

运行结果如下:

{‘name’: [‘germey’], ‘age’: [‘25’]}

可以看到,这样就成功转回为字典类型了。

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 中带有中文参数时,有时可能会导致乱码的问题,此时可以用这个方法可以将中文字符转化为 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 编码后的结果,这里利用 unquote 方法还原,结果如下:

https://www.baidu.com/s?wd=壁纸

可以看到,利用 unquote 方法可以方便地实现解码。

本节中,我们介绍了 parse 模块的一些常用 URL 处理方法。有了这些方法,我们可以方便地实现 URL 的解析和构造,建议熟练掌握。

4. 分析 Robots 协议

利用 urllib 的 robotparser 模块,我们可以实现网站 Robots 协议的分析。本节中,我们来简单了解一下该模块的用法。

1. 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 描述了搜索爬虫的名称,这里将其设置为 * 则代表该协议对任何爬取爬虫有效。比如,我们可以设置:

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 的一些常见写法。

爬虫名称

大家可能会疑惑,爬虫名是从哪儿来的?为什么就叫这个名?其实它是有固定名字的了,比如百度的就叫作 BaiduSpider。表 2- 列出了一些常见搜索爬虫的名称及对应的网站。

表 一些常见搜索爬虫的名称及其对应的网站

爬虫名称 名称 网站
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
Bingbot 必应 www.bing.com

robotparser

了解 Robots 协议之后,我们就可以使用 robotparser 模块来解析 robots.txt 了。该模块提供了一个类 RobotFileParser,它可以根据某网站的 robots.txt 文件来判断一个爬虫是否有权限来爬取这个网页。

该类用起来非常简单,只需要在构造方法里传入 robots.txt 的链接即可。首先看一下它的声明:

urllib.robotparser.RobotFileParser(url=’’)

当然,也可以在声明时不传入,默认为空,最后再使用 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(‘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 对象,然后通过 set_url 方法设置了 robots.txt 的链接。当然,不用这个方法的话,可以在声明时直接用如下方法设置:

rp = RobotFileParser(‘https://www.baidu.com/robots.txt‘)

接着利用 can_fetch 方法判断网页是否可以被抓取。

运行结果如下:

True
True
False

这里同样可以使用 parse 方法执行读取和分析,示例如下:
可以看到这里我们利用 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

由此我们可以看到,Baiduspider 没有限制 homepage 页面的抓取,而 Googlebot 则限制了 homepage 页面的抓取。

这里同样可以使用 parse 方法执行读取和分析,示例如下:

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 模块的基本用法和实例,利用它,我们可以方便地判断哪些页面可以抓取,哪些页面不可以抓取。

5. 总结

本节内容比较多,我们介绍了 urllib 的 request、error、parse、robotparser 模块的基本用法。这些是一些基础模块,其中有一些模块的实用性还是很强的,比如我们可以利用 parse 模块来进行 URL 的各种处理。

本节代码:https://github.com/Python3WebSpider/UrllibTest

Request库

上一节中,我们了解了 urllib 的基本用法,但是其中确实有不方便的地方,比如处理网页验证和 Cookie 时,需要写 Opener 和 Handler 来处理。另外我们要实现 POST、PUT 等请求时写法也不太方便。

为了更加方便地实现这些操作,就有了更为强大的库 requests,有了它,Cookie、登录验证、代理设置等操作都不是事儿。

接下来,让我们领略一下它的强大之处吧。

1. 准备工作

在开始之前,请确保已经正确安装好了 requests 库,如尚未安装可以使用 pip3 来安装:

pip3 install requests

更加详细的安装说明可以参考 https://setup.scrape.center/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[:100])
print(r.cookies)

运行结果如下:

<class ‘requests.models.Response’>
200
<class ‘str’>

<meta http-equiv=content-type content=text/html;charse
<RequestsCookieJar[<Cookie BDORZ=27315 for .baidu.com/>]>

这里我们调用 get 方法实现与 urlopen 相同的操作,得到一个 Response 对象,然后分别输出了 Response 的类型、状态码、响应体的类型、内容以及 Cookie。

通过运行结果可以发现,它的返回类型是 requests.models.Response,响应体的类型是字符串 str,Cookie 的类型是 RequestsCookieJar。

使用 get 方法成功实现一个 GET 请求,这倒不算什么,更方便之处在于其他的请求类型依然可以用一句话来完成,示例如下:

import requests

r = requests.get(‘https://httpbin.org/get‘)
r = requests.post(‘https://httpbin.org/post‘)
r = requests.put(‘https://httpbin.org/put‘)
r = requests.delete(‘https://httpbin.org/delete‘)
r = requests.patch(‘https://httpbin.org/patch‘)

这里分别用 post、put、delete 等方法实现了 POST、PUT、DELETE 等请求。是不是比 urllib 简单太多了?

其实这只是冰山一角,更多的还在后面。

3. GET 请求

HTTP 中最常见的请求之一就是 GET 请求,下面首先来详细了解一下利用 requests 构建 GET 请求的方法。

基本实例

首先,构建一个最简单的 GET 请求,请求的链接为 https://httpbin.org/get,该网站会判断如果客户端发起的是 GET 请求的话,它返回相应的请求信息:

import requests

r = requests.get(‘https://httpbin.org/get‘)
print(r.text)

运行结果如下:

{
“args”: {},
“headers”: {
“Accept”: “/“,
“Accept-Encoding”: “gzip, deflate”,
“Host”: “httpbin.org”,
“User-Agent”: “python-requests/2.22.0”,
“X-Amzn-Trace-Id”: “Root=1-5e6e3a2e-6b1a28288d721c9e425a462a”
},
“origin”: “17.20.233.237”,
“url”: “https://httpbin.org/get
}

可以发现,我们成功发起了 GET 请求,返回结果中包含请求头、URL、IP 等信息。

那么,对于 GET 请求,如果要附加额外的信息,一般怎样添加呢?比如现在想添加两个参数,其中 name 是 germey,age 是 25,URL 就可以写成如下内容:

https://httpbin/get?name=germey&age=25

要构造这个请求链接,是不是要直接写成这样呢?

r = requests.get(‘https://httpbin.org/get?name=germey&age=25‘)

这样也可以,但是是不是有点不人性化呢?这些参数还需要我们手动去拼接,实现起来有点不优雅。

一般情况下,这种信息我们利用 params 这个参数就可以直接传递了,示例如下:

import requests

data = {
‘name’: ‘germey’,
‘age’: 25
}
r = requests.get(‘https://httpbin.org/get‘, params=data)
print(r.text)

运行结果如下:

{
“args”: {
“age”: “25”,
“name”: “germey”
},
“headers”: {
“Accept”: “/“,
“Accept-Encoding”: “gzip, deflate”,
“Host”: “httpbin.org”,
“User-Agent”: “python-requests/2.10.0”
},
“origin”: “122.4.215.33”,
“url”: “https://httpbin.org/get?age=22&name=germey
}

在这里我们把 URL 参数通过一个字典的形式传给 get 方法的 params 参数,通过返回信息我们可以判断,请求的链接自动被构造成了:https://httpbin.org/get?age=22&name=germey,这样我们就不用再去自己构造 URL 了,非常方便。

另外,网页的返回类型实际上是 str 类型,但是它很特殊,是 JSON 格式的。所以,如果想直接解析返回结果,得到一个 JSON 格式的数据的话,可以直接调用 json 方法。示例如下:

import requests

r = requests.get(‘https://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 形式的字符串,那么如果请求普通的网页,则肯定能获得相应的内容了。下面以一个实例页面 https://ssr1.scrape.center/ 来试一下,我们再加上一点提取信息的逻辑,将代码完善成如下的样子:

import requests
import re

r = requests.get(‘https://ssr1.scrape.center/‘)
pattern = re.compile(‘<h2.?>(.?)‘, re.S)
titles = re.findall(pattern, r.text)
print(titles)

在这个例子中我们用到了最基础的正则表达式来匹配出所有的问题内容。关于正则表达式的相关内容,我们会在下一节详细介绍,这里作为实例来配合讲解。

运行结果如下:

[‘肖申克的救赎 - The Shawshank Redemption’, ‘霸王别姬 - Farewell My Concubine’, ‘泰坦尼克号 - Titanic’, ‘罗马假日 - Roman Holiday’, ‘这个杀手不太冷 - Léon’, ‘魂断蓝桥 - Waterloo Bridge’, ‘唐伯虎点秋香 - Flirting Scholar’, ‘喜剧之王 - The King of Comedy’, ‘楚门的世界 - The Truman Show’, ‘活着 - To Live’]

我们发现,这里成功提取出了所有的电影标题,一个最基本的抓取和提取流程就完成了。

抓取二进制数据

在上面的例子中,我们抓取的是网站的一个页面,实际上它返回的是一个 HTML 文档。如果想抓取图片、音频、视频等文件,应该怎么办呢?

图片、音频、视频这些文件本质上都是由二进制码组成的,由于有特定的保存格式和对应的解析方式,我们才可以看到这些形形色色的多媒体。所以,想要抓取它们,就要拿到它们的二进制数据。

下面以示例网站的站点图标为例来看一下:

import requests

r = requests.get(‘https://scrape.center/favicon.ico‘)
print(r.text)
print(r.content)

这里抓取的内容是站点图标,也就是在浏览器每一个标签上显示的小图标,如图所示:

image-20210704202919308

这里打印了 Response 对象的两个属性,一个是 text,另一个是 content。

运行结果如图所示,分别是 r.text 和 r.content 的结果。

image-20210704203039567

image-20210704202959490

可以注意到,前者出现了乱码,后者结果前带有一个 b,这代表是 bytes 类型的数据。由于图片是二进制数据,所以前者在打印时转化为 str 类型,也就是图片直接转化为字符串,这理所当然会出现乱码。

上面返回的结果我们并不能看懂,它实际上是图片的二进制数据,没关系,我们将刚才提取到的信息保存下来就好了,代码如下:

import requests

r = requests.get(‘https://scrape.center/favicon.ico‘)
with open(‘favicon.ico’, ‘wb’) as f:
f.write(r.content)

这里用了 open 方法,它的第一个参数是文件名称,第二个参数代表以二进制写的形式打开,可以向文件里写入二进制数据。

运行结束之后,可以发现在文件夹中出现了名为 favicon.ico 的图标,如图所示。

image-20210704203204899

这样,我们就把二进制数据成功保存成一张图片了,这个小图标就被我们成功爬取下来了。

同样地,音频和视频文件我们也可以用这种方法获取。

添加 headers

我们知道,在发起一个 HTTP 请求的时候,会有一个请求头 Request Headers,那么这个怎么来设置呢?

很简单,我们使用 headers 参数就可以完成了。

在刚才的实例中,实际上我们是没有设置 Request Headers 信息的,如果不设置,某些网站会发现这不是一个正常的浏览器发起的请求,网站可能会返回异常的结果,导致网页抓取失败。

要添加 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://ssr1.scrape.center/‘, headers=headers)
print(r.text)

当然,我们可以在 headers 这个参数中任意添加其他的字段信息。

4. POST 请求

前面我们了解了最基本的 GET 请求,另外一种比较常见的请求方式是 POST。使用 requests 实现 POST 请求同样非常简单,示例如下:

import requests

data = {‘name’: ‘germey’, ‘age’: ‘25’}
r = requests.post(“https://httpbin.org/post“, data=data)
print(r.text)

这里还是请求 https://httpbin.org/post,该网站可以判断如果请求是 POST 方式,就把相关请求信息返回。

运行结果如下:

{
“args”: {},
“data”: “”,
“files”: {},
“form”: {
“age”: “25”,
“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.22.0”,
“X-Amzn-Trace-Id”: “Root=1-5e6e3b52-0f36782ea980fce53c8c6524”
},
“json”: null,
“origin”: “17.20.232.237”,
“url”: “https://httpbin.org/post
}

可以发现,我们成功获得了返回结果,其中 form 部分就是提交的数据,这就证明 POST 请求成功发送了。

5. 响应

发送请求后,得到的自然就是响应。在上面的实例中,我们使用 text 和 content 获取了响应的内容。此外,还有很多属性和方法可以用来获取其他信息,比如状态码、响应头、Cookie 等。示例如下:

import requests

r = requests.get(‘https://ssr1.scrape.center/‘)
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 属性得到响应头,输出 cookies 属性得到 Cookie,输出 url 属性得到 URL,输出 history 属性得到请求历史。

运行结果如下:

<class ‘int’> 200
<class ‘requests.structures.CaseInsensitiveDict’> {‘Server’: ‘nginx/1.17.8’, ‘Date’: ‘Sat, 30 May 2020 16:56:40 GMT’, ‘Content-Type’: ‘text/html; charset=utf-8’, ‘Transfer-Encoding’: ‘chunked’, ‘Connection’: ‘keep-alive’, ‘Vary’: ‘Accept-Encoding’, ‘X-Frame-Options’: ‘DENY’, ‘X-Content-Type-Options’: ‘nosniff’, ‘Strict-Transport-Security’: ‘max-age=15724800; includeSubDomains’, ‘Content-Encoding’: ‘gzip’}
<class ‘requests.cookies.RequestsCookieJar’> <RequestsCookieJar[]>
<class ‘str’> https://ssr1.scrape.center/
<class ‘list’> []

可以看到,headers 和 cookies 这两个属性得到的结果分别是 CaseInsensitiveDict 和 RequestsCookieJar 类型。

在第一章我们知道,状态码是用来表示响应状态的,比如返回 200 代表我们得到的响应是没问题的,上面的例子正好输出的结果也是 200,所以我们可以通过判断 Response 的状态码来知道爬取是否爬取成功。

requests 还提供了一个内置的状态码查询对象 requests.codes,用法示例如下:

import requests

r = requests.get(‘https://ssr1.scrape.center/‘)
exit() if not r.status_code == requests.codes.ok else print(‘Request Successfully’)

这里通过比较返回码和内置的成功的返回码,来保证请求得到了正常响应,输出成功请求的消息,否则程序终止,这里我们用 requests.codes.ok 得到的是成功的状态码 200。

这样的话,我们就不用再在程序里面写状态吗对应的数字了,用字符串表示状态码会显得更加直观。

当然,肯定不能只有 ok 这个条件码。

下面列出了返回码和相应的查询条件:

# 信息性状态码
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’,),

# 重定向状态码
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

# 客户端错误状态码
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’,),

# 服务端错误状态码
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 请求以及 Response 对象。下面我们再来了解下 requests 的一些高级用法,如文件上传、Cookie 设置、代理设置等。

文件上传

我们知道 requests 可以模拟提交一些数据。假如有的网站需要上传文件,我们也可以用它来实现,这非常简单,示例如下:

import requests

files = {‘file’: open(‘favicon.ico’, ‘rb’)}
r = requests.post(‘https://httpbin.org/post‘, files=files)
print(r.text)

在前一节中我们保存了一个文件 favicon.ico,这次用它来模拟文件上传的过程。需要注意的是,favicon.ico 需要和当前脚本在同一目录下。如果有其他文件,当然也可以使用其他文件来上传,更改下代码即可。

运行结果如下:

{
“args”: {},
“data”: “”,
“files”: {
“file”: “data:application/octet-stream;base64,AAABAAI…”
},
“form”: {},
“headers”: {
“Accept”: “/“,
“Accept-Encoding”: “gzip, deflate”,
“Content-Length”: “6665”,
“Content-Type”: “multipart/form-data; boundary=41fc691282cc894f8f06adabb24f05fb”,
“Host”: “httpbin.org”,
“User-Agent”: “python-requests/2.22.0”,
“X-Amzn-Trace-Id”: “Root=1-5e6e3c0b-45b07bdd3a922e364793ef48”
},
“json”: null,
“origin”: “16.20.232.237”,
“url”: “https://httpbin.org/post
}

以上省略部分内容,这个网站会返回响应,里面包含 files 这个字段,而 form 字段是空的,这证明文件上传部分会单独有一个 files 字段来标识。

前面我们使用 urllib 处理过 Cookie,写法比较复杂,而有了 requests,获取和设置 Cookie 只需一步即可完成。

我们先用一个实例看一下获取 Cookie 的过程:

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/>]>
BDORZ=27315

这里我们首先调用 cookies 属性即可成功得到 Cookie,可以发现它是 RequestCookieJar 类型。然后用 items 方法将其转化为元组组成的列表,遍历输出每一个 Cookie 条目的名称和值,实现 Cookie 的遍历解析。

当然,我们也可以直接用 Cookie 来维持登录状态,下面我们以 GitHub 为例来说明一下,首先我们登录 GitHub,然后将 Headers 中的 Cookie 内容复制下来,如图所示。

image-20200301214840166

这里可以替换成你自己的 Cookie,将其设置到 Headers 里面,然后发送请求,示例如下:

import requests

headers = {
‘Cookie’: ‘_octo=GH1.1.1849343058.1576602081; _ga=GA1.2.90460451.1576602111; _Host-user_session_same_site=nbDv62kHNjp4N5KyQNYZ208waeqsmNgxFnFC88rnV7gTYQw; device_id=a7ca73be0e8f1a81d1e2ebb5349f9075; user_session=nbDv62kHNjp4N5KyQNYZ208waeqsmNgxFnFC88rnV7gTYQw; logged_in=yes; dotcom_user=Germey; tz=Asia%2FShanghai; has_recent_activity=1; _gat=1; _gh_sess=your_session_info’,
‘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://github.com/‘, headers=headers)
print(r.text)

我们发现,结果中包含了登录后才能包含的结果,如图所示:

image-20200301215251376

可以看到这里包含了我的 GitHub 用户名信息,你如果尝试之后同样可以得到你的用户信息。

得到这样类似的结果,就说明我们用 Cookie 就成功模拟了登录状态,这样我们就能爬取登录之后才能看到的页面了。

当然,我们也可以通过 cookies 参数来设置 Cookie 的信息,这里我们可以构造一个 RequestsCookieJar 对象,然后把刚才复制的 Cookie 处理下并赋值,示例如下:

import requests

cookies = ‘_octo=GH1.1.1849343058.1576602081; _ga=GA1.2.90460451.1576602111; _Host-user_session_same_site=nbDv62kHNjp4N5KyQNYZ208waeqsmNgxFnFC88rnV7gTYQw; device_id=a7ca73be0e8f1a81d1e2ebb5349f9075; user_session=nbDv62kHNjp4N5KyQNYZ208waeqsmNgxFnFC88rnV7gTYQw; logged_in=yes; dotcom_user=Germey; tz=Asia%2FShanghai; has_recent_activity=1; _gat=1; _gh_sess=your_session_info’
jar = requests.cookies.RequestsCookieJar()
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’
}
for cookie in cookies.split(‘;’):
key, value = cookie.split(‘=’, 1)
jar.set(key, value)
r = requests.get(‘https://github.com/‘, cookies=jar, headers=headers)
print(r.text)

这里我们首先新建了一个 RequestCookieJar 对象,然后将复制下来的 cookies 利用 split 方法分割,接着利用 set 方法设置好每个 Cookie 的 key 和 value,然后通过调用 requests 的 get 方法并传递给 cookies 参数即可。

测试后,发现同样可以正常登录。

Session 维持

在 requests 中,如果直接利用 get 或 post 等方法的确可以做到模拟网页的请求,但是这实际上是相当于不同的 Session,也就是说相当于你用了两个浏览器打开了不同的页面。

设想这样一个场景,第一个请求利用 requests 的 post 方法登录了某个网站,第二次想获取成功登录后的自己的个人信息,又用了一次 requests 的 get 方法去请求个人信息页面。

实际上,这相当于打开了两个浏览器,是两个完全独立的操作,对应两个完全不相关的 Session,能成功获取个人信息吗?那当然不能。

有人可能说了,我在两次请求时设置一样的 Cookies 不就行了?可以,但这样做起来显得很烦琐,我们有更简单的解决方法。

其实解决这个问题的主要方法就是维持同一个 Session,也就是相当于打开一个新的浏览器选项卡而不是新开一个浏览器。但是我又不想每次设置 Cookies,那该怎么办呢?这时候就有了新的利器 —— Session 对象。

利用它,我们可以方便地维护一个 Session,而且不用担心 Cookie 的问题,它会帮我们自动处理好。

我们先做一个小实验吧,如果沿用之前的写法,示例如下:

import requests

requests.get(‘https://httpbin.org/cookies/set/number/123456789‘)
r = requests.get(‘https://httpbin.org/cookies‘)
print(r.text)

这里我们请求了一个测试网址 https://httpbin.org/cookies/set/number/123456789。请求这个网址时,可以设置一个 Cookie 条目,名称叫作 number,内容是 123456789,随后又请求了 https://httpbin.org/cookies,此网址可以获取当前的 Cookie 信息。

这样能成功获取到设置的 Cookie 吗?试试看。

运行结果如下:

{
“cookies”: {}
}

这并不行。

这时候,我们再用刚才所说的 Session 试试看:

import requests

s = requests.Session()
s.get(‘https://httpbin.org/cookies/set/number/123456789‘)
r = s.get(‘https://httpbin.org/cookies‘)
print(r.text)

再看下运行结果:

{
“cookies”: {“number”: “123456789”}
}

这些可以看到 Cookies 被成功获取了!这下能体会到同一个会话和不同会话的区别了吧!

所以,利用 Session,可以做到模拟同一个会话而不用担心 Cookie 的问题。它通常用于模拟登录成功之后再进行下一步的操作。

Session 在平常用得非常广泛,可以用于模拟在一个浏览器中打开同一站点的不同页面,后面会有专门的章节来讲解这部分内容。

SSL 证书验证

现在很多网站都要求使用 HTTPS 协议,但是有些网站可能并没有设置好 HTTPS 证书,或者网站的 HTTPS 证书可能并不被 CA 机构认可,这时候,这些网站可能就会出现 SSL 证书错误的提示。

比如这个示例网站:https://ssr2.scrape.center/,如果我们用 Chrome 浏览器打开这个 URL,则会提示「您的连接不是私密连接」这样的错误,如图所示:

image-20210704204017465

我们可以在浏览器中通过一些设置来忽略证书的验证。

但是如果我们想用 requests 来请求这类网站,会遇到什么问题呢?我们用代码来试一下:

import requests

response = requests.get(‘https://ssr2.scrape.center/‘)
print(response.status_code)

运行结果如下:

requests.exceptions.SSLError: HTTPSConnectionPool(host=’ssr2.scrape.center’, port=443): Max retries exceeded with url: / (Caused by SSLError(SSLCertVerificationError(1, ‘[SSL: CERTIFICATE_VERIFY_FAILED] certificate verify failed: unable to get local issuer certificate (_ssl.c:1056)’)))

可以看到,这里直接抛出了 SSLError 错误,原因就是因为我们请求的 URL 的证书是无效的。

那如果我们一定要爬取这个网站怎么办呢?我们可以使用 verify 参数控制是否验证证书,如果将其设置为 False,在请求时就不会再验证证书是否有效。如果不加 verify 参数的话,默认值是 True,会自动验证。

我们改写代码如下:

import requests

response = requests.get(‘https://ssr2.scrape.center/‘, verify=False)
print(response.status_code)

这样就会打印出请求成功的状态码:

/usr/local/lib/python3.7/site-packages/urllib3/connectionpool.py:857: 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://ssr2.scrape.center/‘, verify=False)
print(response.status_code)

或者通过捕获警告到日志的方式忽略警告:

import logging
import requests

logging.captureWarnings(True)
response = requests.get(‘https://ssr2.scrape.center/‘, verify=False)
print(response.status_code)

当然,我们也可以指定一个本地证书用作客户端证书,这可以是单个文件(包含密钥和证书)或一个包含两个文件路径的元组:

import requests

response = requests.get(‘https://ssr2.scrape.center/‘, cert=(‘/path/server.crt’, ‘/path/server.key’))
print(response.status_code)

当然,上面的代码是演示实例,我们需要有 crt 和 key 文件,并且指定它们的路径。另外注意,本地私有证书的 key 必须是解密状态,加密状态的 key 是不支持的。

超时设置

在本机网络状况不好或者服务器网络响应太慢甚至无响应时,我们可能会等待特别久的时间才可能收到响应,甚至到最后收不到响应而报错。为了防止服务器不能及时响应,应该设置一个超时时间,即超过了这个时间还没有得到响应,那就报错。这需要用到 timeout 参数。这个时间的计算是发出请求到服务器返回响应的时间。示例如下:

import requests

r = requests.get(‘https://httpbin.org/get‘, timeout=1)
print(r.status_code)

通过这样的方式,我们可以将超时时间设置为 1 秒,如果 1 秒内没有响应,那就抛出异常。

实际上,请求分为两个阶段,即连接(connect)和读取(read)。

上面设置的 timeout 将用作连接和读取这二者的 timeout 总和。

如果要分别指定,就可以传入一个元组:

r = requests.get(‘https://httpbin.org/get‘, timeout=(5, 30))

如果想永久等待,可以直接将 timeout 设置为 None,或者不设置直接留空,因为默认是 None。这样的话,如果服务器还在运行,但是响应特别慢,那就慢慢等吧,它永远不会返回超时错误的。其用法如下:

r = requests.get(‘https://httpbin.org/get‘, timeout=None)

或直接不加参数:

r = requests.get(‘https://httpbin.org/get‘)

身份认证

在上一节我们讲到,在访问启用了基本身份认证的网站时,我们会首先遇到一个认证窗口,例如:https://ssr3.scrape.center/,如图所示。

image-20210704202140395

这个网站就是启用了基本身份认证,在上一节中我们可以利用 urllib 来实现身份的校验,但实现起来相对繁琐。那在 reqeusts 中怎么做呢?当然也有办法。

我们可以使用 requests 自带的身份认证功能,通过 auth 参数即可设置,示例如下:

import requests
from requests.auth import HTTPBasicAuth

r = requests.get(‘https://ssr3.scrape.center/‘, auth=HTTPBasicAuth(‘admin’, ‘admin’))
print(r.status_code)

这个示例网站的用户名和密码都是 admin,在这里我们可以直接设置。

如果用户名和密码正确的话,请求时就会自动认证成功,会返回 200 状态码;如果认证失败,则返回 401 状态码。

当然,如果参数都传一个 HTTPBasicAuth 类,就显得有点烦琐了,所以 requests 提供了一个更简单的写法,可以直接传一个元组,它会默认使用 HTTPBasicAuth 这个类来认证。

所以上面的代码可以直接简写如下:

import requests

r = requests.get(‘https://ssr3.scrape.center/‘, auth=(‘admin’, ‘admin’))
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/,在此就不再赘述了。

代理设置

对于某些网站,在测试的时候请求几次,能正常获取内容。但是一旦开始大规模爬取,对于大规模且频繁的请求,网站可能会弹出验证码,或者跳转到登录认证页面,更甚者可能会直接封禁客户端的 IP,导致一定时间段内无法访问。

那么,为了防止这种情况发生,我们需要设置代理来解决这个问题,这就需要用到 proxies 参数。可以用这样的方式设置:

import requests

proxies = {
‘http’: ‘http://10.10.10.10:1080‘,
‘https’: ‘http://10.10.10.10:1080‘,
}
requests.get(‘https://httpbin.org/get‘, proxies=proxies)

当然,直接运行这个实例可能不行,因为这个代理可能是无效的,可以直接搜索寻找有效的代理并替换试验一下。

若代理需要使用上文所述的身份认证,可以使用类似 http://user:password@host:port 这样的语法来设置代理,示例如下:

import requests

proxies = {‘https’: ‘http://user:password@10.10.10.10:1080/',}
requests.get(‘https://httpbin.org/get‘, 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://httpbin.org/get‘, proxies=proxies)

Prepared Request

我们使用 requests 库的 get 和 post 方法当然直接可以发送请求,但有没有想过,这个请求在 requests 内部是怎么实现的呢?

实际上,requests 在发送请求的时候,是在内部构造了一个 Request 对象,并给这个对象赋予了各种参数,包括 url、headers、data 等等,然后直接把这个 Request 对象发送出去,请求成功后会再得到一个 Response 对象,再解析即可。

那么这个 Request 是什么类型呢?实际上它就是 Prepared Request。

我们深入一下,不用 get 方法,直接构造一个 Prepared Request 对象来试试,代码如下:

from requests import Request, Session

url = ‘https://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”,
“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”,
“X-Amzn-Trace-Id”: “Root=1-5e5bd6a9-6513c838f35b06a0751606d8”
},
“json”: null,
“origin”: “167.220.232.237”,
“url”: “http://httpbin.org/post
}

可以看到,我们达到了同样的 POST 请求效果。

有了 Request 这个对象,就可以将请求当作独立的对象来看待,这样在一些场景中我们可以直接操作这个 Request 对象,更灵活地实现请求的调度和各种操作。

更多的用法可以参考 requests 的官方文档:http://docs.python-requests.org/

7. 总结

本节的 requests 库的基本用法就介绍到这里了,怎么样?有没有感觉它比 urllib 使用起来更为方便。本节内容需要好好掌握,在后文我们会在实战中使用 requests 完成一个网站的爬取,巩固 requests 的相关知识。

本节代码:https://github.com/Python3WebSpider/RequestsTest。

正则表达式

在上一节中,我们已经可以用 requests 来获取网页的源代码,得到 HTML 代码。但我们真正想要的数据是包含在 HTML 代码之中的,怎么才能从 HTML 代码中获取我们想要的信息呢?正则表达式就是其中一个有效的方法。

本节中,我们了解一下正则表达式的相关用法。正则表达式是处理字符串的强大工具,它有自己特定的语法结构,有了它,实现字符串的检索、替换、匹配验证都不在话下。

当然,对于爬虫来说,有了它,从 HTML 里提取想要的信息就非常方便了。

1. 实例引入

说了这么多,可能我们对它到底是个什么还是比较模糊,下面就用几个实例来看一下正则表达式的用法。

打开开源中国提供的正则表达式测试工具 http://tool.oschina.net/regex/,输入待匹配的文本,然后选择常用的正则表达式,就可以得出相应的匹配结果了。例如,这里输入待匹配的文本,具体如下:

Hello, my phone number is 010-86432100 and email is cqc@cuiqingcai.com, and my website is https://cuiqingcai.com

这段字符串中包含了一个电话号码和一个电子邮件和一个 URL,接下来就尝试用正则表达式提取出来,如图所示。

在网页右侧选择「匹配 Email 地址」,就可以看到下方出现了文本中的 E-mail。

如果选择「匹配网址 URL」,就可以看到下方出现了文本中的 URL。

在网页右侧选择 “匹配 Email 地址”,就可以看到下方出现了文本中的 E-mail。如果选择 “匹配网址 URL”,就可以看到下方出现了文本中的 URL。是不是非常神奇?

其实,这里就是用了正则表达式匹配,也就是用一定的规则将特定的文本提取出来。比如,电子邮件开头是一段字符串,然后是一个 @ 符号,最后是某个域名,这是有特定的组成格式的。另外,对于 URL,开头是协议类型,然后是冒号加双斜线,最后是域名加路径。

对于 URL 来说,可以用下面的正则表达式匹配:

[a-zA-z]+://[^\s]*

用这个正则表达式去匹配一个字符串,如果这个字符串中包含类似 URL 的文本,就会被提取出来。

这个正则表达式看上去是乱糟糟的一团,其实不然,这里面都是有特定的语法规则的。比如,a-z 代表匹配任意的小写字母,\s 表示匹配任意的空白字符,* 就代表匹配前面的字符任意多个,这一长串的正则表达式就是这么多匹配规则的组合。

写好正则表达式后,就可以拿它去一个长字符串里匹配查找了。不论这个字符串里面有什么,只要符合我们写的规则,统统可以找出来。对于网页来说,如果想找出网页源代码里有多少 URL,用匹配 URL 的正则表达式去匹配即可。

上面我们说了几个匹配规则,表 2- 列出了常用的匹配规则。

表 2- 常用的匹配规则

模  式 描  述
\w 匹配字母、数字及下划线
\W 匹配不是字母、数字及下划线的字符
\s 匹配任意空白字符,等价于 [\t\n\r\f]
\S 匹配任意非空字符
\d 匹配任意数字,等价于 [0-9]
\D 匹配任意非数字的字符
\A 匹配字符串开头
\Z 匹配字符串结尾,如果存在换行,只匹配到换行前的结束字符串
\z 匹配字符串结尾,如果存在换行,同时还会匹配换行符
\G 匹配最后匹配完成的位置
\n 匹配一个换行符
\t 匹配一个制表符
^ 匹配一行字符串的开头
$ 匹配一行字符串的结尾
. 匹配任意字符,除了换行符,当 re.DOTALL 标记被指定时,则可以匹配包括换行符的任意字符
[...] 用来表示一组字符,单独列出,比如 [amk] 匹配 am 或 k
[^...] 不在 [] 中的字符,比如 匹配除了 abc 之外的字符
* 匹配 0 个或多个表达式
+ 匹配 1 个或多个表达式
? 匹配 0 个或 1 个前面的正则表达式定义的片段,非贪婪方式
{n} 精确匹配 n 个前面的表达式
{n, m} 匹配 n 到 m 次由前面正则表达式定义的片段,贪婪方式
`a b`
() 匹配括号内的表达式,也表示一个组

看完了之后,可能有点晕晕的吧,不过不用担心,后面我们会详细讲解一些常见规则的用法。

其实正则表达式不是 Python 独有的,它也可以用在其他编程语言中。Python 的 re 库提供了整个正则表达式的实现,利用这个库,可以在 Python 中使用正则表达式。在 Python 中写正则表达式几乎都用这个库,下面就来了解它的一些常用方法。

2. 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(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,最后得到的内容就只有数字 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 了。原因可想而知,贪婪匹配是尽可能匹配多的字符,非贪婪匹配就是尽可能匹配少的字符。当 .*? 匹配到 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)
in ()
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)

这个修饰符的作用是使。匹配包括换行符在内的所有字符。此时运行结果如下:

1234567

这个 re.S 在网页匹配中经常用到。因为 HTML 节点经常会有换行,加上它,就可以匹配节点与节点之间的换行了。

另外,还有一些修饰符,在必要的情况下也可以使用,如表 2- 所示。

表 2- 修饰符及其描述

修饰符 描  述
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'>

可以看到,这里成功匹配到了原字符串。

这些是写正则表达式常用的几个知识点,熟练掌握它们对后面写正则表达式非常有帮助。

前面提到过,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 = ‘’’

经典老歌


经典老歌列表




‘’’

可以观察到,ul 节点里有许多 li 节点,其中 li 节点中有的包含 a 节点,有的不包含 a 节点,a 节点还有一些相应的属性 —— 超链接和歌手名。

首先,我们尝试提取 class 为 active 的 li 节点内部的超链接包含的歌手名和歌名,此时需要提取第三个 li 节点下 a 节点的 singer 属性和文本。

此时正则表达式可以以 li 开头,然后寻找一个标志符 active,中间的部分可以用 .*? 来匹配。接下来,要提取 singer 这个属性值,所以还需要写入 singer="(.*?)",这里需要提取的部分用小括号括起来,以便用 group 方法提取出来,它的两侧边界是双引号。然后还需要匹配 a 节点的文本,其中它的左边界是 >,右边界是 </a>。然后目标内容依然用 (.*?) 来匹配,所以最后的正则表达式就变成了:

<li.?active.?singer=”(.?)”>(.?)

然后再调用 search 方法,它会搜索整个 HTML 文本,找到符合正则表达式的第一个内容返回。

另外,由于代码有换行,所以这里第三个参数需要传入 re.S。整个匹配代码如下:

result = re.search(‘<li.?active.?singer=”(.?)”>(.?)‘, html, re.S)
if result:
print(result.group(1), result.group(2))

由于需要获取的歌手和歌名都已经用小括号包围,所以可以用 group 方法获取。

运行结果如下:

齐秦 往事随风

可以看到,这正是 class 为 active 的 li 节点内部的超链接包含的歌手名和歌名。

如果正则表达式不加 active(也就是匹配不带 class 为 active 的节点内容),那会怎样呢?我们将正则表达式中的 active 去掉,代码改写如下:

result = re.search(‘<li.?singer=”(.*?)”>(.?)‘, 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=”(.*?)”>(.?)‘, html)
if result:
print(result.group(1), result.group(2))

运行结果如下:

beyond 光辉岁月

可以看到,结果变成了第四个 li 节点的内容。这是因为第二个和第三个 li 节点都包含了换行符,去掉 re.S 之后,.*? 已经不能匹配换行符,所以正则表达式不会匹配到第二个和第三个 li 节点,而第四个 li 节点中不包含换行符,所以成功匹配。

由于绝大部分的 HTML 文本都包含了换行符,所以尽量都需要加上 re.S 修饰符,以免出现匹配不到的问题。

4. findall

前面我们介绍了 search 方法的用法,它可以返回匹配正则表达式的第一个内容,但是如果想要获取匹配正则表达式的所有内容,那该怎么办呢?这时就要借助 findall 方法了。该方法会搜索整个字符串,然后返回匹配正则表达式的所有内容。

还是上面的 HTML 文本,如果想获取所有 a 节点的超链接、歌手和歌名,就可以将 search 方法换成 findall 方法。如果有返回结果的话,就是列表类型,所以需要遍历一下来依次获取每组内容。代码如下:

results = re.findall(‘<li.?href=”(.?)”.?singer=”(.*?)”>(.?)‘, 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 方法。

5. sub

除了使用正则表达式提取信息外,有时候还需要借助它来修改文本。比如,想要把一串文本中的所有数字都去掉,如果只用字符串的 replace 方法,那就太烦琐了,这时可以借助 sub 方法。示例如下:

import re

content = ‘54aK54yr5oiR54ix5L2g’
content = re.sub(‘\d+’, ‘’, content)
print(content)

运行结果如下:

aKyroiRixLg

这里只需要给第一个参数传入 \d+ 来匹配所有的数字,第二个参数为替换成的字符串(如果去掉该参数的话,可以赋值为空),第三个参数是原字符串。

在上面的 HTML 文本中,如果想获取所有 li 节点的歌名,直接用正则表达式来提取可能比较烦琐。比如,可以写成这样子:

results = re.findall(‘<li.?>\s?(<a.?>)?(\w+)()?\s?‘, html, re.S)
for result in results:
print(result[1])

运行结果如下:

一路上有你
沧海一声笑
往事随风
光辉岁月
记事本
但愿人长久

此时借助 sub 方法就比较简单了。可以先用 sub 方法将 a 节点去掉,只留下文本,然后再利用 findall 提取就好了:

html = re.sub(‘<a.?>|‘, ‘’, html)
print(html)
results = re.findall(‘<li.*?>(.
?)‘, html, re.S)
for result in results:
print(result.strip())

运行结果如下:


经典老歌



经典老歌列表



  • 一路上有你


  • 沧海一声笑


  • 往事随风

  • 光辉岁月

  • 记事本


  • 但愿人长久



一路上有你
沧海一声笑
往事随风
光辉岁月
记事本
但愿人长久

可以看到,a 节点经过 sub 方法处理后就没有了,然后再通过 findall 方法直接提取即可。可以看到,在适当的时候,借助 sub 方法可以起到事半功倍的效果。

6. compile

前面所讲的方法都是用来处理字符串的方法,最后再介绍一下 compile 方法,这个方法可以将正则字符串编译成正则表达式对象,以便在后面的匹配中复用。示例代码如下:

import re

content1 = ‘2019-12-15 12:00’
content2 = ‘2019-12-17 12:55’
content3 = ‘2019-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)

例如,这里有 3 个日期,我们想分别将 3 个日期中的时间去掉,这时可以借助 sub 方法。该方法的第一个参数是正则表达式,但是这里没有必要重复写 3 个同样的正则表达式,此时可以借助 compile 方法将正则表达式编译成一个正则表达式对象,以便复用。

运行结果如下:

2019-12-15 2019-12-17 2019-12-22

另外,compile 还可以传入修饰符,例如 re.S 等修饰符,这样在 searchfindall 等方法中就不需要额外传了。所以,compile 方法可以说是给正则表达式做了一层封装,以便我们更好地复用。

7. 总结

到此为止,正则表达式的基本用法就介绍完了,后面会通过具体的实例来讲解正则表达式的用法。

本节代码:https://github.com/Python3WebSpider/RegexTest。


爬虫编程基础
http://mavericreate.top/Blogs/2025/08/28/爬虫编程基础/
作者
唐浩天
发布于
2025年8月28日
许可协议