許多人使用 Python 作為一個 shell 腳本的替代,用來實(shí)現(xiàn)常用系統(tǒng)任務(wù)的自動化,如文件的操作,系統(tǒng)的配置等。本章的主要目標(biāo)是描述光宇編寫腳本時候經(jīng)常遇到的一些功能。例如,解析命令行選項(xiàng)、獲取有用的系統(tǒng)配置數(shù)據(jù)等等。第5章也包含了與文件和目錄相關(guān)的一般信息。
你希望你的腳本接受任何用戶認(rèn)為最簡單的輸入方式。包括將命令行的輸出通過管道傳遞給該腳本、 重定向文件到該腳本,或在命令行中傳遞一個文件名或文件名列表給該腳本。
Python 內(nèi)置的 fileinput
模塊讓這個變得簡單。如果你有一個下面這樣的腳本:
#!/usr/bin/env python3
import fileinput
with fileinput.input() as f_input:
for line in f_input:
print(line, end='')
那么你就能以前面提到的所有方式來為此腳本提供輸入。假設(shè)你將此腳本保存為 filein.py
并將其變?yōu)榭蓤?zhí)行文件, 那么你可以像下面這樣調(diào)用它,得到期望的輸出:
$ ls | ./filein.py # Prints a directory listing to stdout.
$ ./filein.py /etc/passwd # Reads /etc/passwd to stdout.
$ ./filein.py < /etc/passwd # Reads /etc/passwd to stdout.
fileinput.input()
創(chuàng)建并返回一個 FileInput
類的實(shí)例。 該實(shí)例除了擁有一些有用的幫助方法外,它還可被當(dāng)做一個上下文管理器使用。 因此,整合起來,如果我們要寫一個打印多個文件輸出的腳本,那么我們需要在輸出中包含文件名和行號,如下所示:
>>> import fileinput
>>> with fileinput.input('/etc/passwd') as f:
>>> for line in f:
... print(f.filename(), f.lineno(), line, end='')
...
/etc/passwd 1 ##
/etc/passwd 2 # User Database
/etc/passwd 3 #
<other output omitted>
通過將它作為一個上下文管理器使用,可以確保它不再使用時文件能自動關(guān)閉, 而且我們在之類還演示了 FileInput
的一些有用的幫助方法來獲取輸出中的一些其他信息。
你想向標(biāo)準(zhǔn)錯誤打印一條消息并返回某個非零狀態(tài)碼來終止程序運(yùn)行
你有一個程序像下面這樣終止,拋出一個 SystemExit
異常,使用錯誤消息作為參數(shù)。例如:
raise SystemExit('It failed!')
它會將消息在 sys.stderr
中打印,然后程序以狀態(tài)碼1退出。
本節(jié)雖然很短小,但是它能解決在寫腳本時的一個常見問題。 也就是說,當(dāng)你想要終止某個程序時,你可能會像下面這樣寫:
import sys
sys.stderr.write('It failed!\n')
raise SystemExit(1)
如果你直接將消息作為參數(shù)傳給 SystemExit()
,那么你可以省略其他步驟, 比如 import 語句或?qū)㈠e誤消息寫入 sys.stderr
你的程序如何能夠解析命令行選項(xiàng)(位于 sys.argv 中)
argparse
模塊可被用來解析命令行選項(xiàng)。下面一個簡單例子演示了最基本的用法:
# search.py
'''
Hypothetical command-line tool for searching a collection of
files for one or more text patterns.
'''
import argparse
parser = argparse.ArgumentParser(description='Search some files')
parser.add_argument(dest='filenames',metavar='filename', nargs='*')
parser.add_argument('-p', '--pat',metavar='pattern', required=True,
dest='patterns', action='append',
help='text pattern to search for')
parser.add_argument('-v', dest='verbose', action='store_true',
help='verbose mode')
parser.add_argument('-o', dest='outfile', action='store',
help='output file')
parser.add_argument('--speed', dest='speed', action='store',
choices={'slow','fast'}, default='slow',
help='search speed')
args = parser.parse_args()
# Output the collected arguments
print(args.filenames)
print(args.patterns)
print(args.verbose)
print(args.outfile)
print(args.speed)
該程序定義了一個如下使用的命令行解析器:
bash % python3 search.py -h
usage: search.py [-h] [-p pattern] [-v] [-o OUTFILE] [--speed {slow,fast}]
[filename [filename ...]]
Search some files
positional arguments:
filename
optional arguments:
-h, --help show this help message and exit
-p pattern, --pat pattern
text pattern to search for
-v verbose mode
-o OUTFILE output file
--speed {slow,fast} search speed
下面的部分演示了程序中的數(shù)據(jù)部分。仔細(xì)觀察 print()語句的打印輸出。
bash % python3 search.py foo.txt bar.txt
usage: search.py [-h] -p pattern [-v] [-o OUTFILE] [--speed {fast,slow}]
[filename [filename ...]]
search.py: error: the following arguments are required: -p/--pat
bash % python3 search.py -v -p spam --pat=eggs foo.txt bar.txt
filenames = ['foo.txt', 'bar.txt']
patterns = ['spam', 'eggs']
verbose = True
outfile = None
speed = slow
bash % python3 search.py -v -p spam --pat=eggs foo.txt bar.txt -o results
filenames = ['foo.txt', 'bar.txt']
patterns = ['spam', 'eggs']
verbose = True
outfile = results
speed = slow
bash % python3 search.py -v -p spam --pat=eggs foo.txt bar.txt -o results \
--speed=fast
filenames = ['foo.txt', 'bar.txt']
patterns = ['spam', 'eggs']
verbose = True
outfile = results
speed = fast
對于選項(xiàng)值的進(jìn)一步處理由程序來決定,用你自己的邏輯來替代 print()
函數(shù)。
argparse
模塊是標(biāo)準(zhǔn)庫中最大的模塊之一,擁有大量的配置選項(xiàng)。 本節(jié)只是演示了其中最基礎(chǔ)的一些特性,幫助你入門。
為了解析命令行選項(xiàng),你首先要創(chuàng)建一個 ArgumentParser
實(shí)例, 并使用 add_argument()
方法聲明你想要支持的選項(xiàng)。 在每個add-argument()
調(diào)用中,dest
參數(shù)指定解析結(jié)果被指派給屬性的名字。 metavar
參數(shù)被用來生成幫助信息。action
參數(shù)指定跟屬性對應(yīng)的處理邏輯, 通常的值為 store
,被用來存儲某個值或講多個參數(shù)值收集到一個列表中。 下面的參數(shù)收集所有剩余的命令行參數(shù)到一個列表中。在本例中它被用來構(gòu)造一個文件名列表:
parser.add_argument(dest='filenames',metavar='filename', nargs='*')
下面的參數(shù)根據(jù)參數(shù)是否存在來設(shè)置一個 Boolean
標(biāo)志:
parser.add_argument('-v', dest='verbose', action='store_true',
help='verbose mode')
下面的參數(shù)接受一個單獨(dú)值并將其存儲為一個字符串:
parser.add_argument('-o', dest='outfile', action='store',
help='output file')
下面的參數(shù)說明允許某個參數(shù)重復(fù)出現(xiàn)多次,并將它們追加到一個列表中去。required
標(biāo)志表示該參數(shù)至少要有一個。-p
和 --pat
表示兩個參數(shù)名形式都可使用。
parser.add_argument('-p', '--pat',metavar='pattern', required=True,
dest='patterns', action='append',
help='text pattern to search for')
最后,下面的參數(shù)說明接受一個值,但是會將其和可能的選擇值做比較,以檢測其合法性:
parser.add_argument('--speed', dest='speed', action='store',
choices={'slow','fast'}, default='slow',
help='search speed')
一旦參數(shù)選項(xiàng)被指定,你就可以執(zhí)行 parser.parse()
方法了。 它會處理 sys.argv
的值并返回一個結(jié)果實(shí)例。 每個參數(shù)值會被設(shè)置成該實(shí)例中add_argument()
方法的dest
參數(shù)指定的屬性值。
還很多種其他方法解析命令行選項(xiàng)。 例如,你可能會手動的處理 sys.argv
或者使用 getopt
模塊。 但是,如果你采用本節(jié)的方式,將會減少很多冗余代碼,底層細(xì)節(jié) argparse
模塊已經(jīng)幫你處理了。 你可能還會碰到使用 optparse
庫解析選項(xiàng)的代碼。 盡管 optparse
和 argparse
很像,但是后者更先進(jìn),因此在新的程序中你應(yīng)該使用它。
你寫了個腳本,運(yùn)行時需要一個密碼。此腳本是交互式的,因此不能將密碼在腳本中硬編碼, 而是需要彈出一個密碼輸入提示,讓用戶自己輸入。
這時候 Python 的 getpass
模塊正是你所需要的。你可以讓你很輕松的彈出密碼輸入提示, 并且不會在用戶終端回顯密碼。下面是具體代碼:
import getpass
user = getpass.getuser()
passwd = getpass.getpass()
if svc_login(user, passwd): # You must write svc_login()
print('Yay!')
else:
print('Boo!')
在此代碼中,svc_login()
是你要實(shí)現(xiàn)的處理密碼的函數(shù),具體的處理過程你自己決定。
注意在前面代碼中getpass.getuser()
不會彈出用戶名的輸入提示。 它會根據(jù)該用戶的shell環(huán)境或者會依據(jù)本地系統(tǒng)的密碼庫(支持 pwd 模塊的平臺)來使用當(dāng)前用戶的登錄名,
如果你想顯示的彈出用戶名輸入提示,使用內(nèi)置的 input
函數(shù):
user = input('Enter your username: ')
還有一點(diǎn)很重要,有些系統(tǒng)可能不支持 getpass()
方法隱藏輸入密碼。 這種情況下,Python 會提前警告你這些問題(例如它會警告你說密碼會以明文形式顯示)
你需要知道當(dāng)前終端的大小以便正確的格式化輸出。
使用 os.get_terminal_size()
函數(shù)來做到這一點(diǎn)。
代碼示例:
>>> import os
>>> sz = os.get_terminal_size()
>>> sz
os.terminal_size(columns=80, lines=24)
>>> sz.columns
80
>>> sz.lines
24
>>>
有太多方式來得知終端大小了,從讀取環(huán)境變量到執(zhí)行底層的 ioctl()
函數(shù)等等。 不過,為什么要去研究這些復(fù)雜的辦法而不是僅僅調(diào)用一個簡單的函數(shù)呢?
你想執(zhí)行一個外部命令并以 Python 字符串的形式獲取執(zhí)行結(jié)果。
使用 subprocess.check_output()
函數(shù)。例如:
import subprocess
out_bytes = subprocess.check_output(['netstat','-a'])
這段代碼執(zhí)行一個指定的命令并將執(zhí)行結(jié)果以一個字節(jié)字符串的形式返回。 如果你需要文本形式返回,加一個解碼步驟即可。例如:
out_text = out_bytes.decode('utf-8')
如果被執(zhí)行的命令以非零碼返回,就會拋出異常。 下面的例子捕獲到錯誤并獲取返回碼:
try:
out_bytes = subprocess.check_output(['cmd','arg1','arg2'])
except subprocess.CalledProcessError as e:
out_bytes = e.output # Output generated before error
code = e.returncode # Return code
默認(rèn)情況下,check_output()
僅僅返回輸入到標(biāo)準(zhǔn)輸出的值。 如果你需要同時收集標(biāo)準(zhǔn)輸出和錯誤輸出,使用 stderr
參數(shù):
out_bytes = subprocess.check_output(['cmd','arg1','arg2'],
stderr=subprocess.STDOUT)
如果你需要用一個超時機(jī)制來執(zhí)行命令,使用 timeout
參數(shù):
try:
out_bytes = subprocess.check_output(['cmd','arg1','arg2'], timeout=5)
except subprocess.TimeoutExpired as e:
...
通常來講,命令的執(zhí)行不需要使用到底層 shell 環(huán)境(比如 sh、bash)。 一個字符串列表會被傳遞給一個低級系統(tǒng)命令,比如 os.execve()
。 如果你想讓命令被一個 shell 執(zhí)行,傳遞一個字符串參數(shù),并設(shè)置參數(shù) shell=True
. 有時候你想要 Python 去執(zhí)行一個復(fù)雜的 shell 命令的時候這個就很有用了,比如管道流、I/O 重定向和其他特性。例如:
out_bytes = subprocess.check_output('grep python | wc > out', shell=True)
需要注意的是在 shell 中執(zhí)行命令會存在一定的安全風(fēng)險,特別是當(dāng)參數(shù)來自于用戶輸入時。 這時候可以使用 shlex.quote()
函數(shù)來講參數(shù)正確的用雙引用引起來。
使用 check_output()
函數(shù)是執(zhí)行外部命令并獲取其返回值的最簡單方式。 但是,如果你需要對子進(jìn)程做更復(fù)雜的交互,比如給它發(fā)送輸入,你得采用另外一種方法。 這時候可直接使用 subprocess.Popen
類。例如:
import subprocess
# Some text to send
text = b'''
hello world
this is a test
goodbye
'''
# Launch a command with pipes
p = subprocess.Popen(['wc'],
stdout = subprocess.PIPE,
stdin = subprocess.PIPE)
# Send the data and get the output
stdout, stderr = p.communicate(text)
# To interpret as text, decode
out = stdout.decode('utf-8')
err = stderr.decode('utf-8')
subprocess
模塊對于依賴 TTY 的外部命令不合適用。 例如,你不能使用它來自動化一個用戶輸入密碼的任務(wù)(比如一個 ssh 會話)。 這時候,你需要使用到第三方模塊了,比如基于著名的 expect
家族的工具(pexpect 或類似的)
你想喲啊復(fù)制或移動文件和目錄,但是又不想調(diào)用 shell 命令。
shutil
模塊有很多便捷的函數(shù)可以復(fù)制文件和目錄。使用起來非常簡單,比如:
import shutil
# Copy src to dst. (cp src dst)
shutil.copy(src, dst)
# Copy files, but preserve metadata (cp -p src dst)
shutil.copy2(src, dst)
# Copy directory tree (cp -R src dst)
shutil.copytree(src, dst)
# Move src to dst (mv src dst)
shutil.move(src, dst)
這些函數(shù)的參數(shù)都是字符串形式的文件或目錄名。 底層語義模擬了類似的 Unix 命令,如上面的注釋部分。
默認(rèn)情況下,對于符號鏈接而已這些命令處理的是它指向的東西。 例如,如果源文件是一個符號鏈接,那么目標(biāo)文件將會是符號鏈接指向的文件。 如果你只想復(fù)制符號鏈接本身,那么需要指定關(guān)鍵字參數(shù) follow_symlinks
,如下:
如果你想保留被復(fù)制目錄中的符號鏈接,像這樣做:
shutil.copytree(src, dst, symlinks=True)
copytree()
可以讓你在復(fù)制過程中選擇性的忽略某些文件或目錄。 你可以提供一個忽略函數(shù),接受一個目錄名和文件名列表作為輸入,返回一個忽略的名稱列表。例如:
def ignore_pyc_files(dirname, filenames):
return [name in filenames if name.endswith('.pyc')]
shutil.copytree(src, dst, ignore=ignore_pyc_files)
Since ignoring filename patterns is common, a utility function ignore_patterns() has already been provided to do it. For example:
shutil.copytree(src, dst, ignore=shutil.ignore_patterns(‘~’,’.pyc’))
使用 shutil
復(fù)制文件和目錄也忒簡單了點(diǎn)吧。 不過,對于文件元數(shù)據(jù)信息,copy2()
這樣的函數(shù)只能盡自己最大能力來保留它。 訪問時間、創(chuàng)建時間和權(quán)限這些基本信息會被保留, 但是對于所有者、ACLs、資源 fork 和其他更深層次的文件元信息就說不準(zhǔn)了, 這個還得依賴于底層操作系統(tǒng)類型和用戶所擁有的訪問權(quán)限。 你通常不會去使用shutil.copytree()
函數(shù)來執(zhí)行系統(tǒng)備份。 當(dāng)處理文件名的時候,最好使用os.path
中的函數(shù)來確保最大的可移植性(特別是同時要適用于 Unix 和 Windows)。 例如:
>>> filename = '/Users/guido/programs/spam.py'
>>> import os.path
>>> os.path.basename(filename)
'spam.py'
>>> os.path.dirname(filename)
'/Users/guido/programs'
>>> os.path.split(filename)
('/Users/guido/programs', 'spam.py')
>>> os.path.join('/new/dir', os.path.basename(filename))
'/new/dir/spam.py'
>>> os.path.expanduser('~/guido/programs/spam.py')
'/Users/guido/programs/spam.py'
>>>
使用copytree()
復(fù)制文件夾的一個棘手的問題是對于錯誤的處理。 例如,在復(fù)制過程中,函數(shù)可能會碰到損壞的符號鏈接,因?yàn)闄?quán)限無法訪問文件的問題等等。 為了解決這個問題,所有碰到的問題會被收集到一個列表中并打包為一個單獨(dú)的異常,到了最后再拋出。 下面是一個例子:
try:
shutil.copytree(src, dst)
except shutil.Error as e:
for src, dst, msg in e.args[0]:
# src is source name
# dst is destination name
# msg is error message from exception
print(dst, src, msg)
如果你提供關(guān)鍵字參數(shù) ignore_dangling_symlinks=True
, 這時候 copytree()
會忽略掉無效符號鏈接。
本節(jié)演示的這些函數(shù)都是最常見的。不過,shutil
還有更多的和復(fù)制數(shù)據(jù)相關(guān)的操作。 它的文檔很值得一看,參考 Python documentation
你需要創(chuàng)建或解壓常見格式的歸檔文件(比如.tar, .tgz或.zip)
shutil
模塊擁有兩個函數(shù)—— make_archive()
和 unpack_archive()
可派上用場。 例如:
>>> import shutil
>>> shutil.unpack_archive('Python-3.3.0.tgz')
>>> shutil.make_archive('py33','zip','Python-3.3.0')
'/Users/beazley/Downloads/py33.zip'
>>>
make_archive()
的第二個參數(shù)是期望的輸出格式。 可以使用 get_archive_formats()
獲取所有支持的歸檔格式列表。例如:
>>> shutil.get_archive_formats()
[('bztar', "bzip2'ed tar-file"), ('gztar', "gzip'ed tar-file"),
('tar', 'uncompressed tar file'), ('zip', 'ZIP file')]
>>>
Python 還有其他的模塊可用來處理多種歸檔格式(比如 tarfile, zipfile, gzip, bz2)的底層細(xì)節(jié)。 不過,如果你僅僅只是要創(chuàng)建或提取某個歸檔,就沒有必要使用底層庫了。 可以直接使用shutil
中的這些高層函數(shù)。
這些函數(shù)還有很多其他選項(xiàng),用于日志打印、預(yù)檢、文件權(quán)限等等。 參考 shutil 文檔
你需要寫一個涉及到文件查找操作的腳本,比如對日志歸檔文件的重命名工具, 你不想在 Python 腳本中調(diào)用 shell,或者你要實(shí)現(xiàn)一些 shell 不能做的功能。
查找文件,可使用 os.walk()
函數(shù),傳一個頂級目錄名給它。 下面是一個例子,查找特定的文件名并答應(yīng)所有符合條件的文件全路徑:
#!/usr/bin/env python3.3
import os
def findfile(start, name):
for relpath, dirs, files in os.walk(start):
if name in files:
full_path = os.path.join(start, relpath, name)
print(os.path.normpath(os.path.abspath(full_path)))
if __name__ == '__main__':
findfile(sys.argv[1], sys.argv[2])
保存腳本為文件 findfile.py,然后在命令行中執(zhí)行它。 指定初始查找目錄以及名字作為位置參數(shù),如下:
os.walk()
方法為我們遍歷目錄樹, 每次進(jìn)入一個目錄,它會返回一個三元組,包含相對于查找目錄的相對路徑,一個該目錄下的目錄名列表, 以及那個目錄下面的文件名列表。
對于每個元組,只需檢測一下目標(biāo)文件名是否在文件列表中。如果是就使用 os.path.join()
合并路徑。 為了避免奇怪的路徑名比如 ././foo//bar
,使用了另外兩個函數(shù)來修正結(jié)果。 第一個是 os.path.abspath()
,它接受一個路徑,可能是相對路徑,最后返回絕對路徑。 第二個是os.path.normpath()
,用來返回正常路徑,可以解決雙斜桿、對目錄的多重引用的問題等。
盡管這個腳本相對于 UNIX 平臺上面的很多查找公交來講要簡單很多,它還有跨平臺的優(yōu)勢。 并且,還能很輕松的加入其他的功能。 我們再演示一個例子,下面的函數(shù)打印所有最近被修改過的文件:
#!/usr/bin/env python3.3
import os
import time
def modified_within(top, seconds):
now = time.time()
for path, dirs, files in os.walk(top):
for name in files:
fullpath = os.path.join(path, name)
if os.path.exists(fullpath):
mtime = os.path.getmtime(fullpath)
if mtime > (now - seconds):
print(fullpath)
if __name__ == '__main__':
import sys
if len(sys.argv) != 3:
print('Usage: {} dir seconds'.format(sys.argv[0]))
raise SystemExit(1)
modified_within(sys.argv[1], float(sys.argv[2]))
在此函數(shù)的基礎(chǔ)之上,使用 os,os.path,glob 等類似模塊,你就能實(shí)現(xiàn)更加復(fù)雜的操作了。 可參考5.11小節(jié)和5.13小節(jié)等相關(guān)章節(jié)。
怎樣讀取普通.ini 格式的配置文件?
configparser
模塊能被用來讀取配置文件。例如,假設(shè)你有如下的配置文件:
; config.ini
; Sample configuration file
[installation]
library=%(prefix)s/lib
include=%(prefix)s/include
bin=%(prefix)s/bin
prefix=/usr/local
# Setting related to debug configuration
[debug]
log_errors=true
show_warnings=False
[server]
port: 8080
nworkers: 32
pid-file=/tmp/spam.pid
root=/www/root
signature:
=================================
Brought to you by the Python Cookbook
=================================
下面是一個讀取和提取其中值的例子:
>>> from configparser import ConfigParser
>>> cfg = ConfigParser()
>>> cfg.read('config.ini')
['config.ini']
>>> cfg.sections()
['installation', 'debug', 'server']
>>> cfg.get('installation','library')
'/usr/local/lib'
>>> cfg.getboolean('debug','log_errors')
True
>>> cfg.getint('server','port')
8080
>>> cfg.getint('server','nworkers')
32
>>> print(cfg.get('server','signature'))
\=================================
Brought to you by the Python Cookbook
\=================================
>>>
如果有需要,你還能修改配置并使用 cfg.write()
方法將其寫回到文件中。例如:
>>> cfg.set('server','port','9000')
>>> cfg.set('debug','log_errors','False')
>>> import sys
>>> cfg.write(sys.stdout)
[installation]
library = %(prefix)s/lib
include = %(prefix)s/include
bin = %(prefix)s/bin
prefix = /usr/local
[debug]
log_errors = False
show_warnings = False
[server]
port = 9000
nworkers = 32
pid-file = /tmp/spam.pid
root = /www/root
signature =
=================================
Brought to you by the Python Cookbook
=================================
>>>
配置文件作為一種可讀性很好的格式,非常適用于存儲程序中的配置數(shù)據(jù)。 在每個配置文件中,配置數(shù)據(jù)會被分組(比如例子中的“installation”、 “debug” 和 “server”)。 每個分組在其中指定對應(yīng)的各個變量值。
對于可實(shí)現(xiàn)同樣功能的配置文件和 Python 源文件是有很大的不同的。 首先,配置文件的語法要更自由些,下面的賦值語句是等效的:
prefix=/usr/local
prefix: /usr/local
配置文件中的名字是不區(qū)分大小寫的。例如:
>>> cfg.get('installation','PREFIX')
'/usr/local'
>>> cfg.get('installation','prefix')
'/usr/local'
>>>
在解析值的時候,getboolean()
方法查找任何可行的值。例如下面都是等價的:
log_errors = true
log_errors = TRUE
log_errors = Yes
log_errors = 1
或許配置文件和 Python 代碼最大的不同在于,它并不是從上而下的順序執(zhí)行。 文件是安裝一個整體被讀取的。如果碰到了變量替換,它實(shí)際上已經(jīng)被替換完成了。 例如,在下面這個配置中,prefix
變量在使用它的變量之前后之后定義都是可以的:
[installation]
library=%(prefix)s/lib
include=%(prefix)s/include
bin=%(prefix)s/bin
prefix=/usr/local
ConfigParser
有個容易被忽視的特性是它能一次讀取多個配置文件然后合并成一個配置。 例如,假設(shè)一個用戶像下面這樣構(gòu)造了他們的配置文件:
; ~/.config.ini
[installation]
prefix=/Users/beazley/test
[debug]
log_errors=False
讀取這個文件,它就能跟之前的配置合并起來。如:
>>> # Previously read configuration
>>> cfg.get('installation', 'prefix')
'/usr/local'
>>> # Merge in user-specific configuration
>>> import os
>>> cfg.read(os.path.expanduser('~/.config.ini'))
['/Users/beazley/.config.ini']
>>> cfg.get('installation', 'prefix')
'/Users/beazley/test'
>>> cfg.get('installation', 'library')
'/Users/beazley/test/lib'
>>> cfg.getboolean('debug', 'log_errors')
False
>>>
仔細(xì)觀察下 prefix
變量是怎樣覆蓋其他相關(guān)變量的,比如 library
的設(shè)定值。 產(chǎn)生這種結(jié)果的原因是變量的改寫采取的是后發(fā)制人策略,以最后一個為準(zhǔn)。 你可以像下面這樣做試驗(yàn):
>>> cfg.get('installation','library')
'/Users/beazley/test/lib'
>>> cfg.set('installation','prefix','/tmp/dir')
>>> cfg.get('installation','library')
'/tmp/dir/lib'
>>>
最后還有很重要一點(diǎn)喲啊注意的是 Python 并不能支持.ini 文件在其他程序(比如 windows 應(yīng)用程序)中的所有特性。 確保你已經(jīng)參閱了 configparser 文檔中的語法詳情以及支持特性。
你希望在腳本和程序中將診斷信息寫入日志文件。
The easiest way to add logging to simple programs is to use the logging module. For example: 打印日志最簡單方式是使用 logging
模塊。例如:
import logging
def main():
# Configure the logging system
logging.basicConfig(
filename='app.log',
level=logging.ERROR
)
# Variables (to make the calls that follow work)
hostname = 'www.python.org'
item = 'spam'
filename = 'data.csv'
mode = 'r'
# Example logging calls (insert into your program)
logging.critical('Host %s unknown', hostname)
logging.error("Couldn't find %r", item)
logging.warning('Feature is deprecated')
logging.info('Opening file %r, mode=%r', filename, mode)
logging.debug('Got here')
if __name__ == '__main__':
main()
上面五個日志調(diào)用(critical(), error(), warning(), info(), debug())以降序方式表示不同的嚴(yán)重級別。 basicConfig()
的 level
參數(shù)是一個過濾器。 所有級別低于此級別的日志消息都會被忽略掉。 每個 logging 操作的參數(shù)是一個消息字符串,后面再跟一個或多個參數(shù)。 構(gòu)造最終的日志消息的時候我們使用了%操作符來格式化消息字符串。
運(yùn)行這個程序后,在文件 app.log
中的內(nèi)容應(yīng)該是下面這樣:
CRITICAL:root:Host www.python.org unknown
ERROR:root:Could not find 'spam'
If you want to change the output or level of output, you can change the parameters to the basicConfig() call. For example: 如果你想改變輸出等級,你可以修改 basicConfig()
調(diào)用中的參數(shù)。例如:
logging.basicConfig(
filename='app.log',
level=logging.WARNING,
format='%(levelname)s:%(asctime)s:%(message)s')
最后輸出變成如下:
CRITICAL:2012-11-20 12:27:13,595:Host www.python.org unknown
ERROR:2012-11-20 12:27:13,595:Could not find 'spam'
WARNING:2012-11-20 12:27:13,595:Feature is deprecated
上面的日志配置都是硬編碼到程序中的。如果你想使用配置文件, 可以像下面這樣修改 basicConfig()
調(diào)用:
import logging
import logging.config
def main():
# Configure the logging system
logging.config.fileConfig('logconfig.ini')
...
創(chuàng)建一個下面這樣的文件,名字叫 logconfig.ini
:
[loggers]
keys=root
[handlers]
keys=defaultHandler
[formatters]
keys=defaultFormatter
[logger_root]
level=INFO
handlers=defaultHandler
qualname=root
[handler_defaultHandler]
class=FileHandler
formatter=defaultFormatter
args=('app.log', 'a')
[formatter_defaultFormatter]
format=%(levelname)s:%(name)s:%(message)s
如果你想修改配置,可以直接編輯文件 logconfig.ini 即可。
盡管對于 logging
模塊而已有很多更高級的配置選項(xiàng), 不過這里的方案對于簡單的程序和腳本已經(jīng)足夠了。 只想在調(diào)用日志操作前先執(zhí)行下 basicConfig()函數(shù)方法,你的程序就能產(chǎn)生日志輸出了。
如果你想要你的日志消息寫到標(biāo)準(zhǔn)錯誤中,而不是日志文件中,調(diào)用 basicConfig() 時不傳文件名參數(shù)即可。例如:
logging.basicConfig(level=logging.INFO)
basicConfig()
在程序中只能被執(zhí)行一次。如果你稍后想改變?nèi)罩九渲茫?就需要先獲取 root logger
,然后直接修改它。例如:
logging.getLogger().level = logging.DEBUG
需要強(qiáng)調(diào)的是本節(jié)只是演示了 logging
模塊的一些基本用法。 它可以做更多更高級的定制。 關(guān)于日志定制化一個很好的資源是 Logging Cookbook
你想給某個函數(shù)庫增加日志功能,但是又不能影響到那些不使用日志功能的程序。
對于想要執(zhí)行日志操作的函數(shù)庫而已,你應(yīng)該創(chuàng)建一個專屬的 logger
對象,并且像下面這樣初始化配置:
# somelib.py
import logging
log = logging.getLogger(__name__)
log.addHandler(logging.NullHandler())
# Example function (for testing)
def func():
log.critical('A Critical Error!')
log.debug('A debug message')
使用這個配置,默認(rèn)情況下不會打印日志。例如:
>>> import somelib
>>> somelib.func()
>>>
不過,如果配置過日志系統(tǒng),那么日志消息打印就開始生效,例如:
>>> import logging
>>> logging.basicConfig()
>>> somelib.func()
CRITICAL:somelib:A Critical Error!
>>>
通常來講,你不應(yīng)該在函數(shù)庫代碼中自己配置日志系統(tǒng),或者是已經(jīng)假定有個已經(jīng)存在的日志配置了。
調(diào)用 getLogger(__name__)
創(chuàng)建一個和調(diào)用模塊同名的 logger 模塊。 由于模塊都是唯一的,因此創(chuàng)建的 logger 也將是唯一的。
log.addHandler(logging.NullHandler())
操作將一個空處理器綁定到剛剛已經(jīng)創(chuàng)建好的 logger 對象上。 一個空處理器默認(rèn)會忽略調(diào)用所有的日志消息。 因此,如果使用該函數(shù)庫的時候還沒有配置日志,那么將不會有消息或警告出現(xiàn)。
還有一點(diǎn)就是對于各個函數(shù)庫的日志配置可以是相互獨(dú)立的,不影響其他庫的日志配置。 例如,對于如下的代碼:
>>> import logging
>>> logging.basicConfig(level=logging.ERROR)
>>> import somelib
>>> somelib.func()
CRITICAL:somelib:A Critical Error!
>>> # Change the logging level for 'somelib' only
>>> logging.getLogger('somelib').level=logging.DEBUG
>>> somelib.func()
CRITICAL:somelib:A Critical Error!
DEBUG:somelib:A debug message
>>>
在這里,根日志被配置成僅僅輸出 ERROR 或更高級別的消息。 不過 ,somelib
的日志級別被單獨(dú)配置成可以輸出 debug 級別的消息,它的優(yōu)先級比全局配置高。 像這樣更改單獨(dú)模塊的日志配置對于調(diào)試來講是很方便的, 因?yàn)槟銦o需去更改任何的全局日志配置——只需要修改你想要更多輸出的模塊的日志等級。
Logging HOWTO 詳細(xì)介紹了如何配置日志模塊和其他有用技巧,可以參閱下。
你想記錄程序執(zhí)行多個任務(wù)所花費(fèi)的時間
time
模塊包含很多函數(shù)來執(zhí)行跟時間有關(guān)的函數(shù)。 盡管如此,通常我們會在此基礎(chǔ)之上構(gòu)造一個更高級的接口來模擬一個計時器。例如:
import time
class Timer:
def __init__(self, func=time.perf_counter):
self.elapsed = 0.0
self._func = func
self._start = None
def start(self):
if self._start is not None:
raise RuntimeError('Already started')
self._start = self._func()
def stop(self):
if self._start is None:
raise RuntimeError('Not started')
end = self._func()
self.elapsed += end - self._start
self._start = None
def reset(self):
self.elapsed = 0.0
@property
def running(self):
return self._start is not None
def __enter__(self):
self.start()
return self
def __exit__(self, *args):
self.stop()
這個類定義了一個可以被用戶根據(jù)需要啟動、停止和重置的計時器。 它會在 elapsed
屬性中記錄整個消耗時間。 下面是一個例子來演示怎樣使用它:
def countdown(n):
while n > 0:
n -= 1
# Use 1: Explicit start/stop
t = Timer()
t.start()
countdown(1000000)
t.stop()
print(t.elapsed)
# Use 2: As a context manager
with t:
countdown(1000000)
print(t.elapsed)
with Timer() as t2:
countdown(1000000)
print(t2.elapsed)
本節(jié)提供了一個簡單而實(shí)用的類來實(shí)現(xiàn)時間記錄以及耗時計算。 同時也是對使用 with 語句以及上下文管理器協(xié)議的一個很好的演示。
在計時中要考慮一個底層的時間函數(shù)問題。一般來說, 使用time.time()
或 time.clock()
計算的時間精度因操作系統(tǒng)的不同會有所不同。 而使用 time.perf_counter()
函數(shù)可以確保使用系統(tǒng)上面最精確的計時器。
上述代碼中由 Timer
類記錄的時間是鐘表時間,并包含了所有休眠時間。 如果你只想計算該進(jìn)程所花費(fèi)的 CPU 時間,應(yīng)該使用 time.process_time()
來代替:
t = Timer(time.process_time)
with t:
countdown(1000000)
print(t.elapsed)
time.perf_counter()
和 time.process_time()
都會返回小數(shù)形式的秒數(shù)時間。 實(shí)際的時間值沒有任何意義,為了得到有意義的結(jié)果,你得執(zhí)行兩次函數(shù)然后計算它們的差值。
更多關(guān)于計時和性能分析的例子請參考14.13小節(jié)。
你想對在 Unix 系統(tǒng)上面運(yùn)行的程序設(shè)置內(nèi)存或 CPU 的使用限制。
resource
模塊能同時執(zhí)行這兩個任務(wù)。例如,要限制 CPU 時間,可以像下面這樣做:
import signal
import resource
import os
def time_exceeded(signo, frame):
print("Time's up!")
raise SystemExit(1)
def set_max_runtime(seconds):
# Install the signal handler and set a resource limit
soft, hard = resource.getrlimit(resource.RLIMIT_CPU)
resource.setrlimit(resource.RLIMIT_CPU, (seconds, hard))
signal.signal(signal.SIGXCPU, time_exceeded)
if __name__ == '__main__':
set_max_runtime(15)
while True:
pass
程序運(yùn)行時,SIGXCPU
信號在時間過期時被生成,然后執(zhí)行清理并退出。
要限制內(nèi)存使用,設(shè)置可使用的總內(nèi)存值即可,如下:
import resource
def limit_memory(maxsize):
soft, hard = resource.getrlimit(resource.RLIMIT_AS)
resource.setrlimit(resource.RLIMIT_AS, (maxsize, hard))
像這樣設(shè)置了內(nèi)存限制后,程序運(yùn)行到?jīng)]有多余內(nèi)存時會拋出 MemoryError
異常。
在本節(jié)例子中,setrlimit()
函數(shù)被用來設(shè)置特定資源上面的軟限制和硬限制。 軟限制是一個值,當(dāng)超過這個值的時候操作系統(tǒng)通常會發(fā)送一個信號來限制或通知該進(jìn)程。 硬限制是用來指定軟限制能設(shè)定的最大值。通常來講,這個由系統(tǒng)管理員通過設(shè)置系統(tǒng)級參數(shù)來決定。 盡管硬限制可以改小一點(diǎn),但是最好不要使用用戶進(jìn)程去修改。
setrlimit()
函數(shù)還能被用來設(shè)置子進(jìn)程數(shù)量、打開文件數(shù)以及類似系統(tǒng)資源的限制。 更多詳情請參考 resource
模塊的文檔。
需要注意的是本節(jié)內(nèi)容只能適用于 Unix 系統(tǒng),并且不保證所有系統(tǒng)都能如期工作。 比如我們在測試的時候,它能在 Linux 上面正常運(yùn)行,但是在 OS X 上卻不能。
你想通過腳本啟動瀏覽器并打開指定的 URL 網(wǎng)頁
webbrowser
模塊能被用來啟動一個瀏覽器,并且與平臺無關(guān)。例如:
>>> import webbrowser
>>> webbrowser.open('http://www.python.org')
True
>>>
它會使用默認(rèn)瀏覽器打開指定網(wǎng)頁。如果你還想對網(wǎng)頁打開方式做更多控制,還可以使用下面這些函數(shù):
>>> # Open the page in a new browser window
>>> webbrowser.open_new('http://www.python.org')
True
>>>
>>> # Open the page in a new browser tab
>>> webbrowser.open_new_tab('http://www.python.org')
True
>>>
這樣就可以打開一個新的瀏覽器窗口或者標(biāo)簽,只要瀏覽器支持就行。
如果你想指定瀏覽器類型,可以使用webbrowser.get()
函數(shù)來指定某個特定瀏覽器。例如:
>>> c = webbrowser.get('firefox')
>>> c.open('http://www.python.org')
True
>>> c.open_new_tab('http://docs.python.org')
True
>>>
對于支持的瀏覽器名稱列表可查閱 Python 文檔
在腳本中打開瀏覽器有時候會很有用。例如,某個腳本執(zhí)行某個服務(wù)器發(fā)布任務(wù), 你想快速打開一個瀏覽器來確保它已經(jīng)正常運(yùn)行了。 或者是某個程序以 HTML 網(wǎng)頁格式輸出數(shù)據(jù),你想打開瀏覽器查看結(jié)果。 不管是上面哪種情況,使用webbrowser
模塊都是一個簡單實(shí)用的解決方案。