Как я боролся с кодировками в консоли
До тех пор, пока мы не вносим каких-либо «поправок» в проинициализировавшуюся систему ввода-вывода и используем только оператор print с unicode строками, всё идёт более-менее нормально вне зависимости от ОС.
«Чудеса» начинаются дальше — если мы поменяли какие-либо кодировки (см. чуть дальше) или воспользовались модулем logging для вывода на экран. Вроде бы настроив ожидаемое поведение в Linux, в Windows получаешь «мусор» в utf-8. Начинаешь править под Win — вылезает 1251 в консоли…
Теоретический экскурс
- Кодировка по-умолчанию модуля sys — sys.getdefaultencoding()
- Предпочитаемая кодировка для текущей локали — locale.getpreferredencoding()
- Кодировка стандартных потоков sys.stdout, sys.stderr
- Кое-какую смуту вносит и кодировка самого файла, но договоримся, что у нас всё унифицировано и все файлы в utf-8 и содержат корректный заголовок
- «Бонусом» идёт любовь стандартных потоков выдавать исключения, если, даже при корректно установленной кодировке, не найдётся нужного символа при печати unicode строки
Ищем решение
Очевидно, чтобы избавиться от всех этих проблем, надо как-то привести их к единообразию.
И вот тут начинается самое интересное:
# -*- coding: utf-8 -*- >>> import sys >>> import locale >>> print sys.getdefaultencoding() ascii >>> print locale.getpreferredencoding() # linux UTF-8 >>> print locale.getpreferredencoding() # win32/rus cp1251 # и самое интересное: >>> print sys.stdout.encoding # linux UTF-8 >>> print sys.stdout.encoding # win32 cp866
Ага! Оказывается «система» у нас живёт вообще в ASCII. Как следствие — попытка по-простому работать с вводом/выводом заканчивается «любимым» исключением UnicodeEncodeError/UnicodeDecodeError .
Кроме того, как замечательно видно из примера, если в linux у нас везде utf-8, то в Windows — две разных кодировки — так называемая ANSI, она же cp1251, используемая для графической части и OEM, она же cp866, для вывода текста в консоли. OEM кодировка пришла к нам со времён DOS-а и, теоретически, может быть также перенастроена специальными командами, но на практике никто этого давно не делает.
До недавнего времени я пользовался распространённым способом исправить эту неприятность:
#!/usr/bin/env python # -*- coding: utf-8 -*- # ============== # Main script file # ============== import sys reload(sys) sys.setdefaultencoding('utf-8') # или import locale sys.setdefaultencoding(locale.getpreferredencoding()) # .
И это, в общем-то, работало. Работало до тех пор, пока пользовался print -ом. При переходе к выводу на экран через logging всё сломалось.
Угу, подумал я, раз «оно» использует кодировку по-умолчанию, — выставлю-ка я ту же кодировку, что в консоли:
sys.setdefaultencoding(sys.stdout.encoding or sys.stderr.encoding)
- В Win32 текст печатается кракозябрами, явно напоминающими cp1251
- При запуске с перенаправленным выводом опять получаем не то, что ожидалось
- Периодически, при попытке напечатать текст, где есть преобразованный в unicode символ типа ① ( ① ), «любезно» добавленный автором в какой-нибудь заголовок, снова получаем UnicodeEncodeError !
import sys import codecs sys.stdout = codecs.getwriter('cp866')(sys.stdout,'replace')
Этот код позволяет убить двух зайцев — выставить нужную кодировку и защититься от исключений при печати всяких умляутов и прочей типографики, отсутствующей в 255 символах cp866.
Осталось сделать этот код универсальным — откуда мне знать OEM кодировку на произвольном сферическом компе? Гугление на предмет готовой поддержки ANSI/OEM кодировок в python ничего разумного не дало, посему пришлось немного вспомнить WinAPI
UINT GetOEMCP(void); // Возвращает системную OEM кодовую страницу как число UINT GetANSICP(void); // то же для ANSI кодовой странцы
# -*- coding: utf-8 -*- import sys import codecs def setup_console(sys_enc="utf-8"): reload(sys) try: # для win32 вызываем системную библиотечную функцию if sys.platform.startswith("win"): import ctypes enc = "cp%d" % ctypes.windll.kernel32.GetOEMCP() #TODO: проверить на win64/python64 else: # для Linux всё, кажется, есть и так enc = (sys.stdout.encoding if sys.stdout.isatty() else sys.stderr.encoding if sys.stderr.isatty() else sys.getfilesystemencoding() or sys_enc) # кодировка для sys sys.setdefaultencoding(sys_enc) # переопределяем стандартные потоки вывода, если они не перенаправлены if sys.stdout.isatty() and sys.stdout.encoding != enc: sys.stdout = codecs.getwriter(enc)(sys.stdout, 'replace') if sys.stderr.isatty() and sys.stderr.encoding != enc: sys.stderr = codecs.getwriter(enc)(sys.stderr, 'replace') except: pass # Ошибка? Всё равно какая - работаем по-старому.
Задача вторая. Раскрашиваем вывод
Насмотревшись на отладочный вывод Джанги в связке с werkzeug, захотелось чего-то подобного для себя. Гугление выдаёт несколько проектов разной степени проработки и удобности — от простейшего наследника logging.StreamHandler , до некоего набора, при импорте автоматически подменяющего стандартный StreamHandler.
Попробовав несколько из них, я, в итоге, воспользовался простейшим наследником StreamHandler, приведённом в одном из комментов на Stack Overflow и пока вполне доволен:
class ColoredHandler( logging.StreamHandler ): def emit( self, record ): # Need to make a actual copy of the record # to prevent altering the message for other loggers myrecord = copy.copy( record ) levelno = myrecord.levelno if( levelno >= 50 ): # CRITICAL / FATAL color = '\x1b[31;1m' # red elif( levelno >= 40 ): # ERROR color = '\x1b[31m' # red elif( levelno >= 30 ): # WARNING color = '\x1b[33m' # yellow elif( levelno >= 20 ): # INFO color = '\x1b[32m' # green elif( levelno >= 10 ): # DEBUG color = '\x1b[35m' # pink else: # NOTSET and anything else color = '\x1b[0m' # normal myrecord.msg = (u"%s%s%s" % (color, myrecord.msg, '\x1b[0m')).encode('utf-8') # normal logging.StreamHandler.emit( self, myrecord )
Однако, в Windows всё это работать, разумеется, отказалось. И если раньше можно было «включить» поддержку ansi-кодов в консоли добавлением «магического» ansi.dll из проекта symfony куда-то в недра системных папок винды, то, начиная (кажется) с Windows 7 данная возможность окончательно «выпилена» из системы. Да и заставлять юзера копировать какую-то dll в системную папку тоже как-то «не кошерно».
Снова обращаемся к гуглу и, снова, получаем несколько вариантов решения. Все варианты так или иначе сводятся к подмене вывода ANSI escape-последовательностей вызовом WinAPI для управления атрибутами консоли.
Побродив некоторое время по ссылкам, набрёл на проект colorama. Он как-то понравился мне больше остального. К плюсам именно этого проекта ст́оит отнести, что подменяется весь консольный вывод — можно выводить раскрашенный текст простым print u»\x1b[31;40mЧто-то красное на чёрном\x1b[0m» если вдруг захочется поизвращаться.
Сразу замечу, что текущая версия 0.1.18 содержит досадный баг, ломающий вывод unicode строк. Но простейшее решение я привёл там же при создании issue.
Собственно осталось объединить оба пожелания и начать пользоваться вместо традиционных «костылей»:
# -*- coding: utf-8 -*- import sys import codecs import copy import logging #: Is ANSI printing available ansi = not sys.platform.startswith("win") def setup_console(sys_enc='utf-8', use_colorama=True): """ Set sys.defaultencoding to `sys_enc` and update stdout/stderr writers to corresponding encoding .. note:: For Win32 the OEM console encoding will be used istead of `sys_enc` """ global ansi reload(sys) try: if sys.platform.startswith("win"): #. код, показанный выше if use_colorama and sys.platform.startswith("win"): try: # пробуем подключить colorama для винды и взводим флаг `ansi`, если всё получилось from colorama import init init() ansi = True except: pass class ColoredHandler( logging.StreamHandler ): def emit( self, record ): # Need to make a actual copy of the record # to prevent altering the message for other loggers myrecord = copy.copy( record ) levelno = myrecord.levelno if( levelno >= 50 ): # CRITICAL / FATAL color = '\x1b[31;1m' # red elif( levelno >= 40 ): # ERROR color = '\x1b[31m' # red elif( levelno >= 30 ): # WARNING color = '\x1b[33m' # yellow elif( levelno >= 20 ): # INFO color = '\x1b[32m' # green elif( levelno >= 10 ): # DEBUG color = '\x1b[35m' # pink else: # NOTSET and anything else color = '\x1b[0m' # normal myrecord.msg = (u"%s%s%s" % (color, myrecord.msg, '\x1b[0m')).encode('utf-8') # normal logging.StreamHandler.emit( self, myrecord )
#!/usr/bin/env python # -*- coding: utf-8 -*- from setupcon import setup_console setup_console('utf-8', False) #. # или если будем пользоваться раскрашиванием логов import setupcon setupcon.setup_console() import logging #. if setupcon.ansi: logging.getLogger().addHandler(setupcon.ColoredHandler())
На этом всё. Из потенциальных доработок осталось проверить работоспособность под win64 python и, возможно, добаботать ColoredHandler чтобы проверял себя на isatty, как в более сложных примерах на том же StackOverflow.
Итоговый вариант получившегося модуля можно забрать на dumpz.org
Python: Set STDOUT Encoding to UTF-8
There are several ways to set the stdin/stdout to UTF-8.
- In python code, add sys.stdout.reconfigure(encoding=»utf-8″)
- By environment variable. Set PYTHONIOENCODING to «utf-8»
- By environment variable. Set PYTHONUTF8 to 1
- By python command option: -X utf8
In python code
import sys sys.stdin.reconfigure(encoding="utf-8") sys.stdout.reconfigure(encoding="utf-8") print("α")
Set the environment variable
Set the environment variable PYTHONIOENCODING to «utf-8»
on Microsoft Windows , you can run this PowerShell command:
[Environment]::SetEnvironmentVariable("PYTHONIOENCODING", "utf-8", "User")
Python output encoding error: cp1252.py
If you don’t set the stdout to utf8, you may run into problems on Microsoft Windows in emacs.
Here’s helpful code for debugging. They should all be «UTF-8» .
import sys # sys._enablelegacywindowsfsencoding() print(sys.getfilesystemencoding()) print(sys.getdefaultencoding()) print(sys.stdout.encoding) # sys.getfilesystemencodeerrors() print("α")