将可能包含非ASCII Unicode字符的PowerShell输出解码为Python字符串

6
我需要将从Python调用的PowerShell标准输出解码为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返回的stdout字节?
更新
经过一些实验后,我更加困惑了。通过chcp返回的我的控制台中的代码页是437。我将网络适配器名称更改为包含非ASCII和非CP437字符的名称。在运行Get-NetAdapter | select Name | fl的交互式PowerShell会话中,它正确地显示了名称,甚至包括其非CP437字符。当我从Python中调用PowerShell时,非ASCII字符被转换为最接近的ASCII字符(例如,ā变成a,ž变成z),并且.decode(ascii)工作得很好。这种行为(以及相应的解决方案)是否取决于Windows版本?我使用的是Windows 10,但用户可能使用旧版Windows,从Windows 7开始。

1
如果您的实际问题是如何将 powershell 输出作为 Unicode 文本获取,则应将其放入标题中(我不知道“默认 Windows 显示语言编码”应该是什么)。检查 powershell 是否接受显式参数来指定其 stdout 编码($OutputEncoding)。无关:在 Windows 上使用字符串传递命令,即使用 'a | b | c' 而不是 ['a', '|', 'b', '|', 'c'] - jfs
1
(1) 你的代码使用了二进制模式。在这种情况下,stdout 是以字节方式传输的。 universal_newlines=True 启用文本模式(是的,拼写不太直观)。(2) cp437 和 cp1252 的编码都与 ASCII 编码兼容,适用于 ASCII 字符(如果执行 .decode('ascii', 'strict') 操作成功,则意味着 stdout 中的所有字节都在 ASCII 范围内。它无法区分 cp437 和 cp1252)。 - jfs
很好。我会说universal_newlines符合要求。 - Eriks Dobelis
是的,这就是我提到乱码的原因。运行 print(check_output(['powershell', 'echo É'])) 会得到什么结果?(我不确定如何在 PowerShell 中写 'echo É')。如果输出中看到 b'\x90',则编码为 cp437。如果看到 b'\xc9',则编码为cp1252。另外,如果您不想调用 .decode('utf-8'),可以使用 for line in io.TextIOWrapper(process.stdout, encoding='utf-8'): - jfs
1
@J.F.Sebastian,管道输出的编码似乎使用控制台输出代码页。我测试了各种代码页,例如使用1252:ctypes.windll.kernel32.SetConsoleOutputCP(1252);``p = subprocess.Popen('powershell echo $([char]0xc9)', stdout=subprocess.PIPE);``p.stdout.read()。奇怪的是,如果我传递creationflags=DETACHED_PROCESS,这样powershell.exe就不会附加到控制台,它甚至没有一个合理的ANSI代码页默认值。它根本没有输出任何东西。 - Eryk Sun
显示剩余9条评论
2个回答

4

输出字符编码可能取决于特定的命令,例如:

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

在PowerShell会话中,使用PYTHONIOENCODING环境变量设置子脚本的字符编码。我选择了utf-32作为输出编码,以使其与Windows ANSI和OEM代码页有所不同,以进行演示。

请注意,父Python脚本的stdout编码是OEM代码页(在此示例中为cp437)--该脚本是从Windows控制台运行的。如果将父Python脚本的输出重定向到文件/管道,则Python 3默认使用ANSI代码页(例如cp1252)。

要解码可能包含当前OEM代码页无法解码的字符的powershell输出,您可以临时设置[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'

在stdout方面,fltee都使用[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' is replaced with b'\r\n' despite using binary API such as os.read/os.write (msvcrt.setmode(sys.stdout.fileno(), os.O_BINARY) has no effect here)
  • b'\r\n' is appended if there is no newline in the output:

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

    Output:

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

    The newline is appended to the piped output.

如果忽略孤立的代理,那么设置UTF8Encoding可以通过管道传递所有Unicode字符,包括非BMP字符。如果配置了$env:PYTHONIOENCODING = "utf-8:ignore",则可以在Python中使用文本模式。
在交互式powershell中运行Get-NetAdapter | select Name | fl将正确地显示名称,即使是非cp437字符。
如果未重定向标准输出,则使用Unicode API将字符打印到控制台 - 如果控制台(TrueType)字体支持它,则可以显示任何[BMP] Unicode字符。
当我从python调用powershell时,非ASCII字符被转换为最接近的ASCII字符(例如ā变成a,ž变成z),.decode(ascii)工作得很好。这可能是由于System.Text.InternalDecoderBestFitFallback设置为[Console]::OutputEncoding而导致的 - 如果一个Unicode字符不能编码成给定的编码,则被传递给回退(使用一个最佳匹配字符或'?'代替原始字符)。
如果我们忽略cp65001中的错误和一系列在后续版本中支持的新编码,则行为应该是相同的。是否与Windows版本有关呢?我在Windows 10上运行,但用户可能在Windows 7及以下版本上运行。

我假设您已经配置了py.exe以运行Python 3。为了让其他人受益,请添加“-3”选项。否则,非常赞。答案很好。 - Eryk Sun
就PowerShell的对象管道而言,似乎你必须彻底重新发明轮子才能获得一个二进制管道,避免你遇到的文本编码和LF转换为CRLF的问题。cmd shell只是在创建管道的过程中重定向自己的标准句柄。一旦进程建立,它就不会像中间人一样妨碍操作。 - Eryk Sun
当我提问时,我没有想到问题会如此困难。感谢您所投入的时间和详细的答复! - Eriks Dobelis
看起来你是最有资格回答关于Windows控制台编码的类似问题之一。 - Géry Ogam

-1

我已经在使用Python3了。它通过标签明确标记为Python-3.x问题。此外,您可以从代码(b'')中看到这是Python3。 - Eriks Dobelis
这个 bug 和问题无关。该 bug 是关于如何将命令行传递给 Windows,而问题是关于 subprocess 的 stdout 编码的。 - jfs
实际上它们是同一件事情:如果你在Windows上使用Unicode(W)API,你就可以让它正常工作,而无需进行解码/编码。 - sorin
1
@sorin,你有什么建议吗?从Python中以其他方式调用它? - Eriks Dobelis
1
@sorin:请提供一个Python 3的代码示例,无论“chcp”返回什么或“mbcs”对应什么,都将子进程的stdout作为Unicode返回。 - jfs

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接