最近用到了一个库tqdm很有意思,它可以直接在终端中输出百分比进度条,还有预计时间,很不错,今天特地来研究下这个库。

以下是运行效果:

http://7xq6lu.com1.z0.glb.clouddn.com/tqdm.png
http://7xq6lu.com1.z0.glb.clouddn.com/tqdm.png

那么就来看看这个库吧

用法

首先这个库的用法是这样的,很简单,这样运行后,就会有输出进度条了。简直丧心病狂的简单,那到底是怎么做到的呢!

1
2
3
from tqdm import tqdm
for i in tqdm(range(10000)):
...

tqdm

点击tqdm后进入源代码模式

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
def tqdm(iterable, desc='', total=None, leave=False, file=sys.stderr,
mininterval=0.5, miniters=1):
"""
给定一个可迭代对象,返回一个可以迭代的对象,但是会打印出进度条来,并且每当有一个元素被迭代的时候就可以更新运行时间和剩余时间以及进度条
desc: 包含一个简短的string,用来描述进度条,会在一行的最前面
total: 可以给定预期的迭代次数,如果没有给定,将会使用len(iterable)
如果leave为False,则tqdm在完成后删除屏幕上的进度条
如果少于mininterval秒或者miniters个迭代上次进度条更新的时候已经过了,将不会再次更新,也就是说,首先会检测是否到迭代个数了,然后才会检测时间
file:表示输出位置,默认是sys.stderr
"""
if total is None:
try:
total = len(iterable)
except TypeError:
total = None
# 如果给定desc,才会有prefix
prefix = desc+': ' if desc else ''
sp = StatusPrinter(file)
sp.print_status(prefix + format_meter(0, total, 0))
start_t = last_print_t = time.time()
last_print_n = 0
n = 0
for obj in iterable:
# 居然是一个生成器函数,每次迭代会返回迭代元素
# 然后继续执行下面的逻辑,然后再在此处暂停
yield obj
# Now the object was created and processed, so we can print the meter.
n += 1
# 首先判断迭代次数
if n - last_print_n >= miniters:
# We check the counter first, to reduce the overhead of time.time()
cur_t = time.time()
# 其次判断间隔时间
if cur_t - last_print_t >= mininterval:
sp.print_status(prefix + format_meter(n, total, cur_t-start_t))
last_print_n = n
last_print_t = cur_t
# 如果leave为False,最后屏幕上就会清除进度条
if not leave:
sp.print_status('')
sys.stdout.write('\r')
else:
if last_print_n < n:
cur_t = time.time()
# 将进度条输满
sp.print_status(prefix + format_meter(n, total, cur_t-start_t))
file.write('\n')

可以看到主逻辑还是很简单的,但是关键是在于这个StatusPrinterformat_meter是如何在终端上输出的

format_meter

这个函数就是专门来控制进度条打印的,传入已经完成的迭代次数,总的迭代次数和已经耗费的时间

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
def format_meter(n, total, elapsed):
if n > total:
total = None
elapsed_str = format_interval(elapsed)
# 一秒执行几个
rate = '%5.2f' % (n / elapsed) if elapsed else '?'
if total:
# 如果有总数,那么将输出进度条
# 目前进行的比例
frac = float(n) / total
N_BARS = 10
# 进度条的长度
bar_length = int(frac*N_BARS)
# 整个进度条
bar = '#'*bar_length + '-'*(N_BARS-bar_length)
# 进行的百分比 %%输出百分号
percentage = '%3d%%' % (frac * 100)
# 剩余时间
left_str = format_interval(elapsed / n * (total-n)) if n else '?'
# 返回进度条和时间
return '|%s| %d/%d %s [elapsed: %s left: %s, %s iters/sec]' % (
bar, n, total, percentage, elapsed_str, left_str, rate)
else:
# 如果没有total,则直接返回目前进行的,所花时间和速度字符串
return '%d [elapsed: %s, %s iters/sec]' % (n, elapsed_str, rate)

其中format_interval是用来将时间格式化的函数

1
2
3
4
5
6
7
8
def format_interval(t):
# 首先求出时分秒,divmod输出分别是商和余数
mins, s = divmod(int(t), 60)
h, m = divmod(mins, 60)
if h:
return '%d:%02d:%02d' % (h, m, s)
else:
return '%02d:%02d' % (m, s)

可以看到,这里就是对进度条的长度之类做了控制,进度条总长一定,然后进行的长度和剩余的长度根据已经执行的进行控制,然后格式化时间以及剩余时间,返回一个字符串。

StatusPrinter

源代码很少,关键在于self.file.write

1
2
3
4
5
6
7
8
9
10
class StatusPrinter(object):
def __init__(self, file):
self.file = file
# 记录上次的打印长度
self.last_printed_len = 0
def print_status(self, s):
self.file.write('\r'+s+' '*max(self.last_printed_len-len(s), 0))
self.file.flush()
self.last_printed_len = len(s)

这里涉及一个\r\n的区分,\r是回车,就是回到这一行的开始,\n是走纸,会到下一行,但是位置还在目前的列,所以\r\n就是到下一行的行首,但是Linux只用\n来换行,所以这里用\r来回到开头进行打印

然后后面的就很好理解了,比如这一次打印长度80,下一次是50,那么多余的30就要用空白符来覆盖掉。

小结

这就是tqdm库的主要部分了,看起来挺简单的,要是自己做,需要控制很多细节问题,不过真的是学到了。