Décoder la sortie powershell contenant peut-être des caractères unicode non ascii dans une chaîne python

je dois décoder powershell stdout appelé de python dans une chaîne de Python.

Mon but ultime est d'obtenir dans une forme d'une liste de chaînes de caractères les noms des cartes réseau dans Windows. Ma fonction actuelle ressemble à cela et fonctionne bien sur Windows 10 avec la langue anglaise:

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'rn'):
        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

, d'Autres utilisateurs ont des langues différentes, donc value.decode('ascii') échoue pour certains d'entre eux. Par exemple: un utilisateur a déclaré que le passage à decode('ISO 8859-2') fonctionne bien pour lui (donc il n'est pas de l'UTF-8). Comment puis-je connaître l'encodage pour décoder les octets stdout retournés par call à powershell?

UPDATE

après quelques expériences, je suis encore plus confus. Codepage dans ma console comme retourné par chcp est 437. J'ai changé le nom de l'adaptateur réseau en un nom contenant des caractères non-ascii et non-cp437. Dans interactive powershell, l'exécution Get-NetAdapter | select Name | fl affichait correctement le nom, même son caractère non-cp437. Quand j'ai appelé powershell à partir de Python, les caractères non-ascii ont été convertis en caractères ASCII les plus proches (par exemple, ❚ à a, ž à z) et .decode(ascii) a bien fonctionné. Ce comportement (et la solution correspondante) pourrait-il dépendre de la version de Windows? Je suis sur Windows 10, mais les utilisateurs pourraient être sur les anciens Windows vers Windows 7.

4
demandé sur Cœur 2015-11-26 13:22:50

2 réponses

le codage du caractère de sortie peut dépendre de commandes spécifiques, par exemple:

#!/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)))

sortie

cp437
b"\xff\xfe\x00\x00\x0c'\x00\x00\r\x00\x00\x00\n\x00\x00\x00"
'\u270c\r\n'
Le caractère

( U+270C ) est reçu avec succès.

le codage des caractères du script enfant est défini en utilisant PYTHONIOENCODING envvar à l'intérieur de la session PowerShell. J'ai choisi utf-32 pour le codage de sortie de sorte qu'il serait différent de Windows ANSI et OEM pages de code pour la démonstration.

notez que l'encodage stdout du script parent Python est la page de code OEM ( cp437 dans ce cas) -- le script est exécuté depuis la console Windows. Si vous redirigez la sortie du script parent Python vers un fichier/pipe, alors la page de code ANSI (par exemple, cp1252 ) est utilisée par défaut dans Python 3.

pour décoder la sortie powershell qui pourrait contenir des caractères indécodables dans la page de code OEM actuelle, vous pourrait mettre [Console]::OutputEncoding Temporairement (inspiré par les commentaires de @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()))

sortie

cp437
'\u270c\n'
'\u270c\n'
'\u270c\n'

à la fois fl et tee utiliser [Console]::OutputEncoding pour stdout (le comportement par défaut est comme si | Write-Output est ajouté aux pipelines). tee utilise utf-16, pour enregistrer un texte dans un fichier. La sortie montre que la valeur de l'option ( U+270C ) est décodée avec succès.

$OutputEncoding est utilisé pour décoder des octets dans le milieu d'un pipeline:

#!/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])

sortie

b'\xf0\x9f\x98\x8a\r\n'

qui est correcte: b'\xf0\x9f\x98\x8a'.decode('utf-8') == u'\U0001f60a' . Avec le défaut $OutputEncoding (ascii) nous obtiendrions b'????\r\n' à la place.

Note:

  • b'\n' est remplacé par b'\r\n' malgré l'utilisation D'API binaires tels que os.read/os.write ( msvcrt.setmode(sys.stdout.fileno(), os.O_BINARY) n'a aucun effet ici)
  • b'\r\n' est ajouté s'il n'y a pas de nouvelle ligne dans la sortie:

    #!/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()))
    

    sortie:

    piped:   b'no newline in the input\r\n'
    no pipe: b'no newline in the input'
    

    la nouvelle ligne est ajoutée à la sortie pipée.

si nous ignorons les substituts solitaires, le paramètre UTF8Encoding permet de passer par des pipes tous les caractères Unicode incluant les caractères non-BMP. Le mode texte pourrait être utilisé en Python Si $env:PYTHONIOENCODING = "utf-8:ignore" est configurer.

dans l'exécution interactive powershell Get-NetAdapter | select Name | fl affichait correctement le nom, même son caractère non-cp437.

si stdout n'est pas redirigé, alors L'API Unicode est utilisée, pour imprimer des caractères à la console -- n'importe quel caractère Unicode [BMP] peut être affiché si la police de la console (TrueType) le supporte.

quand j'ai appelé powershell des caractères non-ascii de python ont été convertis en caractères ascii les plus proches (par ex.decode (ascii) a bien fonctionné.

cela pourrait être dû à System.Text.InternalDecoderBestFitFallback défini pour [Console]::OutputEncoding -- si un caractère Unicode ne peut pas être encodé dans un encodage donné, alors il est passé à la repli (soit un caractère mieux adapté ou '?' est utilisé à la place du caractère original).

ce comportement (et la solution correspondante) pourrait-il être Windows dépendant de la version? Je suis sur Windows 10, mais les utilisateurs pourraient être sur les anciens Windows vers Windows 7.

si nous ignorons les bogues dans cp65001 et une liste de nouveaux encodages qui sont supportés dans les versions ultérieures, alors le comportement devrait être le même.

3
répondu jfs 2018-05-20 07:38:31

c'est un bug Python 2 déjà marqué wontfix: https://bugs.python.org/issue19264

je dois utiliser Python 3 si vous voulez le faire fonctionner sous Windows.

-1
répondu sorin 2015-11-26 15:26:26