cgi --- 通用網(wǎng)關(guān)接口支持?

源代碼: Lib/cgi.py

Deprecated since version 3.11, will be removed in version 3.13: The cgi module is deprecated (see PEP 594 for details and alternatives).


通用網(wǎng)關(guān)接口 (CGI) 腳本的支持模塊

本模塊定義了一些工具供以 Python 編寫的 CGI 腳本使用。

The global variable maxlen can be set to an integer indicating the maximum size of a POST request. POST requests larger than this size will result in a ValueError being raised during parsing. The default value of this variable is 0, meaning the request size is unlimited.

概述?

CGI 腳本是由 HTTP 服務(wù)器發(fā)起調(diào)用,通常用來處理通過 HTML <FORM><ISINDEX> 元素提交的用戶輸入。

在大多數(shù)情況下,CGI 腳本存放在服務(wù)器的 cgi-bin 特殊目錄下。 HTTP 服務(wù)器將有關(guān)請求的各種信息(例如客戶端的主機(jī)名、所請求的 URL、查詢字符串以及許多其他內(nèi)容)放在腳本的 shell 環(huán)境中,然后執(zhí)行腳本,并將腳本的輸出發(fā)回到客戶端。

腳本的輸入也會被連接到客戶端,并且有時表單數(shù)據(jù)也會以此方式來讀?。辉谄渌麜r候表單數(shù)據(jù)會通過 URL 的“查詢字符串”部分來傳遞。 本模塊的目標(biāo)是處理不同的應(yīng)用場景并向 Python 腳本提供一個更為簡單的接口。 它還提供了一些工具為腳本調(diào)試提供幫助,而最近增加的還有對通過表單上傳文件的支持(如果你的瀏覽器支持該功能的話)。

CGI 腳本的輸出應(yīng)當(dāng)由兩部分組成,并由一個空行分隔。 前一部分包含一些標(biāo)頭,它們告訴客戶端后面會提供何種數(shù)據(jù)。 生成一個最小化標(biāo)頭部分的 Python 代碼如下所示:

print("Content-Type: text/html")    # HTML is following
print()                             # blank line, end of headers

后一部分通常為 HTML,提供給客戶端軟件來顯示格式良好包含標(biāo)題的文本、內(nèi)聯(lián)圖片等內(nèi)容。 下面是打印一段簡單 HTML 的 Python 代碼:

print("<TITLE>CGI script output</TITLE>")
print("<H1>This is my first CGI script</H1>")
print("Hello, world!")

使用 cgi 模塊?

先在開頭添加 import cgi。

當(dāng)你在編寫一個新腳本時,請考慮加上這些語句:

import cgitb
cgitb.enable()

這會激活一個特殊的異常處理句柄,它將在發(fā)生任何錯誤時將詳細(xì)錯誤報告顯示到 web 瀏覽器中。 如果你不希望向你的腳本的用戶顯示你的程序的內(nèi)部細(xì)節(jié),你可以改為將報告保存到文件中,使用這樣的代碼即可:

import cgitb
cgitb.enable(display=0, logdir="/path/to/logdir")

在腳本開發(fā)期間使用此特性會很有幫助。 cgitb 所產(chǎn)生的報告提供了在追蹤程序問題時能為你節(jié)省大量時間的信息。 你可以在完成測試你的腳本并確信它能正確工作之后再移除 cgitb 行。

To get at submitted form data, use the FieldStorage class. If the form contains non-ASCII characters, use the encoding keyword parameter set to the value of the encoding defined for the document. It is usually contained in the META tag in the HEAD section of the HTML document or by the Content-Type header. This reads the form contents from the standard input or the environment (depending on the value of various environment variables set according to the CGI standard). Since it may consume standard input, it should be instantiated only once.

FieldStorage 實(shí)例可以像 Python 字典一樣來檢索。 它允許通過 in 運(yùn)算符進(jìn)行成員檢測,也支持標(biāo)準(zhǔn)字典方法 keys() 和內(nèi)置函數(shù) len()。 包含空字符串的表單字段會被忽略而不會出現(xiàn)在字典中;要保留這樣的值,請在創(chuàng)建 FieldStorage 實(shí)例時為可選的 keep_blank_values 關(guān)鍵字形參提供一個真值。

舉例來說,下面的代碼(假定 Content-Type 標(biāo)頭和空行已經(jīng)被打?。z查字段 nameaddr 是否均被設(shè)為非空字符串:

form = cgi.FieldStorage()
if "name" not in form or "addr" not in form:
    print("<H1>Error</H1>")
    print("Please fill in the name and addr fields.")
    return
print("<p>name:", form["name"].value)
print("<p>addr:", form["addr"].value)
...further form processing here...

在這里的字段通過 form[key] 來訪問,它們本身就是 FieldStorage (或 MiniFieldStorage,取決于表單的編碼格式) 的實(shí)例。 實(shí)例的 value 屬性會產(chǎn)生字段的字符串值。 getvalue() 方法直接返回這個字符串;它還接受可選的第二個參數(shù)作為當(dāng)請求的鍵不存在時要返回的默認(rèn)值。

如果提交的表單數(shù)據(jù)包含一個以上的同名字段,由 form[key] 所提取的對象將不是一個 FieldStorageMiniFieldStorage 實(shí)例而是由這種實(shí)例組成的列表。 類似地,在這種情況下,form.getvalue(key) 將會返回一個字符串列表。 如果你預(yù)計到這種可能性(當(dāng)你的 HTML 表單包含多個同名字段時),請使用 getlist() 方法,它總是返回一個值的列表(這樣你就不需要對只有單個項的情況進(jìn)行特別處理)。 例如,這段代碼拼接了任意數(shù)量的 username 字段,以逗號進(jìn)行分隔:

value = form.getlist("username")
usernames = ",".join(value)

如果一個字段是代表上傳的文件,請通過 value 屬性訪問該值或是通過 getvalue() 方法以字節(jié)形式將整個文件讀入內(nèi)存。 這可能不是你想要的結(jié)果。 你可以通過測試 filename 屬性或 file 屬性來檢測上傳的文件。 然后你可以從 file 屬性讀取數(shù)據(jù),直到它作為 FieldStorage 實(shí)例的垃圾回收的一部分被自動關(guān)閉 (read()readline() 方法將返回字節(jié)數(shù)據(jù)):

fileitem = form["userfile"]
if fileitem.file:
    # It's an uploaded file; count lines
    linecount = 0
    while True:
        line = fileitem.file.readline()
        if not line: break
        linecount = linecount + 1

FieldStorage 對象還支持在 with 語句中使用,該語句結(jié)束時將自動關(guān)閉它們。

如果在獲取上傳文件的內(nèi)容時遇到錯誤(例如,當(dāng)用戶點(diǎn)擊回退或取消按鈕中斷表單提交時)該字段中對象的 done 屬性值將被設(shè)為 -1。

文件上傳標(biāo)準(zhǔn)草案考慮到了從一個字段上傳多個文件的可能性(使用遞歸的 multipart/* 編碼格式)。 當(dāng)這種情況發(fā)生時,該條目將是一個類似字典的 FieldStorage 條目。 這可以通過檢測它的 type 屬性來確定,該屬性應(yīng)當(dāng)是 multipart/form-data (或者可能是匹配 multipart/* 的其他 MIME 類型)。 在這種情況下,它可以像最高層級的表單對象一樣被遞歸地迭代處理。

當(dāng)一個表單按“舊”格式提交時(即以查詢字符串或是單個 application/x-www-form-urlencoded 類型的數(shù)據(jù)部分的形式),這些條目實(shí)際上將是 MiniFieldStorage 類的實(shí)例。 在這種情況下,list, filefilename 屬性將總是為 None。

通過 POST 方式提交并且也帶有查詢字符串的表單將同時包含 FieldStorageMiniFieldStorage 條目。

在 3.4 版更改: file 屬性會在創(chuàng)建 FieldStorage 實(shí)例的垃圾回收操作中被自動關(guān)閉。

在 3.5 版更改: FieldStorage 類增加了上下文管理協(xié)議支持。

更高層級的接口?

前面的部分解釋了如何使用 FieldStorage 類來讀取 CGI 表單數(shù)據(jù)。 本部分則會描述一個更高層級的接口,它被添加到此類中以允許人們以更為可讀和自然的方式行事。 這個接口并不會完全取代前面的部分所描述的技巧 --- 例如它們在高效處理文件上傳時仍然很有用處。

此接口由兩個簡單的方法組成。 你可以使用這兩個方法以通用的方式處理表單數(shù)據(jù),而無需擔(dān)心在一個名稱下提交的值是只有一個還是有多個。

在前面的部分中,你已學(xué)會當(dāng)你預(yù)期用戶在一個名稱下提交超過一個值的時候編寫以下代碼:

item = form.getvalue("item")
if isinstance(item, list):
    # The user is requesting more than one item.
else:
    # The user is requesting only one item.

這種情況很常見,例如當(dāng)一個表單包含具有相同名稱的一組復(fù)選框的時候:

<input type="checkbox" name="item" value="1" />
<input type="checkbox" name="item" value="2" />

但是在多數(shù)情況下,一個表單中的一個特定名稱只對應(yīng)一個表單控件。 因此你可能會編寫包含以下代碼的腳本:

user = form.getvalue("user").upper()

這段代碼的問題在于你絕不能預(yù)期客戶端會向你的腳本提供合法的輸入。 舉例來說,如果一個好奇的用戶向查詢字符串添加了另一個 user=foo 對,則該腳本將會崩潰,因為在這種情況下 getvalue("user") 方法調(diào)用將返回一個列表而不是字符串。 在一個列表上調(diào)用 upper() 方法是不合法的(因為列表并沒有這個方法)因而會引發(fā) AttributeError 異常。

因此,讀取表單數(shù)據(jù)值的正確方式應(yīng)當(dāng)總是使用檢查所獲取的值是單一值還是值列表的代碼。 這很麻煩并且會使腳本缺乏可讀性。

一種更便捷的方式是使用這個更高層級接口所提供的 getfirst()getlist() 方法。

FieldStorage.getfirst(name, default=None)?

此方法總是只返回與表單字段 name 相關(guān)聯(lián)的單一值。 此方法在同一名稱下提交了多個值的情況下將僅返回第一個值。 請注意所接收的值順序在不同瀏覽器上可能發(fā)生變化因而是不確定的。 1 如果指定的表單字段或值不存在則此方法將返回可選形參 default 所指定的值。 如果未指定此形參則默認(rèn)值為 None

FieldStorage.getlist(name)?

此方法總是返回與表單字段 name 相關(guān)聯(lián)的值列表。 如果 name 指定的表單字段或值不存在則此方法將返回一個空列表。 如果指定的表單字段只包含一個值則它將返回只有一項的列表。

使用這兩個方法你將能寫出優(yōu)雅簡潔的代碼:

import cgi
form = cgi.FieldStorage()
user = form.getfirst("user", "").upper()    # This way it's safe.
for item in form.getlist("item"):
    do_something(item)

函數(shù)?

這些函數(shù)在你想要更多控制,或者如果你想要應(yīng)用一些此模塊中在其他場景下實(shí)現(xiàn)的算法時很有用處。

cgi.parse(fp=None, environ=os.environ, keep_blank_values=False, strict_parsing=False, separator='&')?

在環(huán)境中或從某個文件中解析一個查詢 (文件默認(rèn)為 sys.stdin)。 keep_blank_values, strict_parsingseparator 形參會被原樣傳給 urllib.parse.parse_qs()。

cgi.parse_multipart(fp, pdict, encoding='utf-8', errors='replace', separator='&')?

解析 multipart/form-data 類型(用于文件上傳)的輸入。 參數(shù)中 fp 為輸入文件,pdict 為包含 Content-Type 標(biāo)頭中的其他形參的字典,encoding 為請求的編碼格式。

urllib.parse.parse_qs() 那樣返回一個字典:其中的鍵為字段名稱,值為對應(yīng)字段的值列表。 對于非文件字段,其值均為字符串列表。

這很容易使用,但如果你預(yù)期要上傳巨量字節(jié)數(shù)據(jù)時就不太適合了 --- 在這種情況下,請改用更為靈活的 FieldStorage 類。

在 3.7 版更改: 增加了 encodingerrors 形參。 對于非文件字段,其值現(xiàn)在為字符串列表而非字節(jié)串列表。

在 3.10 版更改: 增加了 separator 形參。

cgi.parse_header(string)?

將一個 MIME 標(biāo)頭 (例如 Content-Type) 解析為一個主值和一個參數(shù)字典。

cgi.test()?

對 CGI 執(zhí)行健壯性檢測,適于作為主程序。 寫入最小化的 HTTP 標(biāo)頭并以 HTML 格式來格式化提供給腳本的所有信息。

cgi.print_environ()?

以 HTML 格式來格式化 shell 環(huán)境。

cgi.print_form(form)?

以 HTML 格式來格式化表單。

cgi.print_directory()?

以 HTML 格式來格式化當(dāng)前目錄。

cgi.print_environ_usage()?

以 HTML 格式打印有用的環(huán)境變量列表(供 CGI 使用)。

對于安全性的關(guān)注?

有一條重要的規(guī)則:如果你發(fā)起調(diào)用一個外部程序(通過 os.system(), os.popen() 或其他具有類似功能的函數(shù)),需要非常確定你不會把從客戶端接收的任意字符串直接傳給 shell。 這是一個著名的安全漏洞,網(wǎng)絡(luò)中聰明的黑客可以通過它來利用容易上當(dāng)?shù)?CGI 腳本發(fā)起調(diào)用任何 shell 命令。 即便 URL 的一部分或字段名稱也是不可信任的,因為請求并不一定是來自你的表單!

為了安全起見,如果你必須將從表單獲取的字符串傳給 shell 命令,你應(yīng)當(dāng)確保該字符串僅包含字母數(shù)字類字符、連字符、下劃線和句點(diǎn)。

在 Unix 系統(tǒng)上安裝你的 CGI 腳本?

請閱讀你的 HTTP 服務(wù)器的文檔并咨詢你所用系統(tǒng)的管理員來找到 CGI 腳本應(yīng)當(dāng)安裝到哪個目錄;通常是服務(wù)器目錄樹中的 cgi-bin 目錄。

請確保你的腳本可被“其他人”讀取和執(zhí)行;Unix 文件模式應(yīng)為八進(jìn)制數(shù) 0o755 (使用 chmod 0755 filename)。 請確保腳本的第一行包含 #! 且位置是從第 1 列開始,后面帶有 Python 解釋器的路徑名,例如:

#!/usr/local/bin/python

請確保該 Python 解釋器存在并且可被“其他人”執(zhí)行。

請確保你的腳本需要讀取或?qū)懭氲娜魏挝募挤謩e是“其他人”可讀取或可寫入的 --- 它們的模式應(yīng)為可讀取 0o644 或可寫入 0o666。 這是因為出于安全理由,HTTP 服務(wù)器是作為沒有任何特殊權(quán)限的 "nobody" 用戶來運(yùn)行腳本的。 它只能讀取(寫入、執(zhí)行)任何人都能讀?。▽懭?、執(zhí)行)的文件。 執(zhí)行時的當(dāng)前目錄(通常為服務(wù)器的 cgi-bin 目錄)和環(huán)境變量集合也與你在登錄時所得到的不同。 特別地,不可依賴于 shell 的可執(zhí)行文件搜索路徑 (PATH) 或 Python 模塊搜索路徑 (PYTHONPATH) 的任何相關(guān)設(shè)置。

如果你需要從 Python 的默認(rèn)模塊搜索路徑之外的目錄載入模塊,你可以在導(dǎo)入其他模塊之前在你的腳本中改變路徑。 例如:

import sys
sys.path.insert(0, "/usr/home/joe/lib/python")
sys.path.insert(0, "/usr/local/lib/python")

(在此方式下,最后插入的目錄將最先被搜索?。?/p>

針對非 Unix 系統(tǒng)的指導(dǎo)會有所變化;請查看你的 HTTP 服務(wù)器的文檔(通常會有關(guān)于 CGI 腳本的部分)。

測試你的 CGI 腳本?

很不幸,當(dāng)你在命令行中嘗試 CGI 腳本時它通常會無法運(yùn)行,而能在命令行中完美運(yùn)行的腳本則可能會在運(yùn)行于服務(wù)器時神秘地失敗。 但有一個理由使你仍然應(yīng)當(dāng)在命令行中測試你的腳本:如果它包含語法錯誤,Python 解釋器將根本不會執(zhí)行它,而 HTTP 服務(wù)器將很可能向客戶端發(fā)送令人費(fèi)解的錯誤信息。

假定你的腳本沒有語法錯誤,但它仍然無法起作用,你將別無選擇,只能繼續(xù)閱讀下一節(jié)。

調(diào)試 CGI 腳本?

首先,請檢查是否有安裝上的小錯誤 --- 仔細(xì)閱讀上面關(guān)于安裝 CGI 腳本的部分可以使你節(jié)省大量時間。 如果你不確定你是否正確理解了安裝過程,請嘗試將此模塊 (cgi.py) 的副本作為 CGI 腳本安裝。 當(dāng)作為腳本被發(fā)起調(diào)用時,該文件將以 HTML 格式轉(zhuǎn)儲其環(huán)境和表單內(nèi)容。 請給它賦予正確的模式等,并向它發(fā)送一個請求。 如果它是安裝在標(biāo)準(zhǔn)的 cgi-bin 目錄下,應(yīng)該可以通過在你的瀏覽器中輸入表單的 URL 來向它發(fā)送請求。

http://yourhostname/cgi-bin/cgi.py?name=Joe+Blow&addr=At+Home

如果此操作給出類型為 404 的錯誤,說明服務(wù)器找不到此腳本 -- 也許你需要將它安裝到不同的目錄。 如果它給出另一種錯誤,說明存在安裝問題,你應(yīng)當(dāng)解決此問題才能繼續(xù)操作。 如果你得到一個格式良好的環(huán)境和表單內(nèi)容清單(在這個例子中,應(yīng)當(dāng)會列出的有字段 "addr" 值為 "At Home" 以及 "name" 值為 "Joe Blow"),則說明 cgi.py 腳本已正確安裝。 如果你為自己的腳本執(zhí)行了同樣的過程,現(xiàn)在你應(yīng)該能夠調(diào)試它了。

下一步驟可以是在你的腳本中調(diào)用 cgi 模塊的 test() 函數(shù):用這一條語句替換它的主代碼

cgi.test()

這將產(chǎn)生從安裝 cgi.py 文件本身所得到的相同結(jié)果。

當(dāng)某個常規(guī) Python 腳本觸發(fā)了未處理的異常,(無論出于什么原因:模塊名稱出錯、文件無法打開等),Python 解釋器就會打印出一條完整的跟蹤信息并退出。在 CGI 腳本觸發(fā)異常時,Python 解釋器依然會如此,但最有可能的是,跟蹤信息只會停留在某個 HTTP 服務(wù)日志文件中,或者被完全丟棄。

幸運(yùn)的是,只要執(zhí)行 某些 代碼,就可以利用 cgitb 模塊將跟蹤信息發(fā)送給瀏覽器。將以下幾行代碼加到代碼頂部:

import cgitb
cgitb.enable()

然后再運(yùn)行一下看;發(fā)生問題時應(yīng)能看到詳細(xì)的報告,或許能讓崩潰的原因更清晰一些。

如果懷疑是 cgitb 模塊導(dǎo)入的問題,可以采用一個功能更強(qiáng)的方法(只用到內(nèi)置模塊):

import sys
sys.stderr = sys.stdout
print("Content-Type: text/plain")
print()
...your code here...

這得靠 Python 解釋器來打印跟蹤信息。輸出的類型為純文本,不經(jīng)過任何 HTML 處理。如果代碼正常,則客戶端會顯示原有的 HTML。如果觸發(fā)了異常,很可能在輸出前兩行后會顯示一條跟蹤信息。因為不會繼續(xù)進(jìn)行 HTML 解析,所以跟蹤信息肯定能被讀到。

常見問題和解決方案?

  • 大部分 HTTP 服務(wù)器會對 CGI 腳本的輸出進(jìn)行緩存,等腳本執(zhí)行完畢再行輸出。這意味著在腳本運(yùn)行時,不可能在客戶端屏幕上顯示出進(jìn)度情況。

  • 請查看上述安裝說明。

  • 請查看 HTTP 服務(wù)器的日志文件。(在另一個單獨(dú)窗口中執(zhí)行 tail -f logfile 可能會很有用!)

  • 一定要先檢查腳本是否有語法錯誤,做法類似:python script.py

  • 如果腳本沒有語法錯誤,試著在腳本的頂部添加 import cgitb; cgitb.enable()。

  • 當(dāng)調(diào)用外部程序時,要確保其可被讀取。通常這意味著采用絕對路徑名------ 在 CGI 腳本中, PATH 的值通常沒什么用。

  • 在讀寫外部文件時,要確保其能被 CGI 腳本歸屬的用戶讀寫:通常是運(yùn)行網(wǎng)絡(luò)服務(wù)的用戶,或由網(wǎng)絡(luò)服務(wù)的 suexec 功能明確指定的一些用戶。

  • 不要試圖給 CGI 腳本賦予 set-uid 模式。這在大多數(shù)系統(tǒng)上都行不通,出于安全考慮也不應(yīng)如此。

附注

1

請注意,新版的 HTML 規(guī)范確實(shí)注明了請求字段的順序,但判斷請求是否合法非常繁瑣和容易出錯,可能來自不符合要求的瀏覽器,甚至不是來自瀏覽器。