Расшифруйте вывод 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). Как узнать кодировку для декодирования байтов стандартного вывода, возвращаемых при вызове PowerShell?

UPDATE

После некоторых экспериментов я еще больше растерялся. Кодовая страница в моей консоли, возвращаемая 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.

Ответ 1

Кодировка выходного символа может зависеть от конкретных команд, например:

#!/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 + 270C) символ получен успешно.

Кодировка символов дочернего сценария устанавливается с помощью envvar PYTHONIOENCODING внутри сеанса PowerShell. Я выбрал utf-32 для выходной кодировки, чтобы она отличалась от кодовых страниц Windows ANSI и OEM для демонстрации.

Обратите внимание, что стандартная кодировка родительского сценария Python - это кодовая страница OEM (в данном случае cp437) - сценарий запускается из консоли Windows. Если вы перенаправите вывод родительского сценария Python в файл/канал, то в Python 3 по умолчанию будет использоваться кодовая страница ANSI (например, cp1252).

Чтобы декодировать вывод powershell, который может содержать символы, которые невозможно декодировать в текущей кодовой странице OEM, вы можете временно установить [Console]::OutputEncoding (вдохновлено @eryksun comments):

#!/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 позволяет передавать по каналам все символы Юникода, включая символы не-BMP. Текстовый режим может использоваться в Python, если настроен $env:PYTHONIOENCODING = "utf-8:ignore".

В интерактивном режиме PowerShell Get-NetAdapter | select Name | fl правильно отображалось имя, даже не символ cp437.

Если стандартный вывод не перенаправлен, то для печати символов в консоль используется API-интерфейс Unicode - любой символ [BMP] Unicode может отображаться, если его поддерживает шрифт консоли (TrueType).

Когда я вызывал powershell из python, символы не-ascii были преобразованы в наиболее близкие символы ascii (например, от a до a, от z до z), и .decode(ascii) работал хорошо.

Это может быть связано с тем, что System.Text.InternalDecoderBestFitFallback установлено для [Console]::OutputEncoding - если символ Unicode не может быть закодирован в заданной кодировке, то он передается в качестве запасного варианта (вместо знака используется либо наиболее подходящий символ, либо '?'). оригинальный персонаж).

Может ли это поведение (и, соответственно, решение) зависеть от версии Windows? Я нахожусь на Windows 10, но пользователи могли быть на более старой Windows до Windows 7.

Если мы игнорируем ошибки в cp65001 и список новых кодировок, которые поддерживаются в более поздних версиях, то поведение должно быть таким же.

Ответ 2

Это ошибка Python 2, уже отмеченная как wontfix: https://bugs.python.org/issue19264

Я должен использовать Python 3, если вы хотите заставить его работать под Windows.