实现进度条

1. 实战目标

对于进度条你一定不陌生,只要使用电脑就一定见过它,当软件的一个功能需要执行一定时间时就会提供进度条来显示当前任务的执行情况,比如杀毒软件扫描磁盘。

一些python命令行工具也会显示进度条,如pip 在安装第三方库时,有一个步骤就是下载安装包,这个过程中就会通过进度条来告知用户当前的下载进度。

本次实战目标就是使用python实现一个简单的可以在终端显示的进度条。

2. 考察知识点

  1. 回车符 \r
  2. sys.stdout.write
  3. 进度条的基本思路
  4. 类的定义与使用
  5. for循环

3. 思路讲解与实现

3.1 解决进度条变化

实现进度条,首先要解决的是如何让进度条的内容始终在一行输出,这样才能实现动态变化的效果。可日常使用print输出时,每一次调用print函数都会让光标移动到下一行,这是因为print函数默认在输出结束时输出一个换行符\n,便于下一次输出不会覆盖上一行的输出。

但实现进度条时,恰恰需要让下一次的输出覆盖上一次的输出,为了便于控制输出的内容,我没有使用print函数,而是直接使用了sys.stdout.write,它将数据写入到标准输出流,也就是控制台或终端,它不会自动换行,如果你经常阅读python项目的源码就会注意到,很多源码里都喜欢用它来实现终端的输出。

想要解决换行的问题,需要使用到\r 回车符,它让光标回到行首,不会开启新的一行,运行下面的代码,来体验它的功能。

import time
import sys

sys.stdout.write("###\r")
time.sleep(1)

sys.stdout.write("########\r")
time.sleep(1)

sys.stdout.write("################\r")

终端的光标会一直停留在第一行,下一次输出的内容会覆盖掉前一次的输出,由于下一次输出的字符串更长,因此你会觉得是进度条变长了。

3.2 进度条基本概念

进度条长度
进度条的长度相当于分母,当前进度相当于分子,这样才能算出进度的百分比,进度条长度

进度条显示符
它用来显示进度条,常见的有# 和 *

终端的长度

进度条受限于终端,终端大,进度条可以显示的内容就多,反之可以显示的内容就少,终端的长度决定了进度条可以显示多少个字符。

需要显示的进度字符个数

class EasyProcessBar():
    def __init__(self, name, total):
        self.name = name                # 进度条名称
        self.total = total              # 进度总数
        self.complete_count = 0         # 已经完成的进度数
        self.char_count = 0             # 需要显示的进度字符个数
        self.char_show_count = 0        # 已经显示的进度字符个数
        self.bar_char = "#"             # 进度条显示符

        sz = os.get_terminal_size()     # 获取终端长和宽
        self.char_count = sz.columns - len(name) - 15

os.get_terminal_size() 返回当前终端的长和宽,sz.columns 就是终端的长度,你可以理解为终端从左到有的长度,这个长度减去name的长度再减去15 ,就是需要显示的字符数量,这个值是怎么算计算的呢?

进度条需要一个名字,在进度条的最前面显示,在进度条的后面预留15个字符串的长度来显示百分比,中间的部分才是进度条变化的部分,这部分需要显示的字符数量就是self.char_count

3.3 计算显示进度的字符个数

进度条长度是没有单位的,这个值你可以随意设置,代码需要提供一个方法来更新进度,比如你把进度条长度设置为1000, 那么你需要提供一个update方法,这个方法提供一个incr参数表示这一次方法调用要增加多少进度,比如初始状态,第一次调用增加的进度是10,那么当前进度就是1%, 有了这个比例,就可以计算进度条里需要增加的字符数量了:0.01 * self.char_count, 这个值的计算结果就是本次进度条要显示的长度。

字符# 是不能分隔的,如果进度增加的程度还不足以增加一个#字符,进度条就不能变化,毕竟屏幕大小优先,我们需要用字符数量的变化来反应出进度的变化。

    def process_string(self):
        """
        进度
        :return:
        """
        ratio = self.complete_count/self.total                  # 已经完成的百分比
        char_show_count = int(ratio * self.char_count)          # 需要显示的#的数量
        if char_show_count > self.char_show_count:
            self.char_show_count = char_show_count

3.4 计算进度条要显示的内容

最终,我们需要在终端里输出一个字符串,这个字符串的长度必须是固定的,只有这样才会让显示百分比的地方固定不变

# 进度条,预留出足够的宽度给百分比
        process = "{process:<{count}}".format(process=self.bar_char*self.char_show_count, count=self.char_count)
        percent = '{:.0%}'.format(ratio)            # 百分比
        line = f"{self.name} {process} {percent}\r"
        return line

这段代码里对字符串的格式操作,很有必要仔细讲解

  1. "{process:<{count}}" 格式化过程中,要对process变量进行填充,字符串的长度是固定的,为count, 这里面的小于号< 表示 左对齐
  2. '{:.0%}' 表示将数字转换百分比的形式,而且只保留整数部分,如果想保留到小数点后两位,可以这样写 '{:.2%}'
  3. f"{self.name} {process} {percent}\r" process和 percent 前面都做了格式化了,这段代码使用了f-string 语法来格式化字符串,在字符串的末尾加上换行符

3.5 更新进度

    def update(self, incr=1):
        self.complete_count += incr
        if self.complete_count >= self.total:
            self.complete_count = self.total

        self.flush()

    def flush(self):
        sys.stdout.write(self.process_string)
        sys.stdout.flush()

update 用来更新进度条,这个方法是由进度条的调用者来负责调用的,调用者根据实际的进度情况来调用update方法,默一次调用将已完成进度加1

if __name__ == '__main__':
    epb = EasyProcessBar("download", 130)
    for i in range(130):
        epb.update()
        time.sleep(random.uniform(0.1, 0.5))

我设置进度条总长度为130 , 在for循环里,模拟实际的工作进度,为此我使用sleep方法随机sleep一段时间,实际使用时,则是要根据具体的业务进度来调用update方法。

4. 完整代码

import os
import sys
import time
import random


class EasyProcessBar():
    def __init__(self, name, total):
        self.name = name                # 进度条名称
        self.total = total              # 进度总数
        self.complete_count = 0         # 已经完成的进度数
        self.char_count = 0             # 需要显示的进度字符个数
        self.char_show_count = 0        # 已经显示的进度字符个数
        self.bar_char = "#"             # 进度条显示符

        sz = os.get_terminal_size()     # 获取终端长和宽
        self.char_count = sz.columns - len(name) - 15

    @property
    def process_string(self):
        """
        进度
        :return:
        """
        ratio = self.complete_count/self.total                  # 已经完成的百分比
        char_show_count = int(ratio * self.char_count)          # 需要显示的#的数量
        if char_show_count > self.char_show_count:
            self.char_show_count = char_show_count

        if self.bar_char == "#":
            self.bar_char = "*"
        else:
            self.bar_char = "#"

        # 进度条,预留出足够的宽度给百分比
        process = "{process:<{count}}".format(process=self.bar_char*self.char_show_count, count=self.char_count)
        percent = '{:.0%}'.format(ratio)            # 百分比
        line = f"{self.name} {process} {percent}\r"
        return line

    def update(self, incr=1):
        self.complete_count += incr
        if self.complete_count >= self.total:
            self.complete_count = self.total

        self.flush()

    def flush(self):
        sys.stdout.write(self.process_string)
        sys.stdout.flush()


if __name__ == '__main__':
    epb = EasyProcessBar("download", 130)
    for i in range(130):
        epb.update()
        time.sleep(random.uniform(0.1, 0.5))


扫描关注, 与我技术互动

QQ交流群: 211426309

加入知识星球, 每天收获更多精彩内容

分享日常研究的python技术和遇到的问题及解决方案