基本 HTTP GET 请求urllib.request
在深入了解 HTTP 请求是什么及其工作原理之前,您将通过向示例网址 。您还将向模拟发出 GET 请求休息API 对于一些JSON 数据。如果您想了解 POST 请求,您将了解它们稍后在教程中 ,一旦你有了更多的了解urllib.request
.
谨防: 根据您的具体设置,您可能会发现其中一些示例不起作用。如果是这样,请跳至常见部分urllib.request
错误 用于故障排除。
如果您遇到此处未涵盖的问题,请务必在下面使用精确且可重现的示例进行评论。
首先,您需要向www.example.com
,服务器将返回一条 HTTP 消息。确保您使用的是 Python 3 或更高版本,然后使用urlopen()
函数来自urllib.request
:
>>> >>> from urllib.request import urlopen
>>> with urlopen ( "https://www.example.com" ) as response :
... body = response . read ()
...
>>> body [: 15 ]
b'<!doctype html>'
在此示例中,您导入urlopen()
从urllib.request
。使用上下文管理器 with
,您发出请求并收到响应urlopen()
。然后,您读取响应正文并关闭响应对象。这样,您就可以显示正文的前 15 个位置,并注意到它看起来像一个 HTML 文档。
你在这!您已成功提出请求,并且收到了回复。通过检查内容,您可以判断它很可能是 HTML 文档。请注意,正文的打印输出前面是b
。这表明一个字节文字 ,您可能需要对其进行解码。在本教程的后面,您将学习如何将字节转换为细绳 ,将它们写入文件 ,或者将它们解析为字典 .
如果您想要调用 REST API 来获取 JSON 数据,该过程仅略有不同。在以下示例中,您将请求{JSON} 占位符 对于一些虚假的待办事项数据:
>>> >>> 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()
功能与body
将返回的 JSON 字节解码并解析为Python字典 。瞧!
如果您足够幸运能够无错误地使用端点 ,例如这些示例中的那些,那么上面的内容可能就是您所需要的urllib.request
。话又说回来,你可能会发现这还不够。
现在,在做一些事情之前urllib.request
故障排除时,您将首先了解 HTTP 消息的底层结构并了解如何urllib.request
处理它们。这种理解将为解决许多不同类型的问题提供坚实的基础。
HTTP 消息的具体细节
了解您在使用过程中可能遇到的一些问题urllib.request
,您需要检查响应是如何表示的urllib.request
。为此,您将受益于对什么是一个高层次的概述。HTTP消息 是,这就是您将在本节中得到的内容。
在进行高级概述之前,先简要介绍一下参考来源。如果您想深入了解技术领域,互联网工程任务组 (IETF) 有一套广泛的征求意见 (RFC) 文件。这些文档最终成为 HTTP 消息等内容的实际规范。RFC 7230,第 1 部分:消息语法和路由 例如,都是关于 HTTP 消息的。
如果您正在寻找一些比 RFC 更容易理解的参考材料,那么Mozilla 开发者网络 (MDN) 有大量的参考文章。例如,他们的文章HTTP消息 虽然仍然是技术性的,但更容易理解。
现在您已经了解了这些重要的参考信息来源,在下一节中您将获得对初学者友好的 HTTP 消息概述。
了解什么是 HTTP 消息
简而言之,HTTP 消息可以理解为文本,以流的形式传输字节 ,结构遵循 RFC 7230 指定的准则。解码的 HTTP 消息可以简单到两行:
GET / HTTP/1.1
Host: www.google.com
这指定了一个GET 根请求(/
) 使用HTTP/1.1
协议。唯一的标头 需要的是主机,www.google.com
。目标服务器有足够的信息来利用该信息做出响应。
响应的结构与请求类似。 HTTP 消息有两个主要部分,元数据 和身体 。在上面的请求示例中,消息都是没有正文的元数据。另一方面,响应确实有两个部分:
HTTP/1.1 200 OK
Content-Type: text/html; charset=ISO-8859-1
Server: gws
(... other headers ...)
<!doctype html><html itemscope="" itemtype="http://schema.org/WebPage"
...
响应以状态行 指定 HTTP 协议HTTP/1.1
和状态200 OK
。状态行之后,可以得到很多键值对,比如Server: gws
,代表所有响应标头 。这是响应的元数据。
元数据之后有一个空行,用作标题和正文之间的分隔符。空行后面的所有内容都构成了正文。这是您使用时读取的部分urllib.request
.
笔记 :空行在技术上通常被称为换行符 。 HTTP 消息中的换行符必须是 Windows 样式回车 (\r
)与一个行尾 (\n
)。在类Unix 系统中,换行符通常只是一个行结尾 (\n
).
您可以假设所有 HTTP 消息都遵循这些规范,但有些消息可能会违反这些规则或遵循较旧的规范。它是异常地 不过,这很少会引起任何问题。所以,请记住它,以防遇到奇怪的错误!
在下一节中,您将了解如何urllib.request
处理原始 HTTP 消息。
了解如何做urllib.request
代表一条 HTTP 消息
使用时将与之交互的 HTTP 消息的主要表示形式urllib.request
是个HTTP响应 目的。这urllib.request
模块本身依赖于低层http
模块,您不需要直接与之交互。您最终确实使用了一些数据结构http
不过,提供了诸如HTTPResponse
和HTTPMessage
.
笔记 :Python 中表示 HTTP 响应和消息的对象的内部命名可能有点令人困惑。您通常只与以下实例交互HTTPResponse
,而要求 事情的最终结果是在内部处理的。
你可能会认为HTTPMessage
是一种基类,其中HTTPResponse
继承自,但事实并非如此。HTTPResponse
直接继承自io.BufferedIOBase
,而HTTPMessage
类继承自电子邮件.消息.EmailMessage .
这EmailMessage
在源代码中定义为包含一堆标头和有效负载的对象,因此它不一定是电子邮件。HTTPResponse
只是简单地使用HTTPMessage
作为其标头的容器。
但是,如果您谈论的是 HTTP 本身而不是其 Python 实现,那么您将 HTTP 响应视为一种 HTTP 消息是正确的。
当您提出请求时urllib.request.urlopen()
,你得到一个HTTPResponse
对象作为回报。花一些时间探索HTTPResponse
对象与打印() 和目录() 查看属于它的所有不同方法和属性:
>>> >>> 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
<http.client.HTTPMessage object at 0x000001E029D9F4F0>
>>> 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
header,那么你可以使用单数.getheader("Server")
在HTTPResponse
或使用方括号 ([]
) 语法.headers
从HTTPMessage
:
>>> >>> response . getheader ( "Server" )
'ECS (nyb/1D16)'
>>> response . headers [ "Server" ]
'ECS (nyb/1D16)'
说实话,您可能不需要像这样直接与标题交互。您最可能需要的信息可能已经有一些内置的帮助方法,但现在您知道了,以防您需要更深入地挖掘!
关闭HTTPResponse
这HTTPResponse
对象有很多共同点文件对象 。这HTTPResponse
类继承自IO基类 ,就像文件对象一样,这意味着您必须注意打开和关闭。
在简单的程序中,如果忘记关闭,您不太可能注意到任何问题HTTPResponse
对象。然而,对于更复杂的项目,这可能会显着减慢执行速度并导致难以查明的错误。
问题的出现是因为输入输出 (I/O) 流是有限的。每个HTTPResponse
要求流在读取时保持畅通。如果您从不关闭流,这最终将阻止打开任何其他流,并且可能会干扰其他程序甚至您的操作系统。
所以,请确保关闭您的HTTPResponse
物体!为了方便起见,您可以使用上下文管理器,如示例中所示。您还可以通过显式调用来获得相同的结果.close()
在响应对象上:
>>> >>> from urllib.request import urlopen
>>> response = urlopen ( "https://www.example.com" )
>>> body = response . read ()
>>> response . close ()
在此示例中,您不使用上下文管理器,而是显式关闭响应流。不过,上面的示例仍然存在问题,因为在调用之前可能会引发异常.close()
,防止正确拆卸。要使此调用无条件(正如它应该的那样),您可以使用尝试……除了 块与两个else
和一个finally
条款:
>>> >>> 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
,然后关闭它。
也就是说,这正是上下文管理器所做的事情,并且with
通常首选语法。不仅是with
语法更简洁,更具可读性,但它也可以保护您免受讨厌的遗漏错误的影响。换句话说,它可以更好地防止意外忘记关闭对象:
>>> >>> from urllib.request import urlopen
>>> with urlopen ( "https://www.example.com" ) as response :
... response . read ( 50 )
... response . read ( 50 )
...
b'<!doctype html>\n<html>\n<head>\n <title>Example D'
b'omain</title>\n\n <meta charset="utf-8" />\n <m'
在此示例中,您导入urlopen()
来自urllib.request
模块。您使用with
关键字与.urlopen()
来分配HTTPResponse
变量的对象response
。然后,您读取响应的前 50 个字节,然后读取接下来的 50 个字节,所有这些都在with
堵塞。最后,您关闭with
块,执行请求并运行其块内的代码行。
使用此代码,您可以显示两组,每组五十个字节。这HTTPResponse
一旦退出,对象就会关闭with
块作用域,这意味着.read()
方法只会返回空字节对象:
>>> >>> import urllib.request
>>> with urllib . request . urlopen ( "https://www.example.com" ) as response :
... response . read ( 50 )
...
b'<!doctype html>\n<html>\n<head>\n <title>Example D'
>>> response . read ( 50 )
b''
在此示例中,第二个读取 50 个字节的调用超出了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
此示例表明,一旦您阅读了回复,就无法再次阅读。如果您已完全读取响应,则即使响应未关闭,后续尝试也只会返回一个空字节对象。您必须再次提出请求。
在这方面,响应与文件对象不同,因为对于文件对象,您可以使用以下命令多次读取它:。寻找() 方法,其中HTTPResponse
不支持。即使关闭响应后,您仍然可以访问标头和其他元数据。
探索文本、八位字节和位
在到目前为止的大多数示例中,您从以下位置读取响应正文HTTPResponse
,立即显示结果数据,并注意到它显示为字节对象 。这是因为计算机中的文本信息不是以字母的形式存储或传输的,而是以字节的形式存储或传输的!
通过线路发送的原始 HTTP 消息被分解为一系列字节 ,有时称为八位位组 。字节为 8-bit 大块。例如,01010101
是一个字节。要了解有关二进制、位和字节的更多信息,请查看Python 中的按位运算符 .
那么如何用字节来表示字母呢?一个字节有 256 种可能的组合,您可以为每个组合分配一个字母。您可以分配00000001
到A
, 00000010
到B
, 等等。ASCII码 字符编码很常见,使用这种类型的系统编码 128 个字符,这对于像英语这样的语言来说已经足够了。这是特别方便的,因为只需一个字节就可以表示所有字符,并且还有剩余空间。
所有标准英文字符(包括大写字母、标点符号和数字)都适合 ASCII。另一方面,日语被认为有大约 5 万个表意字符,所以 128 个字符根本不够用!即使理论上 1 个字节内可用的 256 个字符对于日语来说也不够。因此,为了适应世界上所有的语言,有许多不同的系统来编码字符。
尽管有很多系统,但您可以信赖的一件事是,它们总是会被分解为字节 。大多数服务器,如果无法解析MIME类型 和字符编码,默认为application/octet-stream
,字面意思是字节流。然后谁收到消息就可以计算出字符编码。
处理字符编码
正如您可能已经猜到的那样,问题经常出现,因为存在许许多多不同的潜在字符编码。当今占主导地位的字符编码是UTF-8 ,这是一个实现统一码 。幸运的是,百分之九十八的网页 今天都是用UTF-8编码的!
UTF-8 占据主导地位是因为它可以有效地处理数量惊人的字符。它处理 Unicode 定义的所有 1,112,064 个潜在字符,包括中文、日语、阿拉伯语(从右到左的脚本)、俄语和更多字符集,包括表情符号 !
UTF-8 仍然高效,因为它使用可变数量的字节来编码字符,这意味着对于许多字符,它只需要一个字节,而对于其他字符,它可能需要最多四个字节。
笔记 :要了解有关 Python 编码的更多信息,请查看 .
虽然 UTF-8 占主导地位,并且假设 UTF-8 编码通常不会出错,但您仍然会一直遇到不同的编码。好消息是,在使用时您不需要成为编码专家来处理它们urllib.request
.
从字节到字符串
当你使用urllib.request.urlopen()
,响应的主体是一个字节对象。您可能要做的第一件事是将字节对象转换为字符串。也许你想做一些网页抓取 。为此,您需要解码 字节。要使用 Python 解码字节,您需要找出的是字符编码 用过的。编码,尤其是在涉及字符编码时,通常被称为字符集 .
如前所述,98% 的情况下,默认使用 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 ])
<!doctype html>
<html>
<head>
在此示例中,您获取从返回的字节对象response.read()
并使用 bytes 对象对其进行解码.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 ])
<!doctype html>
<html>
<head>
在此示例中,您调用.get_content_charset()
于.headers
的对象response
并用它来解码。这是一种解析的便捷方法Content-Type
头,以便您可以轻松地将字节解码为文本。
从字节到文件
如果您想将字节解码为文本,现在就可以开始了。但是如果您想将响应正文写入文件怎么办?好吧,你有两个选择:
将字节直接写入文件
将字节解码为 Python 字符串,然后将该字符串编码回文件中
第一种方法是最简单的,但第二种方法允许您根据需要更改编码。要更详细地了解文件操作,请查看 Real Python在 Python 中读写文件(指南) .
要将字节直接写入文件而无需解码,您需要内置打开() 函数,并且您需要确保使用写入二进制模式:
>>> >>> 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()
在wb
模式绕过解码或编码的需要,并将 HTTP 消息正文的字节转储到example.html
文件。写入操作后输出的数字表示已写入的字节数。就是这样!您已将字节直接写入文件,而无需进行任何编码或解码。
现在假设您有一个不使用 UTF-8 的 URL,但您想将内容写入使用 UTF-8 的文件。为此,您首先将字节解码为字符串,然后将字符串编码到文件中,并指定字符编码。
谷歌的主页似乎根据您所在的位置使用不同的编码。在欧洲和美国的大部分地区,它使用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 似乎有多层检查,用于确定提供网页的语言和编码。这意味着您可以指定接受语言标头 ,这似乎覆盖了您的 IP 位置。用不同的方式尝试一下区域设置标识符 看看你能得到什么编码!
写入文件后,您应该能够在浏览器或文本编辑器中打开生成的文件。大多数现代文本处理器可以自动检测字符编码。
如果存在编码错误并且您使用 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 显式停止进程并引发异常,但在显示文本的程序中,例如您正在查看此页面的浏览器,您可能会发现臭名昭著的替换字符 :
A Replacement Character
带白色问号 (�)、正方形 (□) 和矩形 (▯) 的黑色菱形通常用作无法解码的字符的替换。
有时,解码似乎有效,但会导致难以理解的序列,例如æ–‡å—化‘。 ,这也表明使用了错误的字符集。在日本,他们甚至有一个词来形容由于字符编码问题而出现乱码的文本,莫吉巴克 ,因为这些问题在互联网时代之初就困扰着他们。
这样,您现在应该能够使用从返回的原始字节写入文件了urlopen()
。在下一节中,您将学习如何使用以下命令将字节解析为 Python 字典:json 模块。
从字节到字典
为了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
在此示例中,您使用json
的终点httpbin ,一项允许您尝试不同类型的请求和响应的服务。这json
端点模拟返回 JSON 数据的典型 API。请注意,.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 <em>W
onderWidgets</em> are great', 'Who <em>buys</em> WonderWidgets'], 'title': 'Ove
rview', 'type': 'all'}], 'title': 'Sample Slide Show'}}
如您所见,json
模块自动处理解码并生成 Python 字典。几乎所有 API 都以 JSON 形式返回键值信息,尽管您可能会遇到一些与XML 。为此,您可能需要研究一下Python 中的 XML 解析器路线图 .
这样,您应该对字节和编码有足够的了解,否则会很危险!在下一节中,您将学习如何排除故障并修复使用时可能遇到的一些常见错误urllib.request
.
常见的urllib.request
烦恼
在这个世界上你可能会遇到各种各样的问题荒野 网络,无论您是否使用urllib.request
或不。在本节中,您将学习如何处理入门时的一些最常见错误:403
错误 和TLS/SSL 证书错误 。不过,在查看这些特定错误之前,您将首先学习如何实现错误处理 更一般地使用时urllib.request
.
实施错误处理
在将注意力转向特定错误之前,提高代码优雅地处理各种错误的能力将会得到回报。 Web 开发经常受到错误的困扰,您可能会投入大量时间来明智地处理错误。在这里,您将学习如何在使用时处理 HTTP、URL 和超时错误urllib.request
.
HTTP 状态代码 伴随状态行中的每个响应。如果您可以读取响应中的状态代码,则请求已达到目标。虽然这很好,但只有当响应代码以2
。例如,200
和201
代表请求成功。如果状态码是404
或者500
,例如,出了点问题,并且urllib.request
将提出HTTP错误 .
有时会发生错误,提供的 URL 不正确,或者由于其他原因无法建立连接。在这些情况下,urllib.request
将提出一个网址错误 .
最后,有时服务器根本不响应。也许您的网络连接速度很慢,服务器已关闭,或者服务器被编程为忽略特定请求。为了解决这个问题,你可以通过timeout
论证urlopen()
提出一个超时错误 经过一定时间后。
处理这些异常的第一步是捕获它们。您可以捕获其中产生的错误urlopen()
与一个try
… except
块,利用HTTPError
, URLError
, 和TimeoutError
课程:
# 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
在指定的秒数后引发。十秒通常是等待响应的良好时间,但与往常一样,这在很大程度上取决于您需要向其发出请求的服务器。
现在您已准备好优雅地处理各种错误,包括但不限于接下来将介绍的错误。
处理403
错误
您现在将使用make_request()
函数来提出一些请求httpstat.us ,这是一个用于测试的模拟服务器。该模拟服务器将返回具有您请求的状态代码的响应。如果您提出请求https://httpstat.us/200
,例如,您应该期望200
回复。
httpstat.us 等 API 用于确保您的应用程序可以处理可能遇到的所有不同状态代码。 httpbin 也有这个功能,但是 httpstat.us 有更全面的状态代码选择。它甚至还拥有臭名昭著和半官方的 418
返回消息的状态码我是一个茶壶 !
与make_request()
您在上一节中编写的函数,以交互模式运行脚本:
随着-i
标志,该命令将运行脚本交互模式 。这意味着它将执行脚本然后打开Python REPL 之后,您现在可以调用刚刚定义的函数:
>>> >>> make_request ( "https://httpstat.us/200" )
200
(b'200 OK', <http.client.HTTPResponse object at 0x0000023D612660B0>)
>>> make_request ( "https://httpstat.us/403" )
403 Forbidden
在这里你尝试了200
和403
httpstat.us 的端点。这200
端点按预期执行并返回响应正文和响应对象。这403
端点只是打印了错误消息并且没有返回任何内容,这也符合预期。
这403 状态表示服务器理解该请求但不会满足它。这是您可能遇到的常见错误,尤其是在网页抓取时。在许多情况下,您可以通过传递一个来解决它用户代理 标头。
笔记 :有两个密切相关的 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-Agent
和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")
要自定义随请求发送的标头,您首先必须实例化一个要求 带有 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
标头。
修复 SSLCERTIFICATE_VERIFY_FAILED
错误
另一个常见错误是由于 Python 无法访问所需的安全证书。要模拟此错误,您可以使用一些已知有错误 SSL 证书的模拟站点,这些站点由badssl.com 。您可以向其中之一提出请求,例如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 : <urlopen error [SSL: CERTIFICATE_VERIFY_FAILED]
certificate verify failed: unable to get local issuer certificate (_ssl.c:997)>
在这里,向具有已知错误 SSL 证书的地址发出请求将导致CERTIFICATE_VERIFY_FAILED
这是一种URLError
.
SSL 代表安全套接字层。这是一种用词不当,因为 SSL 已被弃用,取而代之的是 TLS,传输层安全 。有时旧术语仍然存在!这是一种加密网络流量的方法,这样假设的监听者就无法窃听通过线路传输的信息。
如今,大多数网站地址前面都没有http://
但是通过https://
,与s 代表安全的 . HTTPS 连接必须通过 TLS 加密。urllib.request
可以处理 HTTP 和 HTTPS 连接。
HTTPS 的详细信息远远超出了本教程的范围,但您可以将 HTTPS 连接视为涉及两个阶段:握手 和信息的传递。握手确保连接安全。有关 Python 和 HTTPS 的更多信息,请查看使用 Python 探索 HTTPS .
为了确保特定服务器是安全的,发出请求的程序依赖于受信任的证书存储。服务器的证书在握手阶段进行验证。 Python 使用操作系统的证书存储 。如果 Python 找不到系统的证书存储,或者该存储已过期,那么您将遇到此错误。
笔记 :在以前版本的 Python 中,默认行为urllib.request
曾是not 验证证书,这导致公众号 476 默认启用证书验证。默认值更改为Python 3.4.3 .
有时,Python 可以访问的证书存储已过期,或者无论出于何种原因,Python 都无法访问它。这很令人沮丧,因为有时您可以从浏览器访问该 URL,浏览器认为该 URL 是安全的,但实际上urllib.request
仍然会引发此错误。
您可能会想选择不验证证书,但这会导致您的连接失败不安全感 并且绝对是不建议 :
>>> >>> import ssl
>>> from urllib.request import urlopen
>>> unverified_context = ssl . _create_unverified_context ()
>>> urlopen ( "https://superfish.badssl.com/" , context = unverified_context )
<http.client.HTTPResponse object at 0x00000209CBE8F220>
在这里您导入ssl 模块,它允许您创建一个未经验证的上下文 。然后您可以将此上下文传递给urlopen()
并访问已知的错误 SSL 证书。由于未检查 SSL 证书,因此连接成功。
在采取这些绝望的措施之前,请尝试更新您的操作系统或更新您的 Python 版本。如果失败,那么您可以从requests
库并安装certifi
:
PS> python -m venv venv
PS> .\ venv \ Scripts \ activate
(venv) PS> python -m pip install certifi
$ python3 -m venv venv
$ source venv/bin/activate.sh
(venv) $ python3 -m pip install certifi
证明书 是您可以使用的证书集合,而不是系统的集合。您可以通过使用以下命令创建 SSL 上下文来完成此操作certifi
证书包而不是操作系统的证书包:
>>> >>> 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 )
<http.client.HTTPResponse object at 0x000001C7407C3490>
在此示例中,您使用了certifi
充当您的 SSL 证书存储,并且您使用它成功连接到具有已知良好 SSL 证书的站点。请注意,而不是._create_unverified_context()
, 你用.create_default_context()
.
这样,您就可以保持安全,而不会遇到太多麻烦!在下一部分中,您将涉足身份验证的世界。
经过身份验证的请求
身份验证是一个广泛的主题,如果您处理的身份验证比此处介绍的内容复杂得多,那么这可能是一个很好的起点requests
包裹。
在本教程中,您将仅介绍一种身份验证方法,该方法作为您为验证请求而必须进行的调整类型的示例。urllib.request
确实有许多其他有助于身份验证的功能,但本教程不会介绍这些功能。
最常见的身份验证工具之一是不记名令牌,由RFC 6750 。它经常被用作开放认证 ,但也可以单独使用。它也是最常见的一种标题,您可以将其与当前的标题一起使用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',
<http.client.HTTPResponse object at 0x0000023D612642E0>)
在此示例中,您向 httpbin 发出请求/bearer
端点,模拟承载身份验证。它会接受任何字符串作为令牌。它只需要 RFC 6750 指定的正确格式。名称has 成为Authorization
,或有时小写authorization
,和值has 成为Bearer
,其与标记之间有一个空格。
笔记 :如果您使用任何形式的令牌或秘密信息,请务必适当保护这些令牌。例如,不要将它们提交到 GitHub 存储库,而是将它们存储为临时文件环境变量 .
恭喜,您已使用不记名令牌成功完成身份验证!
另一种形式的身份验证称为基本访问认证 ,这是一种非常简单的身份验证方法,仅比在标头中发送用户名和密码稍好一些。是非常没有安全感的!
当今最常用的协议之一是OAuth(开放授权) 。如果您曾经使用 Google、GitHub 或 Facebook 登录过另一个网站,那么您就使用过 OAuth。 OAuth 流程通常涉及您想要交互的服务和身份服务器之间的一些请求,从而产生短暂的不记名令牌。然后,可以通过持有者身份验证使用该持有者令牌一段时间。
身份验证的大部分内容都归结为了解目标服务器使用的特定协议并仔细阅读文档以使其正常工作。
POST 请求urllib.request
您已经发出了很多 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。
这anything
来自 httpbin 的端点充当一种回显,返回它收到的所有信息,以便您可以检查所发出请求的详细信息。在这种情况下,您可以确认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"
}
To 连载 这次你使用的字典json.dumps()
代替urlencode()
。您还明确添加内容类型标头 值为application/json
。有了这些信息,httpbin 服务器就可以在接收端反序列化 JSON。在其响应中,您可以看到下面列出的数据json
钥匙。
笔记 :有时需要以纯文本形式发送 JSON 数据,这种情况下的步骤如上,只是您设置Content-Type
作为text/plain; charset=UTF-8
。其中许多必需品取决于您要向其发送数据的服务器或 API,因此请务必阅读文档并进行实验!
这样,您现在就可以开始发出 POST 请求了。本教程不会详细介绍其他请求方法,例如PUT 。可以说,您还可以通过传递一个来显式设置该方法method
实例化的关键字参数请求对象 .
请求包生态系统
总而言之,本教程的最后一部分致力于阐明 Python 的 HTTP 请求的包生态系统。由于软件包众多,没有明确的标准,可能会造成混乱。也就是说,每个包都有用例,这意味着您有更多选择!
什么是urllib2
和urllib3
?
要回答这个问题,你需要回到早期的Python,一直回到1.2版本,当时最原始的Python网址库 被介绍了。 1.6版本左右,进行了改版urllib2 被添加,它与原来的一起生活urllib
。当 Python 3 出现时,最初的urllib
已被弃用,并且urllib2
放弃了2
,取原来的urllib
姓名。它也分为几部分:
urllib.错误
urllib.parse
urllib.请求
urllib.响应
urllib.robotparser
那么呢urllib3 ?这是一个第三方库开发的urllib2
还在附近。它与标准库无关,因为它是一个独立维护的库。有趣的是,requests
库实际使用urllib3
在引擎盖下,也是如此pip !
我应该什么时候使用requests
超过urllib.request
?
主要的答案是易用性和安全性。urllib.request
被认为是一个低级库,它公开了有关 HTTP 请求工作原理的大量细节。蟒蛇文档 为了urllib.request
毫不犹豫地推荐requests
作为更高级别的 HTTP 客户端接口。
如果您日复一日地与许多不同的 REST API 交互,那么requests
强烈推荐。这requests
该库标榜自己是“为人类而构建”,并已成功围绕 HTTP 创建了直观、安全且简单的 API。它通常被认为是首选图书馆!如果您想了解更多关于requests
库,查看 Real Python请求指南 .
举例说明如何requests
当涉及到字符编码时,事情就变得更容易了。你会记得urllib.request
,您必须了解编码并采取一些步骤来确保无错误的体验。这requests
包将其抽象出来,并将通过使用解析编码沙代 ,一个通用字符编码检测器,以防万一有什么有趣的事情。
如果您的目标是了解有关标准 Python 的更多信息以及它如何处理 HTTP 请求的详细信息,那么urllib.request
是进入这个领域的好方法。您甚至可以更进一步,使用非常低级的http模块 。另一方面,您可能只想将依赖关系保持在最低限度,这urllib.request
是有能力的。
为什么是requests
不是标准库的一部分?
也许你想知道为什么requests
目前还不是 Python 核心的一部分。
这是一个复杂的问题,没有硬性且快速的答案。关于原因有很多猜测,但有两个原因似乎很突出:
requests
还有其他第三方依赖项也需要集成。
requests
需要保持敏捷性,并且可以在标准库之外更好地做到这一点。
这requests
库具有第三方依赖项。整合requests
进入标准库意味着还集成chardet
, certifi
, 和urllib3
等。另一种选择是从根本上改变requests
仅使用 Python 现有的标准库。这不是一件简单的任务!
整合requests
也意味着开发这个库的现有团队将不得不放弃对设计和实现的完全控制,让位于PEP 做决定的过程。
HTTP 规范和建议一直在变化,高级库必须足够敏捷才能跟上。如果需要修补安全漏洞或添加新的工作流程,requests
团队的构建和发布速度比 Python 发布过程的一部分要快得多。据称,有时他们会在发现漏洞十二小时后发布安全修复程序!
有关这些问题及更多问题的有趣概述,请查看将请求添加到标准库 ,总结了 Python 语言峰会上的讨论肯尼思·赖茨 ,Requests 的创建者和维护者。
因为这种敏捷性对于requests
及其底层urllib3
,自相矛盾的陈述requests
对于经常使用的标准库来说太重要了。这是因为 Python 社区很大程度上依赖于requests
将其集成到 Python 核心中的敏捷性可能会损害它和 Python 社区。
在 GitHub 存储库问题板上requests
,发布了一个问题,要求包含标准库中的请求 。的开发商requests
和urllib3
插话,主要是说他们可能会失去自己维护它的兴趣。有些人甚至表示他们将分叉存储库并继续为自己的用例开发它们。
话虽如此,请注意requests
库 GitHub 存储库托管在 Python 软件基金会的帐户下。仅仅因为某些东西不是 Python 标准库的一部分,并不意味着它不是生态系统不可或缺的一部分!
看来目前的情况对Python核心团队和维护者来说都是有利的。requests
。虽然对于新手来说可能会有点困惑,但现有的结构为 HTTP 请求提供了最稳定的体验。
还需要注意的是,HTTP 请求本质上是复杂的。urllib.request
不会试图粉饰太多。它公开了 HTTP 请求的许多内部工作原理,这就是它被称为低级模块的原因。您的选择requests
相对urllib.request
实际上取决于您的特定用例、安全问题和偏好。