urllib的使用

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

在 Python2 中,有 urlliburllib2 两个库来实现 HTTP 请求的发送。而在 Python3 中,urllib2 库已经不存在了,统一为了 urllib

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

  • request: 这是最基本的 HTTP 请求模块,可以模拟请求的发送。就像在浏览器里输入网址然后按下回车一样,只需要给库方法传入 URL 以及额外的参数,就可以模拟实现发送请求的过程了。

  • error: 异常处理模块。如果出现请求异常,那么我们可以捕获这些异常,然后进行重试或其他操作以保证程序运行不会意外终止。

  • parse: 一个工具模块。提供了许多 URL 的处理方法,例如拆分、解析、合并等。

  • robotparser: 主要用来识别网站的 robots.txt 文件,然后判断哪些网站可以爬,哪些网站不可以,它其实用得比较少。

发送请求

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

urlopen

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

下面我们体会一下 request 模块的强大之处。这里以 Python 官网为例,我们把这个网页抓取下来:

import urllib.request

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

上面的代码不你能正确执行,修改代码如下:

import gzip
import io
import urllib.request

url = "https://www.python.org"
request = urllib.request.Request(url)
request.add_header('Accept-Encoding', 'gzip')  # 告诉服务器你支持 Gzip

response = urllib.request.urlopen(request)

# 检查是否使用了 Gzip 压缩
if response.info().get('Content-Encoding') == 'gzip':
    # 解压缩数据
    buffer = io.BytesIO(response.read())
    decompressed_data = gzip.GzipFile(fileobj=buffer).read()
    print(decompressed_data.decode('utf-8'))
else:
    # 如果没有压缩,直接解码
    print(response.read().decode('utf-8'))

运行结果如图 2-1 所示。

image 2025 06 17 13 09 45 732
Figure 1. 图 2-1 运行结果

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

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

import urllib.request

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

输出结果如下:

<class 'http.client.HTTPResponse'>

可以看出,响应是一个 HTTPResponse 类型的对象,主要包含 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, VSO,VEO'), ('Vary', 'Cookie'), ('Strict-Transport-Security', 'max-age=63072000; includeSubDomains')]
nginx

其中前两个输出分别是响应的状态码和响应的头信息;最后一个输出是调用 getheader 方法,并传入参数 Server,获取了响应头中 Server 的值,结果是 nginx,意思为服务器是用 Nginx 搭建的。

利用最基本的 urlopen 方法,已经可以完成对简单网页的 GET 请求抓取。

如果想给链接传递一些参数,又该怎么实现呢? 首先看一下 urlopen 方法的 API:

urllib.request.urlopen(url, data=None, [timeout,]*,cafile=None, capath=None, cadefault=False, context=None)

可以发现,除了第一个参数用于传递 URL 之外,我们还可以传递其他内容,例如 data(附加数据)、timeout(超时时间)等。

接下来就详细说明一下 urlopen 方法中几个参数的用法。

  • data参数

    data 参数是可选的。在添加该参数时,需要使用 bytes 方法将参数转化为字节流编码格式的内容,即 bytes 类型。另外,如果传递了这个参数,那么它的请求方式就不再是 GET,而是 POST 了。

    下面用实例来看一下:

    import urllib.parse
    import urllib.request
    
    data = bytes(urllib.parse.urlencode({'name': 'germey'}), encoding='utf-8')
    response = urllib.request.urlopen('https://httpbin.org/post', data=data)
    print(response.read().decode('utf-8'))

    这里我们传递了一个参数 name,值是 germey,需要将它转码成 bytes 类型。转码时采用了 bytes 方法,该方法的第一个参数得是 str(字符串)类型,因此用 urllib.parse 模块里的 urlencode 方法将字典参数转化为字符串;第二个参数用于指定编码格式,这里指定为 utf-8

    此处我们请求的站点是 www.httpbin.org,它可以提供 HTTP 请求测试。本次我们请求的 URL 为 https://www.httpbin.org/post ,这个链接可以用来测试 POST 请求,能够输出请求的一些信息,其中就包含我们传递的 data 参数。

    上面实例的运行结果如下:

    {
      "args": {},
      "data": "",
      "files": {},
      "form": {
        "name": "germey"
      },
      "headers": {
        "Accept-Encoding": "identity",
        "Content-Length": "11",
        "Content-Type": "application/x-www-form-urlencoded",
        "Host": "httpbin.org",
        "User-Agent": "Python-urllib/3.13",
        "X-Amzn-Trace-Id": "Root=1-679495bf-599605be3a3642aa625446ce"
      },
      "json": null,
      "origin": "34.80.45.10",
      "url": "https://httpbin.org/post"
    }

    可以发现我们传递的参数出现在了 form 字段中,这表明是模拟表单提交,以 POST 方式传输数据。

  • timeout参数

    timeout 参数用于设置超时时间,单位为秒,意思是如果请求超出了设置的这个时间,还没有得到响应,就会抛出异常。如果不指定该参数,则会使用全局默认时间。这个参数支持 HTTP、HTTPS、FTP 请求。

    下面用实例来看—下:

    import urllib.request
    
    response = urllib.request.urlopen('https://httpbin.org/get', timeout=0.1)
    print(response.read())

    运行结果可能如下:

    During handling of the above exception, another exception occurred:
    
    Traceback (most recent call last):
      File "D:\BaiduSyncdisk\sourceCode\python\python_learn_roadmap\src\Python3WebScrapyDevelopInAction\UrllibTest\demo5.py", line 3, in <module>
    ...
    urllib.error.URLError: <urlopen error _ssl.c:1001: The handshake operation timed out>

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

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

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

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

    运行结果如下:

    TIME OUT

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

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

  • 其它参数

    除了 data 参数和 timeout 参数,urlopen 方法还有 context 参数,该参数必须是 ssl.SSLContext 类型,用来指定 SSL 的设置。

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

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

    至此,我们讲解了 urlopen 方法的用法,通过这个最基本的方法,就可以完成简单的请求和网页抓取。

  • Request

    利用 urlopen 方法可以发起最基本的请求,但它那几个简单的参数并不足以构建一个完整的请求。如果需要往请求中加入 Headers 等信息,就得利用更强大的 Request 类来构建请求了。

    首先,我们用实例感受一下 Request 类的用法:

    import urllib.request
    
    request = urllib.request.Request('https://python.org')
    response = urllib.request.urlopen(request)
    print(response.read().decode('utf-8'))

    可以发现,我们依然是用 urlopen 方法来发送请求,只不过这次该方法的参数不再是 URL,而是一个 Request 类型的对象。通过构造这个数据结构,一方面可以将请求独立成一个对象,另一方面可更加丰富和灵活地配置参数。

    下面我们看一下可以通过怎样的参数来构造 Request 类,构造方法如下:

    class urllib.request.Request(url, data=None, headers={}, origin_req_host=None, unverifiable=False, method=None)
    • 第一个参数 url 用于请求 URL,这是必传参数,其他的都是可选参数。

    • 第二个参数 data 如果要传数据,必须传 bytes 类型的。如果数据是字典,可以先用 urllib.parse 模块里的 urlencode 方法进行编码。

    • 第三个参数 headers 是一个字典,这就是请求头,我们在构造请求时,既可以通过 headers 参数直接构造此项,也可以通过调用请求实例的 add_header 方法添加。

      添加请求头最常见的方法就是通过修改 User-Agent 来伪装浏览器。默认的 User-AgentPython-urllib,我们可以通过修改这个值来伪装浏览器。例如要伪装火狐浏览器,就可以把 User-Agent 设置为:

      Mozilla/5.0 (X11; U; Linux i686) Gecko/20071127 Firefox/2.0.0.11
    • 第四个参数 origin_req_host 指的是请求方的 host 名称或者 IP 地址。

    • 第五个参数 unverifiable 表示请求是否是无法验证的,默认取值是 False,意思是用户没有足够的权限来接收这个请求的结果。例如,请求一个 HTML 文档中的图片,但是没有自动抓取图像的权限,这时 unverifiable 的值就是 True

    • 第六个参数 method 是一个字符串,用来指示请求使用的方法,例如 GETPOSTPUT 等。

    下面我们传入多个参数尝试构建 Request 类:

    from urllib import request, parse
    
    url = 'https://httpbin.org/post'
    headers = {
        'User-Agent': 'Mozilla/4.0 (compatible; MSIE 5.5; Windows NT)',
        'Host': 'httpbin.org'
    }
    dict = {'name': 'germey'}
    data = bytes(parse.urlencode(dict), encoding='utf-8')
    req = request.Request(url=url, data=data, headers=headers, method='POST')
    response = request.urlopen(req)
    print(response.read().decode('utf-8'))

    这里我们通过 4 个参数构造了一个 Request 类,其中的 url 即请求 URL,headers 中指定了 User-AgentHostdataurlencode 方法和 bytes 方法把字典数据转成字节流格式。另外,指定了请求方式为 POST。

    运行结果如下:

    {
      "args": {},
      "data": "",
      "files": {},
      "form": {
        "name": "germey"
      },
      "headers": {
        "Accept-Encoding": "identity",
        "Content-Length": "11",
        "Content-Type": "application/x-www-form-urlencoded",
        "Host": "httpbin.org",
        "User-Agent": "Mozilla/4.0 (compatible; MSIE 5.5; Windows NT)",
        "X-Amzn-Trace-Id": "Root=1-67949de7-52e6f15b2e605e767378dfcc"
      },
      "json": null,
      "origin": "34.80.45.10",
      "url": "https://httpbin.org/post"
    }

    观察结果可以发现,我们成功设置了 dataheadersmethod

    通过 add_header 方法添加 headers 的方式如下:

    req = request.Request(url=url, data=data, method='POST')
    req.add_header('User-Agent', 'Mozilla/4.0 (compatible; MSIE 5.5; Windows NT)')

    有了 Request 类,我们就可以更加方便地构建请求,并实现请求的发送啦。

  • 高级用法

    我们已经可以构建请求了,那么对于一些更高级的操作(例如 Cookie 处理、代理设置等),又该怎么实现呢?

    此时需要更强大的工具,于是 Handler 登场了。简而言之,Handler 可以理解为各种处理器,有专门处理登录验证的、处理 Cookie 的、处理代理设置的。利用这些 Handler,我们几乎可以实现 HTTP 请求中所有的功能。

    首先介绍一下 urllib.request 模块里的 BaseHandler 类,这是其他所有 Handler 类的父类。它提供了最基本的方法,例如 default_openprotocol_request 等。

    会有各种 Handler 子类继承 BaseHandler 类,接下来举几个子类的例子如下。

    • HTTPDefaultErrorHandler 用于处理 HTTP 响应错误,所有错误都会抛出 HTTPError 类型的异常。

    • HTTPRedirectHandler 用于处理重定向。

    • HTTPCookieProcessor 用于处理 Cookie

    • ProxyHandler 用于设置代理,代理默认为空。

    • HTTPPasswordMgr 用于管理密码,它维护着用户名密码的对照表。

    • HTTPBasicAuthHandler 用于管理认证,如果一个链接在打开时需要认证,那么可以用这个类来解决认证问题。

    关于这些类如何使用,现在先不急着了解,后面会用实例演示。

    另一个比较重要的类是 OpenerDirector,我们可以称之为 Opener。我们之前用过的 urlopen 方法,实际上就是 urllib 库为我们提供的一个 Opener

    那么,为什么要引入 Opener 呢? 因为需要实现更高级的功能。之前使用的 Request 类和 urlopen 类相当于类库已经封装好的极其常用的请求方法,利用这两个类可以完成基本的请求,但是现在我们需要实现更高级的功能,就需要深入一层进行配置,使用更底层的实例来完成操作,所以这里就用到了 Opener

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

    下面用几个实例来看看 Handler 类和 Opener 类的用法。

    • 验证

    在访问某些网站时,例如 https://ssr3.scrape.center ,可能会弹出这样的认证窗口,如图 2-2 所示。

    image 2025 01 25 16 35 15 808
    Figure 2. 图 2-2 认证窗口

    遇到这种情况,就表示这个网站启用了基本身份认证,英文叫作 HTTP Basic Access Authentication,这是一种登录验证方式,允许网页浏览器或其他客户端程序在请求网站时提供用户名和口令形式的身份凭证。

    那么爬虫如何请求这样的页面呢?借助 HTTPBasicAuthHandler 模块就可以完成,相关代码如下:

    from urllib.request import HTTPPasswordMgrWithDefaultRealm, HTTPBasicAuthHandler, build_opener
    from urllib.error import URLError
    
    username = 'admin'
    password = 'admin'
    url = 'https://static3.scrape.center/'
    
    p = HTTPPasswordMgrWithDefaultRealm()
    p.add_password(None, url, username, password)
    auth_handler = HTTPBasicAuthHandler(p)
    opener = build_opener(auth_handler)
    
    try:
        result = opener.open(url)
        html = result.read().decode('utf-8')
        print(html)
    except URLError as e:
        print(e.reason)

    这里首先实例化了一个 HTTPBasicAuthHandler 对象 auth_handler,其参数是 HTTPPasswordMgrWithDefaultRealm 对象,它利用 add_password 方法添加用户名和密码,这样就建立了一个用来处理验证的 Handler 类。

    然后将刚建立的 auth_handler 类当作参数传入 build_opener 方法,构建一个 Opener,这个 Opener 在发送请求时就相当于已经验证成功了。

    最后利用 Opener 类中的 open 方法打开链接,即可完成验证。这里获取的结果就是验证成功后的页面源码内容。

    • 代理

    做爬虫的时候,免不了要使用代理,如果要添加代理,可以这样做:

    from urllib.error import URLError
    from urllib.request import ProxyHandler, build_opener
    
    proxy_handler = ProxyHandler({
        'http': 'http://127.0.0.1:8080',
        'https': 'https://127.0.0.1:8080',
    })
    opener = build_opener(proxy_handler)
    try:
        response = opener.open('http://www.baidu.com/')
        print(response.read().decode('utf-8'))
    except URLError as e:
        print(e.reason)

    这里需要我们事先在本地搭建一个 HTTP 代理,并让其运行在 8080 端口上。

    上面使用了 proxyHandler,其参数是一个字典,键名是协议类型(例如 HTTP 或者 HTTPS 等)、键值是代理链接,可以添加多个代理。

    然后利用这个 Handlerbuild_opener 方法构建了一个 Opener,之后发送请求即可。

    • Cookie

    处理 Cookie 需要用到相关的 Handler

    我们先用实例来看看怎样获取网站的 Cookie,相关代码如下:

    import http.cookiejar, urllib.request
    
    cookie = http.cookiejar.CookieJar()
    handler = urllib.request.HTTPCookieProcessor(cookie)
    opener = urllib.request.build_opener(handler)
    response = opener.open('https://www.baidu.com')
    for item in cookie:
        print(item.name + "=" + item.value)

    首先,必须声明一个 CookieJar 对象。然后需要利用 HTTPCookieProcessor 构建一个 Handler,最后利用 build_opener 方法构建 Opener,执行 open 函数即可。

    运行结果如下:

    BAIDUID=F9529F238F411BFF08E94199204F1050:FG=1
    BIDUPSID=F9529F238F411BFF848961940DEC9115
    PSTM=1737795045
    BDSVRTM=0
    BD_HOME=1

    可以看到,这里分别输出了每个 Cookie 条目的名称和值。

    既然能输出,那么可不可以输出文件格式的内容呢?我们知道 Cookie 实际上也是以文本形式保存的。因此答案当然是肯定的,这里通过下面的实例来看看:

    import urllib.request, http.cookiejar
    
    filename = 'cookie.txt'
    cookie = http.cookiejar.MozillaCookieJar(filename)
    handler = urllib.request.HTTPCookieProcessor(cookie)
    opener = urllib.request.build_opener(handler)
    response = opener.open('https://www.baidu.com')
    cookie.save(ignore_discard=True, ignore_expires=True)

    这时需要将 CookieJar 换成 MozillaCookieJar,它会在生成文件时用到,是 CookieJar 的子类,可以用来处理跟 Cookie 和文件相关的事件,例如读取和保存 Cookie,可以将 Cookie 保存成 Mozilla 型浏览器的 Cookie 格式。

    运行上面的实例之后,会发现生成了一个 cookie.txt 文件,该文件内容如下:

    # Netscape HTTP Cookie File
    # http://curl.haxx.se/rfc/cookie_spec.html
    # This is a generated file!  Do not edit.
    
    .baidu.com	TRUE	/	FALSE	1769331199	BAIDUID	C34B5B19C759F06D59EB26D5C1658A06:FG=1
    .baidu.com	TRUE	/	FALSE	3885278846	BIDUPSID	C34B5B19C759F06D99ECC787FF44838F
    .baidu.com	TRUE	/	FALSE	3885278846	PSTM	1737795198
    www.baidu.com	FALSE	/	FALSE		BDSVRTM	0
    www.baidu.com	FALSE	/	FALSE		BD_HOME	1

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

    要保存 LWP 格式的 Cookie 文件,可以在声明时就进行修改:

    cookie = http.cookiejar.LWPCookieJar(filename)

    此时生成的内容如下:

    #LWP-Cookies-2.0
    Set-Cookie3: BAIDUID=1F30EEDA35C7A94320275F991CA5B3A5:FG=1; path="/"; domain=".baidu.com"; path_spec; domain_dot; expires="2021-05-30 16:06:39Z"; comment=bd; version=0
    Set-Cookie3: BIDUPSID=1F30EEDA35C7A9433C97CF6245CBC3B3; path="/"; domain=".baidu.com"; path_spec; domain_dot; expires="2088-06-17 19:20:46Z"; version=0
    Set-Cookie3: H_PS_PSSID=31626_1440_21124_31069_31254_31594_30841_31673_31464_31715_30823; path="/"; domain=".baidu.com"; path_spec; domain_dot; discard; version=0
    Set-Cookie3: PSTM=1590854799; path="/"; domain=".baidu.com"; path_spec; domain_dot; expires="2088-06-17 19:20:46Z"; version=0
    Set-Cookie3: BDSVRTM=11; path="/"; domain="www.baidu.com"; path_spec; discard; version=0
    Set-Cookie3: BD_HOME=1; path="/"; domain="www.baidu.com"; path_spec; discard; version=0

    由此看来,不同格式的 Cookie 文件差异还是比较大的。

    那么,生成 Cookie 文件后,怎样从其中读取内容并加以利用呢?

    下面我们以 LWPCookieJar 格式为例来看一下:

    import urllib.request, http.cookiejar
    
    cookie = http.cookiejar.LWPCookieJar()
    cookie.load('cookie.txt', ignore_discard=True, ignore_expires=True)
    handler = urllib.request.HTTPCookieProcessor(cookie)
    opener = urllib.request.build_opener(handler)
    response = opener.open('https://www.baidu.com')
    print(response.read().decode('utf-8'))

    可以看到,这里调用 load 方法来读取本地的 Cookie 文件,获取了 Cookie 的内容。这样做的前提是我们首先生成了 LWPCookieJar 格式的 Cookie 并保存成了文件。读取 Cookie 之后,使用同样的方法构建 Handler 类和 Opener 类即可完成操作。

    运行结果正常的话,会输出百度网页的源代码。

    通过上面的方法,我们就可以设置绝大多数请求的功能。

处理异常

我们已经了解了如何发送请求,但是在网络不好的情况下,如果出现了异常,该怎么办呢?这时要是不处理这些异常,程序很可能会因为报错而终止运行,所以异常处理还是十分有必要的。

urllib 库中的 error 模块定义了由 request 模块产生的异常。当出现问题时,request 模块便会抛出 error 模块中定义的异常。

  • URLError

URLError 类来自 urllib 库的 error 模块,继承自 OSError ,是 error 异常模块的基类,由 request 模块产生的异常都可以通过捕获这个类来处理。

它具有一个属性 reason,即返回错误的原因。

下面用一个实例来看一下:

from urllib import request, error

try:
    response = request.urlopen('https://cuiqingcai.com/404')
except error.URLError as e:
    print(e.reason)

我们打开了一个不存在的页面,照理来说应该会报错,但是我们捕获了 URLError 这个异常,运行结果如下:

Not Found

程序没有直接报错,而是输出了错误原因,这样可以避免程序异常终止,同时异常得到了有效处理。

  • HTTPError

HTTPErrorURLError 的子类,专门用来处理 HTTP 请求错误,例如认证请求失败等。它有如下 3 个属性。

  • Code:返回 HTTP 状态码,例如 404 表示网页不存在,500 表示服务器内部错误等。

  • reason:同父类一样,用于返回错误的原因。

  • headers:返回请求头。

下面我们用几个实例来看看:

from urllib import request, error

try:
    response = request.urlopen('https://cuiqingcai.com/404')
except error.HTTPError as e:
    print(e.reason, e.code, e.headers, sep='\n')

运行结果如下:

Not Found
404
Connection: close
Content-Length: 9379
Server: GitHub.com
Content-Type: text/html; charset=utf-8
Access-Control-Allow-Origin: *
ETag: "64d39a40-24a3"
Content-Security-Policy: default-src 'none'; style-src 'unsafe-inline'; img-src data:; connect-src 'self'
x-proxy-cache: MISS
X-GitHub-Request-Id: F142:167F0E:2910984:297EADC:6794AA3B
Accept-Ranges: bytes
Date: Sat, 25 Jan 2025 09:09:16 GMT
Via: 1.1 varnish
Age: 0
X-Served-By: cache-sin-wsss1830052-SIN
X-Cache: MISS
X-Cache-Hits: 0
X-Timer: S1737796156.919862,VS0,VE407
Vary: Accept-Encoding
X-Fastly-Request-ID: c05c4dc2609f924fe50f15c7d7a8226be60ac76a

依然是打开同样的网址,这里捕获了 HTTPError 异常,输出了 reasoncodeheaders 属性。

因为 URLErrorHTTPError 的父类,所以可以先选择捕获子类的错误,再捕获父类的错误,于是上述代码的更好写法如下:

from urllib import request, error

try:
    response = request.urlopen('https://cuiqingcai.com/404')
except error.HTTPError as e:
    print(e.reason, e.code, e.headers, sep='\n')
except error.URLError as e:
    print(e.reason)
else:
    print('Request Successfully')

这样就可以做到先捕获 HTTPError 获取它的错误原因、状态码、请求头等信息。如果不是 HTTPError 异常,就会捕获 URLError 异常,输出错误原因。最后,用 else 语句来处理正常的逻辑。这是一个较好的异常处理写法。

有时候,reason 属性返回的不一定是字符串,也可能是一个对象。再看下面的实例:

import socket
import urllib.request
import urllib.error

try:
    response = urllib.request.urlopen('https://www.baidu.com', timeout=0.01)
except urllib.error.URLError as e:
    print(type(e.reason))
    if isinstance(e.reason, socket.timeout):
        print('TIME OUT')

这里我们直接设置超时时间来强制抛出 timeout 异常。

运行结果如下:

<class 'TimeoutError'>
TIME OUT

可以发现,reason 属性的结果是 socket.timeout 类。所以这里可以用 isinstance 方法来判断它的类型,做出更详细的异常判断。

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

解析链接

前面说过,urllib 库里还提供了 parse 模块,这个模块定义了处理 URL 的标准接口,例如实现 URL 各部分的抽取、合并以及链接转换。它支持如下协议的 URL 处理: fileftpgopherhdlhttphttpsimapmailtommsnewsnntpprosperorsyncrtsprtspusftpsipsipssnewssvnsvn+sshtelnetwais

下面我们将介绍 parse 模块中的常用方法,看一下它的便捷之处。

  • urlparse

该方法可以实现 URL 的识别和分段,这里先用一个实例来看一下:

from urllib.parse import urlparse

result = urlparse('https://www.baidu.com/index.html;user?id=5#comment')
print(type(result))
print(result)

这里我们利用 urlparse 方法对一个 URL 进行了解析,然后输出了解析结果的类型以及结果本身。

运行结果如下:

<class 'urllib.parse.ParseResult'>
ParseResult(scheme='https', netloc='www.baidu.com', path='/index.html', params='user', query='id=5', fragment='comment')

可以看到,解析结果是一个 parseResult 类型的对象,包含 6 部分,分别是 schemenetlocpathparamsqueryfragment

再观察一下上述实例中的 URL:

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

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

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

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

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

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

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

可以看到,urlParse 方法有 3 个参数。

  • urlstring: 这是必填项,即待解析的 URL。

  • scheme: 这是默认的协议(例如 httphttps 等)。如果待解析的 URL 没有带协议信息,就会将这个作为默认协议。我们用实例来看一下:

    from urllib.parse import urlparse
    
    result = urlparse('www.baidu.com/index.html;user?id=5#comment', scheme='https')
    print(result)

    运行结果如下:

    ParseResult(scheme='https', netloc='', path='www.baidu.com/index.html', params='user', query='id=5', fragment='comment')

    可以发现,这里提供的 URL 不包含最前面的协议信息,但是通过默认的 scheme 参数,返回了结果 https

    假设带上协议信息:

    result = urlparse('http://www.baidu.com/index.html;user?id=5#comment', scheme='https')

    则结果如下:

    ParseResult(scheme='http', netloc='www.baidu.com', path='/index.html', params='user', query='id=5', fragment='comment')

    可见,scheme 参数只有在 URL 中不包含协议信息的时候才生效。如果 URL 中有,就会返回解析出的 scheme

  • allow_fragments: 是否忽略 fragment。如果此项被设置为 False,那么 fragment 部分就会被忽略,它会被解析为 pathparams 或者 query 的一部分,而 fragment 部分为空。

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

result = urlparse('https://www.baidu.com/index.html;user?id=5#comment', allow_fragments=False)
print(result)

运行结果如下:

ParseResult(scheme='https', netloc='www.baidu.com', path='/index.html', params='user', query='id=5#comment', fragment='')

假设 URL 中不包含 paramsquery,我们再通过实例看一下:

result = urlparse('https://www.baidu.com/index.html#comment', allow_fragments=False)
print(result)

运行结果如下:

ParseResult(scheme='https', netloc='www.baidu.com', path='/index.html#comment', params='', query='', fragment='')

可以发现,此时 fragment 会被解析为 path 的一部分。

返回结果 ParseResult 实际上是一个元组,既可以用属性名获取其内容,也可以用索引来顺序获取。实例如下:

result = urlparse('https://www.baidu.com/index.html#comment', allow_fragments=False)
print(result.scheme, result[0], result.netloc, result[1], sep='\n')

这里我们分别用属性名和索引获取了 schemenetloc 运行结果如下:

https
https
www.baidu.com
www.baidu.com

可以发现,两种获取方式都可以成功获取,且结果是一致的。

  • urlunparse

有了 urlparse 方法,相应就会有它的对立方法 urlunparse,用于构造 URL。这个方法接收的参数是一个可迭代对象,其长度必须是 6,否则会抛出参数数量不足或者过多的问题。先用一个实例看下:

from urllib.parse import urlunparse

data = ['https', 'www.baidu.com', 'index.html', 'user', 'a=6', 'comment']
print(urlunparse(data))

这里参数 data 用了列表类型。当然,也可以用其他类型,例如元组或者特定的数据结构。

运行结果如下:

https://www.baidu.com/index.html;user?a=6#comment

这样我们就成功实现了 URL 的构造。

urlsplit

这个方法和 urlparse 方法非常相似,只不过它不再单独解析 params 这一部分(params 会合并到 path 中),只返回 5 个结果。实例如下:

from urllib.parse import urlsplit

result = urlsplit('https://www.baidu.com/index.html;user?id=5#comment')
print(result)

运行结果如下:

SplitResult(scheme='https', netloc='www.baidu.com', path='/index.html;user', query='id=5', fragment='comment')

可以发现,返回结果是 SplitResult,这其实也是一个元组,既可以用属性名获取其值,也可以用索引获取。实例如下:

from urllib.parse import urlsplit

result = urlsplit('https://www.baidu.com/index.html;user?id=5#comment')
print(result.scheme, result[0])

运行结果如下:

https https

urlunsplit

urlunparse 方法类似,这也是将链接各个部分组合成完整链接的方法,传入的参数也是一个可选代对象,例如列表、元组等,唯一区别是这里参数的长度必须为 5。实例如下:

from urllib.parse import urlunsplit

data = ['https','www.baidu.com','index.html','a=6','comment']
print(urlunsplit(data))

运行结果如下:

https://www.baidu.com/index.html?a=6#comment

urljoin

urlunparseurlunsplit 方法都可以完成链接的合并,不过前提都是必须有特定长度的对象,链接的每一部分都要清晰分开。

除了这两种方法,还有一种生成链接的方法,是 urljoin。我们可以提供一个 base_url(基础链接)作为该方法的第一个参数,将新的链接作为第二个参数。urljoin 方法会分析 base_urlschemenetlocpath 这 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 提供了三项内容:schemenetlocpath。如果新的链接里不存在这三项就予以补充;如果存在,就使用新的链接里面的,base_url 中的是不起作用的。

通过 urljoin 方法,我们可以轻松实现链接的解析、拼合与生成。

urlencode

这里我们再介绍一个常用的方法一一 urlencode,它在构造 GET 请求参数的时候非常有用,实例如下:

from urllib.parse import urlencode

params = {
    'name': 'germey',
    'age':25
}

base_url = 'https://www.baidu.com?'
url = base_url + urlencode(params)
print(url)

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

运行结果如下:

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

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

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

parse_qs

有了序列化,必然会有反序列化。利用 parse_gs 方法,可以将一串 GET 请求参数转回字典,实例如下:

from urllib.parse import parse_qs

query = 'name=germey&age=25'
print(parse_qs(query))

运行结果如下:

{'name': ['germey'], 'age': ['25']}

可以看到,URL 的参数成功转回为字典类型。

parse_qsl

parse_qsl 方法用于将参数转化为由元组组成的列表,实例如下:

from urllib.parse import parse_qsl

query = 'name=germey&age=25'
print(parse_qsl(query))

运行结果如下:

[('name', 'germey'), ('age', '25')]

可以看到,运行结果是一个列表,该列表中的每一个元素都是一个元组,元组的第一个内容是参数名,第二个内容是参数值。

quote

该方法可以将内容转化为 URL 编码的格式。当 URL 中带有中文参数时,有可能导致乱码问题,此时用 quote 方法可以将中文字符转化为 URL 编码,实例如下:

from urllib.parse import quote

keyword = '壁纸'
url = 'https://www.baidu.com/s?wd=' + quote(keyword)
print(url)

这里我们声明了一个中文的搜索文字,然后用 quote 方法对其进行 URL 编码,最后得到的结果如下:

https://www.baidu.com/s?wd=%E5%A3%81%E7%BA%B8

unquote

有了 quote 方法,当然就有 unquote 方法,它可以进行 URL 解码,实例如下:

from urllib.parse import unquote

url = 'https://www.baidu.com/s?wd=%E5%A3%81%E7%BA%B8'
print(unquote(url))

这里的 url 是上面得到的 URL 编码结果,利用 unquote 方法将其还原,结果如下:

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

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

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

分析 Robots 协议

利用 urllib 库的 robotparser 模块,可以分析网站的 Robots 协议。我们再来简单了解一下这个模块的用法。

  • Robots协议

Robots 协议也称作爬虫协议、机器人协议,全名为网络爬虫排除标准(Robots Exclusion Protocol),用来告诉爬虫和搜索引擎哪些页面可以抓取、哪些不可以。它通常是一个叫作 robots.txt 的文本文件,一般放在网站的根目录下。

搜索爬虫在访问一个站点时,首先会检查这个站点根目录下是否存在 robots.txt 文件,如果存在,就会根据其中定义的爬取范围来爬取,如果没有找到这个文件,搜索爬虫便会访问所有可直接访问的页面。

下面我们看一个 robots.txt 的样例:

User-agent: *
Disallow: /
Allow: /public/

这限定了所有搜索爬虫只能爬取 public 目录。将上述内容保存成 robots.txt 文件,放在网站的根目录下,和网站的入口文件(例如 index.phpindex.htmlindex.jsp 等)放在一起。

上面样例中的 User-agent 描述了搜索爬虫的名称,这里将其设置为 *,代表 Robots 协议对所有爬取爬虫都有效。例如,我们可以这样设置:

User-agent: Baiduspider

这代表设置的规则对百度爬虫是有效的。如果有多条 User-agent 记录,则意味着有多个爬虫会受到爬取限制,但至少需要指定一条。

Disallow 指定了不允许爬虫爬取的目录,上例设置为 /,代表不允许爬取所有页面。

Allow 一般不会单独使用,会和 Disallow 一起用,用来排除某些限制。上例中我们设置为 /public/,结合 Disallow 的设置,表示所有页面都不允许爬取,但可以爬取 public 目录。

下面再来看几个例子。禁止所有爬虫访问所有目录的代码如下:

User-agent: *
Disallow: /

允许所有爬虫访问所有目录的代码如下:

User-agent:*
Disallow:

另外,直接把 robots.txt 文件留空也是可以的。

禁止所有爬虫访问网站某些目录的代码如下:

User-agent:*
Disallow: /private/
Disallow: /tmp/

只允许某一个爬虫访问所有目录的代码如下:

User-agent: WebCrawler
Disallow:
User-agent: *
Disallow: /

以上是 robots.txt 的一些常见写法。

  • 爬虫名称

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

Table 1. 表 2-1 一些常见搜索爬虫的名称及其对应的网站
爬虫名称 网站名称

BaiduSpider

百度

Googlebot

谷歌

360Spider

360搜索

YoDaoBot

有道

ia_archiver

Alexa

Scooter

altavista

Bingbot

必应

  • robotparser

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

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

urllib.robotparser.RobotFileParser(url='')

当然,也可以不在声明时传入 robots.txt 文件的链接,就让其默认为空,最后再使用 set_url() 方法设置一下也可以。

下面列出了 RobotFileParser 类的几个常用方法。

  • set_url: 用来设置 robots.txt 文件的链接。如果在创建 RobotFileParser 对象时传入了链接,就不需要使用这个方法设置了。

  • read: 读取 robots.txt 文件并进行分析。注意,这个方法执行读取和分析操作,如果不调用这个方法,接下来的判断都会为 False,所以一定记得调用这个方法。这个方法虽不会返回任何内容,但是执行了读取操作。

  • parse: 用来解析 robots.txt 文件,传入其中的参数是 robots.txt 文件中某些行的内容,它会按照 robots.txt 的语法规则来分析这些内容。

  • can_fetch: 该方法有两个参数,第一个是 User-Agent,第二个是要抓取的 URL。返回结果是 True 或 False,表示 User-Agent 指示的搜索引擎是否可以抓取这个 URL。

  • mtime: 返回上次抓取和分析 robots.txt 文件的时间,这对于长时间分析和抓取 robots.txt 文件的搜索爬虫很有必要,你可能需要定期检查以抓取最新的 robots.txt 文件。

  • modified: 它同样对长时间分析和抓取的搜索爬虫很有帮助,可以将当前时间设置为上次抓取和分析 robots.txt 文件的时间。

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

from urllib.robotparser import RobotFileParser

rp = RobotFileParser()
rp.set_url('https://www.baidu.com/robots.txt')
rp.read()
print(rp.can_fetch('Baiduspider', 'https://www.baidu.com'))
print(rp.can_fetch('Baiduspider', 'https://www.baidu.com/homepage/'))
print(rp.can_fetch('Googlebot', 'https://www.baidu.com/homepage/'))

这里以百度为例,首先创建了一个 RobotFileParser 对象 rp,然后通过 set_url 方法设置了 robots.txt 文件的链接。当然,要是不用 set_url 方法,可以在声明对象时直接用如下方法设置:

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

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

运行结果如下:

True
True
False

可以看到,这里我们利用 Baiduspider 可以抓取百度的首页以及 homepage 页面,但是 Googlebot 就不能抓取 homepage 页面。

打开百度的 robots.txt 文件,可以看到如下信息:

User-agent: Baiduspider
Disallow: /baidu
Disallow:/s?
Disallow:/ulink?
Disallow:/link?
Disallow:/home/news/datal
Disallow:/bh

User-agent: Googlebot
Disallow:/baidu
Disallow:/s?
Disallow:/shifen/
Disallow:/homepage/
Disallow:/cpro
Disallow:/ulink?
Disallow:/link?
Disallow:/home/news/data/
Disallow:/bh

不难看出,百度的 robots.txt 文件没有限制 Baiduspider 对百度 homepage 页面的抓取,限制了 Googlebot 对 homepage 页面的抓取。

这里同样可以使用 parse 方法执行对 robots.txt 文件的读取和分析,实例如下:

from urllib.request import urlopen
from urllib.robotparser import RobotFileParser

rp = RobotFileParser
rp.parse(urlopen('https://www.baidu.com/robots.txt').read().decode('utf-8').split('\n'))
print(rp.can_fetch('Baiduspider', 'https://www.baidu.com'))
print(rp.can_fetch('Baiduspider', "https://www.baidu.com/homepage/"))
print(rp.can_fetch('Googlebot',"https://www.baidu.com/homepage/"))

运行结果是一样的:

True
True
False

本节介绍了 robotparser 模块的基本用法和实例,利用此模块,我们可以方便地判断哪些页面能抓取、哪些页面不能。

总结

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