通过重定向/管道/文件接受输入
问题:
你希望你的脚本接受任何用户认为最简单的输入方式。包括将命令行的输出通过管道传递给该脚本、重定向文件到该脚本,或在命令行中传递一个文件名或文件名列表给该脚本
解决方案:
Python 内置的fileinput 模块让这个变得简单。如果你有一个下面这样的脚本:
1 import fileinput 2 3 4 with fileinput.input() as f_input: 5 for line in f_input: 6 print(line, end='')
那么你就能以前面提到的所有方式来为此脚本提供输入。假设你将此脚本保存为filein.py 并将其变为可执行文件,那么你可以像下面这样调用它,得到期望的输出:
$ 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.
终止程序并给出错误信息
问题:
你想向标准错误打印一条消息并返回某个非零状态码来终止程序运行
解决方案:
你有一个程序像下面这样终止,抛出一个SystemExit 异常,使用错误消息作为参数。例如:
raise SystemExit('It failed!')
它会将消息在sys.stderr 中打印,然后程序以状态码1 退出。
解析命令行选项
问题:
你的程序如何能够解析命令行选项(位于sys.argv 中)
解决方案:
argparse 模块可被用来解析命令行选项。下面一个简单例子演示了最基本的用法:
1 #search.py 2 3 import argparse 4 5 6 parser = argparse.ArgumentParser(description='Search some files') 7 8 parser.add_argument(dest='filenames', metavar='filename', nargs='*') 9 10 parser.add_argument('-p', '--pat',metavar='pattern', required=True, 11 dest='patterns', action='append', 12 help='text pattern to search for') 13 14 parser.add_argument('-v', dest='verbose', action='store_true', 15 help='verbose mode') 16 17 parser.add_argument('-o', dest='outfile', action='store', 18 help='output file') 19 20 parser.add_argument('--speed', dest='speed', action='store', 21 choices={'slow','fast'}, default='slow', 22 help='search speed') 23 24 args = parser.parse_args() 25 26 print(args.filenames) 27 print(args.patterns) 28 print(args.verbose) 29 print(args.outfile) 30 print(args.speed)
该程序定义了一个如下使用的命令行解析器:
$ python3 search.py -h usage: search.py [-h] -p pattern [-v] [-o OUTFILE] [--speed {fast,slow}] [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 {fast,slow} search speed
下面的部分演示了程序中的数据部分。仔细观察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
为了解析命令行选项, 你首先要创建一个ArgumentParser 实例, 并使用add argument() 方法声明你想要支持的选项。在每个add-argument() 调用中,dest参数指定解析结果被指派给属性的名字。metavar 参数被用来生成帮助信息。action参数指定跟属性对应的处理逻辑,通常的值为store , 被用来存储某个值或讲多个参数值收集到一个列表中。下面的参数收集所有剩余的命令行参数到一个列表中。在本例中它被用来构造一个文件名列表:
parser.add_argument(dest='filenames',metavar='filename', nargs='*')
下面的参数根据参数是否存在来设置一个Boolean 标志:
parser.add_argument('-v', dest='verbose', action='store_true', help='verbose mode')
下面的参数接受一个单独值并将其存储为一个字符串:
parser.add_argument('-o', dest='outfile', action='store', help='output file')
下面的参数说明允许某个参数重复出现多次,并将它们追加到一个列表中去。required 标志表示该参数至少要有一个。-p 和--pat 表示两个参数名形式都可使用。
parser.add_argument('-p', '--pat',metavar='pattern', required=True, dest='patterns', action='append', help='text pattern to search for')
最后,下面的参数说明接受一个值,但是会将其和可能的选择值做比较,以检测其合法性:
parser.add_argument('--speed', dest='speed', action='store', choices={'slow','fast'}, default='slow', help='search speed')
运行时弹出密码输入提示
问题:
你写了个脚本,运行时需要一个密码。此脚本是交互式的,因此不能将密码在脚本中硬编码,而是需要弹出一个密码输入提示,让用户自己输入
解决方案:
这时候Python 的getpass 模块正是你所需要的。你可以让你很轻松的弹出密码输入提示,并且不会在用户终端回显密码。下面是具体代码:
1 import getpass 2 3 user = input('Enter your username: ') 4 passwd = getpass.getpass() 5 6 def svc_login(user, passwd): 7 if user == 'demon' and passwd == 'demon': 8 return True 9 return 10 11 if svc_login(user, passwd): 12 print('Yay!') 13 else: 14 print('Boom~')
注意:
有些系统可能不支持getpass() 方法隐藏输入密码。这种情况下,Python 会提前警告你这些问题(例如它会警告你说密码会以明文形式显示)
执行外部命令并获取它的输出
问题:
你想执行一个外部命令并以Python 字符串的形式获取执行结果
解决方案:
使用subprocess.check output() 函数。例如:
1 out_bytes = subprocess.check_output(['df', '-h']).decode() 2 print(out_bytes)
默认情况下,check output() 仅仅返回输入到标准输出的值。如果你需要同时收集标准输出和错误输出,使用stderr 参数:
out_bytes = subprocess.check_output(['cmd','arg1','arg2'], stderr=subprocess.STDOUT)
如果你需要用一个超时机制来执行命令,使用timeout 参数:
1 try: 2 out_bytes = subprocess.check_output(['cmd','arg1','arg2'], stderr=subprocess.STDOUT, timeout=5) 3 except Exception as e: 4 print(e)
使用check output() 函数是执行外部命令并获取其返回值的最简单方式。但是,如果你需要对子进程做更复杂的交互,比如给它发送输入,你得采用另外一种方法。这时候可直接使用subprocess.Popen 类。例如:
1 p = subprocess.Popen('ls -l', stdout=subprocess.PIPE, stdin=subprocess.PIPE, shell=True) 2 data = p.stdout.read().decode('utf-8') 3 print(data)
subprocess 模块对于依赖TTY 的外部命令不合适用。例如,你不能使用它来自动化一个用户输入密码的任务(比如一个ssh 会话)。这时候,你需要使用到第三方模块了,比如基于著名的expect 家族的工具(pexpect 或类似的)
复制或者移动文件和目录
问题:
你想要复制或移动文件和目录,但是又不想调用shell 命令
解决方案:
shutil 模块有很多便捷的函数可以复制文件和目录。使用起来非常简单,比如:
1 import shutil 2 import os 3 4 ''' 5 # Copy src to dst. (cp src dst) 6 shutil.copy(src, dst) 7 8 # Copy files, but preserve metadata (cp -p src dst) 9 shutil.copy2(src, dst) 10 11 # Copy directory tree (cp -R src dst) 12 shutil.copytree(src, dst) 13 14 # Move src to dst (mv src dst) 15 shutil.move 16 ''' 17 18 #实例 19 src = '/etc/passwd' 20 dst = '/tmp/passwd.bak' 21 22 if os.path.exists(dst): 23 print('{} exists'.format(dst)) 24 else: 25 shutil.copy('/etc/passwd', '/tmp/passwd.bak') 26 print('{} to {} sucessful'.format(src, dst))
如果你想保留被复制目录中的符号链接,像这样做:
shutil.copytree(src, dst, symlinks=True)
copytree() 可以让你在复制过程中选择性的忽略某些文件或目录。你可以提供一个忽略函数,接受一个目录名和文件名列表作为输入,返回一个忽略的名称列表。例如:
1 def ignore_pyc_files(dirname, filenames): 2 return [name for name in filenames if name.endswith('.pyc')] 3 4 shutil.copytree(src, dst, ignore=ignore_pyc_files)
创建和解压归档文件
问题:
你需要创建或解压常见格式的归档文件(比如.tar, .tgz 或.zip)
解决方案:
shutil 模块拥有两个函数—— make archive() 和unpack archive() 可派上用场。例如:
1 import shutil 2 3 4 #解压zip的文件,第一个传入zip文件的路径,extract_dir是解压文件的目录名称 5 shutil.unpack_archive('/Users/demon/Desktop/python-3.6.2-embed-amd64.zip', extract_dir='/Users/demon/Desktop/python3.6.2') 6 7 #归档文件,把/Users/demon/Desktop/python3.6.2的文件打包成zip文件放到当前目录下 8 shutil.make_archive('python-3.6.2', 'zip','/Users/demon/Desktop/python3.6.2')
通过文件名查找文件
问题:
你需要写一个涉及到文件查找操作的脚本,比如对日志归档文件的重命名工具,你不想在Python 脚本中调用shell,或者你要实现一些shell 不能做的功能
解决方案:
查找文件,可使用os.walk() 函数,传一个顶级目录名给它。下面是一个例子,查找特定的文件名并答应所有符合条件的文件全路径:
1 import os 2 3 4 def findfile(start, name): 5 6 for dirpath, dirnames, filenames in os.walk(start): 7 if name in filenames: 8 full_path = os.path.join(start, dirpath, name ) 9 print(os.path.normpath(os.path.abspath(full_path))) 10 11 if __name__ == '__main__': 12 findfile('/etc/', 'bashrc')
os.walk() 方法为我们遍历目录树,每次进入一个目录,它会返回一个三元组,包含相对于查找目录的相对路径,一个该目录下的目录名列表,以及那个目录下面的文件名列表。
对于每个元组,只需检测一下目标文件名是否在文件列表中。如果是就使用os.path.join() 合并路径。为了避免奇怪的路径名比如././foo//bar ,使用了另外两个函数来修正结果。第一个是os.path.abspath() , 它接受一个路径,可能是相对路径,最后返回绝对路径。第二个是os.path.normpath() ,用来返回正常路径,可以解决双斜杆、对目录的多重引用的问题等。
尽管这个脚本相对于UNIX 平台上面的很多查找公交来讲要简单很多,它还有跨平台的优势。并且,还能很轻松的加入其他的功能。我们再演示一个例子,下面的函数打印所有最近被修改过的文件:
1 import os 2 import time 3 4 5 def modified_within(top, seconds): 6 now = time.time() 7 for path, dirs, files in os.walk(top): 8 for name in files: 9 full_path = os.path.join(path , name) 10 if os.path.exists(full_path): 11 mtime = os.path.getatime(full_path) 12 if mtime > (now - seconds): 13 print(full_path) 14 15 16 if __name__ == "__main__": 17 import sys 18 if len(sys.argv) != 3: 19 print('Usage: {} dir seconds'.format(sys.argv[0])) 20 raise SystemExit(1) 21 22 modified_within(sys.argv[1], float(sys.argv[2]))
读取配置文件
问题:
怎样读取普通.ini 格式的配置文件?
解决方案:
configparser 模块能被用来读取配置文件。例如,假设你有如下的配置文件:
; 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() 方法将其写回到文件中。例如:
1 import sys 2 3 cfg.set('server','port','9000') 4 cfg.set('debug','log_errors','False') 5 6 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 =================================
给简单脚本增加日志功能
问题:
你希望在脚本和程序中将诊断信息写入日志文件
解决方案:
The easiest way to add logging to simple programs is to use the logging module. Forexample: 打印日志最简单方式是使用logging 模块。例如:
1 import logging 2 3 4 def main(): 5 # Configure the logging system 6 logging.basicConfig( 7 filename='app.log', 8 level=logging.ERROR 9 ) 10 11 # Variables (to make the calls that follow work) 12 hostname = 'www.python.org' 13 item = 'spam' 14 filename = 'data.csv' 15 mode = 'r' 16 17 # Example logging calls (insert into your program) 18 logging.critical('Host %s unknown', hostname) 19 logging.error("Couldn't find %r", item) 20 logging.warning('Feature is deprecated') 21 logging.info('Opening file %r, mode=%r', filename, mode) 22 logging.debug('Got here') 23 24 if __name__ == '__main__': 25 main()
上面五个日志调用(critical(), error(), warning(), info(), debug())以降序方式表示不同的严重级别。basicConfig() 的level 参数是一个过滤器。所有级别低于此级别的日志消息都会被忽略掉。每个logging 操作的参数是一个消息字符串,后面再跟一个或多个参数。构造最终的日志消息的时候我们使用了% 操作符来格式化消息字符串。
运行这个程序后,在文件app.log 中的内容应该是下面这样:
CRITICAL:root:Host www.python.org unknown ERROR:root:Couldn't find 'spam'
如果你想改变输出等级,你可以修改basicConfig() 调用中的参数。例如:
1 logging.basicConfig( 2 filename='app.log', 3 level=logging.WARNING, 4 format='%(levelname)s:%(asctime)s:%(message)s' 5 )
最后输出变成如下:
CRITICAL:2017-08-14 21:04:53,869:Host www.python.org unknown ERROR:2017-08-14 21:04:53,869:Couldn't find 'spam' WARNING:2017-08-14 21:04:53,870:Feature is deprecated
上面的日志配置都是硬编码到程序中的。如果你想,可以像下面这样修改basicConfig() 调用:
1 import logging 2 import logging.config 3 4 5 def main(): 6 # Configure the logging system 7 logging.config.fileConfig('logconfig.ini') 8 9 # Variables (to make the calls that follow work) 10 hostname = 'www.python.org' 11 item = 'spam' 12 filename = 'data.csv' 13 mode = 'r' 14 15 # Example logging calls (insert into your program) 16 logging.critical('Host %s unknown', hostname) 17 logging.error("Couldn't find %r", item) 18 logging.warning('Feature is deprecated') 19 logging.info('Opening file %r, mode=%r', filename, mode) 20 logging.debug('Got here') 21 22 23 if __name__ == "__main__": 24 main()
创建一个下面这样的文件,名字叫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
最后输出变成如下:
CRITICAL:root:Host www.python.org unknown ERROR:root:Couldn't find 'spam' WARNING:root:Feature is deprecated INFO:root:Opening file 'data.csv', mode='r'
给函数库增加日志功能
问题:
你想给某个函数库增加日志功能,但是又不能影响到那些不使用日志功能的程序
解决方案:
对于想要执行日志操作的函数库而已,你应该创建一个专属的logger 对象,并且像下面这样初始化配置:
1 #somelib.py 2 3 import logging 4 5 6 log = logging.getLogger(__name__) 7 log.addHandler(logging.NullHandler()) 8 9 # Example function (for testing) 10 def func(): 11 log.critical('A Critical Error!') 12 log.debug('A debug message')
使用这个配置,默认情况下不会打印日志。例如:
>>> import somelib >>> somelib.func()
不过,如果配置过日志系统,那么日志消息打印就开始生效,例如:
>>> import logging >>> logging.basicConfig() >>> somelib.func() CRITICAL:somelib:A Critical Error!
调用getLogger( name ) 创建一个和调用模块同名的logger 模块。由于模块都是唯一的,因此创建的logger 也将是唯一的。
log.addHandler(logging.NullHandler()) 操作将一个空处理器绑定到刚刚已经创建好的logger 对象上。一个空处理器默认会忽略调用所有的日志消息。因此,如果使用该函数库的时候还没有配置日志,那么将不会有消息或警告出现。
1 import time 2 3 4 class Timer: 5 6 def __init__(self, func=time.perf_counter): 7 self.elapsed = 0.0 8 self._func = func 9 self._start = None 10 11 def start(self): 12 if self._start is not None: 13 raise RuntimeError('Already started') 14 self._start = self._func() 15 16 def stop(self): 17 if self._start is None: 18 raise RuntimeError('Not started') 19 end = self._func() 20 self.elapsed += end - self._start 21 self._start = None 22 23 def reset(self): 24 self.elapsed = 0.0 25 26 @property 27 def run(self): 28 return self._start is not None 29 30 def __enter__(self): 31 self.start() 32 return self 33 34 def __exit__(self, *args): 35 self.stop() 36 37 38 def countdown(n): 39 while n > 0: 40 n -= 1 41 42 t = Timer() 43 t.start() 44 countdown(100000) 45 t.stop() 46 print(t.elapsed) 47 48 49 #通过上下文执行 50 with t: 51 countdown(100000) 52 53 print(t.elapsed) 54 55 with Timer() as t2: 56 countdown(100000) 57 print(t2.elapsed)