windows - Decodifique la salida de PowerShell que posiblemente contenga caracteres Unicode no ASCII en una cadena de Python
python-3.x subprocess (2)
Necesito decodificar powerhell stdout llamado desde python en una cadena de python.
Mi objetivo final es obtener en forma de una lista de cadenas los nombres de los adaptadores de red en Windows. Mi función actual se ve así y funciona bien en Windows 10 con idioma inglés:
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
Otros usuarios tienen diferentes idiomas, por lo que
value.decode(''ascii'')
falla para algunos de ellos.
Por ejemplo, un usuario informó que cambiar a
decode(''ISO 8859-2'')
funciona bien para él (por lo que no es UTF-8).
¿Cómo puedo saber la codificación para decodificar los bytes stdout devueltos por la llamada a powershell?
ACTUALIZAR
Después de algunos experimentos, estoy aún más confundido.
La página de códigos en mi consola según lo devuelto por
chcp
es 437. Cambié el nombre del adaptador de red a un nombre que contenga caracteres no ascii y no cp437.
En
Get-NetAdapter | select Name | fl
interactivo ejecutando
Get-NetAdapter | select Name | fl
Get-NetAdapter | select Name | fl
Get-NetAdapter | select Name | fl
muestra correctamente el nombre incluso su carácter no cp437.
Cuando llamé a powershell desde python, los caracteres no ascii se convirtieron en caracteres ascii más cercanos (por ejemplo, ā a a, ž a z) y
.decode(ascii)
funcionó muy bien.
¿Podría este comportamiento (y la correspondiente solución) depender de la versión de Windows?
Estoy en Windows 10, pero los usuarios podrían estar en Windows anterior hasta Windows 7.
Es un error de Python 2 ya marcado como wontfix: https://bugs.python.org/issue19264
Debo usar Python 3 si quieres que funcione en Windows.
La codificación de caracteres de salida puede depender de comandos específicos, por ejemplo:
#!/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)))
Salida
cp437
b"/xff/xfe/x00/x00/x0c''/x00/x00/r/x00/x00/x00/n/x00/x00/x00"
''/u270c/r/n''
El carácter ✌ ( U+270C ) se recibe con éxito.
La codificación de caracteres del script secundario se establece utilizando
PYTHONIOENCODING
envvar dentro de la sesión de PowerShell.
utf-32
para la codificación de salida para que sea diferente de las páginas de códigos ANSI y OEM de Windows para la demostración.
Observe que la codificación stdout del script de Python principal es la página de códigos OEM (
cp437
en este caso): el script se ejecuta desde la consola de Windows.
Si redirige la salida de la secuencia de comandos Python principal a un archivo / tubería, la página de códigos ANSI (por ejemplo,
cp1252
) se usa de forma predeterminada en Python 3.
Para decodificar la salida de PowerShell que puede contener caracteres no codificables en la página de códigos OEM actual, puede configurar
[Console]::OutputEncoding
temporalmente (inspirado en
los comentarios 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()))
Salida
cp437
''/u270c/n''
''/u270c/n''
''/u270c/n''
Tanto
fl
como
tee
usan
[Console]::OutputEncoding
para stdout (el comportamiento predeterminado es como si
| Write-Output
se agregara a las tuberías).
tee
usa utf-16, para guardar un texto en un archivo.
La salida muestra que ✌ (
U+270C
) se decodifica con éxito.
$OutputEncoding
se usa para decodificar bytes en el medio de una tubería:
#!/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])
Salida
b''/xf0/x9f/x98/x8a/r/n''
eso es correcto:
b''/xf0/x9f/x98/x8a''.decode(''utf-8'') == u''/U0001f60a''
.
Con el valor predeterminado
$OutputEncoding
(ascii) obtendríamos
b''????/r/n''
lugar.
Nota:
-
b''/n''
se reemplaza porb''/r/n''
pesar de usar API binaria comoos.read/os.write
(msvcrt.setmode(sys.stdout.fileno(), os.O_BINARY)
no tiene ningún efecto aquí) -
b''/r/n''
se agrega si no hay nueva línea en la salida:#!/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()))
Salida:
piped: b''no newline in the input/r/n'' no pipe: b''no newline in the input''
La nueva línea se agrega a la salida canalizada.
Si ignoramos los sustitutos solitarios, la configuración de
UTF8Encoding
permite pasar por canalizaciones todos los caracteres Unicode, incluidos los que no son BMP.
El modo de texto podría usarse en Python si
$env:PYTHONIOENCODING = "utf-8:ignore"
está configurado.
En
Get-NetAdapter | select Name | fl
interactivo ejecutandoGet-NetAdapter | select Name | fl
Get-NetAdapter | select Name | fl
Get-NetAdapter | select Name | fl
muestra correctamente el nombre incluso su carácter no cp437.
Si stdout no se redirige, se utiliza la API Unicode para imprimir caracteres en la consola; cualquier carácter Unicode [BMP] se puede mostrar si la fuente de la consola (TrueType) lo admite.
Cuando llamé a powershell desde python, los caracteres no ascii se convirtieron en caracteres ascii más cercanos (por ejemplo, ā a a, ž a z) y .decode (ascii) funcionó muy bien.
Puede deberse a
System.Text.InternalDecoderBestFitFallback
establecido para
[Console]::OutputEncoding
: si un carácter Unicode no se puede codificar en una codificación determinada, entonces se pasa a la reserva (¿un mejor ajuste o
''?''
se usa en lugar del carácter original).
¿Podría este comportamiento (y la correspondiente solución) depender de la versión de Windows? Estoy en Windows 10, pero los usuarios podrían estar en Windows anterior hasta Windows 7.
Si ignoramos los errores en cp65001 y una lista de nuevas codificaciones compatibles con versiones posteriores, el comportamiento debería ser el mismo.