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

Eri*_*lis 6 windows unicode powershell subprocess python-3.x

我需要将从 Python 调用的 PowerShell stdout 解码为 Python 字符串。

\n\n

我的最终目标是以字符串列表的形式获取 Windows 上网络适配器的名称。我当前的函数如下所示,并且在使用英语的 Windows 10 上运行良好:

\n\n
def get_interfaces():\n    ps = subprocess.Popen(['powershell', 'Get-NetAdapter', '|', 'select Name', '|', 'fl'], stdout = subprocess.PIPE)\n    stdout, stdin = ps.communicate(timeout = 10)\n    interfaces = []\n    for i in stdout.split(b'\\r\\n'):\n        if not i.strip():\n            continue\n        if i.find(b':')<0:\n            continue\n        name, value = [ j.strip() for j in i.split(b':') ]\n        if name == b'Name':\n            interfaces.append(value.decode('ascii')) # This fails for other users\n    return interfaces\n
Run Code Online (Sandbox Code Playgroud)\n\n

其他用户使用不同的语言,因此value.decode('ascii')其中一些用户会失败。例如,一位用户报告说更改为decode('ISO 8859-2')对他来说效果很好(因此它不是 UTF-8)。我如何知道编码以解码调用 PowerShell 返回的标准输出字节?

\n\n

更新

\n\n

经过一些实验,我更加困惑了。我的控制台返回的代码页chcp是 437。我将网络适配器名称更改为包含非 ASCII 和非 cp437 字符的名称。在运行的交互式 PowerShell 会话中Get-NetAdapter | select Name | fl,它正确显示了名称,甚至是其非 CP437 字符。当我从 Python 调用 PowerShell 时,非 ASCII 字符被转换为最接近的 ASCII 字符(例如,\xc4\x81 转换为 a,\xc5\xbe 转换为 z)并且.decode(ascii)运行良好。此行为(以及相应的解决方案)是否与 Windows 版本相关?我使用的是 Windows 10,但用户可能使用旧版 Windows 直至 Windows 7。

\n

jfs*_*jfs 4

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

\n\n\n\n
#!/usr/bin/env python3\nimport subprocess\nimport sys\n\nencoding = \'utf-32\'\ncmd = r\'\'\'$env:PYTHONIOENCODING = "%s"; py -3 -c "print(\'\\u270c\')"\'\'\' % encoding\ndata = subprocess.check_output(["powershell", "-C", cmd])\nprint(sys.stdout.encoding)\nprint(data)\nprint(ascii(data.decode(encoding)))\n
Run Code Online (Sandbox Code Playgroud)\n\n

输出

\n\n
cp437\nb"\\xff\\xfe\\x00\\x00\\x0c\'\\x00\\x00\\r\\x00\\x00\\x00\\n\\x00\\x00\\x00"\n\'\\u270c\\r\\n\'\n
Run Code Online (Sandbox Code Playgroud)\n\n

\xe2\x9c\x8c ( U+270C ) 字符已成功接收。

\n\n

子脚本的字符编码是PYTHONIOENCODING在 PowerShell 会话中使用 envvar 设置的。我选择了utf-32输出编码,以便它与用于演示的 Windows ANSI 和 OEM 代码页不同。

\n\n

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

\n\n

要解码可能包含当前 OEM 代码页中无法解码的字符的 powershell 输出,您可以[Console]::OutputEncoding临时设置(受@eryksun\'s comments 的启发):

\n\n
#!/usr/bin/env python3\nimport io\nimport sys\nfrom subprocess import Popen, PIPE\n\nchar = ord(\'\xe2\x9c\x8c\')\nfilename = \'U+{char:04x}.txt\'.format(**vars())\nwith Popen(["powershell", "-C", \'\'\'\n    $old = [Console]::OutputEncoding\n    [Console]::OutputEncoding = [Text.Encoding]::UTF8\n    echo $([char]0x{char:04x}) | fl\n    echo $([char]0x{char:04x}) | tee {filename}\n    [Console]::OutputEncoding = $old\'\'\'.format(**vars())],\n           stdout=PIPE) as process:\n    print(sys.stdout.encoding)\n    for line in io.TextIOWrapper(process.stdout, encoding=\'utf-8-sig\'):\n        print(ascii(line))\nprint(ascii(open(filename, encoding=\'utf-16\').read()))\n
Run Code Online (Sandbox Code Playgroud)\n\n

输出

\n\n
cp437\n\'\\u270c\\n\'\n\'\\u270c\\n\'\n\'\\u270c\\n\'\n
Run Code Online (Sandbox Code Playgroud)\n\n

fltee用于[Console]::OutputEncoding标准输出(默认行为就像| Write-Output附加到管道)。tee使用 utf-16 将文本保存到文件中。输出显示 \xe2\x9c\x8c ( U+270C ) 已成功解码。

\n\n

$OutputEncoding用于解码管道中间的字节:

\n\n
#!/usr/bin/env python3\nimport subprocess\n\ncmd = r\'\'\'\n  $OutputEncoding = [Console]::OutputEncoding = New-Object System.Text.UTF8Encoding\n  py -3 -c "import os; os.write(1, \'\\U0001f60a\'.encode(\'utf-8\')+b\'\\n\')" |\n  py -3 -c "import os; print(os.read(0, 512))"\n\'\'\'\nsubprocess.check_call(["powershell", "-C", cmd])\n
Run Code Online (Sandbox Code Playgroud)\n\n

输出

\n\n
b\'\\xf0\\x9f\\x98\\x8a\\r\\n\'\n
Run Code Online (Sandbox Code Playgroud)\n\n

那是对的:b\'\\xf0\\x9f\\x98\\x8a\'.decode(\'utf-8\') == u\'\\U0001f60a\'。使用默认值$OutputEncoding(ascii)我们会得到b\'????\\r\\n\'相反的结果。

\n\n

笔记:

\n\n
    \n
  • b\'\\n\'b\'\\r\\n\'尽管使用二进制 API,但仍被替换为os.read/os.writemsvcrt.setmode(sys.stdout.fileno(), os.O_BINARY)此处无效)
  • \n
  • b\'\\r\\n\'如果输出中没有换行符,则附加:

    \n\n
    #!/usr/bin/env python3\nfrom subprocess import check_output\n\ncmd = \'\'\'py -3 -c "print(\'no newline in the input\', end=\'\')"\'\'\'\ncat = \'\'\'py -3 -c "import os; os.write(1, os.read(0, 512))"\'\'\'  # pass as is\npiped = check_output([\'powershell\', \'-C\', \'{cmd} | {cat}\'.format(**vars())])\nno_pipe = check_output([\'powershell\', \'-C\', \'{cmd}\'.format(**vars())])\nprint(\'piped:   {piped}\\nno pipe: {no_pipe}\'.format(**vars()))\n
    Run Code Online (Sandbox Code Playgroud)\n\n

    输出:

    \n\n
    piped:   b\'no newline in the input\\r\\n\'\nno pipe: b\'no newline in the input\'\n
    Run Code Online (Sandbox Code Playgroud)\n\n

    换行符将附加到管道输出中。

  • \n
\n\n

如果我们忽略单独的代理,则设置UTF8Encoding允许通过管道传递所有 Unicode 字符,包括非 BMP 字符。如果配置的话,可以在 Python 中使用文本模式$env:PYTHONIOENCODING = "utf-8:ignore"

\n\n
\n

在交互式 powershell 运行中,Get-NetAdapter | select Name | fl即使是非 cp437 字符,也能正确显示名称。

\n
\n\n

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

\n\n
\n

当我从 python 调用 powershell 时,非 ascii 字符被转换为最接近的 ascii 字符(例如 \xc4\x81 到 a,\xc5\xbe 到 z),并且 .decode(ascii) 工作得很好。

\n
\n\n

这可能是由于System.Text.InternalDecoderBestFitFallback设置[Console]::OutputEncoding- 如果 Unicode 字符无法以给定编码进行编码,则将其传递给后备(要么使用最适合的字符,要么\'?\'使用而不是原始字符)。

\n\n
\n

此行为(以及相应的解决方案)是否与 Windows 版本相关?我使用的是 Windows 10,但用户可能使用旧版 Windows 直至 Windows 7。

\n
\n\n

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

\n