なぜプログレスバーを自作するのか
Pythonでプログレスバーといえば tqdm が定番ですが、自作することにもメリットがあります。
- 外部依存をゼロにできる(
pip install不要) - ターミナル制御の仕組みを理解できる
- 表示フォーマットを自由にカスタマイズできる
この記事では、キャリッジリターンの基礎から始めて、ETA表示・ANSIカラー・マルチバーまで段階的にプログレスバーを実装していきます。
キャリッジリターンの基本
プログレスバーの核心は、ターミナルの同じ行を繰り返し上書きすることです。これには キャリッジリターン \r を使います。
\r はカーソルを行の先頭に戻す制御文字です。改行せずに出力すれば、前の内容を上書きできます。
import time
for i in range(101):
print(f"\r処理中... {i}%", end="", flush=True)
time.sleep(0.05)
print() # 最後に改行
ポイントは3つです。
\rを文字列の先頭に置くend=""で改行を抑制するflush=Trueでバッファを即時フラッシュする(これがないと表示が更新されないことがあります)
基本的なプログレスバー
数値だけでは味気ないので、バーを描画してみます。[████████░░░░░░░░] 50% (50/100) のような形式を目指します。
import sys
import time
def progress_bar(current, total, bar_length=30):
fraction = current / total
filled = int(bar_length * fraction)
bar = "█" * filled + "░" * (bar_length - filled)
percent = fraction * 100
sys.stdout.write(f"\r[{bar}] {percent:5.1f}% ({current}/{total})")
sys.stdout.flush()
# 使用例
total = 100
for i in range(total + 1):
progress_bar(i, total)
time.sleep(0.03)
print()
実行すると、以下のように表示が更新されていきます。
[█████████████████░░░░░░░░░░░░░] 56.0% (56/100)
sys.stdout.write を使っている理由は、print と違って余計な改行やスペースが入らないためです。
ETA(残り時間)の計算
長い処理では「あとどれくらいかかるのか」が知りたくなります。経過時間から残り時間を推定しましょう。
import sys
import time
def format_time(seconds):
"""秒数を HH:MM:SS 形式にフォーマットする"""
h = int(seconds // 3600)
m = int((seconds % 3600) // 60)
s = int(seconds % 60)
return f"{h:02d}:{m:02d}:{s:02d}"
def progress_bar_eta(current, total, start_time, bar_length=30):
fraction = current / total
filled = int(bar_length * fraction)
bar = "█" * filled + "░" * (bar_length - filled)
percent = fraction * 100
elapsed = time.time() - start_time
if current > 0:
rate = current / elapsed # 1秒あたりの処理数
remaining = (total - current) / rate
eta_str = format_time(remaining)
rate_str = f"{rate:.1f} it/s"
else:
eta_str = "--:--:--"
rate_str = "-- it/s"
sys.stdout.write(
f"\r[{bar}] {percent:5.1f}% | ETA: {eta_str} | {rate_str}"
)
sys.stdout.flush()
# 使用例
total = 200
start = time.time()
for i in range(total + 1):
progress_bar_eta(i, total, start)
time.sleep(0.02)
print()
出力例:
[███████████████░░░░░░░░░░░░░░░] 50.0% | ETA: 00:00:02 | 49.8 it/s
current == 0 のときはゼロ除算を避けるため、ETAをプレースホルダにしています。
ANSIカラー対応
ターミナルは ANSIエスケープシーケンス を使って文字に色をつけることができます。プログレスの進捗に応じてバーの色を変えてみましょう。
主なカラーコードは以下の通りです。
| コード | 色 |
|---|---|
\033[91m | 赤(明るい) |
\033[93m | 黄(明るい) |
\033[92m | 緑(明るい) |
\033[0m | リセット |
import os
import sys
import time
def get_color(fraction):
"""進捗率に応じて色を返す(赤→黄→緑)"""
if fraction < 0.33:
return "\033[91m" # 赤
elif fraction < 0.66:
return "\033[93m" # 黄
else:
return "\033[92m" # 緑
RESET = "\033[0m"
def progress_bar_color(current, total, start_time):
try:
terminal_width = os.get_terminal_size().columns
except OSError:
terminal_width = 80
# バー以外の部分の文字数を概算して、バーの長さを決定
suffix = f" {current/total*100:5.1f}% | ETA: 00:00:00 | 000.0 it/s"
bar_length = max(10, terminal_width - len(suffix) - 4) # [] と余白分
fraction = current / total
filled = int(bar_length * fraction)
color = get_color(fraction)
bar = color + "█" * filled + RESET + "░" * (bar_length - filled)
elapsed = time.time() - start_time
if current > 0:
rate = current / elapsed
remaining = (total - current) / rate
eta_str = format_time(remaining)
rate_str = f"{rate:.1f} it/s"
else:
eta_str = "--:--:--"
rate_str = "-- it/s"
percent = fraction * 100
sys.stdout.write(f"\r[{bar}] {percent:5.1f}% | ETA: {eta_str} | {rate_str}")
sys.stdout.flush()
# format_time は前のセクションと同じ
total = 150
start = time.time()
for i in range(total + 1):
progress_bar_color(i, total, start)
time.sleep(0.02)
print()
os.get_terminal_size() でターミナルの幅を取得し、バーの長さを動的に調整しています。ウィンドウをリサイズしても表示が崩れにくくなります。
マルチバーの実装
複数のタスクを同時に進める場合、バーを縦に並べて表示したくなります。これには ANSIカーソル移動 を使います。
\033[{n}A: カーソルをn行上に移動\033[{n}B: カーソルをn行下に移動
仕組みはシンプルです。複数行を出力した後、カーソルを先頭まで戻して上書きします。
import sys
import time
import random
def format_time(seconds):
h = int(seconds // 3600)
m = int((seconds % 3600) // 60)
s = int(seconds % 60)
return f"{h:02d}:{m:02d}:{s:02d}"
def get_color(fraction):
if fraction < 0.33:
return "\033[91m"
elif fraction < 0.66:
return "\033[93m"
else:
return "\033[92m"
RESET = "\033[0m"
def render_bar(label, current, total, start_time, bar_length=25):
"""1本のバーの文字列を生成して返す"""
fraction = current / total if total > 0 else 0
filled = int(bar_length * fraction)
color = get_color(fraction)
bar = color + "█" * filled + RESET + "░" * (bar_length - filled)
elapsed = time.time() - start_time
if current > 0:
rate = current / elapsed
remaining = (total - current) / rate
eta_str = format_time(remaining)
else:
eta_str = "--:--:--"
percent = fraction * 100
return f"{label}: [{bar}] {percent:5.1f}% ({current}/{total}) ETA: {eta_str}"
def multi_progress(tasks):
"""複数タスクのプログレスバーを表示する"""
n = len(tasks)
start_times = [time.time() for _ in range(n)]
progress = [0] * n
totals = [t["total"] for t in tasks]
labels = [t["label"] for t in tasks]
# 初回表示: 空行を確保
for i in range(n):
print(render_bar(labels[i], 0, totals[i], start_times[i]))
while any(progress[i] < totals[i] for i in range(n)):
# カーソルをn行上に戻す
sys.stdout.write(f"\033[{n}A")
for i in range(n):
if progress[i] < totals[i]:
# タスクごとに異なるスピードで進む
step = random.randint(1, 3)
progress[i] = min(progress[i] + step, totals[i])
line = render_bar(labels[i], progress[i], totals[i], start_times[i])
# 行末まで消去してから改行(前の表示のゴミを消す)
sys.stdout.write(f"\r{line}\033[K\n")
sys.stdout.flush()
time.sleep(0.1)
# 使用例
tasks = [
{"label": "Download ", "total": 100},
{"label": "Extract ", "total": 80},
{"label": "Install ", "total": 120},
]
multi_progress(tasks)
実行すると、ターミナル上で以下のような表示になります。

\033[K は行末までを消去するエスケープシーケンスです。バーの長さが前回より短くなった場合に、ゴミ文字が残るのを防ぎます。
まとめ:ProgressBarクラス
ここまでの要素を1つのクラスにまとめます。
import os
import sys
import time
class ProgressBar:
def __init__(self, total, label="Progress", bar_length=None, color=True):
self.total = total
self.label = label
self.color = color
self.current = 0
self.start_time = None
if bar_length is None:
try:
self.bar_length = max(10, os.get_terminal_size().columns - 60)
except OSError:
self.bar_length = 30
else:
self.bar_length = bar_length
def _get_color(self, fraction):
if not self.color:
return ""
if fraction < 0.33:
return "\033[91m"
elif fraction < 0.66:
return "\033[93m"
return "\033[92m"
def _format_time(self, seconds):
h = int(seconds // 3600)
m = int((seconds % 3600) // 60)
s = int(seconds % 60)
return f"{h:02d}:{m:02d}:{s:02d}"
def update(self, n=1):
if self.start_time is None:
self.start_time = time.time()
self.current = min(self.current + n, self.total)
fraction = self.current / self.total
filled = int(self.bar_length * fraction)
color = self._get_color(fraction)
reset = "\033[0m" if self.color else ""
bar = color + "█" * filled + reset + "░" * (self.bar_length - filled)
elapsed = time.time() - self.start_time
if self.current > 0:
rate = self.current / elapsed
remaining = (self.total - self.current) / rate
eta_str = self._format_time(remaining)
rate_str = f"{rate:.1f} it/s"
else:
eta_str = "--:--:--"
rate_str = "-- it/s"
percent = fraction * 100
line = f"\r{self.label}: [{bar}] {percent:5.1f}% | ETA: {eta_str} | {rate_str}"
sys.stdout.write(line + "\033[K")
sys.stdout.flush()
def finish(self):
self.update(0)
print()
# 使用例
bar = ProgressBar(total=100, label="Training")
for i in range(100):
time.sleep(0.03)
bar.update()
bar.finish()
出力例:
Training: [██████████████████████████████] 100.0% | ETA: 00:00:00 | 32.8 it/s
このクラスは約60行で、tqdm の基本機能をカバーしています。必要に応じてコンテキストマネージャ(__enter__ / __exit__)やイテラブルのラッパーを追加すれば、さらに便利に使えます。