Декодировать вывод PowerShell, возможно, содержащий не-ASCII символы Unicode в строку Python
Мне нужно расшифровать стандартный вывод powershell, вызываемый из python, в строку python.
Моя конечная цель - получить в виде списка строк имена сетевых адаптеров в Windows. Моя текущая функция выглядит следующим образом и хорошо работает в Windows 10 с английским языком:
def get_interfaces():
ps = subprocess.Popen(['powershell', 'Get-NetAdapter', '|', 'select Name', '|', 'fl'], stdout = subprocess.PIPE)
stdout, stdin = ps.communicate(timeout = 10)
interfaces = []
for i in stdout.split(b'\r\n'):
if not i.strip():
continue
if i.find(b':')<0:
continue
name, value = [ j.strip() for j in i.split(b':') ]
if name == b'Name':
interfaces.append(value.decode('ascii')) # This fails for other users
return interfaces
У других пользователей разные языки, поэтому value.decode('ascii')
не удается для некоторых из них. Например, один пользователь сообщил, что переход на decode('ISO 8859-2')
хорошо работает для него (так что это не UTF-8). Как узнать кодировку для декодирования байтов stdout, возвращаемых при вызове powershell?
ОБНОВИТЬ
После некоторых экспериментов я еще больше растерялся. Кодовая страница в моей консоли, возвращенная chcp
437. Я изменил имя сетевого адаптера на имя, содержащее символы не ascii и не cp437. В интерактивном режиме PowerShell Get-NetAdapter | select Name | fl
отображается правильно имя даже его не-cp437 символ. Когда я вызвал powershell из python, символы не-ascii были преобразованы в ближайшие символы ascii (например, à в a, ž в z) и .decode(ascii)
работал хорошо. Может ли это поведение (и, соответственно, решение) зависеть от версии Windows? Я на Windows 10, но пользователи могут быть на старых Windows до Windows 7.
2 ответа
Выходная кодировка символов может зависеть от конкретных команд, например:
#!/usr/bin/env python3
import subprocess
import sys
encoding = 'utf-32'
cmd = r'''$env:PYTHONIOENCODING = "%s"; py -3 -c "print('\u270c')"''' % encoding
data = subprocess.check_output(["powershell", "-C", cmd])
print(sys.stdout.encoding)
print(data)
print(ascii(data.decode(encoding)))
Выход
cp437
b"\xff\xfe\x00\x00\x0c'\x00\x00\r\x00\x00\x00\n\x00\x00\x00"
'\u270c\r\n'
Символ U ( U + 270C) получен успешно.
Кодировка символов дочернего скрипта задается с помощью PYTHONIOENCODING
envvar внутри сессии PowerShell. Я выбрал utf-32
для выходной кодировки, чтобы она отличалась от кодовых страниц Windows ANSI и OEM для демонстрации.
Обратите внимание, что стандартная кодировка родительского скрипта Python является кодовой страницей OEM (cp437
в данном случае) - скрипт запускается из консоли Windows. Если вы перенаправляете вывод родительского скрипта Python в файл / канал, тогда кодовая страница ANSI (например, cp1252
) используется по умолчанию в Python 3.
Чтобы декодировать вывод powershell, который может содержать символы, которые невозможно декодировать в текущей кодовой странице OEM, вы можете установить [Console]::OutputEncoding
временно (вдохновлено комментариями @ eryksun):
#!/usr/bin/env python3
import io
import sys
from subprocess import Popen, PIPE
char = ord('✌')
filename = 'U+{char:04x}.txt'.format(**vars())
with Popen(["powershell", "-C", '''
$old = [Console]::OutputEncoding
[Console]::OutputEncoding = [Text.Encoding]::UTF8
echo $([char]0x{char:04x}) | fl
echo $([char]0x{char:04x}) | tee {filename}
[Console]::OutputEncoding = $old'''.format(**vars())],
stdout=PIPE) as process:
print(sys.stdout.encoding)
for line in io.TextIOWrapper(process.stdout, encoding='utf-8-sig'):
print(ascii(line))
print(ascii(open(filename, encoding='utf-16').read()))
Выход
cp437
'\u270c\n'
'\u270c\n'
'\u270c\n'
И то и другое fl
а также tee
использование [Console]::OutputEncoding
для стандартного вывода (поведение по умолчанию, как будто | Write-Output
добавляется к трубопроводам). tee
использует utf-16, чтобы сохранить текст в файл. Выходные данные показывают, что ✌ ( U + 270C) успешно декодируется.
$OutputEncoding
используется для декодирования байтов в середине конвейера:
#!/usr/bin/env python3
import subprocess
cmd = r'''
$OutputEncoding = [Console]::OutputEncoding = New-Object System.Text.UTF8Encoding
py -3 -c "import os; os.write(1, '\U0001f60a'.encode('utf-8')+b'\n')" |
py -3 -c "import os; print(os.read(0, 512))"
'''
subprocess.check_call(["powershell", "-C", cmd])
Выход
b'\xf0\x9f\x98\x8a\r\n'
это правильно: b'\xf0\x9f\x98\x8a'.decode('utf-8') == u'\U0001f60a'
, По умолчанию $OutputEncoding
(ASCII) мы бы получили b'????\r\n'
вместо.
Замечания:
b'\n'
заменяется наb'\r\n'
несмотря на использование бинарного API, такого какos.read/os.write
(msvcrt.setmode(sys.stdout.fileno(), os.O_BINARY)
здесь не действует)b'\r\n'
добавляется, если в выводе нет новой строки:#!/usr/bin/env python3 from subprocess import check_output cmd = '''py -3 -c "print('no newline in the input', end='')"''' cat = '''py -3 -c "import os; os.write(1, os.read(0, 512))"''' # pass as is piped = check_output(['powershell', '-C', '{cmd} | {cat}'.format(**vars())]) no_pipe = check_output(['powershell', '-C', '{cmd}'.format(**vars())]) print('piped: {piped}\nno pipe: {no_pipe}'.format(**vars()))
Выход:
piped: b'no newline in the input\r\n' no pipe: b'no newline in the input'
Новая строка добавляется к конвейеру.
Если мы игнорируем одиноких суррогатов, то установка UTF8Encoding
позволяет передавать по каналам все символы Unicode, включая символы не-BMP. Текстовый режим может быть использован в Python, если $env:PYTHONIOENCODING = "utf-8:ignore"
настроен.
В интерактивном режиме PowerShell
Get-NetAdapter | select Name | fl
отображается правильно имя даже его не-cp437 символ.
Если стандартный вывод не перенаправлен, то для печати символов на консоль используется Unicode API - любой символ [BMP] Unicode может отображаться, если его поддерживает шрифт консоли (TrueType).
Когда я вызвал powershell из python, символы не-ascii были преобразованы в наиболее близкие символы ascii (например, à к a, ž к z) и.decode(ascii) работали хорошо.
Это может быть связано с System.Text.InternalDecoderBestFitFallback
набор для [Console]::OutputEncoding
- если символ Unicode не может быть закодирован в данной кодировке, то он передается в запасной вариант (либо наиболее подходящий символ, либо '?'
используется вместо оригинального персонажа).
Может ли это поведение (и, соответственно, решение) зависеть от версии Windows? Я на Windows 10, но пользователи могут быть на старых Windows до Windows 7.
Если мы игнорируем ошибки в cp65001 и список новых кодировок, которые поддерживаются в более поздних версиях, то поведение должно быть таким же.
Это ошибка Python 2, уже помеченная как wontfix: https://bugs.python.org/issue19264
Я должен использовать Python 3, если вы хотите, чтобы он работал под Windows.