将命令行输入解码为 Unicode Python 2.7 脚本的最佳方式

Best way to decode command line inputs to Unicode Python 2.7 scripts

我所有的脚本自始至终都使用 Unicode 文字,

from __future__ import unicode_literals

但是当有可能使用字节串调用函数时,这会产生一个问题,我想知道处理这个问题并产生明显有用错误的最佳方法是什么。

我采用的一种常见方法是在它发生时简单地说明这一点,例如

def my_func(somearg):
    """The 'somearg' argument must be Unicode."""
    if not isinstance(arg, unicode):
        raise TypeError("Parameter 'somearg' should be a Unicode")
    # ...

对于所有需要是 Unicode 的参数(并且可能是字节串)。但是,即使我这样做,如果提供的参数对应于此类参数,我的 argparse 命令行脚本也会遇到问题,我想知道这里最好的方法是什么。看来我可以简单地检查这些参数的编码,并使用该编码对它们进行解码,例如

if __name__ == '__main__':
    parser = argparse.ArgumentParser(...)
    parser.add_argument('somearg', ...)
    # ...

    args = parser.parse_args()
    some_arg = args.somearg
    if not isinstance(config_arg, unicode):
        some_arg = some_arg.decode(sys.getfilesystemencoding())

    #...
    my_func(some_arg, ...)

这种方法组合是否是可能接收字节串输入的 Unicode 模块的常见设计模式?具体来说,

我不认为 getfilesystemencoding 一定会为 shell 获得正确的编码,它取决于 shell(并且可以通过 shell 自定义,独立于文件系统)。文件系统编码只关心非 ascii 文件名是如何存储的。

相反,您可能应该查看 sys.stdin.encoding,它将为您提供标准输入的编码。

此外,您可以考虑在添加参数时使用 type 关键字参数:

import sys
import argparse as ap

def foo(str_, encoding=sys.stdin.encoding):
    return str_.decode(encoding)

parser = ap.ArgumentParser()
parser.add_argument('my_int', type=int)
parser.add_argument('my_arg', type=foo)
args = parser.parse_args()

print repr(args)

演示:

$ python spam.py abc hello
usage: spam.py [-h] my_int my_arg
spam.py: error: argument my_int: invalid int value: 'abc'
$ python spam.py 123 hello
Namespace(my_arg=u'hello', my_int=123)
$ python spam.py 123 ollǝɥ
Namespace(my_arg=u'oll\u01dd\u0265', my_int=123)

如果您必须大量使用非 ascii 数据,我强烈建议升级到 python3。那里的一切都容易得多,例如,解析的参数在 python3 上已经是 unicode 了。


由于关于命令行参数编码的信息存在冲突,我决定通过将我的 shell 编码更改为 latin-1 whilst leaving the file system encoding as utf-8. For my tests I use the c-cedilla character 来测试它,这两个编码具有不同的编码:

>>> u'Ç'.encode('ISO8859-1')
'\xc7'
>>> u'Ç'.encode('utf-8')
'\xc3\x87'

现在我创建一个示例脚本:

#!/usr/bin/python2.7
import argparse as ap
import sys

print 'sys.stdin.encoding is ', sys.stdin.encoding
print 'sys.getfilesystemencoding() is', sys.getfilesystemencoding()

def encoded(s):
    print 'encoded', repr(s)
    return s

def decoded_filesystemencoding(s):
    try:
        s = s.decode(sys.getfilesystemencoding())
    except UnicodeDecodeError:
        s = 'failed!'
    return s

def decoded_stdinputencoding(s):
    try:
        s = s.decode(sys.stdin.encoding)
    except UnicodeDecodeError:
        s = 'failed!'
    return s

parser = ap.ArgumentParser()
parser.add_argument('first', type=encoded)
parser.add_argument('second', type=decoded_filesystemencoding)
parser.add_argument('third', type=decoded_stdinputencoding)
args = parser.parse_args()

print repr(args)

然后我将 shell 编码更改为 ISO/IEC 8859-1:

然后我调用脚本:

wim-macbook:tmp wim$ ./spam.py Ç Ç Ç
sys.stdin.encoding is  ISO8859-1
sys.getfilesystemencoding() is utf-8
encoded '\xc7'
Namespace(first='\xc7', second='failed!', third=u'\xc7')

如您所见,命令行参数采用 latin-1 编码,因此第二个命令行参数(使用 sys.getfilesystemencoding)无法解码。第三个命令行参数(使用 sys.stdin.encoding)正确解码。

sys.getfilesystemencoding()(但请参阅示例) 对 OS 数据(例如文件名、环境变量和命令行参数)的编码。

您可以看到选择背后的逻辑:sys.argv[0] 可能是脚本(文件名)的路径,因此很自然地假设它使用与其他文件名和其他项目相同的编码在 argv 列表中使用与 sys.argv[0] 相同的字符编码。 os.environ['PATH'] 包含路径,因此环境变量使用相同的编码也是很自然的:

$ echo 'import sys; print(sys.argv)' >print_argv.py
$ python print_argv.py
['print_argv.py']

注意:sys.argv[0] 脚本文件名,无论您可能有其他命令行参数。

"best way" 取决于您的具体用例,例如,在 Windows 上,您可能应该 use Unicode API directly (CommandLineToArgvW()). On POSIX, if all you need is to pass some argv items to OS functions back (such as os.listdir()) then you could leave them as bytes -- command-line argument can be arbitrary byte sequence, see PEP 0383 -- Non-decodable Bytes in System Character Interfaces:

import os, sys

os.execl(sys.executable, sys.executable, '-c', 'import sys; print(sys.argv)',
         bytes(bytearray(range(1, 0x100))))

如您所见,POSIX 允许传递任何字节(零字节除外)。

显然,您也可以错误配置您的环境:

$ LANG=C PYTHONIOENCODING=latin-1 python -c'import sys;
>   print(sys.argv, sys.stdin.encoding, sys.getfilesystemencoding())' €
(['-c', '\xe2\x82\xac'], 'latin-1', 'ANSI_X3.4-1968') # Linux output

输出显示 使用 utf-8 编码,但语言环境和 PYTHONIOENCODING 的配置不同。

示例表明 sys.argv 可能使用不符合任何标准编码的字符编码进行编码,甚至可能包含 P[=73= 上的任意(零字节除外)二进制数据]IX(无字符编码)。在 Windows 上,我想,您可以粘贴无法使用 ANSI 或 OEM Windows 编码进行编码的 Unicode 字符串,但无论如何您都可以使用 Unicode API 获得正确的值(Python 2 可能会在此处丢弃数据)。

Python 3 使用 Unicode sys.argv 因此它不应该在 Windows 上丢失数据(使用 Unicode API)并且它允许证明 sys.getfilesystemencoding() 用于(不是 sys.stdin.encoding)在 Linux 上解码 sys.argv(其中 sys.getfilesystemencoding() 源自语言环境):

$ LANG=C.UTF-8 PYTHONIOENCODING=latin-1 python3 -c'import sys; print(*map(ascii, sys.argv))' µ
'-c' '\xb5'
$ LANG=C PYTHONIOENCODING=latin-1 python3 -c'import sys; print(*map(ascii, sys.argv))' µ
'-c' '\udcc2\udcb5'
$ LANG=en_US.ISO-8859-15 PYTHONIOENCODING=latin-1 python3 -c'import sys; print(*map(ascii, sys.argv))' µ
'-c' '\xc2\xb5'

输出显示 LANG 在这种情况下定义语言环境,在 Linux 上定义 sys.getfilesystemencoding() 用于解码命令行参数:

$ python3
>>> print(ascii(b'\xc2\xb5'.decode('utf-8')))
'\xb5'
>>> print(ascii(b'\xc2\xb5'.decode('ascii', 'surrogateescape')))
'\udcc2\udcb5'
>>> print(ascii(b'\xc2\xb5'.decode('iso-8859-15')))
'\xc2\xb5'