在Python 3中将二进制字符串转换为字节数组

16
尽管有很多相关的问题,但我找不到符合我的问题的。我想把一个二进制字符串(例如,"0110100001101001")转换成字节数组(同样的例子,b"hi")。
我尝试了这个:
bytes([int(i) for i in "0110100001101001"])

但我得到了:

b'\x00\x01\x01\x00\x01' #... and so on

在Python 3中,这应该如何正确地实现?


3个回答

28

这是按照 Patrick 所提到的第一种方式进行操作的示例:将比特串转换为整数,并每次取 8 位。自然的方法是以相反的顺序生成字节。为了将字节恢复到正确的顺序,我使用 b[::-1] 对 bytearray 进行扩展切片符号,步长为 -1。

def bitstring_to_bytes(s):
    v = int(s, 2)
    b = bytearray()
    while v:
        b.append(v & 0xff)
        v >>= 8
    return bytes(b[::-1])

s = "0110100001101001"
print(bitstring_to_bytes(s))

显然,Patrick的第二种方式更紧凑。 :)

然而,在Python 3中有一种更好的方法:使用int.to_bytes方法:

def bitstring_to_bytes(s):
    return int(s, 2).to_bytes((len(s) + 7) // 8, byteorder='big')

如果 len(s) 被保证是 8 的倍数,则 .to_bytes 的第一个参数可以简化:

return int(s, 2).to_bytes(len(s) // 8, byteorder='big')

如果len(s)不是8的倍数,这将引发OverflowError,某些情况下可能会希望如此。


另一种选择是使用双重否定执行上取整除法。对于整数a和b,可以使用//进行下取整除法。

n = a // b

给出整数n使得
n <= a/b < n + 1
例如,
47 // 10 给出4,以及

-47 // 10 给出-5。因此

-(-47 // 10) 给出5,有效执行取整除法。

因此,在 bitstring_to_bytes 中,我们可以这样做:

return int(s, 2).to_bytes(-(-len(s) // 8), byteorder='big')

然而,很少有人熟悉这种高效且紧凑的习惯用语,因此它通常被认为比较难读。

return int(s, 2).to_bytes((len(s) + 7) // 8, byteorder='big')

5
len(s) // 8 可能会出错,使用(len(s) + 7) // 8代替 - jfs
int.to_bytes本质上是第一个方法--只是在C中比Python更高效地完成。 - Patrick Maupin
@J.F.Sebastian:说得好,你的代码更加健壮,我的代码假设输入的比特串已经被正确构造。另一种计算长度不是8的整数倍的比特串的正确大小的方法是使用“向上取整除法”技巧:-(-len(s) // 8) - PM 2Ring
1
谢谢你的回答!StackOverflow是一个很棒的资源。如果只是使用文档,我可能需要更长的时间才能解决这个问题(而且我可能也不会找到正确的函数)。 :) - Numeri
1
谢谢,@Antoine! - PM 2Ring
显示剩余3条评论

11

你需要将它转换为 int 并每次取 8 个 bit,或将其切成 8 个字节长度的字符串,然后将每个字节字符串转换为 int。在 Python 3 中,如 PM 2Ring 和 J.F Sebastian 的答案所示,intto_bytes() 方法可以非常高效地实现第一种方法。这在 Python 2 中不可用,所以对于那些被困在 Python 2 中的人来说,第二种方法可能更加高效。以下是一个示例:

>>> s = "0110100001101001"
>>> bytes(int(s[i : i + 8], 2) for i in range(0, len(s), 8))
b'hi'

为了简化这个问题,range语句从索引0开始,每次前进8个索引。由于字符串s长度为16个字符,它会给我们两个索引:
>>> list(range(0, 50, 8))
[0, 8, 16, 24, 32, 40, 48]
>>> list(range(0, len(s), 8))
[0, 8]

(我们在这里使用list()来展示从 Python 3 的 range 迭代器中检索的值。)
然后我们可以利用这个迭代器,通过取长度为 8 的子字符串来拆分该字符串:
>>> [s[i : i + 8] for i in range(0, len(s), 8)]
['01101000', '01101001']

然后我们可以将它们转换为二进制整数:

>>> list(int(s[i : i + 8], 2) for i in range(0, len(s), 8))
[104, 105]

最后,我们用bytes()将整个内容封装起来以得到答案。
>>> bytes(int(s[i : i + 8], 2) for i in range(0, len(s), 8))
b'hi'

@KevinGuan 已添加解释。如果符合您的需求,请接受答案。 - Patrick Maupin
@KevinGuan 抱歉,我没注意到!:-) - Patrick Maupin
这段代码过于复杂且效率低下,这里有一个更简单的解决方案:https://dev59.com/qFwY5IYBdhLWcg3wYm8B#32683047 - jfs
@J.F.Sebastian -- 很好的观点。我通常被困在Python 2上,有时会忘记Python 3的增强功能。 - Patrick Maupin
感谢您提供这个出色的答案 - 如果有人想要使用Python 2解决此问题,这就是他们需要的答案。 - Numeri
哇!这个建议对我很有用,因为在Python中使用标准的int()函数处理比特串(位数组)太长了。最好对数组进行切片。谢谢!! - mindOf_L

10
>>> zero_one_string = "0110100001101001"
>>> int(zero_one_string, 2).to_bytes((len(zero_one_string) + 7) // 8, 'big')
b'hi'

它返回一个不可变的字节序列 bytes 对象。如果你想获得一个可变的字节序列 bytearray,那么只需调用 bytearray(b'hi')


谢谢!这可能是三个答案中最安全的,也最明确地针对python3。 - Numeri

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