如果您需要使用 Python 发出 HTTP 请求,那么您可能会发现自己被引导至 brilliantrequests库。尽管它是一个很棒的库,但您可能已经注意到它并不是 Python 的内置部分。如果您出于某种原因更喜欢限制依赖项并坚持使用标准库 Python,那么您可以使用urllib.request!
在本教程中,您将:
了解如何发出基本的HTTP 请求urllib.request
深入了解HTTP 消息的具体细节及其urllib.request表示方式
了解如何处理HTTP 消息的字符编码
探索使用时的一些常见错误urllib.request并学习如何解决它们
将您的脚趾浸入经过身份验证的请求的世界urllib.request
理解为什么两者urllib和requests库都存在以及何时使用其中一个或另一个
如果您听说过 HTTP 请求,包括GET和POST,那么您可能已经准备好学习本教程了。此外,您应该已经使用 Python读取和写入文件,最好使用上下文管理器,至少一次。
最终,您会发现提出请求不一定是令人沮丧的经历,尽管它确实有这样的名声。您经常遇到的许多问题都是由于 Internet 这个奇妙事物的内在复杂性造成的。好消息是该urllib.request模块可以帮助揭开这种复杂性的神秘面纱。
了解更多信息: 单击此处与 290,000 多名 Python 开发人员一起阅读 Real Python Newsletter ,获取新的 Python 教程和新闻,让您成为更高效的 Pythonista。
在深入了解什么是 HTTP 请求及其工作原理之前,您需要通过向示例 URL发出基本的 GET 请求来了解一下。您还将向模拟REST API发出 GET 请求以获取某些JSON数据。如果您想了解 POST 请求,一旦您对urllib.request.
Beware:根据您的具体设置,您可能会发现其中一些示例不起作用。如果是这样,请跳到有关常见 urllib.request 错误 的部分以进行故障排除。如果您遇到此处未涵盖的问题,请务必在下面使用精确且可重现的示例进行评论。
首先,您将向 发出请求www.example.com,服务器将返回一条 HTTP 消息。确保您使用的是 Python 3 或更高版本,然后使用urlopen()函数 from urllib.request:
from urllib.request import urlopen
with urlopen("https://www.example.com") as response:
body = response.read()
body[:15]
b''
在此示例中,您urlopen()从urllib.request. 使用上下文管理器 with,您可以发出请求并接收响应urlopen()。然后您读取响应的主体并关闭响应对象。这样,您就可以显示正文的前十五个位置,并指出它看起来像一个 HTML 文档。
你在这!您已成功发出请求,并收到了回复。通过检查内容,您可以判断它很可能是一个 HTML 文档。请注意,正文的打印输出前面有b。这表示一个bytes literal,您可能需要对其进行解码。在本教程的后面,您将学习如何将字节转换为字符串、将它们写入文件或将它们解析为字典。
如果您要调用 REST API 以获取 JSON 数据,则该过程仅略有不同。在下面的示例中,您将向{JSON}Placeholder请求一些虚假的待办事项数据:
from urllib.request import urlopen
import json
url = "https://jsonplaceholder.typicode.com/todos/1"
with urlopen(url) as response:
body = response.read()
todo_item = json.loads(body)
todo_item
{'userId': 1, 'id': 1, 'title': 'delectus aut autem', 'completed': False}
在此示例中,您所做的与上一个示例中的操作几乎相同。但在这一个中,您导入urllib.request and json,使用json.loads()函数 withbody将返回的 JSON 字节解码和解析为Python 字典。瞧!
如果您足够幸运能够使用无错误的端点,例如这些示例中的端点,那么也许以上就是您需要的全部urllib.request。话又说回来,您可能会发现这还不够。
现在,在进行一些urllib.request故障排除之前,您将首先了解 HTTP 消息的底层结构并了解如何urllib.request处理它们。这种理解将为解决许多不同类型的问题提供坚实的基础。
要了解您在使用 时可能遇到的一些问题,您urllib.request需要检查响应是如何由 表示的urllib.request。为此,您将受益于对HTTP 消息是什么的高级概述,这就是您将在本节中获得的内容。
在进行高级概述之前,先简要说明一下参考来源。如果您想深入了解技术领域,互联网工程任务组 (IETF)有大量的征求意见 (RFC)文档。这些文档最终成为 HTTP 消息之类的实际规范。例如, RFC 7230,第 1 部分:消息语法和路由都是关于 HTTP 消息的。
如果您正在寻找比 RFC 更容易理解的参考资料,那么Mozilla 开发者网络 (MDN)有大量参考文章。例如,他们关于HTTP 消息的文章虽然仍然是技术性的,但更容易理解。
现在您已经了解了这些基本的参考信息来源,在下一节中您将对 HTTP 消息有一个初学者友好的概述。
简而言之,HTTP 消息可以理解为文本,以字节流的形式传输,其结构遵循 RFC 7230 指定的准则。解码后的 HTTP 消息可以像两行一样简单:
GET / HTTP/1.1
Host: www.google.com
这使用协议在根 ( )指定GET请求。唯一需要的标头是主机,. 目标服务器有足够的信息来使用此信息做出响应。/HTTP/1.1www.google.com
响应在结构上类似于请求。HTTP 消息有两个主要部分,元数据和正文。在上面的请求示例中,消息是没有正文的所有元数据。另一方面,响应确实有两个部分:
HTTP/1.1 200 OK
Content-Type: text/html; charset=ISO-8859-1
Server: gws
(... other headers ...)
响应以指定 HTTP 协议和状态的状态行开始。在状态行之后,你会得到许多键值对,例如,代表所有的响应头。这是响应的元数据。HTTP/1.1200 OKServer: gws
在元数据之后,有一个空行,用作标题和正文之间的分隔符。空行之后的所有内容构成主体。这是您在使用urllib.request.
注意:空行在技术上通常称为 换行符 。HTTP 消息中的换行符必须是 Windows 样式的 回车符 ( \r ) 和 行尾 符( \n )。在 类 Unix 系统上,换行符通常只是一个行尾 ( \n )。
您可以假定所有 HTTP 消息都遵循这些规范,但有些消息可能会违反这些规则或遵循旧规范。不过,这很少会引起任何问题。因此,请将其牢记在心,以防遇到奇怪的错误!
在下一节中,您将了解如何urllib.request处理原始 HTTP 消息。
使用时您将与之交互的 HTTP 消息的主要表示urllib.request是HTTPResponse对象。urllib.request模块本身依赖于低级http模块,您不需要直接与之交互。不过,您确实最终会使用一些提供的数据结构http,例如HTTPResponse和HTTPMessage。
注意:在 Python 中表示 HTTP 响应和消息的对象的内部命名可能有点混乱。您通常只与 的实例进行交互HTTPResponse ,而请求端则在内部处理。
您可能认为这HTTPMessage 是一种HTTPResponse 继承自的基类,但事实并非如此。HTTPResponse 直接继承自io.BufferedIOBase ,而HTTPMessage 类继承自 email.message.EmailMessage 。
在源代码中将其EmailMessage 定义为包含一堆标头和有效负载的对象,因此它不一定是电子邮件。HTTPResponse 仅用HTTPMessage 作其标头的容器。
但是,如果您谈论的是 HTTP 本身而不是它的 Python 实现,那么您将 HTTP 响应视为一种 HTTP 消息是正确的。
当您使用 发出请求时urllib.request.urlopen(),您会得到一个HTTPResponse对象作为回报。花一些时间探索HTTPResponse对象pprint()并dir()查看属于它的所有不同方法和属性:
from urllib.request import urlopen
from pprint import pprint
with urlopen("https://www.example.com") as response:
pprint(dir(response))
要显示此代码片段的输出,请单击以展开下面的可折叠部分:
['__abstractmethods__',
'__class__',
'__del__',
'__delattr__',
'__dict__',
'__dir__',
'__doc__',
'__enter__',
'__eq__',
'__exit__',
'__format__',
'__ge__',
'__getattribute__',
'__gt__',
'__hash__',
'__init__',
'__init_subclass__',
'__iter__',
'__le__',
'__lt__',
'__module__',
'__ne__',
'__new__',
'__next__',
'__reduce__',
'__reduce_ex__',
'__repr__',
'__setattr__',
'__sizeof__',
'__str__',
'__subclasshook__',
'_abc_impl',
'_checkClosed',
'_checkReadable',
'_checkSeekable',
'_checkWritable',
'_check_close',
'_close_conn',
'_get_chunk_left',
'_method',
'_peek_chunked',
'_read1_chunked',
'_read_and_discard_trailer',
'_read_next_chunk_size',
'_read_status',
'_readall_chunked',
'_readinto_chunked',
'_safe_read',
'_safe_readinto',
'begin',
'chunk_left',
'chunked',
'close',
'closed',
'code',
'debuglevel',
'detach',
'fileno',
'flush',
'fp',
'getcode',
'getheader',
'getheaders',
'geturl',
'headers',
'info',
'isatty',
'isclosed',
'length',
'msg',
'peek',
'read',
'read1',
'readable',
'readinto',
'readinto1',
'readline',
'readlines',
'reason',
'seek',
'seekable',
'status',
'tell',
'truncate',
'url',
'version',
'will_close',
'writable',
'write',
'writelines']
这是很多方法和属性,但您最终只会使用其中的一小部分。除此之外.read(),重要的通常涉及获取有关标头的信息。
检查所有标头的一种方法是访问对象的.headers属性HTTPResponse。这将返回一个HTTPMessage对象。方便地,您可以HTTPMessage通过调用.items()它来将所有标头作为元组来对待它,就像字典一样:
with urlopen("https://www.example.com") as response:
pass
response.headers
pprint(response.headers.items())
[('Accept-Ranges', 'bytes'),
('Age', '398424'),
('Cache-Control', 'max-age=604800'),
('Content-Type', 'text/html; charset=UTF-8'),
('Date', 'Tue, 25 Jan 2022 12:18:53 GMT'),
('Etag', '"3147526947"'),
('Expires', 'Tue, 01 Feb 2022 12:18:53 GMT'),
('Last-Modified', 'Thu, 17 Oct 2019 07:18:26 GMT'),
('Server', 'ECS (nyb/1D16)'), ('Vary', 'Accept-Encoding'),
('X-Cache', 'HIT'),
('Content-Length', '1256'),
('Connection', 'close')]
现在您可以访问所有响应标头了!您可能不需要这些信息中的大部分,但请放心,某些应用程序确实会用到它。例如,您的浏览器可能会使用标头来读取响应、设置 cookie 并确定适当的缓存生存期。
有一些方便的方法可以从HTTPResponse对象中获取标头,因为这是很常见的操作。您可以.getheaders()直接调用该HTTPResponse对象,它将返回与上面完全相同的元组列表。如果您只对一个标题感兴趣,比如Server标题,那么您可以使用单数.getheader("Server")onHTTPResponse或使用方括号 ( []) 语法 on .headersfrom HTTPMessage:
>>> response.getheader("Server")
'ECS (nyb/1D16)'
>>> response.headers["Server"]
'ECS (nyb/1D16)'
说实话,您可能不需要像这样直接与标题进行交互。您最可能需要的信息可能已经有一些内置的辅助方法,但现在您知道了,以防您需要更深入地挖掘!
该对象与文件对象HTTPResponse有很多共同之处。类继承自类,文件对象也是如此,这意味着您必须注意打开和关闭。HTTPResponseIOBase
在简单的程序中,如果您忘记关闭HTTPResponse对象,您不太可能注意到任何问题。但是,对于更复杂的项目,这会显着降低执行速度并导致难以查明的错误。
出现问题是因为输入/输出(I/O) 流是有限的。每个都HTTPResponse需要在读取时保持流清晰。如果您从不关闭您的流,这最终将阻止任何其他流被打开,并且它可能会干扰其他程序甚至您的操作系统。
所以,一定要关闭你的HTTPResponse对象!为方便起见,您可以使用上下文管理器,如您在示例中所见。您还可以通过显式调用.close()响应对象来获得相同的结果:
from urllib.request import urlopen
response = urlopen("https://www.example.com")
body = response.read()
response.close()
在此示例中,您不使用上下文管理器,而是显式关闭响应流。不过,上面的示例仍然存在问题,因为在调用 之前可能会引发异常.close(),从而阻止正确的拆解。为了使这个调用成为无条件的,就像它应该的那样,你可以使用带有一个和一个子句的try…except块:elsefinally
from urllib.request import urlopen
response = None
try:
response = urlopen("https://www.example.com")
except Exception as ex:
print(ex)
else:
body = response.read()
finally:
if response is not None:
response.close()
.close()在此示例中,您通过使用块实现了对 的无条件调用,finally无论是否引发异常,该块都将始终运行。块中的代码finally首先检查response对象是否存在is not None,然后将其关闭。
也就是说,这正是 aa 上下文管理器所做的,并且with语法通常是首选的。语法不仅with更简洁、更易读,而且还可以保护您免受讨厌的遗漏错误。换句话说,它可以更好地防止意外忘记关闭对象:
from urllib.request import urlopen
with urlopen("https://www.example.com") as response:
response.read(50)
response.read(50)
b'\n\n\n
Example D'b'omain \n\n \n
urlopen()在此示例中,您从urllib.request模块导入。您使用with关键字 with.urlopen()将HTTPResponse对象分配给变量response。然后,您读取响应的前五十个字节,然后读取后面的五十个字节,所有这些都在with块内。最后,您关闭with块,它执行请求并运行其块内的代码行。
使用此代码,您可以显示两组,每组 50 个字节。HTTPResponse一旦退出块范围,该对象将关闭with,这意味着该.read()方法将只返回空字节对象:
import urllib.request
with urllib.request.urlopen("https://www.example.com") as response:
response.read(50)
b'\n\n\n Example D'
response.read(50)
b''
在此示例中,读取五十个字节的第二次调用超出了with范围。在with块之外意味着HTTPResponse关闭,即使您仍然可以访问该变量。如果您尝试从HTTPResponse它关闭时开始读取,它将返回一个空字节对象。
另一点需要注意的是,一旦你一直阅读到最后,你就不能重新阅读回复:
import urllib.request
with urllib.request.urlopen("https://www.example.com") as response:
first_read = response.read()
second_read = response.read()
len(first_read)
1256
len(second_read)
0
这个例子表明,一旦你读过一个回复,你就不能再读了。如果您已完全阅读响应,即使响应未关闭,后续尝试也只会返回一个空字节对象。你必须再次提出请求。
在这方面,响应与文件对象不同,因为对于文件对象,您可以使用不支持的.seek()方法多次读取它。HTTPResponse即使在关闭响应后,您仍然可以访问标头和其他元数据。
到目前为止,在大多数示例中,您从 读取响应主体HTTPResponse,立即显示结果数据,并注意到它显示为字节对象。这是因为计算机中的文本信息不是以字母形式存储或传输的,而是以字节形式存储和传输的!
通过网络发送的原始 HTTP 消息被分解为字节序列,有时称为八位字节。字节是 8位块。例如,01010101是一个字节。要了解有关二进制、位和字节的更多信息,请查看Python 中的位运算符。
那么如何用字节表示字母呢?一个字节有 256 种可能的组合,您可以为每种组合分配一个字母。您可以分配00000001给A、00000010给B等等。相当普遍的ASCII字符编码,使用这种系统编码128个字符,对于像英语这样的语言来说已经足够了。这是特别方便的,因为一个字节就可以表示所有的字符,还有空间。
所有标准英语字符,包括大写字母、标点符号和数字,都适合 ASCII。另一方面,日语被认为有大约五万个符号字符,所以 128 个字符不够用!即使是理论上在一个字节内可用的 256 个字符对于日语来说也远远不够。因此,为了适应世界上所有的语言,有许多不同的字符编码系统。
尽管有很多系统,但您可以信赖的一件事是它们总是被分解成字节。大多数服务器,如果无法解析MIME 类型和字符编码,默认为application/octet-stream,字面意思是字节流。然后谁收到消息就可以计算出字符编码。
问题经常出现是因为,正如您可能已经猜到的那样,有很多很多不同的潜在字符编码。当今占主导地位的字符编码是UTF-8,它是Unicode的一种实现。幸运的是,今天98% 的网页都是用 UTF-8 编码的!
UTF-8 之所以占主导地位,是因为它可以有效地处理数量多得令人难以置信的字符。它处理 Unicode 定义的所有 1,112,064 个潜在字符,包括中文、日文、阿拉伯文(使用从右到左的脚本)、俄文和更多字符集,包括表情符号!
UTF-8 保持高效,因为它使用可变数量的字节来编码字符,这意味着对于许多字符,它只需要一个字节,而对于其他字符,它可能需要多达四个字节。
注意:要了解有关 Python 中编码的更多信息,请查看 Python 中的 Unicode 和字符编码:无痛指南 。
虽然 UTF-8 占主导地位,并且假设 UTF-8 编码通常不会出错,但您仍然会一直遇到不同的编码。好消息是,在使用urllib.request.
当您使用urllib.request.urlopen()时,响应的主体是一个字节对象。您可能想要做的第一件事是将字节对象转换为字符串。也许您想做一些网页抓取。为此,您需要解码字节。要用 Python 解码字节,您只需找出所使用的字符编码即可。编码,特别是指字符编码时,通常称为字符集。
如前所述,在百分之九十八的情况下,默认使用 UTF-8 可能是安全的:
from urllib.request import urlopen
with urlopen("https://www.example.com") as response:
body = response.read()
decoded_body = body.decode("utf-8")
print(decoded_body[:30])
在此示例中,您获取从返回的字节对象并使用字节对象的方法response.read()对其进行解码,并将其作为参数传入。当您打印时,您可以看到它现在是一个字符串。.decode()utf-8 decoded_body
也就是说,听之任之很少是一个好的策略。幸运的是,标头是获取字符集信息的好地方:
from urllib.request import urlopen
with urlopen("https://www.example.com") as response:
body = response.read()
character_set = response.headers.get_content_charset()
character_set
'utf-8'
decoded_body = body.decode(character_set)
print(decoded_body[:30])
在此示例中,您调用.get_content_charset()的.headers对象response并使用它进行解码。这是一种解析Content-Type标头的便捷方法,因此您可以轻松地将字节解码为文本。
如果您想将字节解码为文本,现在就可以开始了。但是,如果您想将响应的主体写入文件怎么办?那么,你有两个选择:
将字节直接写入文件
将字节解码为 Python 字符串,然后将字符串编码回文件
第一种方法最直接,但第二种方法允许您根据需要更改编码。要更详细地了解文件操作,请查看 Real Python 的 Python读写文件(指南)。
要将字节直接写入文件而无需解码,您需要内置open()函数,并且需要确保使用写入二进制模式:
from urllib.request import urlopen
with urlopen("https://www.example.com") as response:
body = response.read()
with open("example.html", mode="wb") as html_file:
html_file.write(body)
1256
使用open()inwb模式绕过解码或编码的需要,并将 HTTP 消息正文的字节转储到example.html文件中。写入操作后输出的数字表示已写入的字节数。就是这样!您已将字节直接写入文件,而无需编码或解码任何内容。
现在假设您有一个不使用 UTF-8 的 URL,但您希望将内容写入具有 UTF-8 的文件。为此,您首先要将字节解码为字符串,然后将字符串编码为文件,并指定字符编码。
Google 的主页似乎根据您所在的位置使用不同的编码。在欧洲和美国的大部分地区,它使用ISO-8859-1编码:
from urllib.request import urlopen
with urlopen("https://www.google.com") as response:
body = response.read()
character_set = response.headers.get_content_charset()
character_set
'ISO-8859-1'
content = body.decode(character_set)
with open("google.html", encoding="utf-8", mode="w") as file:
file.write(content)
14066
在此代码中,您获得了响应字符集并使用它将字节对象解码为字符串。然后将字符串写入文件,使用 UTF-8 对其进行编码。
注意:有趣的是,Google 似乎有多层检查,用于确定使用何种语言和编码为网页提供服务。这意味着您可以指定一个 Accept-Language 标头 ,这似乎会覆盖您的 IP 位置。 用不同的Locale Identifiers 试试,看看你能得到什么编码!
写入文件后,您应该能够在浏览器或文本编辑器中打开生成的文件。大多数现代文本处理器都可以自动检测字符编码。
如果存在编码错误并且您正在使用 Python 读取文件,那么您可能会收到错误消息:
with open("encoding-error.html", mode="r", encoding="utf-8") as file:
lines = file.readlines()
UnicodeDecodeError:
'utf-8' codec can't decode byte
Python 显式停止进程并引发异常,但在显示文本的程序中,例如您正在查看此页面的浏览器,您可能会发现臭名昭著的替换字符:
替换字符
带有白色问号 (�) 的黑色菱形、正方形 (□) 和矩形 (▯) 通常用作无法解码的字符的替代品。
有时,解码似乎有效,但会导致无法理解的序列,例如æ–‡å—化ã '。,这也表明使用了错误的字符集。在日本,他们甚至有一个词来表示由于字符编码问题而出现乱码的文本,Mojibake,因为这些问题在互联网时代开始时就困扰着他们。
这样,您现在应该能够使用从urlopen(). 在下一节中,您将学习如何使用该json模块将字节解析为 Python 字典。
对于application/json响应,您经常会发现它们不包含任何编码信息:
from urllib.request import urlopen
with urlopen("https://httpbin.org/json") as response:
body = response.read()
character_set = response.headers.get_content_charset()
print(character_set)
None
在此示例中,您使用httpbinjson的端点,该服务允许您试验不同类型的请求和响应。端点模拟返回 JSON 数据的典型 API。请注意,该方法在其响应中不返回任何内容。json.get_content_charset()
即使没有字符编码信息,也不会全部丢失。根据RFC 4627,UTF-8 的默认编码是规范的绝对要求application/json。这并不是说每个服务器都遵守规则,但一般来说,您可以假设如果传输 JSON,它几乎总是使用 UTF-8 编码。
幸运的是,json.loads()在引擎盖下解码字节对象,甚至在它可以处理的不同编码方面有一些回旋余地。因此,json.loads()只要它们是有效的 JSON,就应该能够处理您抛给它的大多数字节对象:
import json
json.loads(body)
{'slideshow': {'author': 'Yours Truly', 'date': 'date of publication', 'slides': [{'title': 'Wake up to WonderWidgets!', 'type': 'all'}, {'items': ['Why WonderWidgets are great', 'Who buys WonderWidgets'], 'title': 'Overview', 'type': 'all'}], 'title': 'Sample Slide Show'}}
如您所见,该json模块自动处理解码并生成 Python 字典。几乎所有 API 都以 JSON 形式返回键值信息,尽管您可能会遇到一些使用XML的旧 API 。为此,您可能需要查看Roadmap to XML Parsers in Python。
有了它,您应该对字节和编码有足够的了解,以免造成危险!在下一节中,您将学习如何排除和修复您在使用urllib.request.
无论您是否使用,您都可能在万维网上遇到多种问题urllib.request。在本节中,您将学习如何在开始时处理几个最常见的错误:403错误和TLS/SSL 证书错误。不过,在查看这些特定错误之前,您将首先了解如何在使用urllib.request.
在您将注意力转移到特定错误之前,提高您的代码优雅地处理各种错误的能力将会得到回报。Web 开发被错误所困扰,您可以投入大量时间来明智地处理错误。在这里,您将学习在使用urllib.request.
HTTP 状态代码伴随着状态行中的每个响应。如果您可以在响应中读取状态代码,则请求已到达其目标。虽然这很好,但如果响应代码以 . 开头,您只能认为请求完全成功2。例如,200代表201成功的请求。如果状态代码是404或500,例如,出了点问题,urllib.request将引发HTTPError.
有时会发生错误,提供的 URL 不正确,或者由于其他原因无法建立连接。在这些情况下,urllib.request将引发URLError.
最后,有时服务器就是不响应。也许您的网络连接速度很慢,服务器已关闭,或者服务器被编程为忽略特定请求。为了解决这个问题,您可以在一定时间后将timeout参数传递urlopen()给 raise a 。TimeoutError
处理这些异常的第一步是捕获它们。您可以使用, , 和classes捕获…块中产生urlopen()的错误:tryexceptHTTPErrorURLErrorTimeoutError
# request.py
from urllib.error import HTTPError, URLError
from urllib.request import urlopen
def make_request(url):
try:
with urlopen(url, timeout=10) as response:
print(response.status)
return response.read(), response
except HTTPError as error:
print(error.status, error.reason)
except URLError as error:
print(error.reason)
except TimeoutError:
print("Request timed out")
该函数make_request()将 URL 字符串作为参数,尝试使用 获取来自该 URL 的响应urllib.request,并在发生错误时捕获HTTPError引发的对象。如果 URL 错误,它会捕获一个URLError. 如果它没有任何错误地通过,它只会打印状态并返回一个包含正文和响应的元组。响应将在 之后关闭return。
该函数还调用urlopen()一个timeout参数,这将导致TimeoutError在指定的秒数后引发 a 。十秒通常是等待响应的合适时间,尽管一如既往,这在很大程度上取决于您需要向其发出请求的服务器。
现在您已准备好优雅地处理各种错误,包括但不限于您接下来将介绍的错误。
您现在将使用该make_request()函数向httpstat.us发出一些请求,这是一个用于测试的模拟服务器。此模拟服务器将返回具有您请求的状态代码的响应。例如,如果您向 发出请求https://httpstat.us/200,您应该会得到200回应。
像 httpstat.us 这样的 API 用于确保您的应用程序可以处理它可能遇到的所有不同状态代码。httpbin 也有这个功能,但是 httpstat.us 有更全面的状态码选择。它甚至有臭名昭著的半官方 418状态代码,返回消息我是茶壶!
要与make_request()您在上一节中编写的函数进行交互,请以交互模式运行脚本:
$ python3 -i request.py
使用-i标志,此命令将以交互模式运行脚本。这意味着它将执行脚本,然后打开Python REPL,因此您现在可以调用刚刚定义的函数:
>>> make_request("https://httpstat.us/200")
200
(b'200 OK', )
>>> make_request("https://httpstat.us/403")
403 Forbidden
在这里,您尝试了httpstat.us 的200和403端点。200端点按预期进行并返回响应主体和响应对象。403端点只是打印了错误消息并且没有返回任何内容,这也符合预期。
状态意味着服务器理解请求但403不会满足它。这是您可能会遇到的常见错误,尤其是在网络抓取时。在许多情况下,您可以通过传递User-Agent标头来解决它。
注意:有两个密切相关的 4xx 代码有时会引起混淆:
401 未经授权
403 禁止
401 如果用户未被识别或登录并且必须执行某些操作以获得访问权限,例如登录或注册,服务器应该返回。
403 如果用户已被充分识别但无权访问资源,则应返回该状态。例如,如果您登录到社交媒体帐户并尝试查看某人的私人资料页面,那么您可能会获得一个403 状态。
也就是说,不要完全信任状态代码。错误存在并且在复杂的分布式服务中很常见。有些服务器不是模范公民!
服务器识别发出请求的人或对象的主要方法之一是检查User-Agent标头。发送的原始默认请求urllib.request如下:
GET https://httpstat.us/403 HTTP/1.1
Accept-Encoding: identity
Host: httpstat.us
User-Agent: Python-urllib/3.10
Connection: close
请注意,它User-Agent被列为Python-urllib/3.10. 您可能会发现某些网站会尝试阻止网络抓取工具,而这User-Agent是毫无意义的。话虽如此,您可以设置自己的User-Agentwith urllib.request,但您需要稍微修改一下函数:
# request.py
from urllib.error import HTTPError, URLError
-from urllib.request import urlopen
+from urllib.request import urlopen, Request
-def make_request(url):
+def make_request(url, headers=None):
+ request = Request(url, headers=headers or {})
try:
- with urlopen(url, timeout=10) as response:
+ with urlopen(request, timeout=10) as response:
print(response.status)
return response.read(), response
except HTTPError as error:
print(error.status, error.reason)
except URLError as error:
print(error.reason)
except TimeoutError:
print("Request timed out")
要自定义随请求一起发送的标头,您首先必须Request使用 URL 实例化一个对象。此外,您可以传入关键字参数,headers它接受代表您希望包含的任何标题的标准字典。因此,不是将 URL 字符串直接传递给 ,而是传递urlopen()已Request使用 URL 和标头实例化的对象。
注意:在上面的示例中,当Request 实例化时,如果已定义标题,则需要将其传递给它。否则,传递一个空白对象,例如{} . 你不能通过None ,因为这会导致错误。
要使用这个改进后的函数,请重新启动交互式会话,然后make_request()使用表示标头的字典作为参数进行调用:
body, response = make_request(
"https://www.httpbin.org/user-agent",
{"User-Agent": "Real Python"}
)
200
body
b'{\n "user-agent": "Real Python"\n}\n'
在此示例中,您向 httpbin 发出请求。在这里,您使用user-agent端点返回请求的User-Agent值。因为您使用 的自定义用户代理发出了请求Real Python,所以这就是返回的内容。
但是,有些服务器是严格的,只接受来自特定浏览器的请求。幸运的是,可以User-Agent在网络上找到标准字符串,包括通过用户代理数据库。它们只是字符串,因此您需要做的就是复制要模拟的浏览器的用户代理字符串,并将其用作User-Agent标头的值。
另一个常见错误是由于 Python 无法访问所需的安全证书。要模拟此错误,您可以使用badssl.com提供的一些具有已知错误 SSL 证书的模拟站点。您可以向其中之一发出请求,例如superfish.badssl.com,并亲身体验该错误:
from urllib.request import urlopen
urlopen("https://superfish.badssl.com/")
Traceback (most recent call last):
(...)
ssl.SSLCertVerificationError: [SSL: CERTIFICATE_VERIFY_FAILED]
certificate verify failed: unable to get local issuer certificate (_ssl.c:997)
During handling of the above exception, another exception occurred:
Traceback (most recent call last):
(...)
urllib.error.URLError:
在这里,向具有已知错误 SSL 证书的地址发出请求将CERTIFICATE_VERIFY_FAILED导致URLError.
SSL 代表安全套接字层。这是用词不当,因为 SSL 已被弃用,取而代之的是 TLS,即传输层安全性。有时旧术语只是坚持!这是一种加密网络流量的方法,这样假设的侦听器就无法窃听通过线路传输的信息。
现在,大多数网站地址的前面不是,http://而是s代表安全。HTTPS连接必须通过 TLS 加密。可以处理 HTTP 和 HTTPS 连接。https://urllib.request
HTTPS 的详细信息远远超出了本教程的范围,但您可以将 HTTPS 连接视为涉及两个阶段,即握手和信息传输。握手确保连接安全。有关 Python 和 HTTPS 的更多信息,请查看使用 Python 探索 HTTPS。
为了确定特定服务器是安全的,发出请求的程序依赖于可信证书的存储。服务器的证书在握手阶段进行验证。Python 使用操作系统的证书存储。如果 Python 找不到系统的证书存储,或者存储已过期,那么您将遇到此错误。
注意:在以前的 Python 版本中,默认行为urllib.request 是不验证证书,这导致 PEP 476 默认启用证书验证。 默认值在Python 3.4.3 中更改。
有时 Python 可以访问的证书存储已过时,或者 Python 无法访问它,无论出于何种原因。这是令人沮丧的,因为有时您可以从浏览器访问该 URL,它认为它是安全的,但urllib.request仍然会引发此错误。
您可能想选择不验证证书,但这会使您的连接不安全,绝对不推荐:
import ssl
from urllib.request import urlopen
unverified_context = ssl._create_unverified_context()
urlopen("https://superfish.badssl.com/", context=unverified_context)
在这里您导入ssl模块,它允许您创建一个未验证的上下文。然后,您可以将此上下文传递给urlopen()并访问已知的错误 SSL 证书。连接成功通过,因为未检查 SSL 证书。
在诉诸这些孤注一掷的措施之前,请尝试更新您的操作系统或更新您的 Python 版本。如果失败,那么您可以从requests库中获取一个页面并安装certifi:
$ python3 -m venv venv
$ source venv/bin/activate.sh
(venv) $ python3 -m pip install certifi
certifi是一个证书集合,您可以使用它来代替系统的集合。certifi您可以通过使用证书捆绑而不是操作系统的捆绑创建 SSL 上下文来执行此操作:
import ssl
from urllib.request import urlopen
import certifi
certifi_context = ssl.create_default_context(cafile=certifi.where())
urlopen("https://sha384.badssl.com/", context=certifi_context)
在此示例中,您曾经certifi充当 SSL 证书存储,并使用它成功连接到具有已知良好 SSL 证书的站点。请注意._create_unverified_context(),您使用的不是.create_default_context()。
这样,您就可以保持安全而不会遇到太多麻烦!在下一节中,您将涉足身份验证的世界。
requests身份验证是一个庞大的主题,如果您要处理的身份验证比此处介绍的内容复杂得多,那么这可能是进入该包的一个很好的起点。
在本教程中,您将只介绍一种身份验证方法,它作为您必须进行的调整类型的示例来验证您的请求。urllib.request确实有很多其他功能可以帮助进行身份验证,但本教程不会介绍这些功能。
最常见的身份验证工具之一是不记名令牌,由RFC 6750指定。它通常用作OAuth的一部分,但也可以单独使用。它也是最常见的标题,您可以将其与当前make_request()函数一起使用:
token = "abcdefghijklmnopqrstuvwxyz"
headers = {
"Authorization": f"Bearer {token}"
}
make_request("https://httpbin.org/bearer", headers)
200
(b'{\n "authenticated": true, \n "token": "abcdefghijklmnopqrstuvwxyz"\n}\n',
)
/bearer在此示例中,您向模拟承载身份验证的 httpbin 端点发出请求。它会接受任何字符串作为标记。它只需要 RFC 6750 指定的正确格式。名称必须是Authorization,有时是小写字母authorization,值必须是Bearer,在它和标记之间有一个空格。
注意:如果您使用任何形式的令牌或秘密信息,请务必妥善保护这些令牌。例如,不要将它们提交到 GitHub 存储库,而是将它们存储为临时 环境变量 。
恭喜,您已使用不记名令牌成功通过身份验证!
另一种身份验证形式称为基本访问身份验证,这是一种非常简单的身份验证方法,只比在标头中发送用户名和密码好一点。很没有安全感!
当今最常用的协议之一是OAuth(开放授权)。如果您曾使用 Google、GitHub 或 Facebook 登录其他网站,那么您就使用过 OAuth。OAuth 流程通常涉及您要与之交互的服务和身份服务器之间的一些请求,从而产生一个短暂的不记名令牌。该持有者令牌随后可用于持有者身份验证一段时间。
大部分身份验证归结为了解目标服务器使用的特定协议并仔细阅读文档以使其正常工作。
您发出了很多 GET 请求,但有时您想要发送信息。这就是 POST 请求的用武之地。要使用 发出 POST 请求urllib.request,您不必显式更改方法。您可以只将data对象传递给新Request对象或直接传递给urlopen(). 但是,该data对象必须采用特殊格式。您将make_request()通过添加参数稍微调整您的函数以支持 POST 请求data:
# request.py
from urllib.error import HTTPError, URLError
from urllib.request import urlopen, Request
-def make_request(url, headers=None):
+def make_request(url, headers=None, data=None):
- request = Request(url, headers=headers or {})
+ request = Request(url, headers=headers or {}, data=data)
try:
with urlopen(request, timeout=10) as response:
print(response.status)
return response.read(), response
except HTTPError as error:
print(error.status, error.reason)
except URLError as error:
print(error.reason)
except TimeoutError:
print("Request timed out")
在这里,您只是修改了函数以接受data默认值为 的参数None,并将其直接传递到Request实例化中。不过,这还不是全部。您可以使用两种不同格式中的一种来执行 POST 请求:
表格数据:application/x-www-form-urlencoded
JSON:application/json
第一种格式是最古老的 POST 请求格式,涉及使用百分比编码对数据进行编码,也称为 URL 编码。您可能已经注意到编码为查询字符串的键值对 URL 。键与值用等号 ( =) 分隔,键值对用 & 符号 ( &) 分隔,空格通常被抑制但可以用加号 ( +) 代替。
如果您从 Python 字典开始,要在您的make_request()函数中使用表单数据格式,您需要编码两次:
一次对字典进行 URL 编码
然后再次将结果字符串编码为字节
对于 URL 编码的第一阶段,您将使用另一个urllib模块urllib.parse. 请记住以交互模式启动您的脚本,以便您可以使用该make_request()函数并在 REPL 上运行它:
from urllib.parse import urlencode
post_dict = {"Title": "Hello World", "Name": "Real Python"}
url_encoded_data = urlencode(post_dict)
url_encoded_data
# 'Title=Hello+World&Name=Real+Python'
post_data = url_encoded_data.encode("utf-8")
body, response = make_request(
"https://httpbin.org/anything", data=post_data
)
# 200
print(body.decode("utf-8"))
{
"args": {},
"data": "",
"files": {},
"form": {
"Name": "Real Python",
"Title": "Hello World" },
"headers": {
"Accept-Encoding": "identity",
"Content-Length": "34",
"Content-Type": "application/x-www-form-urlencoded",
"Host": "httpbin.org",
"User-Agent": "Python-urllib/3.10",
"X-Amzn-Trace-Id": "Root=1-61f25a81-03d2d4377f0abae95ff34096" },
"json": null,
"method": "POST",
"origin": "86.159.145.119",
"url": "https://httpbin.org/anything"}
在这个例子中,你:
urlencode()从urllib.parse模块导入
初始化您的 POST 数据,从字典开始
使用urlencode()函数对字典进行编码
使用 UTF-8 编码将生成的字符串编码为字节
向anything端点发出请求httpbin.org
打印 UTF-8 解码的响应正文
UTF-8 编码是类型规范的一部分application/x-www-form-urlencoded。UTF-8 被抢先用于解码正文,因为您已经知道httpbin.org可靠地使用 UTF-8。
来自 httpbin的anything端点充当一种回显,返回它收到的所有信息,以便您可以检查您发出的请求的详细信息。在这种情况下,您可以确认method确实是POST,并且可以看到您发送的数据列在 下form。
要使用 JSON 发出相同的请求,您将使用 将 Python 字典转换为 JSON 字符串json.dumps(),使用 UTF-8 对其进行编码,将其作为data参数传递,最后添加一个特殊的标头以指示数据类型为 JSON:
post_dict = {"Title": "Hello World", "Name": "Real Python"}
import json
json_string = json.dumps(post_dict)
json_string
'{"Title": "Hello World", "Name": "Real Python"}'
post_data = json_string.encode("utf-8")
body, response = make_request(
"https://httpbin.org/anything",
data=post_data,
headers={"Content-Type": "application/json"},
)
200
print(body.decode("utf-8"))
{
"args": {},
"data": "{\"Title\": \"Hello World\", \"Name\": \"Real Python\"}",
"files": {},
"form": {},
"headers": {
"Accept-Encoding": "identity",
"Content-Length": "47",
"Content-Type": "application/json",
"Host": "httpbin.org",
"User-Agent": "Python-urllib/3.10",
"X-Amzn-Trace-Id": "Root=1-61f25a81-3e35d1c219c6b5944e2d8a52"
},
"json": {
"Name": "Real Python",
"Title": "Hello World"
},
"method": "POST",
"origin": "86.159.145.119",
"url": "https://httpbin.org/anything"
}
这次要序列化json.dumps()字典,您使用而不是urlencode(). 您还明确添加了Content-Type值为application/json. 有了这些信息,httpbin 服务器就可以在接收端反序列化 JSON。在其响应中,您可以看到json键下列出的数据。
注意:有时需要将 JSON 数据作为纯文本发送,在这种情况下,步骤与上述相同,只是您设置Content-Type为text/plain; charset=UTF-8. 许多这些必需品取决于您将数据发送到的服务器或 API,因此请务必阅读文档并进行实验!
这样,您现在就可以开始发出 POST 请求了。本教程不会详细介绍其他请求方法,例如PUT。可以这么说,您还可以通过将method关键字参数传递给Request对象的实例化来显式设置方法。
最后,本教程的最后一部分致力于阐明围绕使用 Python 的 HTTP 请求的包生态系统。因为包很多,没有明确的标准,容易造成混淆。也就是说,每个包都有用例,这就意味着你有更多的选择!
要回答这个问题,你需要回到早期的 Python,一路回到 1.2 版本,也就是最初urllib引入的时候。在 1.6 版左右,urllib2添加了一个修改版,它与原来的urllib. 当 Python 3 出现时,原来的urllib版本被弃用,并urllib2删除了2, 并采用了原来的urllib名称。它也分为几个部分:
urllib.error
urllib.parse
urllib.request
urllib.response
urllib.robotparser
那么呢urllib3?urllib2那是仍然存在时开发的第三方库。它与标准库无关,因为它是一个独立维护的库。有趣的是,requests图书馆实际上urllib3在幕后使用,所以pip!
主要的答案是易用性和安全性。urllib.request被认为是一个低级库,它公开了很多关于 HTTP 请求工作的细节。的Python文档urllib.request毫不含糊地推荐requests作为更高级别的 HTTP 客户端接口。
如果您日复一日地与许多不同的 REST API 进行交互,那么requests强烈建议您使用。该requests库标榜自己为“为人类而建”,并成功地围绕 HTTP 创建了一个直观、安全和直接的 API。它通常被认为是首选图书馆!如果您想了解有关该requests库的更多信息,请查看 Real Python指南requests。
requests如何让事情变得更简单的一个例子是字符编码。您会记得,使用 时urllib.request,您必须了解编码并采取一些步骤来确保无差错的体验。该requests包将其抽象化,并将通过使用chardet通用字符编码检测器来解析编码,以防万一有任何有趣的事情。
如果您的目标是了解有关标准 Python 的更多信息以及它如何处理 HTTP 请求的详细信息,那么这urllib.request是进入该领域的好方法。您甚至可以走得更远,使用非常低级的http模块。另一方面,您可能只想将依赖性保持在最低限度,这urllib.request是完全可以做到的。
也许您想知道为什么requests此时它还不是核心 Python 的一部分。
这是一个复杂的问题,没有硬性的快速答案。关于原因有很多猜测,但似乎有两个原因很突出:
requests还有其他需要集成的第三方依赖项。
requests需要保持敏捷并且可以在标准库之外做得更好。
该requests库具有第三方依赖项。集成requests到标准库中还意味着集成chardet、certifi、 和urllib3等。另一种方法是从根本上改变requests为仅使用 Python 现有的标准库。这不是一项简单的任务!
集成requests还意味着开发该库的现有团队将不得不放弃对设计和实施的完全控制,让位于PEP决策过程。
HTTP 规范和建议一直在变化,高级库必须足够敏捷才能跟上。如果有一个安全漏洞需要修补,或者有一个新的工作流程需要添加,requests团队可以比他们作为 Python 发布过程的一部分更快地构建和发布。据推测,有时他们会在发现漏洞后十二小时发布安全修复程序!
有关这些问题和更多问题的有趣概述,请查看将请求添加到标准库,它总结了在 Python 语言峰会上与Requests 的创建者和维护者Kenneth Reitz的讨论。
requests因为这种敏捷性对于它及其底层是如此必要,所以经常使用对于标准库来说太重要urllib3的自相矛盾的陈述。requests这是因为 Python 社区的大部分依赖requests及其敏捷性,将其集成到核心 Python 中可能会损害它和 Python 社区。
在 GitHub 存储库问题板上requests,发布了一个问题,要求将 包含requests在标准库中。和的开发人员requests插话urllib3,主要是说他们可能会对自己维护它失去兴趣。有些人甚至表示他们会分叉存储库并继续为他们自己的用例开发它们。
话虽如此,请注意requests库 GitHub 存储库托管在 Python 软件基金会的帐户下。仅仅因为某些东西不是 Python 标准库的一部分并不意味着它不是生态系统不可或缺的一部分!
目前的情况似乎对 Python 核心团队和requests. 虽然对于新手来说可能有点混乱,但现有结构为 HTTP 请求提供了最稳定的体验。
同样重要的是要注意 HTTP 请求本质上是复杂的。urllib.request不要试图粉饰太多。它公开了 HTTP 请求的许多内部工作原理,这就是为什么它被标榜为低级模块的原因。您的选择实际上取决于您的特定用例、安全问题和偏好requests。urllib.request
您现在已准备好使用它urllib.request来发出 HTTP 请求。现在您可以在您的项目中使用这个内置模块,让它们更长时间地保持无依赖性。您还通过使用较低级别的模块(例如urllib.request.
在本教程中,您已经:
学习了如何发出基本的HTTP 请求urllib.request
探究了HTTP 消息的具体细节并研究了它是如何由urllib.request
弄清楚如何处理HTTP 消息的字符编码
探索了一些使用中的常见错误,urllib.request并学习了如何解决这些错误
将您的脚趾浸入经过身份验证的请求的世界urllib.request
理解为什么两者urllib和requests库都存在以及何时使用其中一个或另一个
您现在可以使用 发出基本的 HTTP 请求urllib.request,并且您还拥有使用标准库深入研究低级 HTTP 领域的工具。最后,您可以根据自己的需要选择是否使用requests或。urllib.request享受探索网络的乐趣!