Python CGI 编程
CGI,全名 Common Gateway Interface,中文通用网关接口,是描述了客户端和服务器程序之间传输数据的一种标准,可以让一个客户端向一个提供网络服务的程序请求数据
CGI 是语言无关的,CGI 程序可以用任何脚本语言或者是完全独立编程语言实现,只要这个语言可以在这个系统上运行
Unix shell script, Python, Ruby, PHP, perl, Tcl, C/C++, 和 Visual Basic 都可以用来编写 CGI 程序
CGI 工作原理
为了更好的了解 CGI 是如何工作的,我们需要了解在网页上点击一个链接或 URL 的简单流程
- 单击一个连接或者在地址栏里输入一个网站按回车键
- 浏览器访问 URL 并连接到 HTTP Web 服务器
- Web 服务器接收到请求信息后会解析 URL,并查找访问的文件在服务器上是否存在,如果存在返回文件的内容,否则返回错误信息
- 览器从服务器上接收信息,并显示接收的文件或者错误信息
CGI 程序可以是 Python 脚本,PERL 脚本,SHELL 脚本,C 或者 C++ 程序等
CGI 架构图
Web 服务器支持及配置
在我们继续进行 CGI 编程前,需要配置我们的 Web 服务器支持 CGI 和能够运行 CGI 的处理程序
配置 Apache 支持 CGI
设置 CGI 目录
ScriptAlias /cgi-bin/ /var/www/cgi-bin/
所有的 CGI 程序都应该保存在一个预先配置的目录,这个目录被称为 CGI 目录,按照惯例,它被命名为 /var/www/cgi-bin 目录
CGI 文件的扩展名为 .cgi,Python CGI 程序也可以使用 .py
为扩展名
一般情况下, Apache Web 服务器设置的运行 cgi-bin 的目录中为 /var/www
你也可以指定其它运行 CGI 脚本的目录,只要修改 httpd.conf
配置文件
<Directory "/var/www/cgi-bin"> AllowOverride None Options +ExecCGI Order allow,deny Allow from all </Directory>
最后使用 AddHandler 指令将 .cgi
、.pl
、 .py
添加到 CGI 处理器
AddHandler cgi-script .cgi .pl .py
该命令是告诉 Apache Web 服务器,对于这些扩展名结尾的文件调用 CGI 处理程序
第一个 CGI 程序
配置好了程序我们就来写一个小小的 Hello World CGI 脚本练练手吧
在你的 /var/www/cgi-bin 目录下新建一个 hello.cgi
的文件,内容如下
cgi-bin/hello.cgi
#!/usr/bin/python # -*- coding: UTF-8 -*- print "Content-type:text/html;charset=utf-8" print # 空行,告诉服务器结束头部,千万不能删 ### 下面是网页的正文 print ''' <!DOCTYPE html> <meta charset="utf-8"> <title>我的第一个 CGI 程序 - 简单教程(twle.cn)</title> <h2>Hello Word! 我是来自简单编程的第一个 CGI 程序</h2> '''
文件保存后需要修改权限,一般修改文件权限为 0755
chmod 755 hello.cgi
打开浏览器访问 http://localhost:8000/cgi-bin/hello.cgi
如果不出任何问题,那么你会看到类似下图的结果
对了,我的 Web 服务器是运行在 8000 端口的,这个需要改成你自己的
这个 hello.cgi
只是一个简单的 Python 脚本
脚本第一行的输出内容 "Content-type:text/html;charset=utf-8" 发送到浏览器并告知浏览器显示的内容类型为 "text/html",编码格式为 "utf-8"
用 print 输出一个空行用于告诉服务器结束头部信息
对了,如果你遇到类似下面的信息,就说明没有执行权限,需要设置为 0755
HTTP 头部字段
在我们第一个 CGI 程序中已经使用 print 语句输出了
Content-type:text/html;charset=utf-8
对的,这就是一个 HTTP 的头部字段,它会发送给浏览器告诉浏览器文件的内容类型和编码格式
HTTP 头部格式一般如下
HTTP 字段名: 字段内容
例如
Content-type: text/html;charset=utf-8
下表列出了 CGI 程序中经常使用的 HTTP 头部字段
字段 | 描述 |
---|---|
Content-type: | 请求的与实体对应的MIME信息 例如: Content-type:text/html |
Expires: Date | 响应过期的日期和时间 |
Location: URL | 用来重定向接收方到非请求URL的位置来完成请求或标识新的资源 |
Last-modified: Date | 请求资源的最后修改时间 |
Content-length: N | 请求的内容长度 |
Set-Cookie: String | 设置 HTTP Cookie |
CGI 环境变量
CGI 程序中,我们还可以获取一次网页请求的请求头信息
CGI 处理器把所有的请求信息和环境信息保存在 CGI 环境变量中
下表列出了常用的 CGI 环境变量
变量名 | 描述 |
---|---|
CONTENT_TYPE | 这个环境变量的值指示所传递来的信息的MIME类型。目前,环境变量 CONTENT_TYPE 一般都是:application/x-www-form-urlencoded 表示数据来自于 HTML 表单 |
CONTENT_LENGTH | 如果服务器与CGI程序信息的传递方式是POST,这个环境变量即使从标准输入STDIN中可以读到的有效数据的字节数。这个环境变量在读取所输入的数据时必须使用。 |
HTTP_COOKIE | 客户机内的 COOKIE 内容。 |
HTTP_USER_AGENT | 提供包含了版本数或其他专有数据的客户浏览器信息。 |
PATH_INFO | 这个环境变量的值表示紧接在CGI程序名之后的其他路径信息。它常常作为CGI程序的参数出现。 |
QUERY_STRING | 如果服务器与CGI程序信息的传递方式是GET,这个环境变量的值即使所传递的信息。这个信息经跟在CGI程序名的后面,两者中间用一个问号'?'分隔。 |
REMOTE_ADDR | 这个环境变量的值是发送请求的客户机的IP地址,例如上面的192.168.1.67。这个值总是存在的。而且它是Web客户机需要提供给Web服务器的唯一标识,可以在CGI程序中用它来区分不同的Web客户机。 |
REMOTE_HOST | 这个环境变量的值包含发送CGI请求的客户机的主机名。如果不支持你想查询,则无需定义此环境变量。 |
REQUEST_METHOD | 提供脚本被调用的方法。对于使用 HTTP/1.0 协议的脚本,仅 GET 和 POST 有意义。 |
SCRIPT_FILENAME | CGI脚本的完整路径 |
SCRIPT_NAME | CGI脚本的的名称 |
SERVER_NAME | 这是你的 WEB 服务器的主机名、别名或IP地址。 |
SERVER_SOFTWARE | 这个环境变量的值包含了调用CGI程序的HTTP服务器的名称和版本号。例如,上面的值为Apache/2.2.14(Unix) |
我们写一个简单的 CGI 脚本来输出 CGI 的环境变量
cgi-bin/env.cgi
#!/usr/bin/python # -*- coding: UTF-8 -*- # filename:test.py import os print "Content-type: text/html;charset=utf-8" print ''' <!DOCTYPE html> <meta charset="utf-8"> <title>CGI 环境变量 - 简单教程(twle.cn)</title> <h1>环境变量</h1> <ul>''' for key in os.environ.keys(): print "<li><span style='color:green'>%30s </span> : %s </li>" % (key,os.environ[key]) print "</ul>"
打开浏览器访问 http://localhost:8000/cgi-bin/env.cgi,数据非常长,我只是截图了其中一小部分
如果不出任何问题,那么你会看到类似下图的结果
GET 和 POST 方法
如果你了解过 HTML ,那么显然知道, HTML 表单可以使用两种方法向服务器提交数据 : GET 方法和 POST 方法
其实我们点击一个 URL 也是使用 GET 方法的
GET 方法传输数据
GET 方法会将要发送的数据编码后,放到请求网页的 URL 上,以 "?" 号分割
https://twle.cn/cgi-bin/form_get.cgi?key1=value1&key2=value2
GET 方法有一些特性
- GET 请求可被缓存
- GET 请求保留在浏览器历史记录中
- GET 请求可被收藏为书签
- GET 请求不应在处理敏感数据时使用
- GET 请求有长度限制
- GET 请求只应当用于取回数据
下面我们就来创建一个 form_get.cgi 来接收 GET 方法传输的数据 name 和 url
/cgi-bin/form_get.cgi?name=简单编程&url=https://www.twle.cn
下面是 form_get.cgi 文件的内容
cgi-bin/form_get.cgi
#!/usr/bin/python # -*- coding: UTF-8 -*- # CGI处理模块 import cgi, cgitb # 创建 FieldStorage 的实例化 form = cgi.FieldStorage() # 获取数据 site_name = form.getvalue('name') site_url = form.getvalue('url') print "Content-type:text/html;charset=utf-8" print print ''' <!DOCTYPE html> <meta charset="utf-8"> <title>GET 请求-简单教程(twle.cn)</title> <p>%s官网:%s</p>''' % (site_name, site_url)
打开浏览器访问 http://localhost:8000/cgi-bin/form_get.cgi?name=简单编程&url=https://www.twle.cn,输出结果如下
HTML GET 表单
我们还可以制作一个表单使用 GET 方法提交数据
因为我们的 form_get.cgi 只接收 name 和 url 参数,那么我们就创建这个表单
get.html
<!DOCTYPE html> <meta charset="utf-8"> <title>GET 表单 - 简单教程 ( twle.cn )</title> <form action="/cgi-bin/form_get.cgi"> 站点名称: <input type="text" name="name" value="简单教程"><br/> 站点 URL: <input type="text" name="url" value="https://www.twle.cn"/><br/> <input type="submit" value="提交" /> </form>
默认情况下 cgi-bin 目录只能存放脚本文件,我们将 get.html 存储在跟目录下
打开浏览器访问 http://localhost:8000/get.html,演示如下
POST 方法传递数据
POST 方法向服务器传递数据是更安全可靠的,像一些敏感信息如用户密码等需要使用 POST 传输数据
因为不会暴露在浏览器地址栏里,不会被背后偷偷走过的同时看到
在服务器端 CGI 脚本方面, GET 和 POST 方法没什么大区别,我们可以直接使用 form_get.cgi
但是为了区分,我们就将 form_get.cgi 复制一份命名为 form_post.cgi
cgi-bin/form_post.cgi
#!/usr/bin/python # -*- coding: UTF-8 -*- # CGI处理模块 import cgi, cgitb # 创建 FieldStorage 的实例化 form = cgi.FieldStorage() # 获取数据 site_name = form.getvalue('name') site_url = form.getvalue('url') print "Content-type:text/html;charset=utf-8" print print ''' <!DOCTYPE html> <meta charset="utf-8"> <title>POST 请求-简单教程(twle.cn)</title> <p>%s官网:%s</p>''' % (site_name, site_url)
然后我们再把 get.html 文件复制一份 post.html 将其中的 method="get" 改成 method="post"
post.html
<!DOCTYPE html> <meta charset="utf-8"> <title>POST 表单 - 简单教程 ( twle.cn )</title> <form action="/cgi-bin/form_post.cgi" method="post"> 站点名称: <input type="text" name="name" value="简单教程"><br/> 站点 URL: <input type="text" name="url" value="https://www.twle.cn"/><br/> <input type="submit" value="提交" /> </form>
打开浏览器访问 http://localhost:8000/post.html,演示如下
注意看地址栏,GET 方法的数据都放在地址栏,POST 方法的没有
通过 CGI 程序传递 checkbox 表单数据
<input type="checkbox" /> 用于提交一个或者多个选项数据
checkbox.html
<!DOCTYPE html> <meta charset="utf-8"> <title>checkbox 表单 - 简单教程(twle.cn)</title> <form action="/cgi-bin/checkbox.cgi" method="POST"> <input type="checkbox" checked name="简单教程" value="on" /> 简单编程 <input type="checkbox" checked name="google" value="on" /> Google <input type="submit" value="选择站点" /> </form>
然后我们写一个 CGI 程序来接收 checkbox 表单数据
cgi-bin/checkbox.cgi
#!/usr/bin/python # -*- coding: UTF-8 -*- # 引入 CGI 处理模块 import cgi, cgitb # 创建 FieldStorage的实例 form = cgi.FieldStorage() # 接收字段数据 if form.getvalue('google'): google_flag = "是" else: google_flag = "否" if form.getvalue('twle'): twle_flag = "是" else: twle_flag = "否" print "Content-type:text/html" print print ''' <!DOCTYPE html> <meta charset="utf-8" > <title>checkbox 表单 - 简单教程(twle.cn)</title> <p> 简单编程是否选择了 : %s</p> <p> Google 是否选择了 : %s</p> ''' % (twle_flag, google_flag)
打开浏览器访问 http://localhost:8000/checkbox.html,演示如下
通过 CGI 程序传递 Radio 数据
<input type="radio" /> 只向服务器传递一个数据
radio.html
<!DOCTYPE html> <meta charset="utf-8"> <title>radio 表单 - 简单编程( twle.cn )</title> <form action="/cgi-bin/radio.cgi" method="post"> <input type="radio" name="site" value="简单教程" /> 简单编程 <input type="radio" name="site" value="google" /> Google <input type="submit" value="提交" /> </form>
然后我们写一个 CGI 脚本 radio.cgi 来接收表单提交的数据
#!/usr/bin/python # -*- coding: UTF-8 -*- # 引入 CGI 处理模块 import cgi, cgitb # 创建 FieldStorage的实例 form = cgi.FieldStorage() # 接收字段数据 if form.getvalue('site'): site = form.getvalue('site') else: site = "提交数据为空" print "Content-type:text/html" print print ''' <!DOCTYPE html> <meta charset="utf-8" > <title>radio 表单 - 简单教程(twle.cn)</title> <p> 选中的网站是: %s</p>''' % site
打开浏览器访问 http://localhost:8000/radio.html,演示如下
通过 CGI 程序传递 <textarea> 文本框多行数据
先创建一个多行文本框表单
textarea.html
<!DOCTYPE html> <meta charset="utf-8"> <title>多行文本框表单 - 简单教程(twle.cn)</title> <form action="/cgi-bin/textarea.cgi" method="post"> <textarea name="textcontent" cols="40" rows="4"> 简单教程 简单编程 </textarea> <br/> <input type="submit" value="提交" /> </form>
然后创建服务器端接收多行文本框的 CGI 脚本
cgi-bin/textarea.cgi
#!/usr/bin/python # -*- coding: UTF-8 -*- # 引入 CGI 处理模块 import cgi, cgitb # 创建 FieldStorage的实例 form = cgi.FieldStorage() # 接收字段数据 if form.getvalue('textcontent'): text_content = form.getvalue('textcontent') else: text_content = "没有内容" print "Content-type:text/html" print print """ <!DOCTYPE html> <meta charset="utf-8"> <title>多行文本框表单- 简单教程(twle.cn)</title> <h2> 输入的内容是:</h2> <pre>%s</pre>""" % text_content
打开浏览器访问 http://localhost:8000/textarea.html 演示如下
通过 CGI 程序传递下拉数据 ( <select> )
先制作一个下拉表单
dropdown.html
<!DOCTYPE html> <meta charset="utf-8"> <title>下拉表单-简单教程(twle.cn)</title> <form action="/cgi-bin/dropdown.cgi"> <select name="dropdown"> <option value="简单教程" selected>简单编程</option> <option value="google">Google</option> </select> <input type="submit" value="提交"/> </form>
然后创建服务器端接收下拉表单数据的 CGI 脚本
cgi-bin/dropdown.cgi
#!/usr/bin/python # -*- coding: UTF-8 -*- # 引入 CGI 处理模块 import cgi, cgitb # 创建 FieldStorage的实例 form = cgi.FieldStorage() # 接收字段数据 if form.getvalue('dropdown'): dropdown_value = form.getvalue('dropdown') else: dropdown_value = "没有内容" print "Content-type:text/html;charset" print print """ <!DOCTYPE html> <meta charset="utf-8"> <title>下拉表单 CGI - 简单教程(www.twle.cn)</title>" <h2> 选中的选项是:%s</h2> """ % dropdown_value
打开浏览器访问 http://localhost:8000/dropdown.html 演示如下
CGI 中使用 Cookie
HTTP 协议是一种无状态协议,两次请求之间没有任何关系,所以 HTTP 本身是不能对用户身份的进行识别
但 Cookie 却可以弥补这个缺陷
Cookie 是用户访问脚本的同时,通过用户的浏览器,在用户硬盘上写入纪录数据 ,当下次用户访问脚本时取回数据信息,从而达到身份判别的功能
Cookie 语法
HTTP Cookie 的发送是通过 HTTP 响应头部 set-cookie 来实现的,它早于内容的发送
语法格式如下
Set-cookie:name=name;expires=date;path=path;domain=domain;secure
各个属性说明
属性 | 说明 |
---|---|
name=name | 需要设置cookie的值 ( name 不能使用 ";" 和 ", "号 ) 多个 name 值时用 " ; " 分隔 例如: name1=name1;name2=name2 |
expires=date | cookie 的有效期限 格式:expires="Wdy,DD-Mon-YYYY HH:MM:SS" |
path=path | 设置 cookie 支持的路径 如果 path 是一个路径,则 cookie 对这个目录下的所有文件及子目录生效,例如: path="/cgi-bin/" 如果 path 是一个文件,则 cookie 指对这个文件生效,例如:path="/cgi-bin/cookie.cgi" |
domain=domain | 对 cookie 生效的域名,例如:domain="www.twle.cn" |
secure | 如果给出此标志,表示 cookie 只能通过 SSL 协议的 https 服务器来传递 |
Cookie 的接收是通过 CGI 环境变量 HTTP_COOKIE 来实现的,CGI 程序可以通过检索该变量获取 Cookie 信息
Cookie 设置
Cookie 的设置非常简单,Cookie 会在 HTTP 响应头部单独发送
下面的代码设置了一个 name="www.twle.cn" 的 Cookie
cgi/cookie_set.cgi
#!/usr/bin/python # -*- coding: UTF-8 -*- # print 'Content-Type: text/html;charset' print 'Set-Cookie: name="www.twle.cn";expires=Wed, 28 Aug 2028 18:30:00 GMT' print print """ <!DOCTYPE html> <meta charset="utf-8"> <title>设置 Cookie - 简单教程 ( twle.cn ) </title> <h1>Cookie set OK!</h1> """
Set-Cookie 可以重复多次来实现设置多个不同属性的 Cookie
打开浏览器访问 http://localhost:8000/cgi-bin/cookie_set.cgi 输出结果如下
读取 Cookie 信息
在 Python CGI 里读取 Cookie 信息非常简单,因为 Cookie 信息都存储在 CGI 的环境变量 HTTP_COOKIE 中
存储格式如下
key1=value1;key2=value2;key3=value3....
我们可以写一个简单的 cgi 脚本演示以下
cgi-bin/cookie_get.cgi
#!/usr/bin/python # -*- coding: UTF-8 -*- # 导入模块 import os import Cookie print "Content-type: text/html;charset=utf-8" print print """ <!DOCTYPE html> <meta charset="utf-8"> <title>读取 Cookie 信息 - 简单教程(twle.cn)</title> <h1>读取 Cookie 信息</h1> """ if 'HTTP_COOKIE' in os.environ: cookie_string=os.environ.get('HTTP_COOKIE') c = Cookie.SimpleCookie() c.load(cookie_string) try: data=c['name'].value print "cookie data: "+data except KeyError: print "cookie 没有设置或者已过期失效"
打开浏览器访问 http://localhost:8000/cgi-bin/cookie_get.cgi 输出结果如下
文件上传
制作一个文件上传表单,需要将 <form> 标签的 enctype 属性设置为 multipart/form-data 和将 method 属性设置为 "POST"
然后放置一个 <input type="file" name="filename" />
标签就可以了
form_upload.html
<!DOCTYPE html> <meta charset="utf-8"> <title>文件上传</title> <form enctype="multipart/form-data" action="/cgi-bin/upload.cgi" method="POST"> <p>选中文件: <input type="file" name="filename" /></p> <p><input type="submit" value="上传" /></p> </form>
然后再编写服务器端接收脚本
cgi-bin/upload.cgi
#!/usr/bin/python # -*- coding: UTF-8 -*- import cgi, os import cgitb; cgitb.enable() form = cgi.FieldStorage() # 获取文件名 fileitem = form['filename'] # 检测文件是否上传 if fileitem.filename: # 设置文件路径 fn = os.path.basename(fileitem.filename) open('/tmp/' + fn, 'wb').write(fileitem.file.read()) message = '文件 "' + fn + '" 上传成功' else: message = '文件没有上传' print "Content-Type: text/html;charset=utf-8" print print """ \n <!DOCTYPE html> <meta charset="utf-8"> <title>文件上传-简单教程</title> <p>%s</p> """ % (message,)
如果你的 Window 系统,那么要使用下面的语句替换文件分隔符
fn = os.path.basename(fileitem.filename.replace("\\", "/" ))
运行以上范例,演示如下
文件下载对话框
下载文件需要设置 Content-Disposition
HTTP 响应头
Content-Disposition: attachment; filename="文件名"
我们先在当前目录下创建一个文件 demo.txt
内容如下
简单教程,简单编程
然后就可以用下面的代码实现下载 demo.txt
文件
cgi-bin/download.cgi
#!/usr/bin/python # -*- coding: UTF-8 -*- # 设置 HTTP 头部 print 'Content-Disposition: attachment; filename="demo.txt"' print print open("demo.txt", "rb").read()
打开浏览器访问 http://localhost:8000/cgi-bin/download.cgi 就能下载 demo.txt 文件了