首页 文章

将可能包含非ascii unicode字符的powershell输出解码为python字符串

提问于
浏览
4

我需要将从python调用的powershell stdout解码为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字节?

UPDATE

经过一些实验,我更加困惑 . 我的控制台中由 chcp 返回的代码页是437.我将网络适配器名称更改为包含非ascii和非cp437字符的名称 . 在交互式PowerShell中运行 Get-NetAdapter | select Name | fl 正确显示名称甚至是非cp437字符 . 当我从python中调用powershell时,非ascii字符转换为最接近的ascii字符(例如,ā到a,ž到z)和 .decode(ascii) 工作得很好 . 这种行为(以及相应的解决方案)可能依赖于Windows版本吗?我在Windows 10上,但用户可能在较旧的Windows上使用Windows 7 .

2 回答

  • 3

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

    #!/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 envvar设置的 . 我已选择 utf-32 作为输出编码,以便它与演示的Windows ANSI和OEM代码页不同 .

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

    要解码可能包含当前OEM代码页中不可解码字符的powershell输出,您可以临时设置 [Console]::OutputEncoding (受@eryksun's 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'
    

    对于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' 替换为 b'\r\n' 尽管使用了二进制API,例如 os.read/os.writemsvcrt.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字符 . 如果配置了 $env:PYTHONIOENCODING = "utf-8:ignore" ,则可以在Python中使用文本模式 .

    在运行Get-NetAdapter的交互式PowerShell中选择名称| fl正确显示名称甚至是非cp437字符 .

    如果未重定向stdout,则使用Unicode API将字符打印到控制台 - 如果控制台(TrueType)字体支持,则可以显示任何[BMP] Unicode字符 .

    当我从python中调用powershell时,非ascii字符被转换为最接近的ascii字符(例如ā到a,ž到z)和.decode(ascii)工作得很好 .

    这可能是由于 System.Text.InternalDecoderBestFitFallback[Console]::OutputEncoding 设置 - 如果Unicode字符无法在给定编码中编码,则将其传递给回退(使用最合适的char或 '?' 而不是原始字符) .

    这种行为(以及相应的解决方案)可能依赖于Windows版本吗?我在Windows 10上,但用户可能在较旧的Windows上使用Windows 7 .

    如果我们忽略cp65001中的错误以及更高版本中支持的新编码列表,那么行为应该是相同的 .

  • -1

    这是一个已被标记为wontfix的Python 2错误:https://bugs.python.org/issue19264

    如果你想让它在Windows下工作,我必须使用Python 3 .

相关问题