第2部分 Web开发

第9章 Web客户端和服务器

如果你拥有来自于CERN的WWW项目的浏览器(World Wide Web,一个分布式超文本系统),就可以浏览本手册的WWW超文本版本。

——Guido van Rossum,1992年11月

(在Python邮件列表中首次提及Web)

本章内容:

简介;

Python Web客户端工具;

Web客户端;

Web(HTTP)服务器;

相关模块。

9.1 简介

由于Web应用程序的涵盖面非常广,因此本书新版中对这一部分进行了重组,针对Web开发划分了多个章节,每个章节介绍一个主题,让读者可以关注Web开发中特定的几个方面。

在深入其中之前,本章将作为Web开发的介绍章节,再次重点讨论客户端/服务器架构,但这次是从Web的角度来了解。本章将为后续章节打下坚实的基础。

9.1.1 Web应用:客户端/服务器计算

Web 应用遵循前面反复提到的客户端/服务器架构。这里说的 Web 客户端是浏览器,即允许用户在万维网上查询文档的应用程序。另一边是Web服务器端,指的是运行在信息提供商的主机上的进程。这些服务器等待客户端和及其文档请求,进行相应的处理,并返回相关的数据。正如大多数客户端/服务器系统中的服务器端一样,Web服务器端“永远”运行。图9-1展示了Web应用的惯用流程。这里,用户运行Web客户端程序(如浏览器),连接因特网上任意位置的Web服务器来获取数据。

第2部分 Web开发 - 图1 图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更详细地展示了因特网的细节。

第2部分 Web开发 - 图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.urlre trieve())就是简单的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地址的各个组件 第2部分 Web开发 - 图3

net_loc可以进一步拆分成多个组件,一些是必备的,另一些是可选的。net_loc字符串如下。

user:passwd@host:port

表9-2介绍了各个组件。

表9-2 网络地址的各个组件 第2部分 Web开发 - 图4

在这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模块中的核心函数 第2部分 Web开发 - 图5

9.2.3 urllib模块/包

核心模块:Python 2和Python 3中的urllib

除非需要写一个更加低层的网络客户端,否则urllib模块就能满足所有需要。urllib提供了一个高级的Web通信库,支持基本的Web协议,如HTTP、FTP和Gopher协议,同时也支持对本地文件的访问。具体来说,urllib模块的功能是利用前面介绍的协议来从因特网、局域网、本地主机上下载数据。使用这个模块就无须用到httplib、ftplib和gopherlib这些模块了,除非需要用到更低层的功能。在那些情况下这些模块可以作为备选方案(注意,大多数以协议名+lib 的方式命名的模块都用于开发相关协议的客户端。但并不是所有情况都是这样的,或许 urllib 应该重命名为“internetlib”或者其他相似的名字)。

Python 2中有urlib、urlparse、urllib2,以及其他内容。在Python 3中,所有这些相关模块都整合进了一个名为urllib的单一包中。urlib和urlib2中的内容整合进了urlib.request模块中,urlparse整合进了urllib.parse中。Python 3中的urlib包还包括response、error和robotparse这些子模块。在继续学习本章后续的示例和练习时需要注意这些区别。

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()文件类型对象的方法 第2部分 Web开发 - 图6

如果打算访问更加复杂的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模块中的核心函数 第2部分 Web开发 - 图7

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文件。下面是这样一个文件的示例。

AuthType        basic

AuthName        "Secure Archive"

AuthUserFile      /www/htdocs/.htpasswd

require        valid-user

在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 #!/usr/bin/env python 2 3 import urllib2 4 5 LDGIN="wesley"6 PASSWD="you' llNever Guess"7 URL=''http://localhost

第2部分 Web开发 - 图9

逐行解释

第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版本。

第2部分 Web开发 - 图10

下面将介绍稍微高级一点的Web客户端。

9.3 Web客户端

Web浏览器是基本的Web客户端。它主要用来在Web上查询或者下载文档。但还可以创建一些具有其他功能的Web客户端。本节将介绍若干这样的客户端。

9.3.1 一个简单的Web爬虫/蜘蛛/机器人

一个稍微复杂的 Web客户端例子就是网络爬虫(又称蜘蛛或机器人)。这些程序可以为了不同目的在因特网上探索和下载页面,其中包括以下几个目的。

为Google和Yahoo这类大型的搜索引擎创建索引。

离线浏览,即将文档下载到本地硬盘,重新设定超链接,为本地浏览器创建镜像。

下载并保存历史记录或归档。

缓存Web页面,节省再次访问Web站点的下载时间。

示例9-3中的crawl.py用来通过起始Web地址(URL),下载该页面和其他后续链接页面,但是仅限于那些与开始页面有相同域名的页面。如果没有这个限制,会耗尽硬盘上的空间!

示例9-3 Web爬虫(crawl.py)

这个爬虫含有两个类:一个用于管理整个爬虫进程(Crawler),另一个获取并解析每个下载到的 Web 页面(Retriever)(这个示例对本书第2版中相同的示例进行了重构)。

第2部分 Web开发 - 图11

第2部分 Web开发 - 图12

第2部分 Web开发 - 图13

逐行解释

第1~10行

该脚本的起始部分包括Python在UNIX上标准的初始化行,同时导入一些程序中会用到的模块和包。下面是一些简单的解释。

cStringIO、formatter、htmllib:使用这些模块中不同的类来解析HTML。

httplib:使用这个模块中定义的一个异常。

os:该模块提供了许多文件系统方面的函数。

sys:使用其中提供的argv来处理命令行参数。

urllib:使用其中的urlretrive()函数来下载Web页面。

urlparse:使用其中的urlparse()和urljoin()函数来处理URL。

第12~29行

Retriever类的任务是从 Web 下载页面,解析每个文档中的链接并在必要的时候把它们加入“to-do”队列。这里为从网上下载的每个页面都创建一个 Retriever 类的实例。Retriever中有若干方法,用来实现相关功能:构造函数(init())、get_file()、download()、和parse_links()。

暂时跳过一些内容,先看get_file(),这个方法将指定的URL转成本地存储的更加安全的文件,即从Web上下载这个文件。基本上,这其实就是将URL的http://前缀移除,丢掉任何为获取主机名而附加的额外信息,如用户名、密码和端口号(第20行)。

没有文件扩展名后缀的URL将添加一个默认的index.html文件名,调用者可以重写这个文件名。可以在第21~23行了解其工作方式,并最终创建了filepath。

最后得到最终的目标路径(第24行),检测它是否为目录。如果是,则不管它,返回“URL-文件路径”键值对。如果进入了if子句,这意味着路径名要么不存在,要么是个普通文件。如果是普通文件,需要将其删除并重新创建同名目录。最终,使用第 28 行的 os.makedirs()创建目标目录及其所有父目录。

现在回到初始化函数init()中。在这里创建了一个Retriever对象,将getfile()返回的URL字符串和对应的文件名作为实例属性存储起来。在当前的设计中,每个下载下来的文件都会创建一个实例。而Web站点会含有许多文件,为每个文件都创建对象实例会导致额外的内存问题。为了降低资源开销,这里创建了_slots变量,表示实例只能拥有self.url和self.file属性。

第31~49行

这里先稍微了解一下爬虫,这个爬虫很聪明地为每个下载下来的文件创建一个Retriever对象。顾名思义,download()方法通过给定的链接在因特网上下载对应的页面(第 34 行)。并将URL作为参数来调用urllib.urlretreive(),将其另存为文件名(即get_file()返回的)。

如果下载成功,则返回文件名(第34行),如果出错,则返回一个以“*”开头的错误提示字符串(第35~36行)。爬虫检查这些返回值,如果没有出错,则调用parse_linkes()解析刚下载下来的页面链接。

在这部分中更重要的方法是parse_linkes()。是的,爬虫的任务是下载Web页面,但递归的爬虫(现在这个就是递归的)会查看所有下载下来的页面中是否含有额外的链接,并进行处理。它首先打开下载到的Web页面,将所有HTML内容提取成单个字符串(第42~44行)。

第45~49行的内容是一个常见的代码片段,其中使用了htmllib.HTMLParse类。这个代码片段是Python程序员代代相传的,现在传到了读者这里。

这段代码的工作方式中,最重要的是parser类不进行I/O,它只处理一个formatter对象。Python 只有一个 formatter 对象,即 formatter.AbstractFormatter,用来解析数据并使用 writer对象来分配其输出内容。同样,Python只有一个有用的writer对象,即formatter.DumbWriter。可以为该对象提供一个可选的文件对象,表示将输出写入文件。如果不提供这个文件对象,则会写入标准输出,但后者一般不是所期望的。为了不让输出写到标准输出,先实例化一个cStringIO对象。StringIO对象会吸收掉这些输出(如果读者对类UNIX系统有所了解,就类似其中的/dev/null)。可以在网上搜索这些类名,找到类似的代码片段和注释。

由于htmllib.HTMLParser很老,从Python 2.6开始弃用了。下一小节将介绍使用另一个较新的工具写更加小巧的示例。这里先继续使用 htmllib.HTMLParser,因为这是个常见的代码片段,而且依然能正确地完成工作。

总之,创建解析器的所有复杂问题都由一个简单的调用完成(第45~46行)。这一部分剩下的代码用于在HTML中进行解析、关闭解析器并将解析后的链接/锚作组成列表返回。

第51~59行

Crawler类是这次演示中的“明星”,管理一个Web站点的完整抓爬过程。如果为应用程序添加线程,就可以为每个待抓爬的站点分别创建实例。Crawler的构造函数在实例化过程中存储了3样东西,第一个是self.q,是一个待下载的链接队列。这个队列的内容在运行过程中会有变化,有页面处理完毕就缩短,在每个下载的页面中发现新的链接则会增长。

Crawler 包含的另两个数值是 self.seen,这是所有已下载链接的一个集合。另一个是self.dom,用于存储主链接的域名,并用这个值来判定后续链接的域名与主域名是否一致。所有这三个值都在初始化方法init()中创建,参见第54~59行。

注意,在第58行使用urlparse.urlparse()解析域名,与在Retriever中通过URL抓取主机名的方式相同。域名是主机名的最后两部分。因为主机名在这里并没什么用,所以可以将第58行和第59行连起来写,但这样可读性就大大降低了。

self.dom = '.'.join(urlparse.urlparse(

url).netloc.split('@')[-1].split(':')[0].split('.')[-2:])

init()的上方,Crawler还有一个名为count的静态数据项。这是一个计数器,用于保持追踪从因特网上下载下来的对象数目。每成功下载一个网页,这个变量就递增1。

第61~105行

除了构造函数以外,Crawler 还有另外一对方法,分别是 get_page()和 go()。go()是一个简单的方法,用于启动Crawler。go()在代码的main部分调用。go()中含有一个循环,用于将队列中所有待下载的新链接处理完毕。而这个类中真正埋头苦干的是get_page()方法。

get_page()使用第一个链接实例化一个Retriever对象,然后开始处理。如果页面成功下载,则递增计数器(否则,在第65~67行忽略发生的错误)并将该链接添加到已下载的集合中(第72行)。使用集合是因为加入的链接顺序不重要,同时集合的查找速度比列表快。

get_page()会查看每个下载完成的页面(在第73~75行会跳过所有非Web页面)中的所有链接,判断是否需要向列表中添加更多的链接(第 77~79 行)。go()中的主循环会继续处理链接,直到队列为空,此时会声明处理成功(第103~105)。

其他域名的链接(第90~91行),或已经下载的链接(第98~99行),已位于队列中等待处理的链接(第96~97行)、邮箱链接等,都会被忽略,不会添加到队列中(第78~80行)。媒体文件也会被忽略(第81~85行)。

第107~124行

main()需要一个URL来启动处理。如果在命令行指定了一个URL(例如,这个脚本被直接调用时,如第108~109行所示),就会使用这个指定的URL。否则,脚本进入交互模式,提示用户输入初始URL。一旦有了初始始链接,就会实例化Crawler并启动(第120~121行)。

下面是一个调用crawl.py的例子。

$ crawl.py

Enter starting URL: http://www.null.com/home/index.html

( 1 )

URL: http://www.null.com/home/index.html

FILE: www.null.com/home/index.html

( 2 )

URL: http://www.null.com/home/order.html

FILE: www.null.com/home/order.html

( 3 )

URL: http://www.null.com/home/synopsis.html

FILE: www.null.com/home/synopsis.html

( 4 )

URL: http://www.null.com/home/overview.html

FILE: www.null.com/home/overview.html

执行后,在本地系统文件中会创建一个名为www.null.com的目录,及一个名为home的子目录。home目录中会含有所有处理过的HTML文件。

如果在阅读了这些代码后,依然想找到一些使用Python编写的爬虫。可以去查看原先的Google Web 爬虫,这个爬虫是用 Python 编写的。更多信息参见 http://infolab.stanford.edu/~backrub/google.html。

9.3.2 解析Web页面

前一小节介绍了爬虫这个Web客户端。爬虫过程中牵扯到了解析链接或在正式调用时进行锚定。长久 以来,在解析Web页面时会用到众所周知的htmllib.HTMLParser这个代码片段。但新改善过的模块和包也出现了。这一小节将介绍其中一些新的内容。

在示例 9-4 中,介绍了一个标准库文件,即 HTMLParse 模块(自 2.2 版本添加)中的HTMLParse类。HTMLParser.HTMLParser用于替换htmllib.HTMLParser。因为前者更简单,可以从更底层的视角观察页面,且可以处理XHTML。而后者比较老,且基于sgmllib模块(意味着必须理解复杂的标准通用标记语言(Standard Generalized Markup Language,SGML),因此也更加复杂。官方文档对HTMLParser.HTMLParser的介绍很少,但这里会提供更多有用的示例。

在Python最著名的3个Web解析器中,这里演示了其中两个:BeautifulSoup和html5lib。这两个库不是标准库,因此需要单独下载。可以在Cheeseshop 或 http://pypi.python.org 下载这两个库。为了方便起见,可以使用easy_install或pip工具进行安装。

而跳过的那个是lxml,这个将作为练习让读者自己来完成。在本章末尾会发现更多例子,将这些例子中的htmllib.HTMLParser替换为lxml能帮读者更好地理解相关知识。

示例9-4中的parser_links.py脚本只用于从输入数据中解析出锚点。给定一个URL后,脚本会提取所有链接,尝试进行必要的调整,生成完整的URL,对这些URL进行排序并显示给用户。其会对每个URL运行所有的3个解析器。尤其对于BeatifulSoup,提供两种不同的解决方案。第一种简单一些,解析所有标签并查找所有锚标签。第二种需要用到SoupStrainer类,它只针对并处理锚标签。

示例9-4 链接解释器(parse_links.py)

这段脚本会使用三种不同的解释器来从HTML锚标签中提取链接。这样可以更好地理解HTMParser标准库模块,以及第三方的BeautifulSoup和html5lib包。

第2部分 Web开发 - 图14

第2部分 Web开发 - 图15

逐行解释

第1~9行

在这个脚本中,使用标准库中的 4 个模块。HTMLParser 是其中一个解析器。另外三个的使用遍及各处。导入的第二组是第三方(非标准库)模块/包。这是标准的导入顺序,即先导入标准模块/包,接着导入第三方模块/包,最后导入应用的本地模块/包。

第11~17行

URL变量含有需要解析的Web页面。可以自由添加、更改或移除这里的URL。output()函数接受一个可迭代且含有链接的变量,将这些链接放到集合中,以移除重复的链接。把它们按字母顺序排序,将其合并到换行符分隔的字符串中,以此呈现给用户。

第19~27行

在 simpleBS()和 fasterBS()函数中用注释强调了对 BeautifulSoup 的使用。在 simpleBS()中,在通过文件句柄实例化BeautifulSoup时开始解析。在其后的一段代码中进行提取,这里使用从PyCon Web站点已经下载下来的页面作为pycon.html。

>>> from BeautifulSoup import BeautifulSoup as BS

>>> f = open('pycon.html')

>>> bs = BS(f)

当获取示例并调用其中的 findAll()方法来请求锚标签('a')时,其返回一个标签列表,如下所示。

>>> type(bs)

<class 'BeautifulSoup.BeautifulSoup'>

>>> tags = bs.findAll('a')

>>> type(tags)

<type 'list'>

>>> len(tags)

19

>>> tag = tags[0]

>>> tag

<a href="/2011/">PyCon 2011 Atlanta</a>

>>> type(tag)

<class 'BeautifulSoup.Tag'>

>>> tag['href']

u'/2011/'

由于 Tag 对象是一个锚,它应该含有一个“href”标签,因此获取其中的内容。接着调用urlparse.urljoin()并传递头URL,以及用于获得全部URL的那个链接。这里是连续的例子(假设使用PyCon URL)。

>>> from urlparse import urljoin

>>> url = 'http://us.pycon.org&#39;

>>> urljoin(url, tag['href'])

u'http://us.pycon.org/2011/&#39;

这个生成器表达式迭代 urlparse、urljoin()从所有锚标签创建的所有最终链接,并将其发至output(),它按前面描述方式处理它们。如果由于使用生成器表达式导致这段代码有点难懂,这里可以将其展开成等价的urlparse.urljoin()形式。

def simpleBS(url, f):

parsed = BeautifulSoup(f)

tags = parsed.findAll('a')

links = [urljoin(url, tag['href']) for tag in tags]

output(links)

从可读性方面考虑,这段代码比单行版本要好一些。建议编写开源或合作性项目时,尽量不要将代码集成到一行中。

尽管simpleBS()函数很易懂,但其中一个缺点是处理效率不高。这里使用BeautifulSoup解析文档中的所有标签,接着查找锚。如果进行过滤,只处理含有锚的标签(并忽略剩余的标签),速度会快一点。

这就是fasterBS()所做的工作,使用SoupStrainer辅助类来完成这个任务(并将这个请求传递给过滤器,只有锚标签会作为 parseOnlyThese 的参数)。使用 SoupStrainer 可以让BeautifulSoup在构建解析树时跳过所有不关心的元素,这样节省了时间和内存。另外,当解析完成后,解析树中只有锚,所以无须在迭代前调用findAll()方法。

第29~42行

在 htmlparser()中,使用标准库中的 HTMLParser.HTMLParser 类进行解析。这里就可以看出为什么BeautifulSoup解析器更加流行,因为其代码更少,比使用HTMLParser更精简。使用HTMLParser在效率上还较低,这是因为后者需要手动构建列表,即创建一个空列表,并重复调用列表的append()方法。

HTMLParser比BeautifulSoup更加底层。需要子类化HTMLParser且必须创建一个名为handle_starttag()的方法,在文件流中每次遇到新标签时就会调用这个方法(第 31~39 行)。这里跳过了所有非锚标签(第33~34行),并将所有锚链接添加到self.data中(第37~39行),在需要的时候初始化self.data(第35~36行)。

为了使用新的解析器,在第40~41行实例化并提供参数。解析器的处理结果放在parser.data中,创建完整的URL并显示出来(第42行),就如同前面的BeautifulSoup例子一样。

第44~49行

最后一个例子使用 html5lib,这是一个遵循 HTML5 标准的 HTML 文档解析器。使用html5lib最简单的方法是对处理内容调用parser()函数(第47行)。其构建并输出一棵自定义simpletree格式的简单树。

还可以选择其他流行格式的树,如minidom、ElementTree、lxml或BeautifulSoup。为了选择其他格式的树,需要将格式的名称作为treebuilder参数传递给parse()。

import html5lib

f = open("pycon.html")

tree = html5lib.parse(f, treebuilder="lxml")

f.close()

除非真的需要一棵特定格式的树,否则 simpletree 通常就足够了。如果尝试运行并解析一个普通文档,需要用下面的形式查看输出结果。

>>> import html5lib

>>> f = open("pycon.html")

>>> tree = html5lib.parse(f)

>>> f.close()

>>> for x in data:

…print x, type(x)

<html> <class 'html5lib.treebuilders.simpletree.DocumentType'>

<html> <class 'html5lib.treebuilders.simpletree.Element'>

<head> <class 'html5lib.treebuilders.simpletree.Element'>

<None> <class 'html5lib.treebuilders.simpletree.TextNode'>

<meta> <class 'html5lib.treebuilders.simpletree.Element'>

<None> <class 'html5lib.treebuilders.simpletree.TextNode'>

<title> <class 'html5lib.treebuilders.simpletree.Element'>

<None> <class 'html5lib.treebuilders.simpletree.TextNode'>

<None> <class 'html5lib.treebuilders.simpletree.CommentNode'>

<img> <class 'html5lib.treebuilders.simpletree.Element'>

<None> <class 'html5lib.treebuilders.simpletree.TextNode'>

<h1> <class 'html5lib.treebuilders.simpletree.Element'>

<a> <class 'html5lib.treebuilders.simpletree.Element'>

<None> <class 'html5lib.treebuilders.simpletree.TextNode'>

<h2> <class 'html5lib.treebuilders.simpletree.Element'>

<None> <class 'html5lib.treebuilders.simpletree.TextNode'>

遍历的大多数项是Element或TextNode对象。在这个例子中不关心TextNode对象,只关心 Element 这种特定的对象是否为锚。为了将 TextNode 过滤掉,在生成器表达式中的 if子句中进行了两次检查,即在第47~49行只检查是否为Element和锚。对于符合要求的标签,提取出对应的“href”属性,合并到完整URL中,并像之前那样输出(第46行)。

第51~72行

这个应用的驱动程序是main()函数,用于处理在第11~14行发现的所有链接。它首先生成一个调用来下载页面,然后立即将数据存入一个StringIO对象中(第65~68行),这样就可以通过procee()迭代这个对象来使用每个解析器(第69行)。

process()函数(第51~62行)将目标URL和StringIO对象作为输入,接着在每个解析器上执行调用,输出结果。对于每两次连续的解析(除了第一次之外),process()还必须为下一个解析器重置StringIO对象(第54、57、60行)。

一旦正确编写完这些代码,就可以运行并查看每个解析器是如何处理 Web 页面的URL,输出其中锚标签中的所有链接(按字母表顺序排序)。BeautifulSoup和html5lib都支持Python 3。

9.3.3 可编程的Web浏览

这是 Web 客户端的最后一小节,本节将介绍一个稍微不同的例子。这个例子使用了Mechanize(基于一个为Perl编写的类似名称的工具),这个工具用来模拟浏览器,并且还有Ruby版本。

在前面的例子(parse_links.py)中,BeautifulSoup是众多用来解析Web页面内容的解析器中的一种。这里继续使用这个解析器。

如果读者希望自己运行示例,需要在自己的系统上同时安装Mechanize和BeautifulSoup。当然,可以自行下载安装,也可以使用easy_install或pip这样的工具安装。

示例9-5显示了mech.py这个脚本,它是一款批处理类型的脚本。其中没有类或函数,只有一个分成七部分的main()函数。每一部分浏览特定Web站点的一个页面,这个特定站点是2011年PyCon会议的Web站点。选择这个站点是因为这个页面不会更改(若想支持新的会议站点,需要修改代码)。

如果对示例代码进行修改,则可以用本例处理许多Web站点,例如,登录基于 Web的电子邮件服务,以订阅经常访问的技术新闻或博客站点。通过阅读mech.py代码,可以了解其工作原理并很容易修改示例代码,让其在其他地方也可以工作。

示例9-5 可以编程的Web浏览方式(mech.py)

在这个类似批处理的脚本中,使用Mechanize这个第三方工具来浏览PyCon 2011 Web站点,用另一个非标准库的工具BeautifulSoup进行解析。

第2部分 Web开发 - 图16

第2部分 Web开发 - 图17

逐行解释

第1~6行

这个脚本非常简单。实际上,没有使用任何标准库中的包或模块,所以这里仅仅导入了Mechanize.Browser和BeautifulSoup.BeautifulSoup类。

第8~14行

首先访问的是PyCon 2011站点的主页面。将URL显示给用户,用于确认(第10行)。注意,这是访问的最终URL,因为原来的链接可能会将用户重定向到其他地方。这一节的最后一部分(第12~14行)通过查看是否含有“Log in”链接来判断用户是否已经登录。

第16~23行

一旦确认了位于登录页面(这个页面至少有一个表单),选择第一个(也是唯一一个)表单,填写验证匿名字段(但除非登录名和密码都是'xxx'),并提交。

第25~36行

如果在登录页面遇到登录错误(第28~32行),需要用正确的凭证信息(提交正确的用户名和密码)来重新提交。

第38~44行

一旦成功进行了验证,就会返回主页面。这是通过检查是否有“Logout”链接完成的(第41~43行,如果没有这个链接,就没有成功登录)。接着单击Account链接。

第46~54行

必须使用电子邮件地址进行注册。可以使用多个邮件地址,但只能有一个主地址。邮件地址是第一个标签,当访问这个页面的Account信息时,使用BeautifulSoup来解析并显示电子邮件地址表格,选取第一行的第一个单元格(第52~53行)。下一步是单击“返回”按钮来返回主页面。

第56~60行

这是所有部分中最短的,这里仅仅是确认已经返回了主页面(第59行),并继续跟踪“Log out”链接。

第62~68行

最后一部分确认位于注销页面且没有登录。这是通过检查页面上是否有“Log in”链接来完成的(第66~67行)。

这个应用简洁明了地演示了Mechanize.Brower的使用。只须将用户在浏览器上的操作映射到正确的方法调用即可。最终要考虑的是页面的开发者是否会修改这个示例所使用的Web页面,页面的变动会导致这里的代码失效。注意,在编写本书时,Mechanize还没有引入Python 3。

总结

这里总结了多种类型的Web客户端。现在可以将注意力转到Web服务器上了。

9.4 Web(HTTP)服务器

到现在,本章已经讨论了如何使用Python建立Web客户端并执行一些任务帮助Web服务器处理一些请求。从本章前面了解到了Python可以用来建立简单和复杂的Web客户端。

但还没有介绍建立Web服务器,这是本节的重点。如果说Google Chrome、Mozilla Firefox、Microsoft IE和Opera浏览器是最流行的一些Web客户端,那么哪些是最常用的Web服务器呢?这些包括Apache、ligHTTPD、Microsoft IIS、LiteSpeed Technologies LiteSpeed和ACME Laboratories thttpd。因为这些服务器都远远超过了应用程序的需求,所以这里仅仅使用Python建立简单但有用的Web服务器

注意,尽管这些服务器很简单且不是用于生产环境的,但可以用于为用户提供开发服务器。Django和Google App Engine开发服务器都基于下一节介绍的BaseHTTPServer模块。

9.4.1 用Python编写简单的Web服务器

要用到的所有基础代码都在 Python 标准库中。读者只须进行基本的定制。要建立一个Web服务器,必须建立一个基本的服务器和一个“处理程序”。

基础的Web服务器是一个模板。其角色是在客户端和服务器端完成必要的HTTP交互。在BaseHTTPServer模块中可以找到一个名叫HTTPServer的服务器基本类。

处理程序是一些处理主要“Web服务”的简单软件。它用于处理客户端的请求,并返回适当的文件,包括静态文件或动态文件。处理程序的复杂性决定了Web服务器的复杂程度。Python标准库提供了3种不同的处理程序。

最基本、最普通的是名为 BaseHTTPResquestHandler 的处理程序,它可以在BaseHTTPServer模块中找到,其中含有一个的基本Web服务器。除了获得客户端的请求外,没有实现其他处理工作,因此必须自己完成其他处理任务,这样就导致了myhttpd.py服务器的出现。

SimpleHTTPServer模块中的SimpleHTTPRequestHandler,建立在BaseHTTPResquestHandler的基础上,以非常直接的形式实现了标准的GET和HEAD请求。这虽然还不算完美,但它已经可以完成一些简单的功能。

最后,查看CGIHTTPServer模块中的CGIHTTPRequestHandler处理程序,这个处理程序可以获取 SimpleHTTPRequestHandler,并添加了对 POST 请求的支持。其可以调用CGI 脚本完成请求处理过程,也可以将生成的 HTML 脚本返回给客户端。本章只会介绍CGI处理服务器,下一章将介绍为什么不应继续在 Web 中使用 CGI,不过依然需要了解这个概念。

为了简化用户体验、提高一致性和降低代码维护开销,这些模块(实际上是其中的类)组合到单个名为server.py的模块中,作为Python 3中http包中的一部分。(类似地,Python 2的httplib(HTTP客户端)模块在Python 3中重命名为http.client。)表9-6总结了这3个模块及其对应的子类,以及Python 3中http.server包下的内容。

表9-6 Web服务器模块和类 第2部分 Web开发 - 图18 ① Python 3.0中移除。

② Python 3.0中新增。

实现一个简单的基础Web服务器

为了理解在SimpleHTTPServer和CGIHTTPServer模块中的其他高级处理程序是如何工作的,这里将对BaseHTTPRequestHandler实现简单的GET处理功能。示例9-6展示了一个可以工作的Web服务器的代码——myhttpd.py。

这个服务器派生自BaseHTTPRequestHandler,只包含一个do_GET()方法(第6~7行),在基础服务器接收到GET请求时调用该方法。在第9行尝试打开客户端传来的路径(移除前导“/”),如果一切正常,将会返回“OK”状态(200),并通过wfile管道将用于下载的Web页面传给用户(第13行),否则将会返回404状态(第15~17行)。

示例9-6 简单的Web服务器(myhttpd.py)

这个简单的 Web 服务器可以读取 GET 请求,获取 Web 页面(.html 文件),并将其返回给调用客户。使用BaseHTTPServer中的BaseHTTPRequestHandler,并实现了do_GET()方法来启用对GET请求的处理。

第2部分 Web开发 - 图19

main()函数只是简单地将Web服务器类实例化,然后启动并进入永不停息的服务器循环,如果通过按Ctrl+C或者类似的键中断则会关闭服务器。如果可以访问并运行这个服务器,就会发现服务器会显示出一些类似这样的登录输出。

myhttpd.py

Welcome to the machine…Press ^C once or twice to quit

localhost - - [26/Aug/2000 03:01:35] "GET /index.html HTTP/1.0" 200 -

localhost - - [26/Aug/2000 03:01:29] code 404, message File Not Found:

x.html

localhost - - [26/Aug/2000 03:01:29] "GET /dummy.html HTTP/1.0" 404 -

localhost - - [26/Aug/2000 03:02:03] "GET /hotlist.htm HTTP/1.0" 200 -

当然,这个小Web服务器太简单了,甚至不能处理普通的文本文件。这些功能留给读者(参见本章末尾的练习9-10)。

功能更强大,代码更少:一个简单的CGI Web服务器

前面那个例子太脆弱了,BaseHTTPServer 非常基础,它不能处理 CGI 请求。在更高层次上,使用SimpleHTTPServer,其提供了do_HEAD()和do_GET()方法,所以无须自行创建这两个方法,就像在BaseHTTPServer中做的那样。

标准库中提供的最高层次(姑且相信是最高层次)服务器是 CGIHTTPServer。除了do_HEAD()和do_GET()方法之外,它还定义了do_POST()方法,可以用于处理表单数据。通过这些便利的工具,可以使用两行代码创建一个支持CGI的开发服务器(这段代码太短了,甚至苦恼如何将其作为一个代码示例添加到本章中,因为读者可以直接在自己的计算机上打出这些代码)。

!/usr/bin/env python

import CGIHTTPServer

CGIHTTPServer.test()

注意,这里不检测是否应该退出服务器,而是让用户在CGIHTTPServer.test()函数输出的结果太多时,使用Ctrl+C快捷键或其他方式退出。只须在Shell中调用这个脚本就会启动服务器。下面就是在Windows上运行这段代码的示例,其与在POSIX机器上的方式类似。

C:\py>python cgihttpd.py

Serving HTTP on 0.0.0.0 port 8000 …

这条命令在默认的8000端口启用服务器(可以在运行时通过命令行提供其他端口号来改变默认端口)。

C:\py\>python cgihttpd.py 8080

Serving HTTP on 0.0.0.0 port 8080 …

为了进行测试,只须查看在源码文件相同目录下是否存在cgi-bin文件夹(以及一些CGI Python脚本)。在测试这个简单的脚本时,无须设置Apache、CGI处理程序前缀,以及其他额外的事情。第10章将介绍如何编写CGI脚本,同时也会介绍为什么不应该使用CGI。

正如你所看到的一样,建立一个Web服务器并在纯Python脚本中运行并不会花太多时间。一般来说,现在是在创建一个运行在Web服务器上的 Web应用。这些服务器模块在开发时只用于创建服务器,与使用什么Web框架或应用无关。

单从效率考虑,真正的服务应该使用更有效率的服务器,如 Apache、ligHTTPD,或本节起始处列出的其他服务器。但这里是想说明通过Python可以简化复杂的事情。

9.5 相关模块

表9-7列出了本章介绍的对Web开发有用的模块,这些模块对Web应用都是有用的。

表9-7 Web编程相关模块 第2部分 Web开发 - 图20

(续表) 第2部分 Web开发 - 图21 ① Python 1.6中新增。

② Python 2.0中新增。

③ Python 2.2中新增。

④ Python 2.3中新增。

⑤ Python 2.4中新增。

⑥ Python 2.5中新增。

⑦ Python 3.5中新增。

9.6 练习

9-1 urllib模块。编写一个程序,接受一个用户输入的URL(或者是一个Web页面或者是一个FTP文件,例如,http://python.org或ftp://ftp.python.org/pub/python/README),然后将其下载到本地,以相同的文件名命名(如果你的系统不支持,也可以把它改成和原文件相似的名字)。Web页面(HTTP)应另存为.htm或.html文件,而FTP文件应保持其扩展名。

9-2 urllib模块。重写示例11-4的grabWeb.py脚本,这个脚本会下载一个Web页面,并显示生成的HTML文件的第一个和最后一个非空白行的文本,应使用urlopen()来代替urlretrieve()直接处理数据(这样就不必先下载所有文件再处理它了)。

9-3 URL和正则表达式。你的浏览器也许会保存你最喜欢的Web站点的URL,以“书签”式的HTML文件(Mozilla发布的浏览器就是如此)或者以“收藏夹”里一组.url文件(IE 即是如此)的形式保存。查看你的浏览器记录“热门链接”的办法,并定位其位置和存储方式。不更改任何文件,剔除对应Web站点(如果给定)的URL和名字,生成一个以名字和链接作为输出的双列列表,并把这些数据保存到硬盘文件中。截取站点名和URL,确保每一行的输出不超过80个字符。

9-4 URL、urllib 模块、异常和正则表达式。作为对上一个问题的延伸,为脚本增加代码来测试收藏的链接。记录下无效链接(及其名字),包括无效的Web站点和已经删除的Web页面。只输出并在磁盘中保存那些依然有效的链接。

练习9-5~练习9-8适用于Web服务器访问日志文件和正则表达式。Web服务器(及其管理员)一般必须维护一个访问日志文件(在 Web 的主服务器目录中通常是logs/access_log文件),用于跟踪请求。在一段时间内,这些文件会变得很大,需要存储起来或截断。思考一下为什么不只保存相关的信息,而删除文件本身以节省磁盘空间呢?下面的这些练习通过正则表达式来归档和分析Web服务器数据。

9-5 计算日志文件中有多少种请求(GET与POS)。

9-6 统计成功下载的页面/数据:显示所有返回值为 200(OK,即没有错误发生)的链接,以及每个链接被访问的次数。

9-7 统计错误:显示所有产生错误的链接(返回值为 400 或 500),以及每个链接被访问的次数。

9-8 跟踪IP地址:对于每个IP地址,输出每个页面/数据下载情况的列表,以及这些链接被访问的次数。

9-9 Web浏览器Cookie和Web站点注册。Core Python Programming或者Core Python LanguageFundamentals的第7、9、13章都涉及了用户登录注册数据库,这几章中创建了基于纯文本、菜单驱动的脚本。将其移植到Web中,可以使用用户名-密码信息来注册Web站点。

选做题:想办法让自己熟悉Web浏览器cookie,并在登录成功后将会话保持4个小时。

9-10 创建Web服务端。示例 9-6中的myhttpd.py代码只能读取HTML文件并将其返回主调客户端。请添加对以“.txt”结尾的纯文本文件的支持。确保返回正确的MIME类型的“text/plain”。选做题:添加对以“.jpg”及“.jpeg”结束的JPEG文件的支持,并返回MIME类型的“image/jpeg”。

练习9-11~练习9-14需要更新示例9-3中的crawl.py这个Web爬虫。

9-11 Web客户端。移植crawler.py,使它使用HTMLParser、BeautifulSoup、html5lib或lxml解析系统。

9-12 Web客户端。作为crawl.py的输入的URL必须以“http://”协议指示符开头,顶级 URL 必须包含一个反斜线,例如:http://www.prenhallprofessional.com/。加强crawl.py的功能,允许用户只输入主机名(没有协议部分,假设它是HTTP),而反斜线是可选的。例如:www.prenhallprofessional.com应该是可接受的输入形式。

9-13 Web客户端。更新crawl.py脚本,使其可以使用“ftp:”形式的链接下载。crawl.py会忽略所有“mailto:”形式的链接。添加功能,使其忽略“telnet:”、“news:”、“gopher:”以及“about:”链接。

9-14 Web客户端。crawl.py脚本仅从相同站点内的Web页面中找到链接,下载.html文件,不会处理/保存图片这类对页面同样有意义的“文件”。对于那些允许 URL缺少末端斜线(/)的服务器,这个脚本也不能处理。给 crawl.py 增添两个类来解决这些问题。

第一个是My404UrlOpener类,这是urllib.FancyURLOpener的子类,仅包含一个方法,http_error_404(),用该方法来判断收到的404错误中是不是包含缺少末端斜线的URL。如果缺少,就添加斜线并重新请求(仅重新请求一次)。如果仍然失败,才返回一个真正的404错误。必须用该类的一个实例来设置urllib._urlopener,这样urllib才能使用它。

另一个类 LinkImageParser 派生自 htmllib.HTMLParser。这个类应有一个用来调用基类构造函数的构造函数,并且初始化一个列表用来保存从Web 页面中解析出的图片文件。应重写handle_image()方法,把图片文件名添加到图片列表中(这样就不会像现在的基类方法那样丢弃它们了)。

最后一组练习针对parse_link.py文件,该文件见本章前面的示例9-4。

9-15 命令行参数。添加命令行参数,让用户可以选择显示一个或多个解析器的输出结果(而不是显示所有结果,可以将默认情况设定为显示所有结果)。

9-16 lxml解析器。下载并安装lxm,向parse_links.py添加对lxml的支持。

9-17 Markup解析器。将爬网程序中的htmllib.HTMLParser替换成Markup解析器。

a)HTMLParser.HTMLParser

b)html5lib

c)BeaufifulSoup

d)lxml

9-18 重构。改变output()函数,让其支持其他形式的输出。

a)写入文件。

b)发送至另一个进程(即写入套接字)。

9-19 Python风格编程。在parse_links.py的逐行解释中,将simpleBS()从较为难理解的单行版本转成了多行版本。对fasterBS()和html5libparser()做相同的事情。

9-20 性能与分析。前面描述了fasterBS()为什么比simpleBS()运行得更好。用timeit工具证明其运行速度更快,并找到一款Python内存工具,实时发现其更节省内存。

描述哪一款内存分析工具可以做到这一点,以及在哪里发现了这款工具。三种标准库中的分析工具(profile、hotshot、cProfile)是否可以显示内存使用信息?

9-21 更好的练习。在 htmlparser()中,假设不想创建一个空列表并重复调用 append()方法来构建列表,而是想通过下面的单行代码使用列表推导式替换第 35~39 行的内容。

self.data = [v for k, v in attrs if k == 'href']

这种替换是否正确?换句话说,替换了后是否能正确执行?为什么?

9-22 数据操作。在parse_links.py中,按字母顺序对URL排序(实际上是词典顺序)。但这样并不是正确组织链接的方式:

http://python.org/psf/

http://python.org/search

http://roundup.sourceforge.net/

http://sourceforge.net/projects/mysql-python

http://twistedmatrix.com/trac/

http://wiki.python.org/moin/

http://wiki.python.org/moin/CgiScripts

http://www.python.org/

相反,根据域名进行排序可能更加合理。

http://python.org/psf/

http://python.org/search

http://wiki.python.org/moin/

http://wiki.python.org/moin/CgiScripts

http://www.python.org/

http://roundup.sourceforge.net/

http://sourceforge.net/projects/mysql-python

http://twistedmatrix.com/trac/

修改代码,让其可以在按字母顺序排序后再按域名排序。