2
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

Python (および任意の言語) でプログレスバーを表示する方法と、その理解

Posted at

概要

Python (および任意の言語) でこんな感じの処理進行度を雑に表示したい。

Reading file   123 / 4000 ...

TL;DR

import sys
import time


def start_progress():
    sys.stdout.write("\033[?7l")


def print_progress(text: str):
    sys.stdout.write(f"\r\033[2K{text}")
    sys.stdout.flush()


def end_progress():
    sys.stdout.write("\033[?7h\n")


n = 20

start_progress()

for i in range(n):
    print_progress(f"Processing {i:3} /  {n}...")
    # なんか処理をする
    time.sleep(0.1)

end_progress()

print("Done!")

詳細

行頭に戻る

一度文字列を表示し、再度同じ行に文字列を上書きするには、 \r を使えばいいです。

ただし、一回目の表示の後改行が入ると、当然ながら次の行に進んでしまいます。
Python の場合 print 関数に end="" を渡すことで、改行を抑制できます。

print("Hello World!", end="")
print("\rHELLO WORLD!!")
出力
>>> print("Hello World!", end=""); print("\rHELLO WORLD!!")
HELLO WORLD!!

標準出力を flush させる

実際のプログラムで使う場合、この \r を使った出力を数回以上繰り返すことになります。

しかしこのままでは、出力がバッファリングされてしまい、途中経過が出力されない可能性があります。

したがって、 標準出力のバッファを .flush() して、強制的に現在の文字列を表示させます。
また、ついでに printsys.stdout.write で置き換えます 1

import sys
sys.stdout.write("Hello World!")
sys.stdout.flush()
sys.stdout.write("\rHELLO WORLD!!")
出力
$ python3 -c 'import sys; sys.stdout.write("Hello World!"); sys.stdout.flush(); sys.stdout.write("\rHELLO WORLD!!")'
HELLO WORLD!!

以前の出力内容を消去する

固定長の文字列を表示するだけなら、このコードで問題ありません。

しかし、二つ目の文字列が一つ目より短かった場合、その長さの差分の文字列が消えないまま残ってしまいます。

sys.stdout.write("Hello World!")
sys.stdout.flush()
sys.stdout.write("\rHi :)\n")
出力
$ python3 -c 'import sys; sys.stdout.write("Hello World!"); sys.stdout.flush(); sys.stdout.write("\rHi :)\n")'
Hi :) World!
     ^~~~~~~ ここが残ってしまっている

これに対して、\r と同時に以前の出力内容を消去することで対処します。

これには、エスケープシーケンス \033[2K が使えます。

エスケープシーケンス (escape sequence) とは、コンピュータシステムにおいて、通常の文字列では表せない特殊な文字や機能を、規定された特別な文字の並びにより表したもの。
Wikipedia - エスケープシーケンス

sys.stdout.write("Hello World!")
sys.stdout.flush()
sys.stdout.write("\r\033[2KHi :)\n")
#                   ^------ この部分
出力
$ python3 -c 'import sys; sys.stdout.write("Hello World!"); sys.stdout.flush(); sys.stdout.write("\r\033[2KHi :)\n")'
Hi :)

エスケープシーケンス \033[2K は、現在カーソルがある行の文字を全て消去します。
これと \r を組み合わせることで、前回の出力内容をリセットした上でカーソルを行頭に戻すことができます。

長い行の自動折り返しを抑制する

まだ問題があります。

可変長かつ長い文字列(ファイルの絶対パスなど)を表示することを想定してみます。

progress.py
def print_progress(text: str):
    sys.stdout.write(f"\r\033[2K{text}")
    sys.stdout.flush()


print_progress("FILE_A.txt")
print_progress("super long file naaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaame")
print_progress("FILE_B.txt")
出力
$ python3 progress.py
super long file naaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
FILE_B.txt

長いファイル名の一部が残ってしまいました。
その理由は、三つ目の表示を消すことで理解できます。

出力
$ python3 progress.py
super long file naaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
aaaaaaaaaaame

このように、長い文字列を表示した際に、行の折り返しが発生してしまっています。
先ほどの実行では、 FILE_B の表示が、折り返した後の行 aaaaaaaaaaame の部分から再開してしまっていたのです。
(ここでは文字幅 32 列のターミナルで実行した出力を再現しています。)

実用上で問題になるということはないでしょうが、せっかくなら綺麗に一行で完結させたいです。

そのためには、また別のエスケープシーケンス \033[?7l\033[?7h を使います。

progress.py
def print_progress(text: str):
    sys.stdout.write(f"\r\033[2K{text}")
    sys.stdout.flush()


sys.stdout.write("\033[?7l")

print_progress("FILE_A.txt")
print_progress("super long file naaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaame")
print_progress("FILE_B.txt")

sys.stdout.write("\033[?7h")
出力
$ python3 progress.py
FILE_B.txt

折り返しが起こるような長いファイル名が残ることなく、綺麗に表示できました。

エスケープシーケンスの意味

\033[?7l / h の構成要素を紐解くと、以下のようになります。

  • \033[: CSI (Control Sequence Introducer)2。シーケンスの始まりを意味する
  • ?: これがプライベートシーケンス = ターミナル開発者が独自に定義できるシーケンスであることを意味する
  • 7l / 7h: なんらかのプライベートシーケンス

つまり、 \033[?7l / h はターミナル製作者が定義したプライベートシーケンスで、その中の 7l / 7h を呼び出している、ということになります。

では「7l 7h で折り返しを設定する」と定義したのが誰かというと、 VT100 というターミナル3です。
Wikipedia によると、それが ANSI をサポートした最初の著名なターミナルでもあるそうです。

The first popular video terminal to support these sequences was the Digital VT100, introduced in 1978.

7l / 7h の具体的な定義は、 VT100 のドキュメントで確認することができます。

DECAWM – Autowrap Mode (DEC Private)

This is a private parameter applicable to set mode (SM) and reset mode (RM) control sequences. The reset state causes any displayable characters received when the cursor is at the right margin to replace any previous characters there. The set state causes these characters to advance to the start of the next line, doing a scroll up if required and permitted.

DEC Private Modes

Parameter Mode Mnemonic Mode Function
7 DECAWM Auto wrap

Set / reset を指定するとのことで、 h / l はそれぞれ High / Low の略でしょう。

さて、VT100 は広く普及し、多くのターミナルエミュレータも VT100 のプライベートシーケンスに追従しているのだそうです。
筆者が使っている iTerm2 も例に漏れず、以下の箇所で 7l / 7h を処理しています。

            case 7:
                self.wraparoundMode = mode;
                break;

注意点として、一度 l を指定すると、セッションが終了するまでずっと autowrap がオフになります。
プログレスバーの表示が終わったら再度 h を指定してオンに戻しておくのがいいでしょう。

まとめると、 \033[?7 は VT100 が定義したエスケープシーケンスであり、 Autowrap mode を設定するものです。
h で有効化 (デフォルト)、 l で無効化できます。
今回は行末での折り返し (Autowrap) をオフにしたいので、 l を指定することで目的が達成できます。

ソースコード全体

ここまでの内容を関数にまとめます。

progress.py
import sys
import time


def start_progress():
    sys.stdout.write("\033[?7l")


def print_progress(text: str):
    sys.stdout.write(f"\r\033[2K{text}")
    sys.stdout.flush()


def end_progress():
    sys.stdout.write("\033[?7h\n")


n = 20

# Autowrap mode をオフにする
start_progress()

for i in range(n):
    # 進行度を一段階表示する
    print_progress(f"Processing {i:3} /  {n}...")
    time.sleep(0.1)

# Autowrap mode をオンにする
end_progress()

print("Done!")

実行すると、 0.1 秒ごとに進行度が更新され、 2 秒後に終了します。

出力
$ python3 progress.py
Processing   0 /  20...  # だんだん数字が変わる
Processing  19 /  20...  # ←終了時点
Done!

これで期待通りのプログレスバーを出力できました。

まとめ

エスケープシーケンスやターミナルエミュレータまわりのことは見て見ぬ振りを続けていましたが、勉強するいい機会になりました。

今回は Python で書きましたが、やっていることはただ文字列を出力しているだけなので、 Bash でも C でも、任意の言語で同じことができます。

参考資料

  1. print の実装 を見ると print はかなり薄いラッパーです。とはいえ、文字列フォーマット等は呼び出し側の責務ということにして、 str 型だけを使える sys.stdout.write を使っていいのではないかと思います。

  2. https://en.wikipedia.org/wiki/ANSI_escape_code#CSI_(Control_Sequence_Introducer)_sequences

  3. こんにちターミナルと呼ばれているものは正確には Terminal Emulator であり、当時物理的なデバイスとして存在した Terminal を Emulate するものです。この辺りについては参考資料もご覧ください。

2
2
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
2
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?