Pythonでスクリプトを書いて標準出力を大量に出すと、やはり単色では見づらいものですね。
そこで最低限の色が付けられないものかと考えました。
もくじ:
0. はじめに
やはり同じようなことを考えた先人はたくさんいるようで、Pythonでもコンソールの文字色や背景色を変えるモジュールがありますね。検索してすぐヒットするのはChalk(pychalk)やcoloramaですが、サードパーティ製なので別途導入する必要があります。
しかしながら多機能は必要ない、環境はできるだけ変更したくない、コピペするだけで使えるのが良い。。。
ということで、素人ながらできるだけ取り回しがしやすいように、いくつか候補を考えてみました。
ひとつはUnixコンソールエミュレータを使うやり方。伝統のCygwin/MinGW、よりモダンなCmder/ConEmu、あと最近はBash on Windows (Linux Subsystem for Windows)でMS謹製の環境を構築するといった選択肢があります。この環境なら、文字列に特定のエスケープシーケンスを入れるだけで色が付きます。
一方で、もちろんWindowsネイティブにはコンソールの文字色を制御するインタフェースがあるので、そちらで頑張るやり方もあります。共用の環境など、変更を加えるのが難しい場合に役立ちます。が、PythonからWindowsのAPIを呼ぶのは結構ハードル高めの印象。最近の.NETとかはそうでもないのかしら。
サンプルは最後に記載します。検証した環境はWindows 10 Pro、Pythonは3.6と2.7でも確認しています。
1. コンソールエミュレータ向け
付けたい色に応じてエスケープシーケンスを挿入します。
\033[
に続けて色などの書式を指定します。標準の31m
(黒)~37m
(白)や、明るめの強調色91m
(黒)~97m
(白)のほか、太字、下線なども指定できます(対応状況は使用するコンソールエミュレータに依存)。終端は\033[0m
で書式をリセットします。
str型のデータにエスケープシーケンスを入れるだけでいいので、途中だけ色変更、みたいなことも簡単です。
1 2 3 | buf_ = "some text" print("\033[32m{}\033[0m".format(buf_)) print("\033[92m{}\033[0m".format(buf_)) |
自分の環境(Cmder)で試したところでは、とりあえず色の変更はできましたが、他の書式設定はそもそも対応していないか、対応していても特に見た目違いがわからない程度でした。
Cmderで試すとこんな感じになりました。
Linuxでも同じやり方で書式を変更できます。下記はUbuntu MATEのターミナルでの例です。
2. Win32 APIを使った着色
coloromaの実装を参考にWin32 APIで作成してみました。ctypesモジュール経由で必要なAPIを呼び出せるようにしています。
基本的にはコンソールの設定変更、標準出力、設定を戻す、の順で繰り返します。
print
の終端文字の指定を駆使して、一行の途中のみオンラインに変更することもできますが、ちょっと面倒なので今回は実装していません。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 | # C互換ライブラリ呼び出しモジュール from ctypes import windll, Structure, byref, wintypes # 必要なAPI (kernel32)をロード class cutil: stdout_handle = windll.kernel32.GetStdHandle(-11) GetConsoleInfo = windll.kernel32.GetConsoleScreenBufferInfo SetConsoleAttribute = windll.kernel32.SetConsoleTextAttribute class console_screen_buffer_info(Structure): _fields_ = [("dwSize", wintypes._COORD), ("dwCursorPosition", wintypes._COORD), ("wAttributes", wintypes.WORD), ("srWindow", wintypes.SMALL_RECT), ("dwMaximumWindowSize", wintypes._COORD)] # 現状の色設定を取得 info_ = cutil.console_screen_buffer_info() cutil.GetConsoleInfo(cutil.stdout_handle, byref(info_)) # 文字色(foreground color)を変更 fg_color = 0x0004 | 0x0008 cutil.SetConsoleAttribute(cutil.stdout_handle, fg_color | info_.wAttributes & 0x0070) # 標準出力 print("some text") # もとの色設定に戻す cutil.SetConsoleAttribute(cutil.stdout_handle, info_.wAttributes) |
ちゃんと色が着けられますね。ただし、行の途中で色を変更する方は有効になりません。
3. .NETを使った着色
今回は使っていませんが、サードパーティ製のモジュールを使えば.NETを経由しても同じことができます。
pythonnet (Python for .NET)モジュールを導入し、
1 | > pip install pythonnet |
C#のコードと同じ要領でConsoleを使って標準出力に文字列を書き込みます。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 | # System名前空間を参照 import clr from System import Console, ConsoleColor # 文字色を変更 if color_ == "blue": Console.ForegroundColor = ConsoleColor.Blue elif color_ == "yellow": Console.ForegroundColor = ConsoleColor.Yellow elif color_ == "red": Console.ForegroundColor = ConsoleColor.Red elif color_ == "green": Console.ForegroundColor = ConsoleColor.Green elif color_ == "cyan": Console.ForegroundColor = ConsoleColor.Cyan elif color_ == "magenta": Console.ForegroundColor = ConsoleColor.Magenta else: Console.ResetColor() # 標準出力 buf_ = "some text" Console.WriteLine("{}".format(buf_)) # もとの色設定に戻す Console.ResetColor() |
4. サンプル
今回のスクリプトを記載しておきます。Windowsの場合、ConEmuまたはCmderであればコンソールエミュレータ向けのエスケープシーケンスを、その他であればWin32 APIを使って文字色を変更します。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 | # -*- coding: utf-8 -*- import os import sys class envs_: win = "win32" default = "default" std_colors = ["black", "red", "green", "yellow", "blue", "magenta", "cyan", "white"] envkeys = list(os.environ.copy().keys()) if sys.platform == "win32": if "CONEMUANSI" in envkeys: # on Cmder or ConEmu CONSOLE_ENV = envs_.default else: # use win32api CONSOLE_ENV = envs_.win # utility for calling windll from ctypes import windll, Structure, byref, wintypes class cutil: stdout_handle = windll.kernel32.GetStdHandle(-11) GetConsoleInfo = windll.kernel32.GetConsoleScreenBufferInfo SetConsoleAttribute = windll.kernel32.SetConsoleTextAttribute class console_screen_buffer_info(Structure): _fields_ = [("dwSize", wintypes._COORD), ("dwCursorPosition", wintypes._COORD), ("wAttributes", wintypes.WORD), ("srWindow", wintypes.SMALL_RECT), ("dwMaximumWindowSize", wintypes._COORD)] else: CONSOLE_ENV = envs_.default def cformat(obj_, color_): # returns stringified object with color codes buf_ = "{}".format(obj_) if CONSOLE_ENV == envs_.default: if color_ in envs_.std_colors: return "\033[9{}m{}\033[0m".format( envs_.std_colors.index(color_), buf_) else: return buf_ else: return buf_ def _f(obj_, color_=""): # shorthand alias return cformat(obj_, color_) def clprint(obj_, color_): # print with format if CONSOLE_ENV == envs_.win: # run with win32 api if color_ == "red": fg_color = 0x0004 | 0x0008 elif color_ == "green": fg_color = 0x0002 | 0x0008 elif color_ == "yellow": fg_color = 0x0006 | 0x0008 elif color_ == "blue": fg_color = 0x0001 | 0x0008 elif color_ == "magenta": fg_color = 0x0005 | 0x0008 elif color_ == "cyan": fg_color = 0x0003 | 0x0008 elif color_ == "gray": fg_color = 0x0007 | 0x0008 else: print(obj_) return # preserve current buffer info info_ = cutil.console_screen_buffer_info() cutil.GetConsoleInfo(cutil.stdout_handle, byref(info_)) # set foreground color cutil.SetConsoleAttribute(cutil.stdout_handle, fg_color | info_.wAttributes & 0x0070) print(obj_) # return to default color cutil.SetConsoleAttribute(cutil.stdout_handle, info_.wAttributes) else: print(cformat(obj_, color_)) def _p(obj_, color_=""): # shorthand alias clprint(obj_, color_) if __name__ == "__main__": print("env: {}".format(CONSOLE_ENV)) print("{} {} {} {} {} {}".format( _f("I think", "yellow"), _f("you", "blue"), _f("wanna", "red"), _f("color", "green"), _f("this", "cyan"), _f("stdout.", "magenta"))) colors = ["yellow", "blue", "red", "green", "cyan", "magenta", "gray", "black", "white"] for color_ in colors: _p("colored line on console [{}]".format(color_), color_) |
モジュールをインポートして使うにはこんな感じ。環境は一応自動的に判別するので特に意識する必要はない、ハズ。
1 2 3 4 5 6 7 | >>> from ccolor import _f, _p # エスケープシーケンス付き文字列 >>> _f("this is test", "magenta") '\x1b[95mthis is test\x1b[0m' # 色付き標準出力(行) >>> _p("this is test", "magenta") this is test |
おわり。