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 編寫(xiě)的 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ā)起調用,通常用來(lái)處理通過(guò) HTML <FORM>
或 <ISINDEX>
元素提交的用戶(hù)輸入。
在大多數情況下,CGI 腳本存放在服務(wù)器的 cgi-bin
特殊目錄下。 HTTP 服務(wù)器將有關(guān)請求的各種信息(例如客戶(hù)端的主機名、所請求的 URL、查詢(xún)字符串以及許多其他內容)放在腳本的 shell 環(huán)境中,然后執行腳本,并將腳本的輸出發(fā)回到客戶(hù)端。
腳本的輸入也會(huì )被連接到客戶(hù)端,并且有時(shí)表單數據也會(huì )以此方式來(lái)讀??;在其他時(shí)候表單數據會(huì )通過(guò) URL 的“查詢(xún)字符串”部分來(lái)傳遞。 本模塊的目標是處理不同的應用場(chǎng)景并向 Python 腳本提供一個(gè)更為簡(jiǎn)單的接口。 它還提供了一些工具為腳本調試提供幫助,而最近增加的還有對通過(guò)表單上傳文件的支持(如果你的瀏覽器支持該功能的話(huà))。
CGI 腳本的輸出應當由兩部分組成,并由一個(gè)空行分隔。 前一部分包含一些標頭,它們告訴客戶(hù)端后面會(huì )提供何種數據。 生成一個(gè)最小化標頭部分的 Python 代碼如下所示:
print("Content-Type: text/html") # HTML is following
print() # blank line, end of headers
后一部分通常為 HTML,提供給客戶(hù)端軟件來(lái)顯示格式良好包含標題的文本、內聯(lián)圖片等內容。 下面是打印一段簡(jiǎn)單 HTML 的 Python 代碼:
print("<TITLE>CGI script output</TITLE>")
print("<H1>This is my first CGI script</H1>")
print("Hello, world!")
使用 cgi 模塊?
先在開(kāi)頭添加 import cgi
。
當你在編寫(xiě)一個(gè)新腳本時(shí),請考慮加上這些語(yǔ)句:
import cgitb
cgitb.enable()
這會(huì )激活一個(gè)特殊的異常處理句柄,它將在發(fā)生任何錯誤時(shí)將詳細錯誤報告顯示到 web 瀏覽器中。 如果你不希望向你的腳本的用戶(hù)顯示你的程序的內部細節,你可以改為將報告保存到文件中,使用這樣的代碼即可:
import cgitb
cgitb.enable(display=0, logdir="/path/to/logdir")
在腳本開(kāi)發(fā)期間使用此特性會(huì )很有幫助。 cgitb
所產(chǎn)生的報告提供了在追蹤程序問(wèn)題時(shí)能為你節省大量時(shí)間的信息。 你可以在完成測試你的腳本并確信它能正確工作之后再移除 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 字典一樣來(lái)檢索。 它允許通過(guò) in
運算符進(jìn)行成員檢測,也支持標準字典方法 keys()
和內置函數 len()
。 包含空字符串的表單字段會(huì )被忽略而不會(huì )出現在字典中;要保留這樣的值,請在創(chuàng )建 FieldStorage
實(shí)例時(shí)為可選的 keep_blank_values 關(guān)鍵字形參提供一個(gè)真值。
舉例來(lái)說(shuō),下面的代碼(假定 Content-Type 標頭和空行已經(jīng)被打?。?huì )檢查字段 name
和 addr
是否均被設為非空字符串:
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...
在這里的字段通過(guò) form[key]
來(lái)訪(fǎng)問(wèn),它們本身就是 FieldStorage
(或 MiniFieldStorage
,取決于表單的編碼格式) 的實(shí)例。 實(shí)例的 value
屬性會(huì )產(chǎn)生字段的字符串值。 getvalue()
方法直接返回這個(gè)字符串;它還接受可選的第二個(gè)參數作為當請求的鍵不存在時(shí)要返回的默認值。
如果提交的表單數據包含一個(gè)以上的同名字段,由 form[key]
所提取的對象將不是一個(gè) FieldStorage
或 MiniFieldStorage
實(shí)例而是由這種實(shí)例組成的列表。 類(lèi)似地,在這種情況下,form.getvalue(key)
將會(huì )返回一個(gè)字符串列表。 如果你預計到這種可能性(當你的 HTML 表單包含多個(gè)同名字段時(shí)),請使用 getlist()
方法,它總是返回一個(gè)值的列表(這樣你就不需要對只有單個(gè)項的情況進(jìn)行特別處理)。 例如,這段代碼拼接了任意數量的 username 字段,以逗號進(jìn)行分隔:
value = form.getlist("username")
usernames = ",".join(value)
如果一個(gè)字段是代表上傳的文件,請通過(guò) value
屬性訪(fǎng)問(wèn)該值或是通過(guò) getvalue()
方法以字節形式將整個(gè)文件讀入內存。 這可能不是你想要的結果。 你可以通過(guò)測試 filename
屬性或 file
屬性來(lái)檢測上傳的文件。 然后你可以從 file
屬性讀取數據,直到它作為 FieldStorage
實(shí)例的垃圾回收的一部分被自動(dòng)關(guān)閉 (read()
和 readline()
方法將返回字節數據):
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
語(yǔ)句中使用,該語(yǔ)句結束時(shí)將自動(dòng)關(guān)閉它們。
如果在獲取上傳文件的內容時(shí)遇到錯誤(例如,當用戶(hù)點(diǎn)擊回退或取消按鈕中斷表單提交時(shí))該字段中對象的 done
屬性值將被設為 -1。
文件上傳標準草案考慮到了從一個(gè)字段上傳多個(gè)文件的可能性(使用遞歸的 multipart/* 編碼格式)。 當這種情況發(fā)生時(shí),該條目將是一個(gè)類(lèi)似字典的 FieldStorage
條目。 這可以通過(guò)檢測它的 type
屬性來(lái)確定,該屬性應當是 multipart/form-data (或者可能是匹配 multipart/* 的其他 MIME 類(lèi)型)。 在這種情況下,它可以像最高層級的表單對象一樣被遞歸地迭代處理。
當一個(gè)表單按“舊”格式提交時(shí)(即以查詢(xún)字符串或是單個(gè) application/x-www-form-urlencoded 類(lèi)型的數據部分的形式),這些條目實(shí)際上將是 MiniFieldStorage
類(lèi)的實(shí)例。 在這種情況下,list
, file
和 filename
屬性將總是為 None
。
通過(guò) POST 方式提交并且也帶有查詢(xún)字符串的表單將同時(shí)包含 FieldStorage
和 MiniFieldStorage
條目。
在 3.4 版更改: file
屬性會(huì )在創(chuàng )建 FieldStorage
實(shí)例的垃圾回收操作中被自動(dòng)關(guān)閉。
在 3.5 版更改: 為 FieldStorage
類(lèi)增加了上下文管理協(xié)議支持。
更高層級的接口?
前面的部分解釋了如何使用 FieldStorage
類(lèi)來(lái)讀取 CGI 表單數據。 本部分則會(huì )描述一個(gè)更高層級的接口,它被添加到此類(lèi)中以允許人們以更為可讀和自然的方式行事。 這個(gè)接口并不會(huì )完全取代前面的部分所描述的技巧 --- 例如它們在高效處理文件上傳時(shí)仍然很有用處。
此接口由兩個(gè)簡(jiǎn)單的方法組成。 你可以使用這兩個(gè)方法以通用的方式處理表單數據,而無(wú)需擔心在一個(gè)名稱(chēng)下提交的值是只有一個(gè)還是有多個(gè)。
在前面的部分中,你已學(xué)會(huì )當你預期用戶(hù)在一個(gè)名稱(chēng)下提交超過(guò)一個(gè)值的時(shí)候編寫(xiě)以下代碼:
item = form.getvalue("item")
if isinstance(item, list):
# The user is requesting more than one item.
else:
# The user is requesting only one item.
這種情況很常見(jiàn),例如當一個(gè)表單包含具有相同名稱(chēng)的一組復選框的時(shí)候:
<input type="checkbox" name="item" value="1" />
<input type="checkbox" name="item" value="2" />
但是在多數情況下,一個(gè)表單中的一個(gè)特定名稱(chēng)只對應一個(gè)表單控件。 因此你可能會(huì )編寫(xiě)包含以下代碼的腳本:
user = form.getvalue("user").upper()
這段代碼的問(wèn)題在于你絕不能預期客戶(hù)端會(huì )向你的腳本提供合法的輸入。 舉例來(lái)說(shuō),如果一個(gè)好奇的用戶(hù)向查詢(xún)字符串添加了另一個(gè) user=foo
對,則該腳本將會(huì )崩潰,因為在這種情況下 getvalue("user")
方法調用將返回一個(gè)列表而不是字符串。 在一個(gè)列表上調用 upper()
方法是不合法的(因為列表并沒(méi)有這個(gè)方法)因而會(huì )引發(fā) AttributeError
異常。
因此,讀取表單數據值的正確方式應當總是使用檢查所獲取的值是單一值還是值列表的代碼。 這很麻煩并且會(huì )使腳本缺乏可讀性。
一種更便捷的方式是使用這個(gè)更高層級接口所提供的 getfirst()
和 getlist()
方法。
- FieldStorage.getfirst(name, default=None)?
此方法總是只返回與表單字段 name 相關(guān)聯(lián)的單一值。 此方法在同一名稱(chēng)下提交了多個(gè)值的情況下將僅返回第一個(gè)值。 請注意所接收的值順序在不同瀏覽器上可能發(fā)生變化因而是不確定的。 1 如果指定的表單字段或值不存在則此方法將返回可選形參 default 所指定的值。 如果未指定此形參則默認值為
None
。
- FieldStorage.getlist(name)?
此方法總是返回與表單字段 name 相關(guān)聯(lián)的值列表。 如果 name 指定的表單字段或值不存在則此方法將返回一個(gè)空列表。 如果指定的表單字段只包含一個(gè)值則它將返回只有一項的列表。
使用這兩個(gè)方法你將能寫(xiě)出優(yōu)雅簡(jiǎn)潔的代碼:
import cgi
form = cgi.FieldStorage()
user = form.getfirst("user", "").upper() # This way it's safe.
for item in form.getlist("item"):
do_something(item)
函數?
這些函數在你想要更多控制,或者如果你想要應用一些此模塊中在其他場(chǎng)景下實(shí)現的算法時(shí)很有用處。
- cgi.parse(fp=None, environ=os.environ, keep_blank_values=False, strict_parsing=False, separator='&')?
在環(huán)境中或從某個(gè)文件中解析一個(gè)查詢(xún) (文件默認為
sys.stdin
)。 keep_blank_values, strict_parsing 和 separator 形參會(huì )被原樣傳給urllib.parse.parse_qs()
。
- cgi.parse_multipart(fp, pdict, encoding='utf-8', errors='replace', separator='&')?
解析 multipart/form-data 類(lèi)型(用于文件上傳)的輸入。 參數中 fp 為輸入文件,pdict 為包含 Content-Type 標頭中的其他形參的字典,encoding 為請求的編碼格式。
像
urllib.parse.parse_qs()
那樣返回一個(gè)字典:其中的鍵為字段名稱(chēng),值為對應字段的值列表。 對于非文件字段,其值均為字符串列表。這很容易使用,但如果你預期要上傳巨量字節數據時(shí)就不太適合了 --- 在這種情況下,請改用更為靈活的
FieldStorage
類(lèi)。在 3.7 版更改: 增加了 encoding 和 errors 形參。 對于非文件字段,其值現在為字符串列表而非字節串列表。
在 3.10 版更改: 增加了 separator 形參。
- cgi.parse_header(string)?
將一個(gè) MIME 標頭 (例如 Content-Type) 解析為一個(gè)主值和一個(gè)參數字典。
- cgi.test()?
對 CGI 執行健壯性檢測,適于作為主程序。 寫(xiě)入最小化的 HTTP 標頭并以 HTML 格式來(lái)格式化提供給腳本的所有信息。
- cgi.print_environ()?
以 HTML 格式來(lái)格式化 shell 環(huán)境。
- cgi.print_form(form)?
以 HTML 格式來(lái)格式化表單。
- cgi.print_directory()?
以 HTML 格式來(lái)格式化當前目錄。
- cgi.print_environ_usage()?
以 HTML 格式打印有用的環(huán)境變量列表(供 CGI 使用)。
對于安全性的關(guān)注?
有一條重要的規則:如果你發(fā)起調用一個(gè)外部程序(通過(guò) os.system()
, os.popen()
或其他具有類(lèi)似功能的函數),需要非常確定你不會(huì )把從客戶(hù)端接收的任意字符串直接傳給 shell。 這是一個(gè)著(zhù)名的安全漏洞,網(wǎng)絡(luò )中聰明的黑客可以通過(guò)它來(lái)利用容易上當的 CGI 腳本發(fā)起調用任何 shell 命令。 即便 URL 的一部分或字段名稱(chēng)也是不可信任的,因為請求并不一定是來(lái)自你的表單!
為了安全起見(jiàn),如果你必須將從表單獲取的字符串傳給 shell 命令,你應當確保該字符串僅包含字母數字類(lèi)字符、連字符、下劃線(xiàn)和句點(diǎn)。
在 Unix 系統上安裝你的 CGI 腳本?
請閱讀你的 HTTP 服務(wù)器的文檔并咨詢(xún)你所用系統的管理員來(lái)找到 CGI 腳本應當安裝到哪個(gè)目錄;通常是服務(wù)器目錄樹(shù)中的 cgi-bin
目錄。
請確保你的腳本可被“其他人”讀取和執行;Unix 文件模式應為八進(jìn)制數 0o755
(使用 chmod 0755 filename
)。 請確保腳本的第一行包含 #!
且位置是從第 1 列開(kāi)始,后面帶有 Python 解釋器的路徑名,例如:
#!/usr/local/bin/python
請確保該 Python 解釋器存在并且可被“其他人”執行。
請確保你的腳本需要讀取或寫(xiě)入的任何文件都分別是“其他人”可讀取或可寫(xiě)入的 --- 它們的模式應為可讀取 0o644
或可寫(xiě)入 0o666
。 這是因為出于安全理由,HTTP 服務(wù)器是作為沒(méi)有任何特殊權限的 "nobody" 用戶(hù)來(lái)運行腳本的。 它只能讀?。▽?xiě)入、執行)任何人都能讀?。▽?xiě)入、執行)的文件。 執行時(shí)的當前目錄(通常為服務(wù)器的 cgi-bin 目錄)和環(huán)境變量集合也與你在登錄時(shí)所得到的不同。 特別地,不可依賴(lài)于 shell 的可執行文件搜索路徑 (PATH
) 或 Python 模塊搜索路徑 (PYTHONPATH
) 的任何相關(guān)設置。
如果你需要從 Python 的默認模塊搜索路徑之外的目錄載入模塊,你可以在導入其他模塊之前在你的腳本中改變路徑。 例如:
import sys
sys.path.insert(0, "/usr/home/joe/lib/python")
sys.path.insert(0, "/usr/local/lib/python")
(在此方式下,最后插入的目錄將最先被搜索?。?/p>
針對非 Unix 系統的指導會(huì )有所變化;請查看你的 HTTP 服務(wù)器的文檔(通常會(huì )有關(guān)于 CGI 腳本的部分)。
測試你的 CGI 腳本?
很不幸,當你在命令行中嘗試 CGI 腳本時(shí)它通常會(huì )無(wú)法運行,而能在命令行中完美運行的腳本則可能會(huì )在運行于服務(wù)器時(shí)神秘地失敗。 但有一個(gè)理由使你仍然應當在命令行中測試你的腳本:如果它包含語(yǔ)法錯誤,Python 解釋器將根本不會(huì )執行它,而 HTTP 服務(wù)器將很可能向客戶(hù)端發(fā)送令人費解的錯誤信息。
假定你的腳本沒(méi)有語(yǔ)法錯誤,但它仍然無(wú)法起作用,你將別無(wú)選擇,只能繼續閱讀下一節。
調試 CGI 腳本?
首先,請檢查是否有安裝上的小錯誤 --- 仔細閱讀上面關(guān)于安裝 CGI 腳本的部分可以使你節省大量時(shí)間。 如果你不確定你是否正確理解了安裝過(guò)程,請嘗試將此模塊 (cgi.py
) 的副本作為 CGI 腳本安裝。 當作為腳本被發(fā)起調用時(shí),該文件將以 HTML 格式轉儲其環(huán)境和表單內容。 請給它賦予正確的模式等,并向它發(fā)送一個(gè)請求。 如果它是安裝在標準的 cgi-bin
目錄下,應該可以通過(guò)在你的瀏覽器中輸入表單的 URL 來(lái)向它發(fā)送請求。
http://yourhostname/cgi-bin/cgi.py?name=Joe+Blow&addr=At+Home
如果此操作給出類(lèi)型為 404 的錯誤,說(shuō)明服務(wù)器找不到此腳本 -- 也許你需要將它安裝到不同的目錄。 如果它給出另一種錯誤,說(shuō)明存在安裝問(wèn)題,你應當解決此問(wèn)題才能繼續操作。 如果你得到一個(gè)格式良好的環(huán)境和表單內容清單(在這個(gè)例子中,應當會(huì )列出的有字段 "addr" 值為 "At Home" 以及 "name" 值為 "Joe Blow"),則說(shuō)明 cgi.py
腳本已正確安裝。 如果你為自己的腳本執行了同樣的過(guò)程,現在你應該能夠調試它了。
下一步驟可以是在你的腳本中調用 cgi
模塊的 test()
函數:用這一條語(yǔ)句替換它的主代碼
cgi.test()
這將產(chǎn)生從安裝 cgi.py
文件本身所得到的相同結果。
當某個(gè)常規 Python 腳本觸發(fā)了未處理的異常,(無(wú)論出于什么原因:模塊名稱(chēng)出錯、文件無(wú)法打開(kāi)等),Python 解釋器就會(huì )打印出一條完整的跟蹤信息并退出。在 CGI 腳本觸發(fā)異常時(shí),Python 解釋器依然會(huì )如此,但最有可能的是,跟蹤信息只會(huì )停留在某個(gè) HTTP 服務(wù)日志文件中,或者被完全丟棄。
幸運的是,只要執行 某些 代碼,就可以利用 cgitb
模塊將跟蹤信息發(fā)送給瀏覽器。將以下幾行代碼加到代碼頂部:
import cgitb
cgitb.enable()
然后再運行一下看;發(fā)生問(wèn)題時(shí)應能看到詳細的報告,或許能讓崩潰的原因更清晰一些。
如果懷疑是 cgitb
模塊導入的問(wèn)題,可以采用一個(gè)功能更強的方法(只用到內置模塊):
import sys
sys.stderr = sys.stdout
print("Content-Type: text/plain")
print()
...your code here...
這得靠 Python 解釋器來(lái)打印跟蹤信息。輸出的類(lèi)型為純文本,不經(jīng)過(guò)任何 HTML 處理。如果代碼正常,則客戶(hù)端會(huì )顯示原有的 HTML。如果觸發(fā)了異常,很可能在輸出前兩行后會(huì )顯示一條跟蹤信息。因為不會(huì )繼續進(jìn)行 HTML 解析,所以跟蹤信息肯定能被讀到。
常見(jiàn)問(wèn)題和解決方案?
大部分 HTTP 服務(wù)器會(huì )對 CGI 腳本的輸出進(jìn)行緩存,等腳本執行完畢再行輸出。這意味著(zhù)在腳本運行時(shí),不可能在客戶(hù)端屏幕上顯示出進(jìn)度情況。
請查看上述安裝說(shuō)明。
請查看 HTTP 服務(wù)器的日志文件。(在另一個(gè)單獨窗口中執行
tail -f logfile
可能會(huì )很有用?。?/p>一定要先檢查腳本是否有語(yǔ)法錯誤,做法類(lèi)似:
python script.py
。如果腳本沒(méi)有語(yǔ)法錯誤,試著(zhù)在腳本的頂部添加
import cgitb; cgitb.enable()
。當調用外部程序時(shí),要確保其可被讀取。通常這意味著(zhù)采用絕對路徑名------ 在 CGI 腳本中,
PATH
的值通常沒(méi)什么用。在讀寫(xiě)外部文件時(shí),要確保其能被 CGI 腳本歸屬的用戶(hù)讀寫(xiě):通常是運行網(wǎng)絡(luò )服務(wù)的用戶(hù),或由網(wǎng)絡(luò )服務(wù)的
suexec
功能明確指定的一些用戶(hù)。不要試圖給 CGI 腳本賦予 set-uid 模式。這在大多數系統上都行不通,出于安全考慮也不應如此。
附注
- 1
請注意,新版的 HTML 規范確實(shí)注明了請求字段的順序,但判斷請求是否合法非常繁瑣和容易出錯,可能來(lái)自不符合要求的瀏覽器,甚至不是來(lái)自瀏覽器。