鍍金池/ 教程/ Python/ 腳本編程與系統(tǒng)管理
類與對(duì)象
模塊與包
數(shù)據(jù)編碼和處理
元編程
網(wǎng)絡(luò)與 Web 編程
數(shù)字日期和時(shí)間
測(cè)試、調(diào)試和異常
字符串和文本
文件與 IO
腳本編程與系統(tǒng)管理
迭代器與生成器
函數(shù)
C 語言擴(kuò)展
并發(fā)編程
數(shù)據(jù)結(jié)構(gòu)和算法

腳本編程與系統(tǒng)管理

許多人使用 Python 作為一個(gè) shell 腳本的替代,用來實(shí)現(xiàn)常用系統(tǒng)任務(wù)的自動(dòng)化,如文件的操作,系統(tǒng)的配置等。本章的主要目標(biāo)是描述光宇編寫腳本時(shí)候經(jīng)常遇到的一些功能。例如,解析命令行選項(xiàng)、獲取有用的系統(tǒng)配置數(shù)據(jù)等等。第5章也包含了與文件和目錄相關(guān)的一般信息。

通過重定向/管道/文件接受輸入

問題

你希望你的腳本接受任何用戶認(rèn)為最簡(jiǎn)單的輸入方式。包括將命令行的輸出通過管道傳遞給該腳本、 重定向文件到該腳本,或在命令行中傳遞一個(gè)文件名或文件名列表給該腳本。

解決方案

Python 內(nèi)置的 fileinput 模塊讓這個(gè)變得簡(jiǎn)單。如果你有一個(gè)下面這樣的腳本:

#!/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)建并返回一個(gè) FileInput類的實(shí)例。 該實(shí)例除了擁有一些有用的幫助方法外,它還可被當(dāng)做一個(gè)上下文管理器使用。 因此,整合起來,如果我們要寫一個(gè)打印多個(gè)文件輸出的腳本,那么我們需要在輸出中包含文件名和行號(hào),如下所示:

>>> 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>

通過將它作為一個(gè)上下文管理器使用,可以確保它不再使用時(shí)文件能自動(dòng)關(guān)閉, 而且我們?cè)谥愡€演示了 FileInput 的一些有用的幫助方法來獲取輸出中的一些其他信息。

終止程序并給出錯(cuò)誤信息

問題

你想向標(biāo)準(zhǔn)錯(cuò)誤打印一條消息并返回某個(gè)非零狀態(tài)碼來終止程序運(yùn)行

解決方案

你有一個(gè)程序像下面這樣終止,拋出一個(gè) SystemExit 異常,使用錯(cuò)誤消息作為參數(shù)。例如:

raise SystemExit('It failed!')

它會(huì)將消息在 sys.stderr 中打印,然后程序以狀態(tài)碼1退出。

討論

本節(jié)雖然很短小,但是它能解決在寫腳本時(shí)的一個(gè)常見問題。 也就是說,當(dāng)你想要終止某個(gè)程序時(shí),你可能會(huì)像下面這樣寫:

import sys
sys.stderr.write('It failed!\n')
raise SystemExit(1)

如果你直接將消息作為參數(shù)傳給 SystemExit() ,那么你可以省略其他步驟, 比如 import 語句或?qū)㈠e(cuò)誤消息寫入 sys.stderr

解析命令行選項(xiàng)

問題

你的程序如何能夠解析命令行選項(xiàng)(位于 sys.argv 中)

解決方案

argparse 模塊可被用來解析命令行選項(xiàng)。下面一個(gè)簡(jiǎn)單例子演示了最基本的用法:

# 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)

該程序定義了一個(gè)如下使用的命令行解析器:

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

對(duì)于選項(xiàng)值的進(jìn)一步處理由程序來決定,用你自己的邏輯來替代 print() 函數(shù)。

討論

argparse 模塊是標(biāo)準(zhǔn)庫中最大的模塊之一,擁有大量的配置選項(xiàng)。 本節(jié)只是演示了其中最基礎(chǔ)的一些特性,幫助你入門。

為了解析命令行選項(xiàng),你首先要?jiǎng)?chuàng)建一個(gè) ArgumentParser 實(shí)例, 并使用 add_argument()方法聲明你想要支持的選項(xiàng)。 在每個(gè)add-argument() 調(diào)用中,dest 參數(shù)指定解析結(jié)果被指派給屬性的名字。 metavar 參數(shù)被用來生成幫助信息。action 參數(shù)指定跟屬性對(duì)應(yīng)的處理邏輯, 通常的值為 store ,被用來存儲(chǔ)某個(gè)值或講多個(gè)參數(shù)值收集到一個(gè)列表中。 下面的參數(shù)收集所有剩余的命令行參數(shù)到一個(gè)列表中。在本例中它被用來構(gòu)造一個(gè)文件名列表:

parser.add_argument(dest='filenames',metavar='filename', nargs='*')

下面的參數(shù)根據(jù)參數(shù)是否存在來設(shè)置一個(gè) Boolean 標(biāo)志:

parser.add_argument('-v', dest='verbose', action='store_true',
                    help='verbose mode')

下面的參數(shù)接受一個(gè)單獨(dú)值并將其存儲(chǔ)為一個(gè)字符串:

parser.add_argument('-o', dest='outfile', action='store',
                    help='output file')

下面的參數(shù)說明允許某個(gè)參數(shù)重復(fù)出現(xiàn)多次,并將它們追加到一個(gè)列表中去。required 標(biāo)志表示該參數(shù)至少要有一個(gè)。-p--pat表示兩個(gè)參數(shù)名形式都可使用。

parser.add_argument('-p', '--pat',metavar='pattern', required=True,
                    dest='patterns', action='append',
                    help='text pattern to search for')

最后,下面的參數(shù)說明接受一個(gè)值,但是會(huì)將其和可能的選擇值做比較,以檢測(cè)其合法性:

parser.add_argument('--speed', dest='speed', action='store',
                    choices={'slow','fast'}, default='slow',
                    help='search speed')

一旦參數(shù)選項(xiàng)被指定,你就可以執(zhí)行 parser.parse()方法了。 它會(huì)處理 sys.argv 的值并返回一個(gè)結(jié)果實(shí)例。 每個(gè)參數(shù)值會(huì)被設(shè)置成該實(shí)例中add_argument() 方法的dest參數(shù)指定的屬性值。

還很多種其他方法解析命令行選項(xiàng)。 例如,你可能會(huì)手動(dòng)的處理 sys.argv或者使用 getopt模塊。 但是,如果你采用本節(jié)的方式,將會(huì)減少很多冗余代碼,底層細(xì)節(jié) argparse 模塊已經(jīng)幫你處理了。 你可能還會(huì)碰到使用 optparse庫解析選項(xiàng)的代碼。 盡管 optparseargparse 很像,但是后者更先進(jìn),因此在新的程序中你應(yīng)該使用它。

運(yùn)行時(shí)彈出密碼輸入提示

問題

你寫了個(gè)腳本,運(yùn)行時(shí)需要一個(gè)密碼。此腳本是交互式的,因此不能將密碼在腳本中硬編碼, 而是需要彈出一個(gè)密碼輸入提示,讓用戶自己輸入。

解決方案

這時(shí)候 Python 的 getpass 模塊正是你所需要的。你可以讓你很輕松的彈出密碼輸入提示, 并且不會(huì)在用戶終端回顯密碼。下面是具體代碼:

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() 不會(huì)彈出用戶名的輸入提示。 它會(huì)根據(jù)該用戶的shell環(huán)境或者會(huì)依據(jù)本地系統(tǒng)的密碼庫(支持 pwd 模塊的平臺(tái))來使用當(dāng)前用戶的登錄名,

如果你想顯示的彈出用戶名輸入提示,使用內(nèi)置的 input 函數(shù):

user = input('Enter your username: ')

還有一點(diǎn)很重要,有些系統(tǒng)可能不支持 getpass()方法隱藏輸入密碼。 這種情況下,Python 會(huì)提前警告你這些問題(例如它會(huì)警告你說密碼會(huì)以明文形式顯示)

獲取終端的大小

問題

你需要知道當(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)用一個(gè)簡(jiǎn)單的函數(shù)呢?

執(zhí)行外部命令并獲取它的輸出

問題

你想執(zhí)行一個(gè)外部命令并以 Python 字符串的形式獲取執(zhí)行結(jié)果。

解決方案

使用 subprocess.check_output() 函數(shù)。例如:

import subprocess
out_bytes = subprocess.check_output(['netstat','-a'])

這段代碼執(zhí)行一個(gè)指定的命令并將執(zhí)行結(jié)果以一個(gè)字節(jié)字符串的形式返回。 如果你需要文本形式返回,加一個(gè)解碼步驟即可。例如:

out_text = out_bytes.decode('utf-8')

如果被執(zhí)行的命令以非零碼返回,就會(huì)拋出異常。 下面的例子捕獲到錯(cuò)誤并獲取返回碼:

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)輸出的值。 如果你需要同時(shí)收集標(biāo)準(zhǔn)輸出和錯(cuò)誤輸出,使用 stderr 參數(shù):

out_bytes = subprocess.check_output(['cmd','arg1','arg2'],
                                    stderr=subprocess.STDOUT)

如果你需要用一個(gè)超時(shí)機(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)。 一個(gè)字符串列表會(huì)被傳遞給一個(gè)低級(jí)系統(tǒng)命令,比如 os.execve() 。 如果你想讓命令被一個(gè) shell 執(zhí)行,傳遞一個(gè)字符串參數(shù),并設(shè)置參數(shù) shell=True . 有時(shí)候你想要 Python 去執(zhí)行一個(gè)復(fù)雜的 shell 命令的時(shí)候這個(gè)就很有用了,比如管道流、I/O 重定向和其他特性。例如:

out_bytes = subprocess.check_output('grep python | wc > out', shell=True)

需要注意的是在 shell 中執(zhí)行命令會(huì)存在一定的安全風(fēng)險(xiǎn),特別是當(dāng)參數(shù)來自于用戶輸入時(shí)。 這時(shí)候可以使用 shlex.quote() 函數(shù)來講參數(shù)正確的用雙引用引起來。

討論

使用 check_output() 函數(shù)是執(zhí)行外部命令并獲取其返回值的最簡(jiǎn)單方式。 但是,如果你需要對(duì)子進(jìn)程做更復(fù)雜的交互,比如給它發(fā)送輸入,你得采用另外一種方法。 這時(shí)候可直接使用 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模塊對(duì)于依賴 TTY 的外部命令不合適用。 例如,你不能使用它來自動(dòng)化一個(gè)用戶輸入密碼的任務(wù)(比如一個(gè) ssh 會(huì)話)。 這時(shí)候,你需要使用到第三方模塊了,比如基于著名的 expect 家族的工具(pexpect 或類似的)

復(fù)制或者移動(dòng)文件和目錄

問題

你想喲啊復(fù)制或移動(dòng)文件和目錄,但是又不想調(diào)用 shell 命令。

解決方案

shutil 模塊有很多便捷的函數(shù)可以復(fù)制文件和目錄。使用起來非常簡(jiǎn)單,比如:

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)情況下,對(duì)于符號(hào)鏈接而已這些命令處理的是它指向的東西。 例如,如果源文件是一個(gè)符號(hào)鏈接,那么目標(biāo)文件將會(huì)是符號(hào)鏈接指向的文件。 如果你只想復(fù)制符號(hào)鏈接本身,那么需要指定關(guān)鍵字參數(shù) follow_symlinks,如下:

如果你想保留被復(fù)制目錄中的符號(hào)鏈接,像這樣做:

shutil.copytree(src, dst, symlinks=True)

copytree() 可以讓你在復(fù)制過程中選擇性的忽略某些文件或目錄。 你可以提供一個(gè)忽略函數(shù),接受一個(gè)目錄名和文件名列表作為輸入,返回一個(gè)忽略的名稱列表。例如:

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ù)制文件和目錄也忒簡(jiǎn)單了點(diǎn)吧。 不過,對(duì)于文件元數(shù)據(jù)信息,copy2() 這樣的函數(shù)只能盡自己最大能力來保留它。 訪問時(shí)間、創(chuàng)建時(shí)間和權(quán)限這些基本信息會(huì)被保留, 但是對(duì)于所有者、ACLs、資源 fork 和其他更深層次的文件元信息就說不準(zhǔn)了, 這個(gè)還得依賴于底層操作系統(tǒng)類型和用戶所擁有的訪問權(quán)限。 你通常不會(huì)去使用shutil.copytree() 函數(shù)來執(zhí)行系統(tǒng)備份。 當(dāng)處理文件名的時(shí)候,最好使用os.path 中的函數(shù)來確保最大的可移植性(特別是同時(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ù)制文件夾的一個(gè)棘手的問題是對(duì)于錯(cuò)誤的處理。 例如,在復(fù)制過程中,函數(shù)可能會(huì)碰到損壞的符號(hào)鏈接,因?yàn)闄?quán)限無法訪問文件的問題等等。 為了解決這個(gè)問題,所有碰到的問題會(huì)被收集到一個(gè)列表中并打包為一個(gè)單獨(dú)的異常,到了最后再拋出。 下面是一個(gè)例子:

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, 這時(shí)候 copytree() 會(huì)忽略掉無效符號(hào)鏈接。

本節(jié)演示的這些函數(shù)都是最常見的。不過,shutil 還有更多的和復(fù)制數(shù)據(jù)相關(guān)的操作。 它的文檔很值得一看,參考 Python documentation

創(chuàng)建和解壓歸檔文件

問題

你需要?jiǎng)?chuàng)建或解壓常見格式的歸檔文件(比如.tar, .tgz或.zip)

解決方案

shutil 模塊擁有兩個(gè)函數(shù)—— make_archive()unpack_archive() 可派上用場(chǎng)。 例如:

>>> 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()的第二個(gè)參數(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é)。 不過,如果你僅僅只是要?jiǎng)?chuàng)建或提取某個(gè)歸檔,就沒有必要使用底層庫了。 可以直接使用shutil中的這些高層函數(shù)。

這些函數(shù)還有很多其他選項(xiàng),用于日志打印、預(yù)檢、文件權(quán)限等等。 參考 shutil 文檔

通過文件名查找文件

問題

你需要寫一個(gè)涉及到文件查找操作的腳本,比如對(duì)日志歸檔文件的重命名工具, 你不想在 Python 腳本中調(diào)用 shell,或者你要實(shí)現(xiàn)一些 shell 不能做的功能。

解決方案

查找文件,可使用 os.walk() 函數(shù),傳一個(gè)頂級(jí)目錄名給它。 下面是一個(gè)例子,查找特定的文件名并答應(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)入一個(gè)目錄,它會(huì)返回一個(gè)三元組,包含相對(duì)于查找目錄的相對(duì)路徑,一個(gè)該目錄下的目錄名列表, 以及那個(gè)目錄下面的文件名列表。

對(duì)于每個(gè)元組,只需檢測(cè)一下目標(biāo)文件名是否在文件列表中。如果是就使用 os.path.join() 合并路徑。 為了避免奇怪的路徑名比如 ././foo//bar ,使用了另外兩個(gè)函數(shù)來修正結(jié)果。 第一個(gè)是 os.path.abspath(),它接受一個(gè)路徑,可能是相對(duì)路徑,最后返回絕對(duì)路徑。 第二個(gè)是os.path.normpath() ,用來返回正常路徑,可以解決雙斜桿、對(duì)目錄的多重引用的問題等。

盡管這個(gè)腳本相對(duì)于 UNIX 平臺(tái)上面的很多查找公交來講要簡(jiǎn)單很多,它還有跨平臺(tái)的優(yōu)勢(shì)。 并且,還能很輕松的加入其他的功能。 我們?cè)傺菔疽粋€(gè)例子,下面的函數(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
    =================================

下面是一個(gè)讀取和提取其中值的例子:

>>> 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
          =================================
>>>

討論

配置文件作為一種可讀性很好的格式,非常適用于存儲(chǔ)程序中的配置數(shù)據(jù)。 在每個(gè)配置文件中,配置數(shù)據(jù)會(huì)被分組(比如例子中的“installation”、 “debug” 和 “server”)。 每個(gè)分組在其中指定對(duì)應(yīng)的各個(gè)變量值。

對(duì)于可實(shí)現(xiàn)同樣功能的配置文件和 Python 源文件是有很大的不同的。 首先,配置文件的語法要更自由些,下面的賦值語句是等效的:

prefix=/usr/local
prefix: /usr/local

配置文件中的名字是不區(qū)分大小寫的。例如:

>>> cfg.get('installation','PREFIX')
'/usr/local'
>>> cfg.get('installation','prefix')
'/usr/local'
>>>

在解析值的時(shí)候,getboolean() 方法查找任何可行的值。例如下面都是等價(jià)的:

log_errors = true
log_errors = TRUE
log_errors = Yes
log_errors = 1

或許配置文件和 Python 代碼最大的不同在于,它并不是從上而下的順序執(zhí)行。 文件是安裝一個(gè)整體被讀取的。如果碰到了變量替換,它實(shí)際上已經(jīng)被替換完成了。 例如,在下面這個(gè)配置中,prefix變量在使用它的變量之前后之后定義都是可以的:

[installation]
library=%(prefix)s/lib
include=%(prefix)s/include
bin=%(prefix)s/bin
prefix=/usr/local

ConfigParser 有個(gè)容易被忽視的特性是它能一次讀取多個(gè)配置文件然后合并成一個(gè)配置。 例如,假設(shè)一個(gè)用戶像下面這樣構(gòu)造了他們的配置文件:

; ~/.config.ini
[installation]
prefix=/Users/beazley/test

[debug]
log_errors=False

讀取這個(gè)文件,它就能跟之前的配置合并起來。如:

>>> # 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ā)制人策略,以最后一個(gè)為準(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 文檔中的語法詳情以及支持特性。

給簡(jiǎn)單腳本增加日志功能

問題

你希望在腳本和程序中將診斷信息寫入日志文件。

解決方案

The easiest way to add logging to simple programs is to use the logging module. For example: 打印日志最簡(jiǎn)單方式是使用 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()

上面五個(gè)日志調(diào)用(critical(), error(), warning(), info(), debug())以降序方式表示不同的嚴(yán)重級(jí)別。 basicConfig()level 參數(shù)是一個(gè)過濾器。 所有級(jí)別低于此級(jí)別的日志消息都會(huì)被忽略掉。 每個(gè) logging 操作的參數(shù)是一個(gè)消息字符串,后面再跟一個(gè)或多個(gè)參數(shù)。 構(gòu)造最終的日志消息的時(shí)候我們使用了%操作符來格式化消息字符串。

運(yùn)行這個(gè)程序后,在文件 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: 如果你想改變輸出等級(jí),你可以修改 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)建一個(gè)下面這樣的文件,名字叫 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 即可。

討論

盡管對(duì)于 logging 模塊而已有很多更高級(jí)的配置選項(xiàng), 不過這里的方案對(duì)于簡(jiǎn)單的程序和腳本已經(jīng)足夠了。 只想在調(diào)用日志操作前先執(zhí)行下 basicConfig()函數(shù)方法,你的程序就能產(chǎn)生日志輸出了。

如果你想要你的日志消息寫到標(biāo)準(zhǔn)錯(cuò)誤中,而不是日志文件中,調(diào)用 basicConfig() 時(shí)不傳文件名參數(shù)即可。例如:

logging.basicConfig(level=logging.INFO)

basicConfig() 在程序中只能被執(zhí)行一次。如果你稍后想改變?nèi)罩九渲茫?就需要先獲取 root logger,然后直接修改它。例如:

logging.getLogger().level = logging.DEBUG

需要強(qiáng)調(diào)的是本節(jié)只是演示了 logging 模塊的一些基本用法。 它可以做更多更高級(jí)的定制。 關(guān)于日志定制化一個(gè)很好的資源是 Logging Cookbook

給函數(shù)庫增加日志功能

問題

你想給某個(gè)函數(shù)庫增加日志功能,但是又不能影響到那些不使用日志功能的程序。

解決方案

對(duì)于想要執(zhí)行日志操作的函數(shù)庫而已,你應(yīng)該創(chuàng)建一個(gè)專屬的 logger 對(duì)象,并且像下面這樣初始化配置:

# 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')

使用這個(gè)配置,默認(rèn)情況下不會(huì)打印日志。例如:

>>> import somelib
>>> somelib.func()
>>>

不過,如果配置過日志系統(tǒng),那么日志消息打印就開始生效,例如:

>>> import logging
>>> logging.basicConfig()
>>> somelib.func()
CRITICAL:somelib:A Critical Error!
>>>

討論

通常來講,你不應(yīng)該在函數(shù)庫代碼中自己配置日志系統(tǒng),或者是已經(jīng)假定有個(gè)已經(jīng)存在的日志配置了。

調(diào)用 getLogger(__name__) 創(chuàng)建一個(gè)和調(diào)用模塊同名的 logger 模塊。 由于模塊都是唯一的,因此創(chuàng)建的 logger 也將是唯一的。

log.addHandler(logging.NullHandler()) 操作將一個(gè)空處理器綁定到剛剛已經(jīng)創(chuàng)建好的 logger 對(duì)象上。 一個(gè)空處理器默認(rèn)會(huì)忽略調(diào)用所有的日志消息。 因此,如果使用該函數(shù)庫的時(shí)候還沒有配置日志,那么將不會(huì)有消息或警告出現(xiàn)。

還有一點(diǎn)就是對(duì)于各個(gè)函數(shù)庫的日志配置可以是相互獨(dú)立的,不影響其他庫的日志配置。 例如,對(duì)于如下的代碼:

>>> 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 或更高級(jí)別的消息。 不過 ,somelib 的日志級(jí)別被單獨(dú)配置成可以輸出 debug 級(jí)別的消息,它的優(yōu)先級(jí)比全局配置高。 像這樣更改單獨(dú)模塊的日志配置對(duì)于調(diào)試來講是很方便的, 因?yàn)槟銦o需去更改任何的全局日志配置——只需要修改你想要更多輸出的模塊的日志等級(jí)。

Logging HOWTO 詳細(xì)介紹了如何配置日志模塊和其他有用技巧,可以參閱下。

實(shí)現(xiàn)一個(gè)計(jì)時(shí)器

問題

你想記錄程序執(zhí)行多個(gè)任務(wù)所花費(fèi)的時(shí)間

解決方案

time模塊包含很多函數(shù)來執(zhí)行跟時(shí)間有關(guān)的函數(shù)。 盡管如此,通常我們會(huì)在此基礎(chǔ)之上構(gòu)造一個(gè)更高級(jí)的接口來模擬一個(gè)計(jì)時(shí)器。例如:

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()

這個(gè)類定義了一個(gè)可以被用戶根據(jù)需要啟動(dòng)、停止和重置的計(jì)時(shí)器。 它會(huì)在 elapsed 屬性中記錄整個(gè)消耗時(shí)間。 下面是一個(gè)例子來演示怎樣使用它:

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é)提供了一個(gè)簡(jiǎn)單而實(shí)用的類來實(shí)現(xiàn)時(shí)間記錄以及耗時(shí)計(jì)算。 同時(shí)也是對(duì)使用 with 語句以及上下文管理器協(xié)議的一個(gè)很好的演示。

在計(jì)時(shí)中要考慮一個(gè)底層的時(shí)間函數(shù)問題。一般來說, 使用time.time()time.clock() 計(jì)算的時(shí)間精度因操作系統(tǒng)的不同會(huì)有所不同。 而使用 time.perf_counter() 函數(shù)可以確保使用系統(tǒng)上面最精確的計(jì)時(shí)器。

上述代碼中由 Timer 類記錄的時(shí)間是鐘表時(shí)間,并包含了所有休眠時(shí)間。 如果你只想計(jì)算該進(jìn)程所花費(fèi)的 CPU 時(shí)間,應(yīng)該使用 time.process_time() 來代替:

t = Timer(time.process_time)
with t:
    countdown(1000000)
print(t.elapsed)

time.perf_counter()time.process_time() 都會(huì)返回小數(shù)形式的秒數(shù)時(shí)間。 實(shí)際的時(shí)間值沒有任何意義,為了得到有意義的結(jié)果,你得執(zhí)行兩次函數(shù)然后計(jì)算它們的差值。

更多關(guān)于計(jì)時(shí)和性能分析的例子請(qǐng)參考14.13小節(jié)。

限制內(nèi)存和 CPU 的使用量

問題

你想對(duì)在 Unix 系統(tǒng)上面運(yùn)行的程序設(shè)置內(nèi)存或 CPU 的使用限制。

解決方案

resource 模塊能同時(shí)執(zhí)行這兩個(gè)任務(wù)。例如,要限制 CPU 時(shí)間,可以像下面這樣做:

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)行時(shí),SIGXCPU 信號(hào)在時(shí)間過期時(shí)被生成,然后執(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)存時(shí)會(huì)拋出 MemoryError 異常。

討論

在本節(jié)例子中,setrlimit() 函數(shù)被用來設(shè)置特定資源上面的軟限制和硬限制。 軟限制是一個(gè)值,當(dāng)超過這個(gè)值的時(shí)候操作系統(tǒng)通常會(huì)發(fā)送一個(gè)信號(hào)來限制或通知該進(jìn)程。 硬限制是用來指定軟限制能設(shè)定的最大值。通常來講,這個(gè)由系統(tǒng)管理員通過設(shè)置系統(tǒng)級(jí)參數(shù)來決定。 盡管硬限制可以改小一點(diǎn),但是最好不要使用用戶進(jìn)程去修改。

setrlimit() 函數(shù)還能被用來設(shè)置子進(jìn)程數(shù)量、打開文件數(shù)以及類似系統(tǒng)資源的限制。 更多詳情請(qǐng)參考 resource 模塊的文檔。

需要注意的是本節(jié)內(nèi)容只能適用于 Unix 系統(tǒng),并且不保證所有系統(tǒng)都能如期工作。 比如我們?cè)跍y(cè)試的時(shí)候,它能在 Linux 上面正常運(yùn)行,但是在 OS X 上卻不能。

啟動(dòng)一個(gè) WEB 瀏覽器

問題

你想通過腳本啟動(dòng)瀏覽器并打開指定的 URL 網(wǎng)頁

解決方案

webbrowser 模塊能被用來啟動(dòng)一個(gè)瀏覽器,并且與平臺(tái)無關(guān)。例如:

>>> import webbrowser
>>> webbrowser.open('http://www.python.org')
True
>>>

它會(huì)使用默認(rèn)瀏覽器打開指定網(wǎng)頁。如果你還想對(duì)網(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
>>>

這樣就可以打開一個(gè)新的瀏覽器窗口或者標(biāo)簽,只要瀏覽器支持就行。

如果你想指定瀏覽器類型,可以使用webbrowser.get() 函數(shù)來指定某個(gè)特定瀏覽器。例如:

>>> c = webbrowser.get('firefox')
>>> c.open('http://www.python.org')
True
>>> c.open_new_tab('http://docs.python.org')
True
>>>

對(duì)于支持的瀏覽器名稱列表可查閱 Python 文檔

討論

在腳本中打開瀏覽器有時(shí)候會(huì)很有用。例如,某個(gè)腳本執(zhí)行某個(gè)服務(wù)器發(fā)布任務(wù), 你想快速打開一個(gè)瀏覽器來確保它已經(jīng)正常運(yùn)行了。 或者是某個(gè)程序以 HTML 網(wǎng)頁格式輸出數(shù)據(jù),你想打開瀏覽器查看結(jié)果。 不管是上面哪種情況,使用webbrowser 模塊都是一個(gè)簡(jiǎn)單實(shí)用的解決方案。