将字节转换为字符串

我正在使用以下代码从外部程序获取标准输出:

>>> from subprocess import *
>>> command_stdout = Popen(['ls', '-l'], stdout=PIPE).communicate()[0]

communication()方法返回一个字节数组:

>>> command_stdout
b'total 0\n-rw-rw-r-- 1 thomas thomas 0 Mar  3 07:03 file1\n-rw-rw-r-- 1 thomas thomas 0 Mar  3 07:03 file2\n'

但是,我想将输出作为普通的 Python 字符串使用。这样我就可以像这样打印它:

>>> print(command_stdout)
-rw-rw-r-- 1 thomas thomas 0 Mar  3 07:03 file1
-rw-rw-r-- 1 thomas thomas 0 Mar  3 07:03 file2

我以为那是binascii.b2a_qp()方法的用途,但是当我尝试它时,我又得到了相同的字节数组:

>>> binascii.b2a_qp(command_stdout)
b'total 0\n-rw-rw-r-- 1 thomas thomas 0 Mar  3 07:03 file1\n-rw-rw-r-- 1 thomas thomas 0 Mar  3 07:03 file2\n'

如何将字节值转换回字符串?我的意思是,使用 “电池” 而不是手动进行操作。我希望它与 Python 3 兼容。

答案

您需要解码 bytes 对象以产生一个字符串:

>>> b"abcde"
b'abcde'

# utf-8 is used here because it is a very common encoding, but you
# need to use the encoding your data is actually in.
>>> b"abcde".decode("utf-8") 
'abcde'

您需要解码该字节字符串,然后将其转换为字符(Unicode)字符串。

在 Python 2 上

encoding = 'utf-8'
'hello'.decode(encoding)

要么

unicode('hello', encoding)

在 Python 3 上

encoding = 'utf-8'
b'hello'.decode(encoding)

要么

str(b'hello', encoding)

我认为这种方式很简单:

bytes_data = [112, 52, 52]
"".join(map(chr, bytes_data))
>> p44

如果您不知道编码,则要以 Python 3 和 Python 2 兼容的方式将二进制输入读取为字符串,请使用古老的 MS-DOS CP437编码:

PY3K = sys.version_info >= (3, 0)

lines = []
for line in stream:
    if not PY3K:
        lines.append(line)
    else:
        lines.append(line.decode('cp437'))

因为编码是未知的,所以希望将非英文符号转换为cp437字符(不翻译英文字符,因为它们在大多数单字节编码和 UTF-8 中都匹配)。

将任意二进制输入解码为 UTF-8 是不安全的,因为您可能会得到以下信息:

>>> b'\x00\x01\xffsd'.decode('utf-8')
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
UnicodeDecodeError: 'utf-8' codec can't decode byte 0xff in position 2: invalid
start byte

同样适用于latin-1 ,它在 Python 2 中很流行(默认值?)。请参见 “ 代码页布局” 中的遗漏点 - 这是 Python 臭名昭著的ordinal not in range

UPDATE 20150604 :有传言称 Python 3 具有surrogateescape错误策略,可将东西编码为二进制数据而不会导致数据丢失和崩溃,但它需要转换测试[binary] -> [str] -> [binary]来验证这两种性能和可靠性。

更新 20170116 :感谢Nearoo 的评论 - 还可以使用backslashreplace替换错误处理程序对所有未知字节进行斜杠转义。这仅适用于 Python 3,因此即使采用这种解决方法,您仍然会从不同的 Python 版本获得不一致的输出:

PY3K = sys.version_info >= (3, 0)

lines = []
for line in stream:
    if not PY3K:
        lines.append(line)
    else:
        lines.append(line.decode('utf-8', 'backslashreplace'))

有关详细信息,请参见Python 的 Unicode 支持

更新 20170119 :我决定实现适用于 Python 2 和 Python 3 的斜杠转义解码。它应该比cp437解决方案要慢,但是在每个 Python 版本上它都应产生相同的结果

# --- preparation

import codecs

def slashescape(err):
    """ codecs error handler. err is UnicodeDecode instance. return
    a tuple with a replacement for the unencodable part of the input
    and a position where encoding should continue"""
    #print err, dir(err), err.start, err.end, err.object[:err.start]
    thebyte = err.object[err.start:err.end]
    repl = u'\\x'+hex(ord(thebyte))[2:]
    return (repl, err.end)

codecs.register_error('slashescape', slashescape)

# --- processing

stream = [b'\x80abc']

lines = []
for line in stream:
    lines.append(line.decode('utf-8', 'slashescape'))

在 Python 3 中 ,默认编码为"utf-8" ,因此您可以直接使用:

b'hello'.decode()

相当于

b'hello'.decode(encoding="utf-8")

另一方面, 在 Python 2 中 ,编码默认为默认的字符串编码。因此,您应该使用:

b'hello'.decode(encoding)

encoding是您想要的编码。

注意:在 Python 2.7 中添加了对关键字参数的支持。

我认为您实际上想要这样:

>>> from subprocess import *
>>> command_stdout = Popen(['ls', '-l'], stdout=PIPE).communicate()[0]
>>> command_text = command_stdout.decode(encoding='windows-1252')

Aaron 的答案是正确的,只是您需要知道要使用哪种编码。而且我相信 Windows 使用的是 “windows-1252”。仅当内容中包含一些不寻常的(非 ASCII)字符时,这才有意义,但这将有所作为。

顺便说一句,这事的事实是,理由是 Python 的移动使用两种不同类型的二进制和文本数据:它不能神奇地将它们转换之间,因为它不知道编码,除非你告诉它!您唯一知道的方法是阅读 Windows 文档(或在此处阅读)。

将 Universal_newlines 设置为 True,即

command_stdout = Popen(['ls', '-l'], stdout=PIPE, universal_newlines=True).communicate()[0]

虽然@Aaron Maenpaa 的答案有效,但最近有用户

有没有更简单的方法? 'fhand.read()。decode(“ASCII”)'[...] 太长了!

您可以使用:

command_stdout.decode()

decode()有一个标准参数

codecs.decode(obj, encoding='utf-8', errors='strict')

要将字节序列解释为文本,您必须知道相应的字符编码:

unicode_text = bytestring.decode(character_encoding)

例:

>>> b'\xc2\xb5'.decode('utf-8')
'µ'

ls命令可能会产生无法解释为文本的输出。 Unix 上的文件名可以是任何字节序列,但斜杠b'/'和零b'\0'除外:

>>> open(bytes(range(0x100)).translate(None, b'\0/'), 'w').close()

尝试使用 utf-8 编码对此类字节汤进行解码会引发UnicodeDecodeError

可能会更糟。如果使用错误的不兼容编码,解码可能会默默失败并产生mojibake

>>> '—'.encode('utf-8').decode('cp1252')
'—'

数据已损坏,但是您的程序仍然不知道发生了故障。

通常,要使用的字符编码不会嵌入字节序列本身。您必须带外传达此信息。有些结果比其他结果更有可能,因此存在可以猜测字符编码的chardet模块。单个 Python 脚本可能在不同位置使用多种字符编码。


可以使用os.fsdecode()函数将ls输出转换为 Python 字符串,该函数即使对于无法解码的文件名也成功(在 Unix 上使用sys.getfilesystemencoding()surrogateescape错误处理程序):

import os
import subprocess

output = os.fsdecode(subprocess.check_output('ls'))

要获取原始字节,可以使用os.fsencode()

如果您传递universal_newlines=True参数,则subprocess locale.getpreferredencoding(False)使用locale.getpreferredencoding(False)解码字节,例如,在 Windows 上可以是cp1252

要即时解码字节流,可以使用io.TextIOWrapper()example

不同的命令可能对其输出使用不同的字符编码,例如dir内部命令( cmd )可能使用 cp437。要解码其输出,可以显式传递编码(Python 3.6+):

output = subprocess.check_output('dir', shell=True, encoding='cp437')

文件名可能不同于os.listdir() (使用 Windows Unicode API),例如, '\xb6'可以替换为'\x14' x14'-Python 的 cp437 编解码器映射b'\x14'来控制字符 U + 0014 而不是 U + 00B6(¶)。要支持带有任意 Unicode 字符的文件名,请参阅将PowerShell 输出可能包含非 ASCII Unicode 字符解码为 Python 字符串。

由于这个问题实际上是在询问subprocess输出,因此您可以使用更直接的方法,因为Popen接受一个编码关键字(在 Python 3.6 + 中):

>>> from subprocess import Popen, PIPE
>>> text = Popen(['ls', '-l'], stdout=PIPE, encoding='utf-8').communicate()[0]
>>> type(text)
str
>>> print(text)
total 0
-rw-r--r-- 1 wim badger 0 May 31 12:45 some_file.txt

其他用户的一般答案是将字节解码为文本:

>>> b'abcde'.decode()
'abcde'

不带参数的情况下,将使用sys.getdefaultencoding() 。如果您的数据不是sys.getdefaultencoding() ,那么必须在decode调用中显式指定编码:

>>> b'caf\xe9'.decode('cp1250')
'café'