Skip to content

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 解释器运行。这有助于排除任何语法或运行时错误。运行结果如下:

1
2
3
4
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头部:

1
2
3
4
5
6
7
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 服务器传递参数:

  1. 它以表单形式请求服务 URL

GET url?param1=val1&param2=val2&param3=val3… HTTP/1.0

其中有效值必须先进行编码,以便将某些保留字符替换为相应的十六进制值。

  1. 它以以下形式请求服务 URL
POST url HTTP/1.0

随后,在发送给服务器的 HTTP 头部中,包含以下头部:

Content-length: N

客户端发送的其余标头以空行结尾。随后,它可以以以下形式发送其数据:

val1&param2=val2&param3=val3…

,其中这些必须像 GET 方法一样预先进行编码。发送给服务器的字符数必须为 N,其中 N 是标头中声明的值:

Content-length: N

12.2.1. Web 服务

以下 Web 服务从客户端接收三个参数:last_namefirst_nameage。它从 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_namefirst_nameage。在 [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. 结果

1
2
3
4
5
6
7
8
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. 结果

1
2
3
4
5
6
7
8
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)

1
2
3
4
5
6
7
8
9
#   !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 服务器发送了信息,而服务器正是利用这些信息来生成响应的。在此,编程客户端并未发送任何关于自身的信息。