12. Python 中的 Web 服务
Web 服务器可以执行 Python 脚本。服务器负责监听客户端的请求。从客户端的角度来看,调用 Web 服务相当于请求该服务的 URL。客户端可以使用任何语言编写,包括 Python。我们需要了解如何与 Web 服务“通信”,即理解 Web 服务器与其客户端之间用于通信的 HTTP 协议。这就是以下程序的目的。
Web 服务脚本将由 WampServer 中的 Apache Web 服务器执行。这些脚本必须放置在特定目录中: <WampServer>\bin\apache\apachex.y.z\cgi-bin,其中 <WampServer> 是 WampServer 的安装文件夹,x.y.z 是 Apache Web 服务器的版本号。
仅将 Python 脚本放置在 <cgi-bin> 文件夹中是不够的。脚本必须在第一行指定要使用的 Python 解释器的路径。该路径以注释形式包含在内:
#!D:\Programs\ActivePython\Python2.7.2\python.exe
读者应根据自身环境调整此路径。
12.1. 客户端/服务器日期和时间应用程序
我们的第一个 Web 服务将是一个日期和时间服务:客户端接收当前的日期和时间。
12.1.1. 服务器
程序(web_02)
| # !D:\Programs\ActivePython\Python2.7.2\python.exe
import time
# headers
print "Content-Type: text/plain\n"
# dispatch time to customer
# localtime: number of milliseconds since 01/01/1970
# "date-time display format
# d: 2-digit day
# m: 2-digit month
# y: 2-digit year
# H: hour 0.23
# M: minutes
# S: seconds
print time.strftime('%d/%m/%y %H:%M:%S',time.localtime())
|
注:
- 第 6 行:脚本必须自行生成部分用于响应客户端的 HTTP 头。这些头将添加到 Apache 服务器自身生成的 HTTP 头中。 第 6 行的 HTTP 头字段告知客户端,将以 text/plain 格式(即未格式化的文本)发送资源。请注意头字段末尾的 "\n",它会在头字段之后生成一个空行。这是必需的:正是这一空行向 HTTP 客户端标记了响应中 HTTP 头字段的结束。接下来是客户端请求的资源,在本例中是未格式化的文本;
- 第 18 行:发送给客户端的资源是显示当前日期和时间的文本。
12.1.2. 两个测试
如前所述,上述脚本可直接在命令窗口中通过 Python 解释器运行。这有助于排除任何语法或运行时错误。运行结果如下:
| cmd>%python% web_02.py
Content-Type: text/plain
24/06/11 11:16:55
|
以这种方式测试完脚本后,即可将其放置在 Apache 服务器的 <cgi-bin> 目录中(参见第 12 段)。现在启动 WampServer 应用程序。这将同时启动 Apache Web 服务器和 MySQL 数据库服务器。目前,我们仅使用 Web 服务器。然后,使用浏览器输入以下 URL:http://localhost/cgi-bin/web_02.py:
- [1]:请求的 URL;
- [2] 处:浏览器显示的响应;
- 在 [3] 中:网页浏览器接收到的源代码。这确实是 Python 脚本发送的代码。
借助某些工具(此处为Firebug,一款Firefox浏览器插件),您可以查看与服务器交换的HTTP头部。在上文中,网页浏览器接收到了以下HTTP头部:
| HTTP/1.1 200 OK
Date: Fri, 24 Jun 2011 09:35:02 GMT
Server: Apache/2.2.6 (Win32) PHP/5.2.5
Keep-Alive: timeout=5, max=100
Connection: Keep-Alive
Transfer-Encoding: chunked
Content-Type: text/plain
|
第7–8行可识别为Python脚本发送的HTTP头。前面的几行是由Apache Web服务器生成的。
12.1.3. 一个编程实现的客户端
该程序(client_web_02)
现在我们将编写一个脚本,使其作为前文所述 Web 服务的客户端。我们将利用 httplib 模块的功能,该模块能简化 HTTP 客户端的编写。
| # -*- coding=utf-8 -*-
import httplib,re
# constant
HOST="localhost"
URL="/cgi-bin/web_02.py"
# connection
connexion=httplib.HTTPConnection(HOST)
# follow-up
connexion.set_debuglevel(1)
# send request
connexion.request("GET", URL)
# response processing
reponse=connexion.getresponse()
# content
contenu=reponse.read()
# locking connection
connexion.close()
print "------\n",contenu,"-----\n"
# recovery of time elements
elements=re.match(r"^(\d\d)/(\d\d)/(\d\d) (\d\d):(\d\d):(\d\d)\s*$",contenu).groups()
print "Jour=%s,Mois=%s,An=%s,Heures=%s,Minutes=%s,Secondes=%s" % (elements[0],elements[1],elements[2],elements[3],elements[4],elements[5])
|
注:
- 第 3 行:需要 re 模块来处理正则表达式,以及 httplib 模块来实现 HTTP 客户端功能;
- 第 9 行:使用第 6 行定义的 HOST 上的 80 端口建立 HTTP 连接;
- 第 11 行:log 模块允许您查看客户端请求和服务器响应的 HTTP 头部;
- 第 13 行:请求 Web 服务 URL。请求方式有两种:使用 HTTP GET 或 POST 命令。两者的区别将在后文说明。此处将使用 HTTP GET 命令进行请求;
- 第 15 行:读取服务器的响应。此处获取完整的响应内容:包括 HTTP 头部以及客户端请求的资源。在响应中,服务器可能已指示客户端进行重定向。在这种情况下,httplib 客户端会自动执行重定向。因此,最终获取的响应即为重定向后的结果;
- 第 17 行:响应包含 HTTP 头部和客户端请求的文档。若要仅获取 HTTP 头部,请使用 [response].getHeaders();若要读取文档,请使用 [response].read();
- 第 19 行:一旦从 Web 服务器获取到响应,便会关闭与该服务器的连接;
- 我们知道服务器发送的文档是一行文本,格式为 15/06/11 14:56:36。第 22–26 行:我们使用正则表达式来提取该行中的各个元素。
12.1.4. 结果
| send: 'GET /cgi-bin/web_02.py HTTP/1.1\r\nHost: localhost\r\nAccept-Encoding: identity\r\n\r\n'
reply: 'HTTP/1.1 200 OK\r\n'
header: Date: Tue, 14 Feb 2012 14:51:07 GMT
header: Server: Apache/2.2.17 (Win32) PHP/5.3.5
header: Transfer-Encoding: chunked
header: Content-Type: text/plain
------
14/02/12 15:51:07
-----
Jour=14,Mois=02,An=12,Heures=15,Minutes=51,Secondes=07
|
注:
- 第1行:客户端发送给Web服务器的HTTP头部;
- 第2–6行:Web服务器响应中的HTTP头部;
- 第 8 行:服务器发送的文档;
- 第 11 行:其处理结果;
12.2. 服务器检索客户端发送的参数
在 HTTP 协议中,客户端有两种方法向 Web 服务器传递参数:
- 它以表单形式请求服务 URL
GET url?param1=val1¶m2=val2¶m3=val3… HTTP/1.0
其中有效值必须先进行编码,以便将某些保留字符替换为相应的十六进制值。
- 它以以下形式请求服务 URL
随后,在发送给服务器的 HTTP 头部中,包含以下头部:
客户端发送的其余标头以空行结尾。随后,它可以以以下形式发送其数据:
val1¶m2=val2¶m3=val3…
,其中这些值必须像 GET 方法一样预先进行编码。发送给服务器的字符数必须为 N,其中 N 是标头中声明的值:
12.2.1. Web 服务
以下 Web 服务从客户端接收三个参数:last_name、first_name 和 age。它从 cgi 模块提供的名为 cgi.FieldStorage 的字典式结构中检索这些参数。参数 parami 的值 vali 是通过 vali = cgi.FieldStorage().getlist("parami") 获取的。如果客户端请求中不存在参数 parami,则返回一个包含
- 0 个元素,如果参数 parami 不在客户端的请求中;
- 若参数 parami 在客户端请求中出现一次,则返回 1 个元素;
- 若参数 parami 在客户端请求中出现 n 次,则返回包含 n 个元素的数组。
获取参数后,脚本会将它们发回给客户端。
该程序(web_03)
| # !D:\Programs\ActivePython\Python2.7.2\python.exe
import cgi
# headers
print "Content-Type: text/plain\n"
# server retrieves information sent by the client
# here firstname=P&lastname=N&age=A
formulaire=cgi.FieldStorage()
# we send them back to the customer
print "informations recues du service web [prenom=%s,nom=%s,age=%s]" % (formulaire.getlist("prenom"),formulaire.getlist("nom"),formulaire.getlist("age"))
|
可通过网页浏览器进行测试:
在 [1] 中,是 Web 服务的 URL。请注意其中包含三个参数:last_name、first_name 和 age。在 [2] 中,是 Web 服务的响应。
12.2.2. GET客户端
该程序(client_web_03_GET)
| # -*- coding=utf-8 -*-
import httplib,urllib
# constant
HOST="localhost"
URL="/cgi-bin/web_03.py"
PRENOM="Jean-Paul"
NOM="de la Huche"
AGE=42
# parameters must be encoded before being sent to the server
params = urllib.urlencode({'nom': NOM, 'prenom': PRENOM, 'age': AGE})
# parameters are set at the end of URL
URL+="?"+params
# connection
connexion=httplib.HTTPConnection(HOST)
# follow-up
connexion.set_debuglevel(1)
# send request
connexion.request("GET",URL)
# response processing
reponse=connexion.getresponse()
# content
contenu=reponse.read()
print contenu,"\n"
# closing the connection
connexion.close()
|
注:
- 第 8–10 行:发送给 Web 服务的 3 个参数的值;
- 第 13 行:这些参数必须进行编码。此操作使用 urllib 模块中的 urlencode 方法实现。该模块在第 3 行被导入。该方法接受一个字典 {param1:val1, param2:val2, ...} 作为参数;
- 第 15 行:在 GET 请求(第 21 行)中,客户端必须将编码后的参数放置在 Web 服务 URL 的末尾;
- 后续几行已在前文中讲解过。
12.2.3. 结果
| dos>%python% client_03_GET.py
send: 'GET /cgi-bin/web_03.py?nom=de+la+Huche&age=42&prenom=Jean-Paul HTTP/1.1\r\nHost: localhost\r\nAccept-Encoding: identity\r\n\r\n'
reply: 'HTTP/1.1 200 OK\r\n'
header: Date: Wed, 15 Jun 2011 13:22:15 GMT
header: Server: Apache/2.2.6 (Win32) PHP/5.2.5
header: Transfer-Encoding: chunked
header: Content-Type: text/plain
informations recues du client [['Jean-Paul'],['de la Huche'],['42']]
|
注:
- 第 2 行:注意参数(last_name、first_name、age)的编码;
- 第 8 行:Web 服务的响应。
12.2.4. POST 客户端
POST 客户端与 GET 客户端类似,不同之处在于编码后的参数不再是目标 URL 的一部分。它们作为 POST 请求的第三个参数传递(第 19 行)。
程序(client_web_03_POST)
| # -*- coding=utf-8 -*-
import httplib,urllib
# constant
HOST="localhost"
URL="/cgi-bin/web_03.py"
PRENOM="Jean-Paul"
NOM="de la Huche"
AGE=42
# parameters must be encoded before being sent to the server
params = urllib.urlencode({'nom': NOM, 'prenom': PRENOM, 'age': AGE})
# connection
connexion=httplib.HTTPConnection(HOST)
# follow-up
connexion.set_debuglevel(1)
# send request
connexion.request("POST",URL,params)
# response processing
reponse=connexion.getresponse()
# content
contenu=reponse.read()
print contenu,"\n"
# closing the connection
connexion.close()
|
12.2.5. 结果
| dos>%python% client_03_POST.py
send: 'POST /cgi-bin/web_03.py HTTP/1.1\r\nHost: localhost\r\nAccept-Encoding:identity\r\nContent-Length: 39\r\n\r\nnom=de+la+Huche&age=42&prenom=Jean-Paul'
reply: 'HTTP/1.1 200 OK\r\n'
header: Date: Fri, 24 Jun 2011 12:03:31 GMT
header: Server: Apache/2.2.6 (Win32) PHP/5.2.5
header: Transfer-Encoding: chunked
header: Content-Type: text/plain
informations recues du service web [prenom=['Jean-Paul'],nom=['de la Huche'],age=['42']]
|
注:
- 注意第 2 行:客户端使用 POST 方法发送编码后的参数:
- HTTP Content-Length 头部指示将发送给 Web 服务的字符数;
- 该 HTTP 头部之后紧跟一个空行,表示 HTTP 头部的结束;
- 随后发送 39 个字符的编码参数。
- 第 8 行:Web 服务的响应。
12.3. 从 Web 服务中检索环境变量
12.3.1. Web 服务
Python CGI 脚本运行在一个具有属性的系统环境中。其属性及其值可在 os.environ 字典中获取。
程序(web_04)
| # !D:\Programs\ActivePython\Python2.7.2\python.exe
import os
# headers
print "Content-Type: text/plain\n"
# environmental news
for (cle,valeur) in os.environ.items():
print "%s : %s" % (cle,valeur)
|
注:
- 第 3 行:必须导入 os 模块才能访问“system”变量。
如果你直接运行上面的脚本(即作为控制台脚本而非 CGI 脚本),将在控制台中看到以下结果:
| Content-Type: text/plain
TMP : C:\Users\SERGET~1\AppData\Local\Temp
COMPUTERNAME : GPORTPERS3
USERDOMAIN : Gportpers3
VS100COMNTOOLS : D:\Programs\dotnet\Visual Studio 10\Common7\Tools\
VISUALSTUDIODIR : D:\Documents\Visual Studio 2010
PSMODULEPATH : C:\Windows\system32\WindowsPowerShell\v1.0\Modules\
COMMONPROGRAMFILES : C:\Program Files (x86)\Common Files
PROCESSOR_IDENTIFIER : Intel64 Family 6 Model 42 Stepping 7, GenuineIntel
PROGRAMFILES : C:\Program Files (x86)
PROCESSOR_REVISION : 2a07
SYSTEMROOT : C:\Windows
PATH : D:\Programs\ActivePython\Python2.7.2\;D:\Programs\ActivePython\Python2.7.2\Scripts;C:\Program Files\Common Files\Microsoft Shared\Windows Live;...
PROGRAMFILES(X86) : C:\Program Files (x86)
WINDOWS_TRACING_FLAGS : 3
TEMP : C:\Users\SERGET~1\AppData\Local\Temp
COMMONPROGRAMFILES(X86) : C:\Program Files (x86)\Common Files
PROCESSOR_ARCHITECTURE : x86
ALLUSERSPROFILE : C:\ProgramData
LOCALAPPDATA : C:\Users\Serge TahÚ\AppData\Local
HOMEPATH : \Users\Serge TahÚ
PROGRAMW6432 : C:\Program Files
USERNAME : Serge TahÚ
LOGONSERVER : \\GPORTPERS3
PROMPT : $P$G
SESSIONNAME : Console
PROGRAMDATA : C:\ProgramData
PATHEXT : .COM;.EXE;.BAT;.CMD;.VBS;.VBE;.JS;.JSE;.WSF;.WSH;.MSC;.py;.pyw
FP_NO_HOST_CHECK : NO
WINDIR : C:\Windows
PYTHON : D:\Programs\python\python2.7.2\python
WINDOWS_TRACING_LOGFILE : C:\BVTBin\Tests\installpackage\csilogfile.log
HOMEDRIVE : C:
SYSTEMDRIVE : C:
COMSPEC : C:\Windows\system32\cmd.exe
NUMBER_OF_PROCESSORS : 8
VBOX_INSTALL_PATH : D:\Programs\systeme\Oracle\VirtualBox\
APPDATA : C:\Users\Serge TahÚ\AppData\Roaming
PROCESSOR_LEVEL : 6
PROCESSOR_ARCHITEW6432 : AMD64
COMMONPROGRAMW6432 : C:\Program Files\Common Files
OS : Windows_NT
PUBLIC : C:\Users\Public
USERPROFILE : C:\Users\Serge TahÚ
|
在网页浏览器中(即执行 CGI 脚本的地方),会得到以下结果:
请注意,根据执行上下文的不同,获取的环境也会有所不同。
12.3.2. 编程客户端
该程序(client_web_04)
| # -*- coding=utf-8 -*-
import httplib
# constant
HOST="localhost"
URL="/cgi-bin/web_04.py"
# connection
connexion=httplib.HTTPConnection(HOST)
# send request
connexion.request("GET", URL)
# response processing
reponse=connexion.getresponse()
# content
print reponse.read()
|
12.3.3. 结果
| SERVER_SOFTWARE : Apache/2.2.17 (Win32) PHP/5.3.5
SCRIPT_NAME : /cgi-bin/web_04.py
SERVER_SIGNATURE :
REQUEST_METHOD : GET
SERVER_PROTOCOL : HTTP/1.1
QUERY_STRING :
SYSTEMROOT : C:\Windows
SERVER_NAME : localhost
REMOTE_ADDR : 127.0.0.1
SERVER_PORT : 80
SERVER_ADDR : 127.0.0.1
DOCUMENT_ROOT : D:/Programs/sgbd/wamp/www/
COMSPEC : C:\Windows\system32\cmd.exe
SCRIPT_FILENAME : D:/Programs/sgbd/wamp/bin/apache/Apache2.2.17/cgi-bin/web_04.py
SERVER_ADMIN : admin@localhost
PATH : D:\Programs\ActivePython\Python2.7.2\;D:\Programs\ActivePython\Python2.7.
2\Scripts;C:\Program Files\Common Files\Microsoft Shared\Windows Live;...
HTTP_HOST : localhost
PATHEXT : .COM;.EXE;.BAT;.CMD;.VBS;.VBE;.JS;.JSE;.WSF;.WSH;.MSC;.py;.pyw
REQUEST_URI : /cgi-bin/web_04.py
WINDIR : C:\Windows
GATEWAY_INTERFACE : CGI/1.1
REMOTE_PORT : 58468
HTTP_ACCEPT_ENCODING : identity
|
请注意,编程客户端收到的响应与网页浏览器并不完全相同。这是因为浏览器向 Web 服务器发送了信息,而服务器正是利用这些信息来生成响应的。在此,编程客户端并未发送任何关于自身的信息。