- 9.1 简介
由于 Web 应用程序的涵盖面非常广,因此本书新版中对这一部分进行了重组,针对 Web开发划分了多个章节,每个章节介绍一个主题,让读者可以关注 Web 开发中特定的几个方面。
在深入其中之前,本章将作为 Web 开发的介绍章节,再次重点讨论客户端/服务器架构,但这次是从 Web 的角度来了解。本章将为后续章节打下坚实的基础。
9.1.1 Web 应用:客户端/服务器计算
Web 应用遵循前面反复提到的客户端/服务器架构。这里说的 Web 客户端是浏览器,即允许用户在万维网上查询文档的应用程序。另一边是 Web 服务器端,指的是运行在信息提供商的主机上的进程。这些服务器等待客户端和及其文档请求,进行相应的处理,并返回相关的数据。正如大多数客户端/服务器系统中的服务器端一样,Web 服务器端“永远”运行。图9-1 展示了 Web 应用的惯用流程。这里,用户运行 Web 客户端程序(如浏览器),连接因特网上任意位置的 Web 服务器来获取数据。
图 9-1 因特网上的 Web 客户端和 Web 服务器。因特网上客户端向服务器端发送一个请求, 然后服务器端响应这个请求并将相应的数据返回给客户端
客户端可以向 Web 服务器端发出各种不同的请求。这些请求可能包括获得一个用于查看的网页视图,或者提交一个包含待处理数据的表单。Web 服务器端首先处理请求,然后会以特定的格式(HTML 等)返回给客户端浏览。
Web 客户端和服务器端交互需要用到特定的“语言”,即 Web 交互需要用到的标准协议,称为 HTTP(HyperText Transfer Protocol,超文本传输)。HTTP 是TCP/IP 的上层协议,这意味着HTTP 协议依靠 TCP/IP 来进行低层的交流工作。它的职责不是发送或者递消息(TCP/IP 协议处理这些),而是通过发送、接受 HTTP 消息来处理客户端的请求。
HTTP 属于无状态协议,因为其不跟踪从一个客户端到另一个客户端的请求信息,这点很像现在使用的客户端/服务器架构。服务器持续运行,但是客户端的活动以单个事件划分的,一旦完成一个客户请求,这个服务事件就停止了。客户端可以随时发送新的请求,但是新的请求会处理成独立的服务请求。由于每个请求缺乏上下文,因此你可能注意到有些 URL 中含有很长的变量和值,这些将作为请求的一部分,以提供一些状态信息。另一种方式是使用“cookie”,即保存在客户端的客户状态信息。在本章的后面部分将会看到如何使用 URL 和cookie 来保存状态信息。
9.1.2 因特网
因特网就像是一个在流动的大池塘,全球范围内互相连接的客户端和服务器端分散在其中。这些客户端和服务器之间含有一系列的链接,就像是漂在水面上互相连接的睡莲一样。客户端用户看不到这些隐藏起来的连接细节。客户端与所访问的服务器端之间进行了抽象, 看起来就像是直接连接的。在底层有隐藏起来的 HTTP、TCP/IP,这些协议将会处理所有的繁重工作,用户无须关心中间的环节信息。因此,将这些执行过程隐藏起来是有好处的。图9-2 更详细地展示了因特网的细节。
图 9-2 因特网的概览。左侧表示的是 Web 客户端的位置,而右侧表示的是 Web 服务器一般所在的位置
值得一提的是,在因特网上传输的数据当中,其中一部分会比较敏感。而在传输过程中, 默认没有加密服务,所以标准协议直接将应用程序发送过来的数据传输出去。为了对传输数据进行加密,需要在普通的套接字上添加一个额外的安全层,称为安全套接字层(Secure SocketLayer,SSL),用来创建一个套接字,加密通过该套接字传输的数据。开发者可以决定是否使用这个额外的安全层。
客户端和服务器在哪里
从图 9-2 可以看到,因特网由多个互相连接的网络组成,所有这些网络之间都和谐(但相对独立)地工作着。图 9-2 的左边关注的是 Web 客户端,即用户在家中通过他们的 ISP 连接,或在公司中使用局域网工作。图中缺少一些专用(但用途广泛)的设备,如防火墙和代理服务器。
防火墙用来阻止对工作(或家庭)网络未授权的访问,如阻止已知的接入点、对每个网络基础进行配置。没有防火墙,入侵者就可能侵入装有服务器的计算机上未受保护的端口, 并获得系统访问权限。网络管理员会封杀大部分端口,只留出常见的服务,如 Web 服务器和安全 shell 访问(SSH),以此降低入侵的概率。安全 shell 访问基于前面提到的 SSL。
代理服务器是另一个有用的工具,它可能会与防火墙一同工作。通过代理服务器,网络管理员可以只让一部分计算机访问网络,也可以更好地监控网络的数据传输。代理服务器另一个有用的是特性是其可以缓存数据。举例说明,如果 Linda 访问了一个代理缓存过的 Web 页面,她的同事 Heather 后来再次访问这个页面时,网页加载速度会快很多,她的浏览器无须与 Web 服务器进行完整的交互,而是从代理服务器获得所有信息。另外,他们公司的 IT 部门知道至少有两个员工在何时访问了这个页面。根据代理服务器的运作方式,这种称为正向代理。
另一种相似的计算机是反向代理。它与正向代理的作用相反(实际上,可以将一台计算机配置成同时进行正向代理和反向代理)。反向代理的行为像是有一个服务器,客户端 可以连接这个服务器。客户端访问这个后端服务器,接着后端服务器在网上进行真正的操作,获得客户端请求的数据。反向代理还可以缓存服务器的数据,如果其作为后端服务器,会将数据直接返回给客户端。
读者可以推测一下,正向代理用来处理缓存数据,更接近客户端。反向代理更接近后端服务器,扮演服务器端角色,如缓存服务器的数据、负载平衡等。反向代理服务器还可以用来作为防火墙或加密数据(通过 SSL、HTTPS、安全 FTP(SFTP)等)。反向代理非常有用,在每天使用因特网的过程中,可能会多次用到反向代理。下面来看这些后端 Web 服务器在哪里。
图 9-2 的右边更关注 Web 服务器以及它们的位置。拥有大型 Web 站点的公司一般会在他们的 ISP 那里有完整的 Web 服务器场。这种方式称为服务器托管,意思是这家公司的服务器与 ISP 的其他客户的服务器放在一起。这些服务器要么向客户提供不同的数据,要么含有备份数据,作为冗余系统的一部分在高需求情形下(含有大量客户时)提供数据。小公司的 Web 站点或许不需要很多硬件和网络设备,在他们的 ISP 那里也许只有一台或若干台托管服务器。
不管是哪种情况,大部分 ISP 的托管服务器都位于骨干网上。由于更接近因特网的核心, 因此这些服务器对因特网的访问速度更快,带宽也更大。这让客户端可以更快地访问服务器, 由于服务器在主干网中,意味着客户端可以直接连接,而无须依次通过许多网络才能访问, 因此可以在相同时间里为更多的客户端提供服务。
因特网协议
这里还需要了解的是,虽然浏览网页是使用因特网最常见的方式,但这既不是唯一的, 也不是最老的方式。因特网比 Web 要早大概 30 年。在 Web 出现之前,因特网主要用于教育和研究目的。那时用到的许多的因特网协议,如 FTP、SMTP 和NNTP,一直沿用到今天。
最初,大家是通过因特网编程才接触到 Python,所以 Python 支持前面讨论到的所有协议。这里对“因特网编程”和“Web 编程”做了区分,后者仅仅关注 Web 方面的应用开发,如Web 客户端和服务器,而这也是本章的核心。
因特网编程涵盖了众多应用,包括使用前面提到的因特网协议的应用,以及网络和套接字编程。这些内容已在本书前面的章节中介绍过了。
9.2 Python Web 客户端工具
有一点需要记清楚,浏览器只是 Web 客户端的一种。任何一个向 Web 服务器端发送请求来获得数据的应用程序都是“客户端”。当然,也可以创建其他的客户端,来在因特网上检索出文档和数据。创建其他客户端的一个重要原因是因为浏览器的能力有限,浏览器主要用于浏览网页内容并同其他 Web 站点交互。另一方面,客户端程序可以完成更多的工作,不仅可以下载数据,还可以存储、操作数据,甚至可以将其传送到另外一个地方或者传给另外一个应用。
使用 urllib 模块下载或者访问 Web 上信息的应用程序(使用 urllib.urlopen()或者 urllib.urlretrieve())就是简单的 Web 客户端。所要做的只是为程序提供一个有效的 Web 地址。
9.2.1 统一资源定位符
简单的网页浏览需要用到名为 URL(统一资源定位符)的 Web 地址。这个地址用来在 Web 上定位一个文档,或者调用一个 CGI 程序来为客户端生成一个文档。URL 是多种统一资源标识符(Uniform Resource Identifier,URI)的一部分。这个超集也可以应对其他将来可能出现的标识符命名约定。一个 URL 是一个简单的 URI,它使用已有的协议或方案(即 http、ftp 等)作为地址的一部分。为了更完整地描述,还要介绍非 URL 的 URI,有时它们称为统一资源名称(Uniform Resource Name,URN),但是现在唯一使用的 URI 只有 URL,而很少听到 URI和 URN,后者只作为可能会用到的 XML 标识符了。
如街道地址一样,Web 地址也有一些结构。美国的街道地址通常形如“号码 街道名称”,例如“123 某某大街”。其他国家的街道地址也有自己的规则。URL 使用这种格式。
prot_sch://net_loc/path;params?query#frag
表 9-1 介绍了 URL 的各个部分。
表 9-1 Web 地址的各个组件
URL 组件 | 描 述 |
prot_sch | 网络协议或下载方案 |
net_loc | 服务器所在地(也许含有用户信息) |
path | 使用斜杠(/)分割的文件或 CGI 应用的路径 |
params | 可选参数 |
query | 连接符(&)分割的一系列键值对 |
frag | 指定文档内特定锚的部分 |
net_loc 可以进一步拆分成多个组件,一些是必备的,另一些是可选的。net_loc 字符串如下。
user:passwd@host:port
表 9-2 介绍了各个组件。
表 9-2 网络地址的各个组件
组 件 | 描 述 |
user | 用户名或登录 |
passwd | 用户密码 |
host | 运行 Web 服务器的计算机名称或地址(必需的) |
port | 端口号(如果不是默认的 80) |
在这 4 个组件中,host 名是最重要的。port 号只有在 Web 服务器运行其他非默认端口号上时才会使用(如果不确定所使用的端口号,可以参见第 2 章)。
用户名和密码只有在使用 FTP 连接时候才有可能用到,而即便使用 FTP,大多数的连接都是匿名的,这时不需要用户名和密码。
Python 支持两种不同的模块,两者分别以不同的功能和兼容性来处理 URL。一种是urlparse,另一种是 urllib。下面将会简单介绍它们的功能。
9.2.2 urlparse 模块
urlpasrse 模块提供了一些基本功能,用于处理 URL 字符串。这些功能包括 urlparse()、urlunparse()和 urljoin()。
urlpasrse.urlunparse()
urlparse()将 URL 字符串拆分成前面描述的一些主要组件。其语法结构如下。
urlparse (urlstr, defProtSch=None, allowFrag=None)
urlparse()将 urlstr 解析成一个 6 元组(prot_sch, net_loc, path, params, query, frag)。前面已经描述了这里的每个组件。如果 urlstr 中没有提供默认的网络协议或下载方案,defProtSch 会指定一个默认的网络协议。allowFrag 标识一个URL 是否允许使用片段。下边是一个给定 URL 经 urlparse()后的输出。
>>> urlparse.urlparse('http://www.python.org/doc/FAQ.html') ('http', 'www.python.org', '/doc/FAQ.html', '', '', '')
urlparse.urlunparse()
urlunparse()的功能与 urlpase()完全相反,其将经 urlparse()处理的 URL 生成 urltup 这个 6 元组(prot_sch, net_loc, path, params, query, frag),拼接成 URL 并返回。因此可以用如下方式表示其等价性:
urlunparse(urlparse(urlstr)) ≡ urlstr
读者或许已经猜到了 urlunpase()的语法。
urlunparse(urltup)
urlparse.urljoin()
在需要处理多个相关的 URL 时我们就需要使用 urljoin()的功能了,例如,一个 Web 页中可能会产生一系列页面URL。urljoin()的语法是如下。
urljoin (baseurl, newurl, allowFrag=None)
urljoin()取得根域名,并将其根路径(net_loc 及其前面的完整路径,但是不包括末端的文件)与 newurl 连接起来。例如:
>>> urlparse.urljoin('http://www.python.org/doc/FAQ.html', ... 'current/lib/lib.htm') 'http://www.python.org/doc/current/lib/lib.html'
表 9-3 总结了 urlparse 中的函数。
表 9-3 urlparse 模块中的核心函数
urlparse 函数 | 描 述 |
urlparse(urlstr, defProtSch=None, allowFrag=None) | 将 urlstr 解析成各个组件,如果在 urlstr 中没有给定协议或者方案,则使用 defProtSch;allowFrag 决定是否允许有 URL 片段 |
urlunparse(urltup) | 将 URL 数据(urltup)的一个元组拼成一个 URL 字符串 |
urljoin(baseurl, newurl, allowFrag=None) | 将 URL 的根域名和 newurl 拼合成一个完整的 URL;allowFrag 的作用和urlpase()相同 |
9.2.3 urllib 模块/包
urllib 模块提供了许多函数,可用于从指定 URL 下载数据,同时也可以对字符串进行编码、解码工作,以便在URL 中以正确的形式显示出来。下面将要介绍的函数包括urlopen()、urlretrieve()、quote()、unquote()、quote_plus()、unquote_plus()和 urlencode()。其中一些方法可以使用 urlopen()方法返回的文件类型对象。
urllib.urlopen()
urlopen()打开一个给定 URL 字符串表示的 Web 连接,并返回文件类型的对象。语法结构如下。
urlopen (urlstr, postQueryData=None)
urlopen()打开 urlstr 所指向的 URL。如果没有给定协议或者下载方案(Scheme),或者 传入了“file”方案,urlopen()会打开一个本地文件。
对于所有的 HTTP 请求,常见的请求类型是“GET”。在这些情况中,向 Web 服务器发送的请求字符串(编码过的键值对,如 urlencode()函数返回的字符串)应该是 urlstr 的一部分。
如果使用“POST”请求方法,请求的字符串(编码过的)应该放到 postQueryData 变量中(本章后续部分将介绍关于“GET”和“POST”方法的更多信息,但这些 HTTP 命令是通用于 Web 编程和 HTTP 本身的,并不特定于 Python)。
一旦连接成功,urlopen()将会返回一个文件类型对象,就像在目标路径下打开了一个可读文件。例如,如果文件对象是 f,那么“句柄”会支持一些读取内容的方法,如 f.read()、f.readline()、f.readlines()、f.close()和 f.fileno()。
此外,f.info()方法可以返回 MIME(Multipurpose Internet Mail Extension,多目标因特网邮件扩展)头文件。这个头文件通知浏览器返回的文件类型,以及可以用哪类应用程序打开。例如,浏览器本身可以查看 HTML、纯文本文件、渲染 PNG(Portable Network Graphics)文件、JPEG(Joint Photographic Experts Group)或者 GIF(Graphics Interchange Format)文件。而其他如多媒体或特殊类型文件需要通过其他应用程序才能打开。
最后,geturl()方法在考虑了所有可能发生的重定向后,从最终打开的文件中获得真实的URL。表 9-4 描述了这些文件类型对象的方法。
表 9-4 urllib.urlopen()文件类型对象的方法
urlopen()对象方法 | 描 述 |
f.read([bytes]) | 从 f 中读出所有或 bytes 个字节 |
f.readline() | 从 f 中读取一行 |
f.readlines() | 从 f 中读出所有行,作为列表返回 |
f.close() | 关闭 f 的 URL 连接 |
f.fileno() | 返回 f 的文件句柄 |
f.info() | 获得 f 的MIME 头文件 |
f.geturl() | 返回 f 的真正 URL |
如果打算访问更加复杂的 URL 或者想要处理更复杂的情况,如基于数字的权限验证、重定位、cookie 等问题,建议使用 urllib2 模块。这个模块依然拥有一个 urlopen()函数,但同时它提供了可以打开各种URL 的其他函数和类。
如果读者使用的是 2.x 版本,这里强烈建议在 2.6 和 3.0 版本中使用 urllib2.urlopen()。因为从 2.6 版本开始,urllib 中弃用了 urlopen 函数,而 3.0 版本中移除了该函数。在阅读前面那个核心模块侧边栏时,已经提到这两个模块中的功能在 Python 3 中都合并进了 urllib.request 中。这表示 2.x 版本中的 urlib2.urlopen()(已经没有 urllib.urlopen()了)在 3.x 版本中需要使用的 urllib.request.urlopen()函数。
urllib.urlretrieve()
urlretrieve()不是用来以文件的形式访问并打开 URL,而是用于下载完整的 HTML,把另存为文件,其语法如下。
urlretrieve(url, filename=None, reporthook=None, data=None)
除了像urlopen()这样从URL 中读取内容,urlretrieve()可以方便地将urlstr 中的整个HTML文件下载到本地硬盘上。下载后的数据可以存成一个 localfile 或者一个临时文件。如果该文件已经复制到本地或者 url 指向的文件就是本地文件,就不会发生后面的下载动作。
如果提供了 downloadStatusHook,则在每块数据下载或传输完成后会调用这个函数。调用时使用以下 3 个参数:目前读入的块数、块的字节数和文件的总字节数。如果正在用文本或图表向用户显示“下载状态”信息,这个函数将会非常有用。
urlretrieve()返回一个二元组(filename, mime_hdrs)。filename 是含有下载数据的本地文件名,mime_hdrs 是 Web 服务器响应后返回的一系列 MIME 文件头。要获得更多的信息,可以查看 mimetools 模块的 Message 类。对本地文件来说,mime_hdrs 是空的。
urllib.quote()和 urllib.quote_plus()
quote*()函数用来获取 URL 数据,并将其编码,使其可以用于 URL 字符串中。具体来说, 必须对某些不能打印的或者不被 Web 服务器作为有效 URL 接收的特殊字符串进行转换。这就是 quote*()函数的功能。quote*()函数的语法如下。
quote(urldata, safe='/')
逗号、下划线、句号、斜线和字母数字这类符号不需要转化,其他的则均需要转换。另外,那些 URL 不能使用的字符前边会被加上百分号(%)同时转换成十六进制,例如,“%xx”,其中,“xx”表示这个字母的 ASCII 码的十六进制值。当调用 quote*()时,urldata 字符串转换成一个可在 URL 字符串中使用的等价字符串。safe 字符串可以包含一系列不能转换的字符, 默认字符是斜线(/)。
quote_plus()与quote()很像,只是它还可以将空格编码成“+”号。下边是一个使用 quote()和 quote_plus()的例子。
>>> name = 'joe mama' >>> number = 6 >>> base = 'http://www/~foo/cgi-bin/s.py' >>> final = '%s?name=%s&num=%d' % (base, name, number) >>> final 'http://www/~foo/cgi-bin/s.py?name=joe mama&num=6' >>> >>> urllib.quote(final) 'http:%3a//www/%7efoo/cgi-bin/s.py%3fname%3djoe%20mama%26num%3d6' >>> >>> urllib.quote_plus(final) 'http%3a//www/%7efoo/cgi-bin/s.py%3fname%3djoe+mama%26num%3d6'
urllib.unquote()和 urllib.unquote_plus()
读者也许已经猜到了,unquote*()函数与 quote*()函数的功能完全相反,前者将所有编码为“%xx”式的字符转换成等价的 ASCII 码值。unquote*()的语法如下。
unquote*(urldata)
调用 unquote()函数将会把 urldata 中所有的 URL 编码字母都解码,并返回字符串。unquote_plus()函数会将加号转换成空格符。
urllib.urlencode()
urlopen()函数接收字典的键值对,并将其编译成字符串,作为 CGI 请求的URL 字符串的一部分。键值对的格式是“键=值”,以连接符(&)划分。另外,键及其对应的值会传到 quote_plus()函数中进行适当的编码。下边是 urlencode()输出的一个例子。
>>> aDict = { 'name': 'Georgina Garcia', 'hmdir': '~ggarcia' } >>> urllib.urlencode(aDict) 'name=Georgina+Garcia&hmdir=%7eggarcia'
urllib 和 urlparse 还有一些其他的函数,这里就不一一叙述了。更多信息可以阅读相关文档。
表 9-5 总结了本节介绍的 urllib 中的函数。
表 9-5 urllib 模块中的核心函数
SSL Support
在对 urllib 做出总结并接触其他示例之前,需要指明的是,urllib 模块通过安全套接字层(SSL)支持开放的 HTTP 连接(socket 模块的核心变化是增加并实现了 SSL)。httplib 模块支持使用“https”连接方案的 URL。除了那两个模块以外,其他支持 SSL 的模块还有 imaplib、poplib 和 smtplib。
9.2.4 使用 urllib2 HTTP 验证的示例
正如前面所提到的,urllib2 可以处理更复杂 URL 的打开问题。例如有基本验证(登录名和密码)需求的 Web 站点。通过验证的最简单方法是使用前边章节描述的URL 中的 net_loc 组件,例如 http://user name:passwd@www.python.org。但这种解决方案的问题是它不具有可编程性。而通过 urllib2,可以用两种不同的方式来解决这个问题。
可以建立一个基础验证处理程序(urllib2.HTTPBasicAuthHandler),同时在根域名和域上注册一个登录密码,这就意味着在 Web 站点上定义了一个安全区域。当完成了这个处理程序后,安装 URL 开启器(opener),通过这个处理程序打开所有的 URL。
域来自 Web 站点的安全部分定义的.htaccess 文件。下面是这样一个文件的示例。
在 Web 站点的这一部分中,AuthName 列出的字符串就是域。通过 htpasswd 命令创建用户名和加密的密码,并安装在.htpasswd 文件中。关于域和Web 验证的更多信息,参见RFC 2617( HTTP 验证:基本和摘要式存取验证), 以及维基百科的页面 https://en.wikipedia.org/wiki/Basic_access_authentication。
另一个创建开启器的办法就是当浏览器提示的时候,通过验证处理程序模拟用户输入用户名和密码,这样就发送了一个带有适当用户请求的授权头。示例 9-1 演示了这两种方法。
示例 9-1 基本 HTTP 验证(urlopen_auth.py)
这段代码使用了前面提到的基本 HTTP 验证知识。这里必须使用 urllib2,因为 urllib 中没有这些功能。
逐行解释
第 1~8 行
普通的初始化过程,外加几个为后续脚本使用的常量。需要注意的是,其中的敏感信息应该位于一个安全的数据库中,或至少来自环境变量或预编译的.pyc 文件,而不是位于源码文件中硬编码的纯文本中。
第 10~17 行
代码的“handler”版本分配了前面提到的一个基本处理程序类,并添加了验证信息。之后该处理程序用于建立一个 URL 开启器,安装该开启器以便所有已打开的URL 都能用到这些验证信息。这段代码改编自Python 官方文档中的 urllib2 模块。
第 19~24 行
“request”版本的代码创建了一个 Request 对象,并在 HTTP 请求中添加了简单的基 64编码的验证头信息。在 for 循环里调用 urlopen()时,该请求用来替换其中的 URL 字符串。注意,原始 URL 内建在 urllib2.Requst 对象中,因此在随后的 urllib2.urlopen()调用中替换 URL 字符串才不会产生问题。这段代码的灵感来自于Mike Foord 和 Lee Harr 在Python Cookbook 上的回复,具体位置如下。
- http://aspn.activestate.com/ASPN/Cookbook/Python/Recipe/305288
- http://aspn.activestate.com/ASPN/Cookbook/Python/Recipe/267197
如果能直接用 Harr 的 HTTPRealmFinder 类就更好了,那样就无须在例子里使用硬编码。
第 26~31 行
代码的剩余部分只是用两种技术分别打开了给定的 URL,并显示服务器返回的 HTML 页面的第一行(转储了其他行),当然,前提是要通过验证。注意,如果验证信息无效会返回一个HTTP 错误(并且不会有HTML)。
程序的输出应当如下所示。
$ python urlopen_auth.py *** Using HANDLER: <html> *** Using REQUEST: <html>
作为 urllib2 官方的 Python 文档的补充,下面这个文件很有帮助。
http://www.voidspace.org.uk/python/articles/urllib2.shtml
9.2.5 将 HTTP 验证示例移植到Python 3 中
在编写本书时,移植这个应用除了使用 2to3 这个转换工具外,还需要更多的精力。当然, 2to3 完成了其中的主要工作,但还需对代码进行一些微调。首先对 urlauth_open.py 脚本运行 2to3 工具。
$ 2to3 -w urlopen_auth.py . . .
在 Windows 平台上可以使用类似的命令完成操作。从前面章节的描述中读者可能已经知道了,这条命令会将代码从 Python 2 转到 Python 3 时发生的改变列出来。原先的代码会用Python 3 版本覆盖掉,并自动生成一个 Python 2 版本的备份。
手动将文件从urlopen_auth.py 重命名为urlopen_auth3.py,将备份后的 urlopen_auth.py.bak命名为 urlopen_auth.py。在 POSIX 系统上,通过下面的命令执行这些操作(在 Windows 平台上,可以通过相应的 DOS 命令或 Windows 图形化操作来完成)。
$ mv urlopen_auth.py urlopen_auth3.py $ mv urlopen_auth.py.bak urlopen_auth.py
这样文件名符合命名规范,并能更好地识别出 Python 2 版本的代码和转换后的 Python 3 版本的代码。而运行这个工具只是一个开始。如果乐观地认为转换后的程序可以直接运行, 会发现其实是太天真了。
$ python3 urlopen_auth3.py *** Using HANDLER: b'<HTML>\n' *** Using REQUEST: Traceback (most recent call last): File "urlopen_auth3.py", line 28, in <module> url = eval('%s_version' % funcType)(URL) File "urlopen_auth3.py", line 22, in request_version b64str = encodestring('%s:%s' % (LOGIN, PASSWD))[:-1] File "/Library/Frameworks/Python.framework/Versions/3.2/lib/ python3.2/base64.py", line 353, in encodestring return encodebytes(s) File "/Library/Frameworks/Python.framework/Versions/3.2/lib/ python3.2/base64.py", line 341, in encodebytes raise TypeError("expected bytes, not %s" % s. class . name )TypeError: expected bytes, not str
这个问题的解决方案和预想的差不多,在第 22 行的字符串的引号之前加上“b”,将其转换成字节字符串,即 b’%s:%s’ % (LOGIN, PASSWD)。如果再次运行,会发现另一个错误, 而这只是Python 2 迁移到 Python 3 时会遇到的众多问题中的一个。
$ python3 urlopen_auth3.py *** Using HANDLER: b'<HTML>\n' *** Using REQUEST: Traceback (most recent call last): File "urlopen_auth3.py", line 28, in <module> url = eval('%s_version' % funcType)(URL) File "urlopen_auth3.py", line 22, in request_version b64str = encodestring(b'%s:%s' % (LOGIN, PASSWD))[:-1] TypeError: unsupported operand type(s) for %: 'bytes' and 'tuple'
很明显,因为字符串不再以单字节为单位,所以字符串格式化运算符不再支持 bytes 对象。因此,需要将字符串作为(Unicode)文本对象来格式化,接着将这个文本转换成 bytes 对象:bytes(‘%s:%s’ % (LOGIN, PASSWD), ‘utf-8’))。通过这些更改,程序的输出更接近期望值了。
$ python3 urlopen_auth3.py *** Using HANDLER: b'<HTML>\n' *** Using REQUEST: b'<HTML>\n'
但结果还是有点问题,因为在 bytes 对象之前使用标记(前导“b”、引号等),而不是直接使用纯文本。为此需要将 print()调用改成 print(str(f.readline(), ‘utf-8’))。现在 Python 3 版本的输出就与 Python 2 的完全相同了。
$ python3 urlopen_auth3.py *** Using HANDLER: <html> *** Using REQUEST: <html>
如你所见,虽然这种移植需要手动逐步调整,但这依然是可行的。另外,前面也提到,urllib、urlib2 和 ulrparse 都合并进了 Python 3 中的 urllib 包中。由于 2to3 所做的工作,已经在前面导入了 urllib.parse,并移除了 handler_version()中多余的定义。可以在示例 9-2 中发现这些改变。
示例 9-2 Python 3 的 HTTP 验证脚本(urlopen_auth3.py)
这是 urlopen_auth.py 脚本的 Python 3 版本。
下面将介绍稍微高级一点的 Web 客户端。