Декодировать вывод 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.

Другие вопросы по тегам