LoginSignup
10
11

More than 1 year has passed since last update.

Python Windows10 において Console 出力に色を付ける

Last updated at Posted at 2020-07-24

はじめに

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でこれを使うには、はじめに有効化する必要があります。
以下のようにします。

python
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 が使えるようになります。
これ以降で、例えば、赤で出力したい場合は、

python
print("\x1b[31m")

と出力すると以降の文字は全て赤で表示されるようになります。
\x1bは Asciiコードの制御文字のESCです。
それに続く[31mがコマンドのようなものです。
もっとも全て赤で出力してしまったら本末転倒なので、

python
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)

を使えばよいです。

VirtualTerminalSequences.py
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()

テストコード

python
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

10
11
1

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
10
11