はじめに
Windows10においてPythonでコンソールにログを出力すると、文字の色が単色なので非常に見づらいです。
最近は、開発環境等で色を付けるのに慣れてしまっているのでこれは避けたいです。
何とか手間をかけずに色付きで表示する方法を探してみました
追記:Linuxでも動くようです。
環境
Windows 10 Pro, version 1909, build 18363.959
Python 3.8.3
Powershell 7.0.2
方法
簡単に言うとVirtual Terminal Sequences(ANSI escape sequences)を使います。
Windows10でこれを使うには、はじめに有効化する必要があります。
以下のようにします。
from ctypes import windll, wintypes, byref
def enable():
INVALID_HANDLE_VALUE = -1
STD_INPUT_HANDLE = -10
STD_OUTPUT_HANDLE = -11
STD_ERROR_HANDLE = -12
ENABLE_VIRTUAL_TERMINAL_PROCESSING = 0x0004
ENABLE_LVB_GRID_WORLDWIDE = 0x0010
hOut = windll.kernel32.GetStdHandle(STD_OUTPUT_HANDLE)
if hOut == INVALID_HANDLE_VALUE:
return False
dwMode = wintypes.DWORD()
if windll.kernel32.GetConsoleMode(hOut, byref(dwMode)) == 0:
return False
dwMode.value |= ENABLE_VIRTUAL_TERMINAL_PROCESSING
# dwMode.value |= ENABLE_LVB_GRID_WORLDWIDE
if windll.kernel32.SetConsoleMode(hOut, dwMode) == 0:
return False
return True
enable()
これが成功すれば、Virtual terminal sequences が使えるようになります。
これ以降で、例えば、赤で出力したい場合は、
print("\x1b[31m")
と出力すると以降の文字は全て赤で表示されるようになります。
\x1b
は Asciiコードの制御文字のESC
です。
それに続く[31m
がコマンドのようなものです。
もっとも全て赤で出力してしまったら本末転倒なので、
print("\x1b[31mRed, \x1b[34mBlue\x1b[0m") # 表示されるのは"Red, Blue"
などと使います。
詳細は、Microsoft Docs: Console Virtual Terminal Sequencesなどをご覧ください。
コード
これをクラスにまとめてみました。
このコードを"VirtualTerminalSequences.py"としてモジュールとして使用できるようにします。
色々定義していますが、
VTS.enable()
VTS.printColored(msg, fc, bc)
VTS.getColorMessage(msg, fc, bc)
を使えばよいです。
import os
from ctypes import wintypes, byref
if os.name == "nt":
from ctypes import windll
####################################################################################################
# VirtualTerminalSequences
####################################################################################################
class VTS:
RESET = "\x1b[0m" # reset
BOLD = "\x1b[1m"
UNDERLINE = "\x1b[4m"
REVERSE = "\x1b[07m" # reverse foreground and background colors
UNDERLINE_OFF = "\x1b[24m"
REVERSE_OFF = "\x1b[27m"
FOREGROUND_COLORS = {
"BLACK": "\x1b[30m",
"RED": "\x1b[31m",
"GREEN": "\x1b[32m",
"YELLOW": "\x1b[33m",
"BLUE": "\x1b[34m",
"MAGENTA": "\x1b[35m",
"CYAN": "\x1b[36m",
"WHITE": "\x1b[37m",
"DEFAULT_COLOR": "\x1b[39m",
"GRAY": "\x1b[90m",
"BRIGHT_RED": "\x1b[91m",
"BRIGHT_GREEN": "\x1b[92m",
"BRIGHT_YELLOW": "\x1b[93m",
"BRIGHT_BLUE": "\x1b[94m",
"BRIGHT_MAGENTA": "\x1b[95m",
"BRIGHT_CYAN": "\x1b[96m",
"BRIGHT_WHITE": "\x1b[97m",
}
BACKGROUND_COLORS = {
"BLACK": "\x1b[40m",
"RED": "\x1b[41m",
"GREEN": "\x1b[42m",
"YELLOW": "\x1b[43m",
"BLUE": "\x1b[44m",
"MAGENTA": "\x1b[45m",
"CYAN": "\x1b[46m",
"WHITE": "\x1b[47m",
"DEFAULT_COLOR": "\x1b[49m",
"GRAY": "\x1b[100m",
"BRIGHT_RED": "\x1b[101m",
"BRIGHT_GREEN": "\x1b[102m",
"BRIGHT_YELLOW": "\x1b[103m",
"BRIGHT_BLUE": "\x1b[104m",
"BRIGHT_MAGENTA": "\x1b[105m",
"BRIGHT_CYAN": "\x1b[106m",
"BRIGHT_WHITE": "\x1b[107m",
}
INVALID_HANDLE_VALUE = -1
STD_OUTPUT_HANDLE = -11
STD_ERROR_HANDLE = -12
ENABLE_VIRTUAL_TERMINAL_PROCESSING = 0x0004
ENABLE_LVB_GRID_WORLDWIDE = 0x0010
@staticmethod
def enable():
if os.name != "nt":
return True
hOut = windll.kernel32.GetStdHandle(VTS.STD_OUTPUT_HANDLE)
if hOut == VTS.INVALID_HANDLE_VALUE:
return False
dwMode = wintypes.DWORD()
if windll.kernel32.GetConsoleMode(hOut, byref(dwMode)) == 0:
return False
dwMode.value |= VTS.ENABLE_VIRTUAL_TERMINAL_PROCESSING
# dwMode.value |= ENABLE_LVB_GRID_WORLDWIDE
if windll.kernel32.SetConsoleMode(hOut, dwMode) == 0:
return False
return True
@staticmethod
def reset():
print(VTS.RESET)
@staticmethod
def isTupleOrList(x):
return isinstance(x, tuple) or isinstance(x, list)
@staticmethod
def printColored(msg, fc="DEFAULT_COLOR", bc="DEFAULT_COLOR"):
print(VTS.getColorMessage(msg, fc, bc))
@staticmethod
def getForegroundColor(value="DEFAULT_COLOR"):
if isinstance(value, str):
return VTS._getForegroundColorName(value)
elif isinstance(value, int):
return VTS._getForegroundColorNumber(value)
elif VTS.isTupleOrList(value) and len(value) == 3:
return VTS._getForegroundColorRGB(*value)
else:
return VTS.FOREGROUND_COLORS["DEFAULT_COLOR"]
@staticmethod
def getBackgroundColor(value="DEFAULT_COLOR"):
if isinstance(value, str):
return VTS._getBackgroundColorName(value)
elif isinstance(value, int):
return VTS._getBackgroundColorNumber(value)
elif VTS.isTupleOrList(value) and len(value) == 3:
return VTS._getBackgroundColorRGB(*value)
else:
return VTS.FOREGROUND_COLORS["DEFAULT_COLOR"]
@staticmethod
def _getForegroundColorName(name="DEFAULT_COLOR"):
name = name.upper()
return VTS.FOREGROUND_COLORS.get(name, VTS.FOREGROUND_COLORS["DEFAULT_COLOR"])
@staticmethod
def _getBackgroundColorName(name="DEFAULT_COLOR"):
name = name.upper()
return VTS.BACKGROUND_COLORS.get(name, VTS.BACKGROUND_COLORS["DEFAULT_COLOR"])
@staticmethod
def _getForegroundColorNumber(n):
if n >= 0 and n <= 255:
return f"\x1b[38;5;{n}m"
return VTS.FOREGROUND_COLORS["DEFAULT_COLOR"]
@staticmethod
def _getForegroundColorRGB(r, g, b):
if r >= 0 and r <= 255 and g >= 0 and g <= 255 and b >= 0 and b <= 255:
return f"\x1b[38;2;{r};{g};{b}m"
return VTS.FOREGROUND_COLORS["DEFAULT_COLOR"]
@staticmethod
def _getBackgroundColorNumber(n):
if n >= 0 and n <= 255:
return f"\x1b[48;5;{n}m"
return VTS.BACKGROUND_COLORS["DEFAULT_COLOR"]
@staticmethod
def _getBackgroundColorRGB(r, g, b):
if r >= 0 and r <= 255 and g >= 0 and g <= 255 and b >= 0 and b <= 255:
return f"\x1b[48;2;{r};{g};{b}m"
return VTS.BACKGROUND_COLORS["DEFAULT_COLOR"]
@staticmethod
def getColorMessage(msg, fc="DEFAULT_COLOR", bc="DEFAULT_COLOR", reset=True):
msg = f"{VTS.getForegroundColor(fc)}{VTS.getBackgroundColor(bc)}{msg}"
if not reset:
return msg
return f"{msg}{VTS.RESET}"
@staticmethod
def getBoldMessage(msg, reset=True):
msg = f"{VTS.BOLD}{msg}"
if not reset:
return msg
return f"{msg}{VTS.RESET}"
@staticmethod
def getUnderlineMessage(msg, reset=True):
msg = f"{VTS.UNDERLINE}{msg}"
if not reset:
return msg
return f"{msg}{VTS.RESET}"
@staticmethod
def getReverseMessage(msg, reset=True):
msg = f"{VTS.REVERSE}{msg}"
if not reset:
return msg
return f"{msg}{VTS.RESET}"
@staticmethod
def testColor():
for kBc, vBc in VTS.BACKGROUND_COLORS.items():
for kFc, vFc in VTS.FOREGROUND_COLORS.items():
string = VTS.getColorMessage(f"{kFc:<14}, {kBc:<14}", kFc, kBc)
print(string)
VTS.reset()
テストコード
import os
from VirtualTerminalSequences import VTS
os.system("CLS") # 画面をクリア
print(f"NORMAL")
print(f"{VTS.BOLD}BOLD{VTS.RESET}")
print(f"{VTS.UNDERLINE}UNDERLINE")
print(f"{VTS.UNDERLINE_OFF}UNDERLINE_OFF")
print(f"{VTS.REVERSE}REVERSE")
print(f"{VTS.REVERSE_OFF}REVERSE_OFF")
[print(f"{VTS._getForegroundColorName(key)}$", end="") for key in VTS.FOREGROUND_COLORS.keys()]
VTS.reset()
[print(f"{VTS._getForegroundColorNumber(n)}$", end="") for n in range(256)]
VTS.reset()
[print(f"{VTS._getForegroundColorRGB(r,0,0)}$", end="") for r in range(256)]
VTS.reset()
[print(f"{VTS._getForegroundColorRGB(0,g,0)}$", end="") for g in range(256)]
VTS.reset()
[print(f"{VTS._getForegroundColorRGB(0,0,b)}$", end="") for b in range(256)]
VTS.reset()
[print(f"{VTS._getBackgroundColorName(key)}_", end="") for key in VTS.BACKGROUND_COLORS.keys()]
VTS.reset()
[print(f"{VTS._getBackgroundColorNumber(n)}_", end="") for n in range(256)]
VTS.reset()
[print(f"{VTS._getBackgroundColorRGB(r,0,0)}_", end="") for r in range(256)]
VTS.reset()
[print(f"{VTS._getBackgroundColorRGB(0,g,0)}_", end="") for g in range(256)]
VTS.reset()
[print(f"{VTS._getBackgroundColorRGB(0,0,b)}_", end="") for b in range(256)]
VTS.reset()
print(VTS.getColorMessage("色々な色", "red", "blue"))
print(VTS.getColorMessage("色々な色", 3, 6))
print(VTS.getColorMessage("色々な色", (200, 200, 200), (100, 100, 100), False))
print(VTS.getUnderlineMessage("河川に下線"))
print(VTS.getBoldMessage("太い痩せ猫", False))
print(VTS.getReverseMessage("裏返る裏切り"))
print(VTS.getReverseMessage("裏返る裏切り"))
string1 = VTS.getColorMessage("abc", "red", "green")
string2 = VTS.getColorMessage("def", "green", "blue")
string3 = VTS.getColorMessage("ghi", "blue", "red")
print(string1 + string2 + string3)
Loggingと組み合わせる
Loggingモジュールを使ってログを出力している場合は、
-
Formatter
で情報毎に色分けする -
Filter
で条件ごとに色分けする -
Logger.info()
などによる出力段階で色分けする
等が考えられます。
サンプル例
ここでは、
-
Filter
でログレベルごとに色分けする -
Formatter
で時刻情報を色分けする
を実装してみました。
import logging
import logging.handlers
import logging.config
from VirtualTerminalSequences import VTS
class ColorFilter(logging.Filter):
def __init__(self):
super().__init__()
VTS.enable()
def filter(self, record):
if record.levelno == logging.DEBUG:
record.msg = VTS.getColorMessage(record.msg, "green")
if record.levelno == logging.INFO:
record.msg = VTS.getColorMessage(record.msg, "cyan")
if record.levelno == logging.WARNING:
record.msg = VTS.getColorMessage(record.msg, "yellow")
if record.levelno == logging.ERROR:
record.msg = VTS.getColorMessage(record.msg, "magenta")
if record.levelno == logging.CRITICAL:
record.msg = VTS.getColorMessage(record.msg, "red")
return True
config = {
"version": 1,
"formatters": {
"normalColor": {
"class":
"logging.Formatter",
"format":
f"{VTS.FOREGROUND_COLORS['BLACK']}{VTS.BACKGROUND_COLORS['WHITE']}" + "{asctime}" + f"{VTS.RESET}" +
" - {message}",
"datefmt":
"%H:%M:%S",
"style":
"{",
},
"detail": {
"class": "logging.Formatter",
"format": "{asctime}.{msecs:03.0f} - {name:<15} - {levelname:<8} - {message}",
"datefmt": "%Y-%m-%d-%H:%M:%S",
"style": "{",
},
},
"filters": {
"color": {
"()": "ColorFilter",
},
},
"handlers": {
"null": {
"class": "logging.NullHandler",
"level": "DEBUG", # logging.DEBUG も可能
},
"console": {
"class": "logging.StreamHandler",
"level": "INFO",
"formatter": "normalColor",
"filters": ["color"],
},
"file": {
"class": "logging.handlers.RotatingFileHandler",
"level": "DEBUG",
"formatter": "detail",
"filename": "log.log",
"maxBytes": 1024 * 1024,
"backupCount": 2,
"encoding": "utf-8",
},
},
"loggers": {
"console": {
"handlers": ["console"],
"level": logging.INFO,
},
"__main__": {
"handlers": ["console", "file"],
"level": logging.DEBUG,
},
},
}
logging.config.dictConfig(config)
参考
Microsoft Docs: Console Virtual Terminal Sequences
Microsoft Docs: getStdHandle
Microsoft Docs: getConsoleMode
Microsoft Docs: setConsoleMode