悠闲博客-blog.yxrjt.cn

Python 网络编程第二章HTTP 和网络应用

更新时间:2025-09-25 13:34点击:42

第二章:HTTP 和网络应用

超文本传输协议HTTP)可能是最广泛使用的应用层协议。最初开发是为了让学者分享 HTML 文档。如今,它被用作互联网上无数应用程序的核心协议,并且是万维网的主要协议。

在本章中,我们将涵盖以下主题:

  • HTTP 协议结构

  • 使用 Python 通过 HTTP 与服务通信

  • 下载文件

  • HTTP 功能,如压缩和 cookies

  • 处理错误

  • URL

  • Python 标准库urllib

  • Kenneth Reitz 的第三方Requests

urllib包是 Python 标准库中用于 HTTP 任务的推荐包。标准库还有一个名为http的低级模块。虽然这提供了对协议几乎所有方面的访问,但它并不是为日常使用而设计的。urllib包有一个更简单的接口,并且处理了我们将在本章中涵盖的所有内容。

第三方Requests包是urllib的一个非常受欢迎的替代品。它具有优雅的界面和强大的功能集,是简化 HTTP 工作流的绝佳工具。我们将在本章末讨论它如何替代urllib使用。

请求和响应

HTTP 是一个应用层协议,几乎总是在 TCP 之上使用。HTTP 协议被故意定义为使用人类可读的消息格式,但仍然可以用于传输任意字节数据。

一个 HTTP 交换包括两个元素。客户端发出的请求,请求服务器提供由 URL 指定的特定资源,以及服务器发送的响应,提供客户端请求的资源。如果服务器无法提供客户端请求的资源,那么响应将包含有关失败的信息。

这个事件顺序在 HTTP 中是固定的。所有交互都是由客户端发起的。服务器不会在没有客户端明确要求的情况下向客户端发送任何内容。

这一章将教你如何将 Python 用作 HTTP 客户端。我们将学习如何向服务器发出请求,然后解释它们的响应。我们将在第九章中讨论编写服务器端应用程序,网络应用

到目前为止,最广泛使用的 HTTP 版本是 1.1,定义在 RFC 7230 到 7235 中。HTTP 2 是最新版本,正式批准时本书即将出版。版本 1.1 和 2 之间的语义和语法大部分保持不变,主要变化在于 TCP 连接的利用方式。目前,HTTP 2 的支持并不广泛,因此本书将专注于版本 1.1。如果你想了解更多,HTTP 2 在 RFC 7540 和 7541 中有记录。

HTTP 版本 1.0,记录在 RFC 1945 中,仍然被一些较老的软件使用。版本 1.1 与 1.0 向后兼容,urllib包和Requests都支持 HTTP 1.1,所以当我们用 Python 编写客户端时,不需要担心连接到 HTTP 1.0 服务器。只是一些更高级的功能不可用。几乎所有现在的服务都使用版本 1.1,所以我们不会在这里讨论差异。如果需要更多信息,可以参考堆栈溢出的问题:stackoverflow.com/questions/246859/http-1-0-vs-1-1

使用 urllib 进行请求

在讨论 RFC 下载器时,我们已经看到了一些 HTTP 交换的例子,第一章网络编程和 Pythonurllib包被分成几个子模块,用于处理我们在使用 HTTP 时可能需要执行的不同任务。为了发出请求和接收响应,我们使用urllib.request模块。

使用urllib从 URL 检索内容是一个简单的过程。打开你的 Python 解释器,然后执行以下操作:

**>>> from urllib.request import urlopen**
**>>> response = urlopen('http://www.debian.org')**
**>>> response**
**<http.client.HTTPResponse object at 0x7fa3c53059b0>**
**>>> response.readline()**
**b'<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01//EN" "http://www.w3.org/TR/html4/strict.dtd">\n'**

我们使用urllib.request.urlopen()函数发送请求并接收www.debian.org上资源的响应,这里是一个 HTML 页面。然后我们将打印出我们收到的 HTML 的第一行。

响应对象

让我们更仔细地看一下我们的响应对象。从前面的例子中我们可以看到,urlopen()返回一个http.client.HTTPResponse实例。响应对象使我们能够访问请求资源的数据,以及响应的属性和元数据。要查看我们在上一节中收到的响应的 URL,可以这样做:

**>>> response.url**
**'http://www.debian.org'**

我们通过类似文件的接口使用readline()read()方法获取请求资源的数据。我们在前一节看到了readline()方法。这是我们使用read()方法的方式:

**>>> response = urlopen('http://www.debian.org')**
**>>> response.read(50)**
**b'g="en">\n<head>\n  <meta http-equiv="Content-Type" c'**

read()方法从数据中返回指定数量的字节。这里是前 50 个字节。调用read()方法而不带参数将一次性返回所有数据。

类似文件的接口是有限的。一旦数据被读取,就无法使用上述函数之一返回并重新读取它。为了证明这一点,请尝试执行以下操作:

**>>> response = urlopen('http://www.debian.org')**
**>>> response.read()**
**b'<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01//EN" "http://www.w3.org/TR/html4/strict.dtd">\n<html lang="en">\n<head>\n  <meta http-equiv**
**...**
**>>> response.read()**
**b''**

我们可以看到,当我们第二次调用read()函数时,它返回一个空字符串。没有seek()rewind()方法,所以我们无法重置位置。因此,最好将read()输出捕获在一个变量中。

readline()read()函数都返回字节对象,httpurllib都不会对接收到的数据进行解码为 Unicode。在本章的后面,我们将看到如何利用Requests库来处理这个问题。

状态码

如果我们想知道我们的请求是否发生了意外情况怎么办?或者如果我们想知道我们的响应在读取数据之前是否包含任何数据怎么办?也许我们期望得到一个大的响应,我们想快速查看我们的请求是否成功而不必读取整个响应。

HTTP 响应通过状态码为我们提供了这样的方式。我们可以通过使用其status属性来读取响应的状态码。

**>>> response.status**
**200**

状态码是告诉我们请求的情况的整数。200代码告诉我们一切都很好。

有许多代码,每个代码传达不同的含义。根据它们的第一个数字,状态码被分为以下几组:

  • 100:信息

  • 200:成功

  • 300:重定向

  • 400:客户端错误

  • 500:服务器错误

一些常见的代码及其消息如下:

  • 200OK

  • 404未找到

  • 500内部服务器错误

状态码的官方列表由 IANA 维护,可以在www.iana.org/assignments/http-status-codes找到。我们将在本章中看到各种代码。

处理问题

状态码帮助我们查看响应是否成功。200 范围内的任何代码表示成功,而 400 范围或 500 范围内的代码表示失败。

应该始终检查状态码,以便我们的程序在出现问题时能够做出适当的响应。urllib包通过在遇到问题时引发异常来帮助我们检查状态码。

让我们看看如何捕获这些异常并有用地处理它们。为此,请尝试以下命令块:

**>>> import urllib.error**
**>>> from urllib.request import urlopen**
**>>> try:**
**...   urlopen('http://www.ietf.org/rfc/rfc0.txt')**
**... except urllib.error.HTTPError as e:**
**...   print('status', e.code)**
**...   print('reason', e.reason)**
**...   print('url', e.url)**
**...**
**status: 404**
**reason: Not Found**
**url: http://www.ietf.org/rfc/rfc0.txt**

在这里,我们请求了不存在的 RFC 0。因此服务器返回了 404 状态代码,urllib已经发现并引发了HTTPError

您可以看到HTTPError提供了有关请求的有用属性。在前面的示例中,我们使用了statusreasonurl属性来获取有关响应的一些信息。

如果网络堆栈中出现问题,那么适当的模块将引发异常。urllib包捕获这些异常,然后将它们包装为URLErrors。例如,我们可能已经指定了一个不存在的主机或 IP 地址,如下所示:

**>>> urlopen('http://192.0.2.1/index.html')**
**...**
**urllib.error.URLError: <urlopen error [Errno 110] Connection timed out>**

在这种情况下,我们已经从192.0.2.1主机请求了index.html192.0.2.0/24 IP 地址范围被保留供文档使用,因此您永远不会遇到使用前述 IP 地址的主机。因此 TCP 连接超时,socket引发超时异常,urllib捕获,重新包装并为我们重新引发。我们可以像在前面的例子中一样捕获这些异常。

HTTP 头部

请求和响应由两个主要部分组成,头部正文。当我们在第一章中使用 TCP RFC 下载器时,我们简要地看到了一些 HTTP 头部,网络编程和 Python。头部是出现在通过 TCP 连接发送的原始消息开头的协议特定信息行。正文是消息的其余部分。它与头部之间由一个空行分隔。正文是可选的,其存在取决于请求或响应的类型。以下是一个 HTTP 请求的示例:

GET / HTTP/1.1
Accept-Encoding: identity
Host: www.debian.com
Connection: close
User-Agent: Python-urllib/3.4

第一行称为请求行。它由请求方法组成,在这种情况下是GET,资源的路径,在这里是/,以及 HTTP 版本1.1。其余行是请求头。每行由一个头部名称后跟一个冒号和一个头部值组成。前述输出中的请求只包含头部,没有正文。

头部用于几个目的。在请求中,它们可以用于传递额外的数据,如 cookies 和授权凭据,并询问服务器首选资源格式。

例如,一个重要的头部是Host头部。许多 Web 服务器应用程序提供了在同一台服务器上使用相同的 IP 地址托管多个网站的能力。为各个网站域名设置了 DNS 别名,因此它们都指向同一个 IP 地址。实际上,Web 服务器为每个托管的网站提供了多个主机名。IP 和 TCP(HTTP 运行在其上)不能用于告诉服务器客户端想要连接到哪个主机名,因为它们都仅仅在 IP 地址上操作。HTTP 协议允许客户端在 HTTP 请求中提供主机名,包括Host头部。

我们将在下一节中查看一些更多的请求头部。

以下是响应的一个示例:

HTTP/1.1 200 OK
Date: Sun, 07 Sep 2014 19:58:48 GMT
Content-Type: text/html
Content-Length: 4729
Server: Apache
Content-Language: en

<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01//EN" "http://www.w3.org/TR/html4/strict.dtd">\n...

第一行包含协议版本、状态代码和状态消息。随后的行包含头部、一个空行,然后是正文。在响应中,服务器可以使用头部通知客户端有关正文长度、响应正文包含的内容类型以及客户端应存储的 cookie 数据等信息。

要查看响应对象的头部,请执行以下操作:

**>>> response = urlopen('http://www.debian.org)**
**>>> response.getheaders()**
**[('Date', 'Sun, 07 Sep 2014 19:58:48 GMT'), ('Server', 'Apache'), ('Content-Location', 'index.en.html'), ('Vary', 'negotiate,accept- language,Accept-Encoding')...**

getheaders()方法以元组列表的形式返回头部(头部名称头部值)。HTTP 1.1 头部及其含义的完整列表可以在 RFC 7231 中找到。让我们看看如何在请求和响应中使用一些头部。

自定义请求

利用标头提供的功能,我们在发送请求之前向请求添加标头。为了做到这一点,我们不能只是使用urlopen()。我们需要按照以下步骤进行:

  • 创建一个Request对象

  • 向请求对象添加标头

  • 使用urlopen()发送请求对象

我们将学习如何自定义一个请求,以检索 Debian 主页的瑞典版本。我们将使用Accept-Language标头,告诉服务器我们对其返回的资源的首选语言。请注意,并非所有服务器都保存多种语言版本的资源,因此并非所有服务器都会响应Accept-Language

首先,我们创建一个Request对象:

**>>> from urllib.request import Request**
**>>> req = Request('http://www.debian.org')**

接下来,添加标头:

**>>> req.add_header('Accept-Language', 'sv')**

add_header()方法接受标头的名称和标头的内容作为参数。Accept-Language标头采用两字母的 ISO 639-1 语言代码。瑞典语的代码是sv

最后,我们使用urlopen()提交定制的请求:

**>>> response = urlopen(req)**

我们可以通过打印前几行来检查响应是否是瑞典语:

**>>> response.readlines()[:5]**
**[b'<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01//EN" "http://www.w3.org/TR/html4/strict.dtd">\n',**
 **b'<html lang="sv">\n',**
 **b'<head>\n',**
 **b'  <meta http-equiv="Content-Type" content="text/html; charset=utf-  8">\n',**
 **b'  <title>Debian -- Det universella operativsystemet </title>\n']**

Jetta bra!Accept-Language标头已经告知服务器我们对响应内容的首选语言。

要查看请求中存在的标头,请执行以下操作:

**>>> req = Request('http://www.debian.org')**
**>>> req.add_header('Accept-Language', 'sv')**
**>>> req.header_items()**
**[('Accept-language', 'sv')]**

当我们在请求上运行urlopen()时,urlopen()方法会添加一些自己的标头:

**>>> response = urlopen(req)**
**>>> req.header_items()**
**[('Accept-language', 'sv'), ('User-agent': 'Python-urllib/3.4'), ('Host': 'www.debian.org')]**

添加标头的一种快捷方式是在创建请求对象的同时添加它们,如下所示:

**>>> headers = {'Accept-Language': 'sv'}**
**>>> req = Request('http://www.debian.org', headers=headers)**
**>>> req.header_items()**
**[('Accept-language', 'sv')]**

我们将标头作为dict提供给Request对象构造函数,作为headers关键字参数。通过这种方式,我们可以一次性添加多个标头,通过向dict添加更多条目。

让我们看看我们可以用标头做些什么其他事情。

内容压缩

Accept-Encoding请求标头和Content-Encoding响应标头可以一起工作,允许我们临时对响应主体进行编码,以便通过网络传输。这通常用于压缩响应并减少需要传输的数据量。

这个过程遵循以下步骤:

  • 客户端发送一个请求,其中在Accept-Encoding标头中列出了可接受的编码

  • 服务器选择其支持的编码方法

  • 服务器使用这种编码方法对主体进行编码

  • 服务器发送响应,指定其在Content-Encoding标头中使用的编码

  • 客户端使用指定的编码方法解码响应主体

让我们讨论如何请求一个文档,并让服务器对响应主体使用gzip压缩。首先,让我们构造请求:

**>>> req = Request('http://www.debian.org')**

接下来,添加Accept-Encoding标头:

**>>> req.add_header('Accept-Encoding', 'gzip')**

然后,借助urlopen()提交请求:

**>>> response = urlopen(req)**

我们可以通过查看响应的Content-Encoding标头来检查服务器是否使用了gzip压缩:

**>>> response.getheader('Content-Encoding')**
**'gzip'**

然后,我们可以使用gzip模块对主体数据进行解压:

**>>> import gzip**
**>>> content = gzip.decompress(response.read())**
**>>> content.splitlines()[:5]**
**[b'<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01//EN" "http://www.w3.org/TR/html4/strict.dtd">',**
 **b'<html lang="en">',**
 **b'<head>',**
 **b'  <meta http-equiv="Content-Type" content="text/html; charset=utf-8">',**
 **b'  <title>Debian -- The Universal Operating System </title>']**

编码已在 IANA 注册。当前列表包括:gzipcompressdeflateidentity。前三个是指特定的压缩方法。最后一个允许客户端指定不希望对内容应用任何编码。

让我们看看如果我们使用identity编码来请求不进行压缩会发生什么:

**>>> req = Request('http://www.debian.org')**
**>>> req.add_header('Accept-Encoding', 'identity')**
**>>> response = urlopen(req)**
**>>> print(response.getheader('Content-Encoding'))**
**None**

当服务器使用identity编码类型时,响应中不包括Content-Encoding标头。

多个值

为了告诉服务器我们可以接受多种编码,我们可以在Accept-Encoding标头中添加更多值,并用逗号分隔它们。让我们试试。我们创建我们的Request对象:

**>>> req = Request('http://www.debian.org')**

然后,我们添加我们的标头,这次我们包括更多的编码:

**>>> encodings = 'deflate, gzip, identity'**
**>>> req.add_header('Accept-Encoding', encodings)**

现在,我们提交请求,然后检查响应的编码:

**>>> response = urlopen(req)**
**>>> response.getheader('Content-Encoding')**
**'gzip'**

如果需要,可以通过添加q值来给特定编码分配相对权重:

**>>> encodings = 'gzip, deflate;q=0.8, identity;q=0.0'**

q值跟随编码名称,并且由分号分隔。最大的q值是1.0,如果没有给出q值,则默认为1.0。因此,前面的行应该被解释为我的首选编码是gzip,我的第二个首选是deflate,如果没有其他可用的编码,则我的第三个首选是identity

内容协商

使用Accept-Encoding标头进行内容压缩,使用Accept-Language标头进行语言选择是内容协商的例子,其中客户端指定其关于所请求资源的格式和内容的首选项。以下标头也可以用于此目的:

  • Accept:请求首选文件格式

  • Accept-Charset:请求以首选字符集获取资源

内容协商机制还有其他方面,但由于支持不一致并且可能变得相当复杂,我们不会在本章中进行介绍。RFC 7231 包含您需要的所有详细信息。如果您发现您的应用程序需要此功能,请查看 3.4、5.3、6.4.1 和 6.5.6 等部分。

内容类型

HTTP 可以用作任何类型文件或数据的传输。服务器可以在响应中使用Content-Type头来通知客户端有关它在主体中发送的数据类型。这是 HTTP 客户端确定如何处理服务器返回的主体数据的主要手段。

要查看内容类型,我们检查响应标头的值,如下所示:

**>>> response = urlopen('http://www.debian.org')**
**>>> response.getheader('Content-Type')**
**'text/html'**

此标头中的值取自由 IANA 维护的列表。这些值通常称为内容类型互联网媒体类型MIME 类型MIME代表多用途互联网邮件扩展,在该规范中首次建立了这种约定)。完整列表可以在www.iana.org/assignments/media-types找到。

对于通过互联网传输的许多数据类型都有注册的媒体类型,一些常见的类型包括:

媒体类型描述
text/htmlHTML 文档
text/plain纯文本文档
image/jpegJPG 图像
application/pdfPDF 文档
application/jsonJSON 数据
application/xhtml+xmlXHTML 文档

另一个感兴趣的媒体类型是application/octet-stream,在实践中用于没有适用的媒体类型的文件。这种情况的一个例子是一个经过 pickle 处理的 Python 对象。它还用于服务器不知道格式的文件。为了正确处理具有此媒体类型的响应,我们需要以其他方式发现格式。可能的方法如下:

  • 检查已下载资源的文件名扩展名(如果有)。然后可以使用mimetypes模块来确定媒体类型(转到第三章,APIs in Action,以查看此示例)。

  • 下载数据,然后使用文件类型分析工具。对于图像,可以使用 Python 标准库的imghdr模块,对于其他类型,可以使用第三方的python-magic包或GNU文件命令。

  • 检查我们正在下载的网站,看看文件类型是否已经在任何地方有文档记录。

内容类型值可以包含可选的附加参数,提供有关类型的进一步信息。这通常用于提供数据使用的字符集。例如:

Content-Type: text/html; charset=UTF-8.

在这种情况下,我们被告知文档的字符集是 UTF-8。参数在分号后面包括,并且它总是采用键/值对的形式。

让我们讨论一个例子,下载 Python 主页并使用它返回的Content-Type值。首先,我们提交我们的请求:

**>>> response = urlopen('http://www.python.org')**

然后,我们检查响应的Content-Type值,并提取字符集:

**>>> format, params = response.getheader('Content-Type').split(';')**
**>>> params**
**' charset=utf-8'**
**>>> charset = params.split('=')[1]**
**>>> charset**
**'utf-8'**

最后,我们通过使用提供的字符集来解码我们的响应内容:

**>>> content = response.read().decode(charset)**

请注意,服务器通常要么在Content-Type头中不提供charset,要么提供错误的charset。因此,这个值应该被视为一个建议。这是我们稍后在本章中查看Requests库的原因之一。它将自动收集关于解码响应主体应该使用的字符集的所有提示,并为我们做出最佳猜测。

用户代理

另一个值得了解的请求头是User-Agent头。使用 HTTP 通信的任何客户端都可以称为用户代理。RFC 7231 建议用户代理应该在每个请求中使用User-Agent头来标识自己。放在那里的内容取决于发出请求的软件,尽管通常包括一个标识程序和版本的字符串,可能还包括操作系统和运行的硬件。例如,我当前版本的 Firefox 的用户代理如下所示:

Mozilla/5.0 (X11; Linux x86_64; rv:24.0) Gecko/20140722 Firefox/24.0 Iceweasel/24.7.0

尽管这里被分成了两行,但它是一个单独的长字符串。正如你可能能够解释的那样,我正在运行 Iceweasel(Debian 版的 Firefox)24 版本,运行在 64 位 Linux 系统上。用户代理字符串并不是用来识别个别用户的。它们只标识用于发出请求的产品。

我们可以查看urllib使用的用户代理。执行以下步骤:

**>>> req = Request('http://www.python.org')**
**>>> urlopen(req)**
**>>> req.get_header('User-agent')**
**'Python-urllib/3.4'**

在这里,我们创建了一个请求并使用urlopen提交了它,urlopen添加了用户代理头到请求中。我们可以使用get_header()方法来检查这个头。这个头和它的值包含在urllib发出的每个请求中,所以我们向每个服务器发出请求时都可以看到我们正在使用 Python 3.4 和urllib库。

网站管理员可以检查请求的用户代理,然后将这些信息用于各种用途,包括以下内容:

  • 为了他们的网站统计分类访问

  • 阻止具有特定用户代理字符串的客户端

  • 发送给已知问题的用户代理的资源的替代版本,比如在解释某些语言(如 CSS)时出现的错误,或者根本不支持某些语言(比如 JavaScript)。

最后两个可能会给我们带来问题,因为它们可能会阻止或干扰我们访问我们想要的内容。为了解决这个问题,我们可以尝试设置我们的用户代理,使其模拟一个知名的浏览器。这就是所谓的欺骗,如下所示:

**>>> req = Request('http://www.debian.org')**
**>>> req.add_header('User-Agent', 'Mozilla/5.0 (X11; Linux x86_64; rv:24.0) Gecko/20140722 Firefox/24.0 Iceweasel/24.7.0')**
**>>> response = urlopen(req)**

服务器将会响应,就好像我们的应用程序是一个普通的 Firefox 客户端。不同浏览器的用户代理字符串可以在网上找到。我还没有找到一个全面的资源,但是通过谷歌搜索浏览器和版本号通常会找到一些信息。或者你可以使用 Wireshark 来捕获浏览器发出的 HTTP 请求,并查看捕获的请求的用户代理头。

Cookies

Cookie 是服务器在响应的一部分中以Set-Cookie头发送的一小段数据。客户端会将 cookie 存储在本地,并在以后发送到服务器的任何请求中包含它们。

服务器以各种方式使用 cookie。它们可以向其中添加一个唯一的 ID,这使它们能够跟踪客户端访问站点的不同区域。它们可以存储一个登录令牌,这将自动登录客户端,即使客户端离开站点然后以后再次访问。它们也可以用于存储客户端的用户偏好或个性化信息的片段,等等。

Cookie 是必需的,因为服务器没有其他方式在请求之间跟踪客户端。HTTP 被称为无状态协议。它不包含一个明确的机制,让服务器确切地知道两个请求是否来自同一个客户端。如果没有 cookie 允许服务器向请求添加一些唯一标识信息,像购物车(这是 cookie 开发的最初问题)这样的东西将变得不可能构建,因为服务器将无法确定哪个篮子对应哪个请求。

我们可能需要在 Python 中处理 cookie,因为没有它们,一些网站的行为不如预期。在使用 Python 时,我们可能还想访问需要登录的站点的部分,登录会话通常通过 cookie 来维护。

我们将讨论如何使用urllib处理 cookie。首先,我们需要创建一个存储服务器将发送给我们的 cookie 的地方:

**>>> from http.cookiejar import CookieJar**
**>>> cookie_jar = CookieJar()**

接下来,我们构建一个名为urllib opener **的东西。这将自动从我们收到的响应中提取 cookie,然后将它们存储在我们的 cookie jar 中:

**>>> from urllib.request import build_opener, HTTPCookieProcessor**
**>>> opener = build_opener(HTTPCookieProcessor(cookie_jar))**

然后,我们可以使用我们的 opener 来发出 HTTP 请求:

**>>> opener.open('http://www.github.com')**

最后,我们可以检查服务器是否发送了一些 cookie:

**>>> len(cookie_jar)**
**2**

每当我们使用opener发出进一步的请求时,HTTPCookieProcessor功能将检查我们的cookie_jar,看它是否包含该站点的任何 cookie,然后自动将它们添加到我们的请求中。它还将接收到的任何进一步的 cookie 添加到 cookie jar 中。

http.cookiejar模块还包含一个FileCookieJar类,它的工作方式与CookieJar相同,但它提供了一个额外的函数,用于轻松地将 cookie 保存到文件中。这允许在 Python 会话之间持久保存 cookie。

值得更详细地查看 cookie 的属性。让我们来检查 GitHub 在前一节中发送给我们的 cookie。

为此,我们需要从 cookie jar 中取出 cookie。CookieJar模块不允许我们直接访问它们,但它支持迭代器协议。因此,一个快速获取它们的方法是从中创建一个list

**>>> cookies = list(cookie_jar)**
**>>> cookies**
**[Cookie(version=0, name='logged_in', value='no', ...),**
 **Cookie(version=0, name='_gh_sess', value='eyJzZxNzaW9uX...', ...)**
**]**

您可以看到我们有两个Cookie对象。现在,让我们从第一个对象中提取一些信息:

**>>> cookies[0].name**
**'logged_in'**
**>>> cookies[0].value**
**'no'**

cookie 的名称允许服务器快速引用它。这个 cookie 显然是 GitHub 用来查明我们是否已经登录的机制的一部分。接下来,让我们做以下事情:

**>>> cookies[0].domain**
**'.github.com'**
**>>> cookies[0].path**
**'/'**

域和路径是此 cookie 有效的区域,因此我们的urllib opener 将在发送到www.github.com及其子域的任何请求中包含此 cookie,其中路径位于根目录下方的任何位置。

现在,让我们来看一下 cookie 的生命周期:

**>>> cookies[0].expires**
**2060882017**

这是一个 Unix 时间戳;我们可以将其转换为datetime

**>>> import datetime**
**>>> datetime.datetime.fromtimestamp(cookies[0].expires)**
**datetime.datetime(2035, 4, 22, 20, 13, 37)**

因此,我们的 cookie 将在 2035 年 4 月 22 日到期。到期日期是服务器希望客户端保留 cookie 的时间。一旦到期日期过去,客户端可以丢弃 cookie,并且服务器将在下一个请求中发送一个新的 cookie。当然,没有什么能阻止客户端立即丢弃 cookie,尽管在一些站点上,这可能会破坏依赖 cookie 的功能。

让我们讨论两个常见的 cookie 标志:

**>>> print(cookies[0].get_nonstandard_attr('HttpOnly'))**
**None**

存储在客户端上的 cookie 可以通过多种方式访问:

  • 由客户端作为 HTTP 请求和响应序列的一部分

  • 由客户端中运行的脚本,比如 JavaScript

  • 由客户端中运行的其他进程,比如 Flash

HttpOnly标志表示客户端只有在 HTTP 请求或响应的一部分时才允许访问 cookie。其他方法应该被拒绝访问。这将保护客户端免受跨站脚本攻击的影响(有关这些攻击的更多信息,请参见第九章Web 应用程序)。这是一个重要的安全功能,当服务器设置它时,我们的应用程序应该相应地行事。

还有一个secure标志:

**>>> cookies[0].secure**
**True**

如果值为 true,则Secure标志表示 cookie 只能通过安全连接发送,例如 HTTPS。同样,如果已设置该标志,我们应该遵守这一点,这样当我们的应用程序发送包含此 cookie 的请求时,它只会将它们发送到 HTTPS URL。

您可能已经发现了一个不一致之处。我们的 URL 已经请求了一个 HTTP 响应,然而服务器却发送了一个 cookie 给我们,要求它只能在安全连接上发送。网站设计者肯定没有忽视这样的安全漏洞吧?请放心,他们没有。实际上,响应是通过 HTTPS 发送的。但是,这是如何发生的呢?答案就在于重定向。

重定向

有时服务器会移动它们的内容。它们还会使一些内容过时,并在不同的位置放上新的东西。有时他们希望我们使用更安全的 HTTPS 协议而不是 HTTP。在所有这些情况下,他们可能会得到请求旧 URL 的流量,并且在所有这些情况下,他们可能更愿意能够自动将访问者发送到新的 URL。

HTTP 状态码的 300 系列是为此目的而设计的。这些代码指示客户端需要采取进一步的行动才能完成请求。最常见的操作是在不同的 URL 上重试请求。这被称为重定向

我们将学习在使用urllib时如何工作。让我们发出一个请求:

**>>> req = Request('http://www.gmail.com')**
**>>> response = urlopen(req)**

很简单,但现在,看一下响应的 URL:

**>>> response.url**
**'https://accounts.google.com/ServiceLogin?service=mail&passive=true&r m=false...'**

这不是我们请求的 URL!如果我们在浏览器中打开这个新的 URL,我们会发现这实际上是 Google 的登录页面(如果您已经有缓存的 Google 登录会话,则可能需要清除浏览器的 cookie 才能看到这一点)。Google 将我们从www.gmail.com重定向到其登录页面,urllib自动跟随了重定向。此外,我们可能已经被重定向了多次。看一下我们请求对象的redirect_dict属性:

**>>> req.redirect_dict**
**{'https://accounts.google.com/ServiceLogin?service=...': 1, 'https://mail.google.com/mail/': 1}**

urllib包将我们通过的每个 URL 添加到这个dict中。我们可以看到我们实际上被重定向了两次,首先是到mail.google.com,然后是到登录页面。

当我们发送第一个请求时,服务器会发送一个带有重定向状态代码的响应,其中之一是 301、302、303 或 307。所有这些都表示重定向。此响应包括一个Location头,其中包含新的 URL。urllib包将向该 URL 提交一个新的请求,在上述情况下,它将收到另一个重定向,这将导致它到达 Google 登录页面。

由于urllib为我们跟随重定向,它们通常不会影响我们,但值得知道的是,urllib返回的响应可能是与我们请求的 URL 不同的 URL。此外,如果我们对单个请求进行了太多次重定向(对于urllib超过 10 次),那么urllib将放弃并引发urllib.error.HTTPError异常。

URL

统一资源定位符,或者URL是 Web 操作的基础,它们已经在 RFC 3986 中正式描述。URL 代表主机上的资源。URL 如何映射到远程系统上的资源完全取决于系统管理员的决定。URL 可以指向服务器上的文件,或者在收到请求时资源可能是动态生成的。只要我们请求时 URL 有效,URL 映射到什么并不重要。

URL 由几个部分组成。Python 使用urllib.parse模块来处理 URL。让我们使用 Python 将 URL 分解为其组成部分:

**>>> from urllib.parse import urlparse**
**>>> result = urlparse('http://www.python.org/dev/peps')**
**>>> result**
**ParseResult(scheme='http', netloc='www.python.org', path='/dev/peps', params='', query='', fragment='')**

urllib.parse.urlparse()函数解释了我们的 URL,并识别http作为方案www.python.org/作为网络位置/dev/peps作为路径。我们可以将这些组件作为ParseResult的属性来访问:

**>>> result.netloc**
**'www.python.org'**
**>>> result.path**
**'/dev/peps'**

对于网上几乎所有的资源,我们将使用httphttps方案。在这些方案中,要定位特定的资源,我们需要知道它所在的主机和我们应该连接到的 TCP 端口(这些组合在一起是netloc组件),我们还需要知道主机上资源的路径(path组件)。

可以通过将端口号附加到主机后来在 URL 中明确指定端口号。它们与主机之间用冒号分隔。让我们看看当我们尝试使用urlparse时会发生什么。

**>>> urlparse('http://www.python.org:8080/')**
**ParseResult(scheme='http', netloc='www.python.org:8080', path='/', params='', query='', fragment='')**

urlparse方法只是将其解释为 netloc 的一部分。这没问题,因为这是urllib.request.urlopen()等处理程序期望它格式化的方式。

如果我们不提供端口(通常情况下),那么http将使用默认端口 80,https将使用默认端口 443。这通常是我们想要的,因为这些是 HTTP 和 HTTPS 协议的标准端口。

路径和相对 URL

URL 中的路径是指主机和端口之后的任何内容。路径总是以斜杠(/)开头,当只有一个斜杠时,它被称为。我们可以通过以下操作来验证这一点:

**>>> urlparse('http://www.python.org/')**
**ParseResult(scheme='http', netloc='www.python.org', path='/', params='', query='', fragment='')**

如果请求中没有提供路径,默认情况下urllib将发送一个请求以获取根目录。

当 URL 中包含方案和主机时(如前面的例子),该 URL 被称为绝对 URL。相反,也可能有相对 URL,它只包含路径组件,如下所示:

**>>> urlparse('../images/tux.png')**
**ParseResult(scheme='', netloc='', path='../images/tux.png', params='', query='', fragment='')**

我们可以看到ParseResult只包含一个path。如果我们想要使用相对 URL 请求资源,那么我们需要提供缺失的方案、主机和基本路径。

通常,我们在已从 URL 检索到的资源中遇到相对 URL。因此,我们可以使用该资源的 URL 来填充缺失的组件。让我们看一个例子。

假设我们已经检索到了www.debian.org的 URL,并且在网页源代码中找到了“关于”页面的相对 URL。我们发现它是intro/about的相对 URL。

我们可以通过使用原始页面的 URL 和urllib.parse.urljoin()函数来创建绝对 URL。让我们看看我们可以如何做到这一点:

**>>> from urllib.parse import urljoin**
**>>> urljoin('http://www.debian.org', 'intro/about')**
**'http://www.debian.org/intro/about'**

通过向urljoin提供基本 URL 和相对 URL,我们创建了一个新的绝对 URL。

在这里,注意urljoin是如何在主机和路径之间填充斜杠的。只有当基本 URL 没有路径时,urljoin才会为我们填充斜杠,就像前面的例子中所示的那样。让我们看看如果基本 URL 有路径会发生什么。

**>>> urljoin('http://www.debian.org/intro/', 'about')**
**'http://www.debian.org/intro/about'**
**>>> urljoin('http://www.debian.org/intro', 'about')**
**'http://www.debian.org/about'**

这将给我们带来不同的结果。请注意,如果基本 URL 以斜杠结尾,urljoin会将其附加到路径,但如果基本 URL 不以斜杠结尾,它将替换基本 URL 中的最后一个路径元素。

我们可以通过在路径前加上斜杠来强制路径替换基本 URL 的所有元素。按照以下步骤进行:

**>>> urljoin('http://www.debian.org/intro/about', '/News')**
**'http://www.debian.org/News'**

如何导航到父目录?让我们尝试标准的点语法,如下所示:

**>>> urljoin('http://www.debian.org/intro/about/', '../News')**
**'http://www.debian.org/intro/News'**
**>>> urljoin('http://www.debian.org/intro/about/', '../../News')**
**'http://www.debian.org/News'**
**>>> urljoin('http://www.debian.org/intro/about', '../News')**
**'http://www.debian.org/News'**

它按我们的预期工作。注意基本 URL 是否有尾随斜杠的区别。

最后,如果“相对”URL 实际上是绝对 URL 呢:

**>>> urljoin('http://www.debian.org/about', 'http://www.python.org')**
**'http://www.python.org'**

相对 URL 完全替换了基本 URL。这很方便,因为这意味着我们在使用urljoin时不需要担心 URL 是相对的还是绝对的。

查询字符串

RFC 3986 定义了 URL 的另一个属性。它们可以包含在路径之后以键/值对形式出现的附加参数。它们通过问号与路径分隔,如下所示:

docs.python.org/3/search.html?q=urlparse&area=default

这一系列参数称为查询字符串。多个参数由&分隔。让我们看看urlparse如何处理它:

**>>> urlparse('http://docs.python.org/3/search.html? q=urlparse&area=default')**
**ParseResult(scheme='http', netloc='docs.python.org', path='/3/search.html', params='', query='q=urlparse&area=default', fragment='')**

因此,urlparse将查询字符串识别为query组件。

查询字符串用于向我们希望检索的资源提供参数,并且通常以某种方式自定义资源。在上述示例中,我们的查询字符串告诉 Python 文档搜索页面,我们要搜索术语urlparse

urllib.parse模块有一个函数,可以帮助我们将urlparse返回的query组件转换为更有用的内容:

**>>> from urllib.parse import parse_qs**
**>>> result = urlparse ('http://docs.python.org/3/search.html?q=urlparse&area=default')**
**>>> parse_qs(result.query)**
**{'area': ['default'], 'q': ['urlparse']}**

parse_qs() 方法读取查询字符串,然后将其转换为字典。看看字典值实际上是以列表的形式存在的?这是因为参数可以在查询字符串中出现多次。尝试使用重复参数:

**>>> result = urlparse ('http://docs.python.org/3/search.html?q=urlparse&q=urljoin')**
**>>> parse_qs(result.query)**
**{'q': ['urlparse', 'urljoin']}**

看看这两个值都已添加到列表中?由服务器决定如何解释这一点。如果我们发送这个查询字符串,那么它可能只选择一个值并使用它,同时忽略重复。您只能尝试一下,看看会发生什么。

通常,您可以通过使用 Web 浏览器通过 Web 界面提交查询并检查结果页面的 URL 来弄清楚对于给定页面需要在查询字符串中放置什么。您应该能够找到搜索文本的文本,从而推断出搜索文本的相应键。很多时候,查询字符串中的许多其他参数实际上并不需要获得基本结果。尝试仅使用搜索文本参数请求页面,然后查看发生了什么。然后,如果预期的结果没有实现,添加其他参数。

如果您向页面提交表单并且结果页面的 URL 没有查询字符串,则该页面将使用不同的方法发送表单数据。我们将在接下来的HTTP 方法部分中查看这一点,同时讨论 POST 方法。

URL 编码

URL 仅限于 ASCII 字符,并且在此集合中,许多字符是保留字符,并且需要在 URL 的不同组件中进行转义。我们通过使用称为 URL 编码的东西来对它们进行转义。它通常被称为百分比编码,因为它使用百分号作为转义字符。让我们对字符串进行 URL 编码:

**>>> from urllib.parse import quote**
**>>> quote('A duck?')**
**'A%20duck%3F'**

特殊字符' '?已被转换为转义序列。转义序列中的数字是十六进制中的字符 ASCII 代码。

需要转义保留字符的完整规则在 RFC 3986 中给出,但是urllib为我们提供了一些帮助我们构造 URL 的方法。这意味着我们不需要记住所有这些!

我们只需要:

  • 对路径进行 URL 编码

  • 对查询字符串进行 URL 编码

  • 使用urllib.parse.urlunparse()函数将它们组合起来

让我们看看如何在代码中使用上述步骤。首先,我们对路径进行编码:

**>>> path = 'pypi'**
**>>> path_enc = quote(path)**

然后,我们对查询字符串进行编码:

**>>> from urllib.parse import urlencode**
**>>> query_dict = {':action': 'search', 'term': 'Are you quite sure this is a cheese shop?'}**
**>>> query_enc = urlencode(query_dict)**
**>>> query_enc**
**'%3Aaction=search&term=Are+you+quite+sure+this+is+a+cheese+shop%3F'**

最后,我们将所有内容组合成一个 URL:

**>>> from urllib.parse import urlunparse**
**>>> netloc = 'pypi.python.org'**
**>>> urlunparse(('http', netloc, path_enc, '', query_enc, ''))**
**'http://pypi.python.org/pypi?%3Aaction=search&term=Are+you+quite+sure +this+is+a+cheese+shop%3F'**

quote()函数已经设置用于特定编码路径。默认情况下,它会忽略斜杠字符并且不对其进行编码。在前面的示例中,这并不明显,尝试以下内容以查看其工作原理:

**>>> from urllib.parse import quote**
**>>> path = '/images/users/+Zoot+/'**
**>>> quote(path)**
**'/images/users/%2BZoot%2B/'**

请注意,它忽略了斜杠,但转义了+。这对路径来说是完美的。

urlencode()函数类似地用于直接从字典编码查询字符串。请注意,它如何正确地对我们的值进行百分比编码,然后使用&将它们连接起来,以构造查询字符串。

最后,urlunparse()方法期望包含与urlparse()结果匹配的元素的 6 元组,因此有两个空字符串。

对于路径编码有一个注意事项。如果路径的元素本身包含斜杠,那么我们可能会遇到问题。示例在以下命令中显示:

**>>> username = '+Zoot/Dingo+'**
**>>> path = 'images/users/{}'.format(username)**
**>>> quote(path)**
**'images/user/%2BZoot/Dingo%2B'**

注意用户名中的斜杠没有被转义吗?这将被错误地解释为额外的目录结构,这不是我们想要的。为了解决这个问题,首先我们需要单独转义可能包含斜杠的路径元素,然后手动连接它们:

**>>> username = '+Zoot/Dingo+'**
**>>> user_encoded = quote(username, safe='')**
**>>> path = '/'.join(('', 'images', 'users', username))**
**'/images/users/%2BZoot%2FDingo%2B'**

注意用户名斜杠现在是百分比编码了吗?我们单独对用户名进行编码,告诉quote不要忽略斜杠,通过提供safe=''参数来覆盖其默认的忽略列表/。然后,我们使用简单的join()函数组合路径元素。

在这里,值得一提的是,通过网络发送的主机名必须严格遵循 ASCII,但是sockethttp模块支持将 Unicode 主机名透明地编码为 ASCII 兼容的编码,因此在实践中我们不需要担心编码主机名。关于这个过程的更多细节可以在codecs模块文档的encodings.idna部分找到。

URL 总结

在前面的部分中,我们使用了相当多的函数。让我们简要回顾一下我们每个函数的用途。所有这些函数都可以在urllib.parse模块中找到。它们如下:

  • 将 URL 拆分为其组件:urlparse

  • 将绝对 URL 与相对 URL 组合:urljoin

  • 将查询字符串解析为dictparse_qs

  • 对路径进行 URL 编码:quote

  • dict创建 URL 编码的查询字符串:urlencode

  • 从组件创建 URL(urlparse的反向):urlunparse

HTTP 方法

到目前为止,我们一直在使用请求来请求服务器向我们发送网络资源,但是 HTTP 提供了更多我们可以执行的操作。我们请求行中的GET是一个 HTTP 方法,有几种方法,比如HEADPOSTOPTIONPUTDELETETRACECONNECTPATCH

我们将在下一章中详细讨论其中的一些,但现在我们将快速查看两种方法。

HEAD 方法

HEAD方法与GET方法相同。唯一的区别是服务器永远不会在响应中包含正文,即使在请求的 URL 上有一个有效的资源。HEAD方法用于检查资源是否存在或是否已更改。请注意,一些服务器不实现此方法,但当它们这样做时,它可以证明是一个巨大的带宽节省者。

我们使用urllib中的替代方法,通过在创建Request对象时提供方法名称:

**>>> req = Request('http://www.google.com', method='HEAD')**
**>>> response = urlopen(req)**
**>>> response.status**
**200**
**>>> response.read()**
**b''**

这里服务器返回了一个200 OK响应,但是正文是空的,这是预期的。

POST 方法

POST方法在某种意义上是GET方法的相反。我们使用POST方法向服务器发送数据。然而,服务器仍然可以向我们发送完整的响应。POST方法用于提交 HTML 表单中的用户输入和向服务器上传文件。

在使用POST时,我们希望发送的数据将放在请求的正文中。我们可以在那里放入任何字节数据,并通过在我们的请求中添加Content-Type头来声明其类型,使用适当的 MIME 类型。

让我们通过一个例子来看看如何通过 POST 请求向服务器发送一些 HTML 表单数据,就像浏览器在网站上提交表单时所做的那样。表单数据始终由键/值对组成;urllib让我们可以使用常规字典来提供这些数据(我们将在下一节中看到这些数据来自哪里):

**>>> data_dict = {'P': 'Python'}**

在发布 HTML 表单数据时,表单值必须以与 URL 中的查询字符串相同的方式进行格式化,并且必须进行 URL 编码。还必须设置Content-Type头为特殊的 MIME 类型application/x-www-form-urlencoded

由于这种格式与查询字符串相同,我们可以在准备数据时使用urlencode()函数:

**>>> data = urlencode(data_dict).encode('utf-8')**

在这里,我们还将结果额外编码为字节,因为它将作为请求的主体发送。在这种情况下,我们使用 UTF-8 字符集。

接下来,我们将构建我们的请求:

**>>> req = Request('http://search.debian.org/cgi-bin/omega', data=data)**

通过将我们的数据作为data关键字参数添加,我们告诉urllib我们希望我们的数据作为请求的主体发送。这将使请求使用POST方法而不是GET方法。

接下来,我们添加Content-Type头:

**>>> req.add_header('Content-Type', 'application/x-www-form-urlencode;  charset=UTF-8')**

最后,我们提交请求:

**>>> response = urlopen(req)**

如果我们将响应数据保存到文件并在网络浏览器中打开它,那么我们应该会看到一些与 Python 相关的 Debian 网站搜索结果。

正式检查

在前一节中,我们使用了 URLhttp://search.debian.org/cgibin/omega,和字典data_dict = {'P': 'Python'}。但这些是从哪里来的呢?

我们通过访问包含我们手动提交以获取结果的表单的网页来获得这些信息。然后我们检查网页的 HTML 源代码。如果我们在网络浏览器中进行上述搜索,那么我们很可能会在www.debian.org页面上,并且我们将通过在右上角的搜索框中输入搜索词然后点击搜索来进行搜索。

大多数现代浏览器允许您直接检查页面上任何元素的源代码。要做到这一点,右键单击元素,这种情况下是搜索框,然后选择检查元素选项,如此屏幕截图所示:

正式检查

源代码将在窗口的某个部分弹出。在前面的屏幕截图中,它位于屏幕的左下角。在这里,您将看到一些代码行,看起来像以下示例:

<form action="http://search.debian.org/cgi-bin/omega"
method="get" name="P">
  <p>
    <input type="hidden" value="en" name="DB"></input>
    **<input size="27" value="" name="P"></input>**
    <input type="submit" value="Search"></input>
  </p>
</form>

您应该看到第二个高亮显示的<input>。这是对应于搜索文本框的标签。高亮显示的<input>标签上的name属性的值是我们在data_dict中使用的键,这种情况下是P。我们data_dict中的值是我们要搜索的术语。

要获取 URL,我们需要在高亮显示的<input>上方查找包围的<form>标签。在这里,我们的 URL 将是action属性的值,search.debian.org/cgi-bin/omega。本书的源代码下载中包含了此网页的源代码,以防 Debian 在您阅读之前更改他们的网站。

这个过程可以应用于大多数 HTML 页面。要做到这一点,找到与输入文本框对应的<input>,然后从包围的<form>标签中找到 URL。如果您不熟悉 HTML,那么这可能是一个反复试验的过程。我们将在下一章中看一些解析 HTML 的更多方法。

一旦我们有了我们的输入名称和 URL,我们就可以构建并提交 POST 请求,就像在前一节中所示的那样。

HTTPS

除非另有保护,所有 HTTP 请求和响应都是以明文发送的。任何可以访问消息传输的网络的人都有可能拦截我们的流量并毫无阻碍地阅读它。

由于网络用于传输大量敏感数据,已经创建了一些解决方案,以防止窃听者阅读流量,即使他们能够拦截它。这些解决方案在很大程度上采用了某种形式的加密。

加密 HTTP 流量的标准方法称为 HTTP 安全,或HTTPS。它使用一种称为 TLS/SSL 的加密机制,并应用于 HTTP 流量传输的 TCP 连接上。HTTPS 通常使用 TCP 端口 443,而不是默认的 HTTP 端口 80。

对于大多数用户来说,这个过程几乎是透明的。原则上,我们只需要将 URL 中的 http 更改为 https。由于urllib支持 HTTPS,因此对于我们的 Python 客户端也是如此。

请注意,并非所有服务器都支持 HTTPS,因此仅将 URL 方案更改为https:并不能保证适用于所有站点。如果是这种情况,连接尝试可能会以多种方式失败,包括套接字超时、连接重置错误,甚至可能是 HTTP 错误,如 400 范围错误或 500 范围错误。然而,越来越多的站点正在启用 HTTPS。许多其他站点正在切换到 HTTPS 并将其用作默认协议,因此值得调查它是否可用,以便为应用程序的用户提供额外的安全性。

Requests

这就是关于urllib包的全部内容。正如你所看到的,访问标准库对于大多数 HTTP 任务来说已经足够了。我们还没有涉及到它的所有功能。还有许多处理程序类我们没有讨论,而且打开接口是可扩展的。

然而,API 并不是最优雅的,已经有几次尝试来改进它。其中一个是非常受欢迎的第三方库Requests。它作为requests包在 PyPi 上可用。它可以通过 Pip 安装,也可以从docs.python-requests.org下载,该网站提供了文档。

Requests库自动化并简化了我们一直在研究的许多任务。最快的说明方法是尝试一些示例。

使用Requests检索 URL 的命令与使用urllib包检索 URL 的命令类似,如下所示:

**>>> import requests**
**>>> response = requests.get('http://www.debian.org')**

我们可以查看响应对象的属性。尝试:

**>>> response.status_code**
**200**
**>>> response.reason**
**'OK'**
**>>> response.url**
**'http://www.debian.org/'**
**>>> response.headers['content-type']**
**'text/html'**

请注意,前面命令中的标头名称是小写的。Requests响应对象的headers属性中的键是不区分大小写的。

响应对象中添加了一些便利属性:

**>>> response.ok**
**True**

ok属性指示请求是否成功。也就是说,请求包含的状态码在 200 范围内。另外:

**>>> response.is_redirect**
**False**

is_redirect属性指示请求是否被重定向。我们还可以通过响应对象访问请求属性:

**>>> response.request.headers**
**{'User-Agent': 'python-requests/2.3.0 CPython/3.4.1 Linux/3.2.0-4- amd64', 'Accept-Encoding': 'gzip, deflate', 'Accept': '*/*'}**

请注意,Requests会自动处理压缩。它在Accept-Encoding头中包括gzipdeflate。如果我们查看Content-Encoding响应,我们会发现响应实际上是gzip压缩的,而Requests会自动为我们解压缩:

**>>> response.headers['content-encoding']**
**'gzip'**

我们可以以更多的方式查看响应内容。要获得与HTTPResponse对象相同的字节对象,执行以下操作:

**>>> response.content**
**b'<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01//EN" "http://www.w3.org/TR/html4/strict.dtd">\n<html lang="en">...**

但是,Requests还会自动解码。要获取解码后的内容,请执行以下操作:

**>>> response.text**
**'<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01//EN" "http://www.w3.org/TR/html4/strict.dtd">\n<html lang="en">\n<head>\n**
**...**

请注意,这现在是str而不是bytesRequests库使用头中的值来选择字符集并将内容解码为 Unicode。如果无法从头中获取字符集,则使用chardet库(pypi.python.org/pypi/chardet)从内容本身进行估计。我们可以看到Requests选择了哪种编码:

**>>> response.encoding**
**'ISO-8859-1'**

我们甚至可以要求它更改已使用的编码:

**>>> response.encoding = 'utf-8'**

更改编码后,对于此响应的text属性的后续引用将返回使用新编码设置解码的内容。

Requests库会自动处理 Cookie。试试这个:

**>>> response = requests.get('http://www.github.com')**
**>>> print(response.cookies)**
**<<class 'requests.cookies.RequestsCookieJar'>**
**[<Cookie logged_in=no for .github.com/>,**
 **<Cookie _gh_sess=eyJzZxNz... for ..github.com/>]>**

Requests库还有一个Session类,允许重复使用 cookie,这类似于使用http模块的CookieJarurllib模块的HTTPCookieHandler对象。要在后续请求中重复使用 cookie,请执行以下操作:

**>>> s = requests.Session()**
**>>> s.get('http://www.google.com')**
**>>> response = s.get('http://google.com/preferences')**

Session对象具有与requests模块相同的接口,因此我们可以像使用“requests.get()”方法一样使用其get()方法。现在,遇到的任何 cookie 都将存储在Session对象中,并且在将来使用get()方法时将随相应的请求发送。

重定向也会自动跟随,方式与使用urllib时相同,并且任何重定向的请求都会被捕获在history属性中。

不同的 HTTP 方法很容易访问,它们有自己的功能:

**>>> response = requests.head('http://www.google.com')**
**>>> response.status_code**
**200**
**>>> response.text**
**''**

自定义标头以类似于使用urllib时的方式添加到请求中:

**>>> headers = {'User-Agent': 'Mozilla/5.0 Firefox 24'}**
**>>> response = requests.get('http://www.debian.org', headers=headers)**

使用查询字符串进行请求是一个简单的过程:

**>>> params = {':action': 'search', 'term': 'Are you quite sure this is a cheese shop?'}**
**>>> response = requests.get('http://pypi.python.org/pypi', params=params)**
**>>> response.url**
**'https://pypi.python.org/pypi?%3Aaction=search&term=Are+you+quite+sur e+this+is+a+cheese+shop%3F'**

Requests库为我们处理所有的编码和格式化工作。

发布也同样简化,尽管我们在这里使用data关键字参数:

**>>> data = {'P', 'Python'}**
**>>> response = requests.post('http://search.debian.org/cgi- bin/omega', data=data)**

使用 Requests 处理错误

Requests中的错误处理与使用urllib处理错误的方式略有不同。让我们通过一些错误条件来看看它是如何工作的。通过以下操作生成一个 404 错误:

**>>> response = requests.get('http://www.google.com/notawebpage')**
**>>> response.status_code**
**404**

在这种情况下,urllib会引发异常,但请注意,Requests不会。 Requests库可以检查状态代码并引发相应的异常,但我们必须要求它这样做:

**>>> response.raise_for_status()**
**...**
**requests.exceptions.HTTPError: 404 Client Error**

现在,尝试在成功的请求上进行测试:

**>>> r = requests.get('http://www.google.com')**
**>>> r.status_code**
**200**
**>>> r.raise_for_status()**
**None**

它不做任何事情,这在大多数情况下会让我们的程序退出try/except块,然后按照我们希望的方式继续。

如果我们遇到协议栈中较低的错误会发生什么?尝试以下操作:

**>>> r = requests.get('http://192.0.2.1')**
**...**
**requests.exceptions.ConnectionError: HTTPConnectionPool(...**

我们已经发出了一个主机不存在的请求,一旦超时,我们就会收到一个ConnectionError异常。

urllib相比,Requests库简化了在 Python 中使用 HTTP 所涉及的工作量。除非您有使用urllib的要求,我总是建议您在项目中使用Requests

总结

我们研究了 HTTP 协议的原则。我们看到如何使用标准库urllib和第三方Requests包执行许多基本任务。

我们研究了 HTTP 消息的结构,HTTP 状态代码,我们可能在请求和响应中遇到的不同标头,以及如何解释它们并用它们来定制我们的请求。我们看了 URL 是如何形成的,以及如何操作和构建它们。

我们看到了如何处理 cookie 和重定向,如何处理可能发生的错误,以及如何使用安全的 HTTP 连接。

我们还介绍了如何以提交网页表单的方式向网站提交数据,以及如何从页面源代码中提取我们需要的参数。

最后,我们看了第三方的Requests包。我们发现,与urllib包相比,Requests自动化并简化了我们可能需要用 HTTP 进行的许多常规任务。这使得它成为日常 HTTP 工作的绝佳选择。

在下一章中,我们将运用我们在这里学到的知识,与不同的网络服务进行详细的交互,查询 API 以获取数据,并将我们自己的对象上传到网络。

栏目分类

联系方式
  • help@yxrjt.cn
  • lgc@yxrjt.cn
  • admin@yxrjt.cn