从零开始Python | Python 中的按位运算符 II 从零开始学python | Python 中的按位运算符 I

网友投稿 691 2022-05-30

从零开始学python | Python 中的按位运算符 I

查看二进制数据

您知道如何读取和解释单个字节。然而,现实世界的数据通常由多个字节组成来传达信息。以float数据类型为例。Python 中的单个浮点数在内存中占用多达 8 个字节。

你怎么看这些字节?

您不能简单地使用按位运算符,因为它们不适用于浮点数:

>>>

>>> 3.14 & 0xff Traceback (most recent call last): File "", line 1, in TypeError: unsupported operand type(s) for &: 'float' and 'int'

您必须忘记您正在处理的特定数据类型,并根据通用字节流来考虑它。这样,字节在按位运算符处理的上下文之外代表什么就无关紧要了。

要bytes()在 Python 中获取浮点数的 ,您可以使用熟悉的struct模块对其进行打包:

>>>

>>> from struct import pack >>> pack(">d", 3.14159) b'@\t!\xf9\xf0\x1b\x86n'

忽略通过第一个参数传递的格式字符。在您进入下面的字节顺序部分之前,它们没有意义。在这个相当晦涩的文本表示背后隐藏着一个包含八个整数的列表:

>>>

>>> list(b"@\t!\xf9\xf0\x1b\x86n") [64, 9, 33, 249, 240, 27, 134, 110]

它们的值对应于用于表示二进制浮点数的后续字节。您可以将它们组合起来生成一个很长的位串:

>>>

>>> from struct import pack >>> "".join([f"{b:08b}" for b in pack(">d", 3.14159)]) '0100000000001001001000011111100111110000000110111000011001101110'

这 64 位是您之前阅读过的双精度符号、指数和尾数。要从float类似的位串合成 a ,您可以颠倒过程:

>>> from struct import unpack >>> bits = "0100000000001001001000011111100111110000000110111000011001101110" >>> unpack( ... ">d", ... bytes(int(bits[i:i+8], 2) for i in range(0, len(bits), 8)) ... ) (3.14159,)

unpack()返回一个元组,因为它允许您一次读取多个值。例如,您可以读取与四个 16 位有符号整数相同的位串:

>>>

>>> unpack( ... ">hhhh", ... bytes(int(bits[i:i+8], 2) for i in range(0, len(bits), 8)) ... ) (16393, 8697, -4069, -31122)

如您所见,必须预先知道位字符串的解释方式,以避免以乱码结束。您需要问自己的一个重要问题是您应该从字节流的哪一端开始读取——向左还是向右。请仔细阅读,找出答案。

字节顺序

关于单个字节中的位顺序没有争议。无论它们在内存中的物理布局如何,您总是会在索引 0 处找到最低有效位,在索引 7 处找到最高有效位。按位移位运算符依赖于这种一致性。

但是,对于多字节数据块中的字节顺序没有达成共识。例如,可以像英文文本一样从左到右阅读包含多于一个字节的信息,或者像阿拉伯语一样从右到左阅读。计算机在二进制流中看到字节,就像人类在句子中看到单词一样。

计算机选择从哪个方向读取字节并不重要,只要它们在任何地方应用相同的规则即可。不幸的是,不同的计算机架构使用不同的方法,这使得它们之间的数据传输具有挑战性。

大端与小端

让我们取一个 32 位无符号整数,对应于数字 1969 10,这是Monty Python首次出现在电视上的年份。对于所有前导零,它具有以下二进制表示 00000000000000000000011110110001 2。

您将如何在计算机内存中存储这样的值?

如果您将内存想象成一个由字节组成的一维磁带,那么您需要将该数据分解为单个字节并将它们排列在一个连续的块中。有些人觉得从左端开始很自然,因为这是他们阅读的方式,而另一些人则更喜欢从右端开始:

当字节从左到右放置时,最重要的字节被分配到最低的内存地址。这被称为大端顺序。相反,当字节从右到左存储时,最低有效字节在前。这就是所谓的小端顺序。

哪种方式更好?

从实际的角度来看,使用一个比另一个没有真正的优势。硬件级别的性能可能会有一些小幅提升,但您不会注意到它们。主要网络协议使用 big-endian 顺序,鉴于IP 寻址的分层设计,这允许它们更快地过滤数据包。除此之外,有些人可能会发现在调试时使用特定的字节顺序更方便。

无论哪种方式,如果你没有做对并将两个标准混为一谈,那么糟糕的事情就会开始发生:

>>> raw_bytes = (1969).to_bytes(length=4, byteorder="big") >>> int.from_bytes(raw_bytes, byteorder="little") 2970025984 >>> int.from_bytes(raw_bytes, byteorder="big") 1969

当您使用一种约定将某个值序列化为字节流并尝试使用另一种约定将其读回时,您将得到一个完全无用的结果。这种情况最有可能发生在通过网络发送数据时,但您也可以在读取特定格式的本地文件时遇到这种情况。例如,Windows 位图的标头始终使用小端,而JPEG可以使用两种字节顺序。

本机字节序

要找出平台的字节顺序,您可以使用以下sys模块:

>>>

>>> import sys >>> sys.byteorder 'little'

但是,您无法更改字节顺序,因为它是您的CPU 架构的内在特征。如果没有诸如QEMU 之类的硬件虚拟化,就不可能出于测试目的模拟它,因此即使是流行的VirtualBox也无济于事。

Notably, the x86 family of processors from Intel and AMD, which power most modern laptops and desktops, are little-endian. Mobile devices are based on low-energy ARM architecture, which is bi-endian, while some older architectures such as the ancient Motorola 68000 were big-endian only.

For information on determining endianness in C, expand the box below.

Checking the Byte Order in C显示隐藏

Once you know the native endianness of your machine, you’ll want to convert between different byte orders when manipulating binary data. A universal way to do so, regardless of the data type at hand, is to reverse a generic bytes() object or a sequence of integers representing those bytes:

>>>

>>> big_endian = b"\x00\x00\x07\xb1" >>> bytes(reversed(big_endian)) b'\xb1\x07\x00\x00'

However, it’s often more convenient to use the struct module, which lets you define standard C data types. In addition to this, it allows you to request a given byte order with an optional modifier:

>>> from struct import pack, unpack >>> pack(">I", 1969) # Big-endian unsigned int b'\x00\x00\x07\xb1' >>> unpack("

The greater-than sign (>) indicates that bytes are laid out in the big-endian order, while the less-than symbol (<) corresponds to little-endian. If you don’t specify one, then native endianness is assumed. There are a few more modifiers, like the exclamation mark (!), which signifies the network byte order.

Network Byte Order

Computer networks are made of heterogeneous devices such as laptops, desktops, tablets, smartphones, and even light bulbs equipped with a Wi-Fi adapter. They all need agreed-upon protocols and standards, including the byte order for binary transmission, to communicate effectively.

在互联网诞生之初,人们决定这些网络协议的字节顺序是big-endian。

想要通过网络进行通信的程序可以获取经典的 C API,它通过套接字层抽象出细节。Python 通过内置socket模块包装该 API 。但是,除非您正在编写自定义二进制协议,否则您可能希望利用更高级别的抽象,例如基于文本的HTTP 协议。

凡socket模块可能是有用的是在字节顺序转换。它从 C API 中公开了一些函数,它们具有独特的、令人费解的名称:

>>>

>>> from socket import htons, htonl, ntohs, ntohl >>> htons(1969) # Host to network (short int) 45319 >>> htonl(1969) # Host to network (long int) 2970025984 >>> ntohs(45319) # Network to host (short int) 1969 >>> ntohl(2970025984) # Network to host (long int) 1969

如果您的主机已经使用大端字节序,则无需执行任何操作。这些值将保持不变。

位掩码

位掩码的工作原理类似于涂鸦模板,可阻止油漆喷涂到表面的特定区域。它允许您隔离这些位以选择性地对其应用某些功能。位掩码涉及您已经阅读过的按位逻辑运算符和按位移位运算符。

您可以在许多不同的上下文中找到位掩码。例如,IP 寻址中的子网掩码实际上是一个位掩码,可以帮助您提取网络地址。可以使用位掩码访问与 RGB 模型中的红色、绿色和蓝色相对应的像素通道。您还可以使用位掩码来定义布尔标志,然后您可以将这些标志打包到位字段中。

有几种常见的与位掩码相关的操作类型。您将在下面快速浏览其中的一些。

得到一点

要读取给定位置上特定位的值,您可以对仅由所需索引处的一位组成的位掩码使用按位与:

>>> def get_bit(value, bit_index): ... return value & (1 << bit_index) ... >>> get_bit(0b10000000, bit_index=5) 0 >>> get_bit(0b10100000, bit_index=5) 32

掩码将抑制除您感兴趣的位之外的所有位。它会导致指数等于位索引的零或二的幂。如果你想得到一个简单的是或否的答案,那么你可以向右移动并检查最不重要的位:

>>>

>>> def get_normalized_bit(value, bit_index): ... return (value >> bit_index) & 1 ... >>> get_normalized_bit(0b10000000, bit_index=5) 0 >>> get_normalized_bit(0b10100000, bit_index=5) 1

这一次,它将对位值进行标准化,使其永远不会超过 1。然后您可以使用该函数来导出布尔值True或False值而不是数值。

设置位

设置一点类似于得到一个。您可以利用与以前相同的位掩码,但不是使用按位 AND,而是使用按位 OR 运算符:

>>>

>>> def set_bit(value, bit_index): ... return value | (1 << bit_index) ... >>> set_bit(0b10000000, bit_index=5) 160 >>> bin(160) '0b10100000'

掩码保留所有原始位,同时在指定索引处强制执行二进制 1。如果该位已经被设置,它的值就不会改变。

取消设置位

要清除一点,您希望复制所有二进制数字,同时在一个特定索引处强制为零。您可以通过再次使用相同的位掩码来实现此效果,但采用反转形式:

>>>

>>> def clear_bit(value, bit_index): ... return value & ~(1 << bit_index) ... >>> clear_bit(0b11111111, bit_index=5) 223 >>> bin(223) '0b11011111'

在正数上使用按位 NOT 总是在 Python 中产生一个负值。虽然这通常是不可取的,但在这里并不重要,因为您会立即应用按位 AND 运算符。反过来,这会触发掩码转换为二进制补码表示,从而获得预期结果。

切换一点

有时,能够定期打开和关闭一些开关很有用。这是按位异或运算符的绝佳机会,它可以像这样翻转您的位:

>>>

>>> def toggle_bit(value, bit_index): ... return value ^ (1 << bit_index) ... >>> x = 0b10100000 >>> for _ in range(5): ... x = toggle_bit(x, bit_index=7) ... print(bin(x)) ... 0b100000 0b10100000 0b100000 0b10100000 0b100000

请注意再次使用相同的位掩码。指定位置上的二进制 1 将使该索引处的位反转其值。在其余位置上使用二进制零将确保其余位将被复制。

按位运算符重载

按位运算符的主要域是整数。这是他们最有意义的地方。但是,您还看到它们在布尔上下文中使用,在其中它们替换了逻辑运算符。Python 为其某些运算符提供了替代实现,并允许您为新数据类型重载它们。

尽管在 Python 中重载逻辑运算符的提议被拒绝了,但您可以为任何按位运算符赋予新的含义。许多流行的库,甚至标准库,都利用了它。

内置数据类型

Python 按位运算符是为以下内置数据类型定义的:

int

bool

set 和 frozenset

dict (自 Python 3.9 起)

这不是一个广为人知的事实,但按位运算符可以从集合代数执行操作,例如并集、交集和对称差,以及合并和更新字典。

注意:在撰写本文时,Python 3.9尚未发布,但您可以使用Docker或pyenv偷看即将推出的语言功能。

当a和b是 Python 集时,则按位运算符对应以下方法:

它们几乎做同样的事情,因此使用哪种语法取决于您。除此之外,还有一个重载的减号运算符 ( -),它实现了两个集合的差值。要查看它们的实际效果,假设您有以下两组水果和蔬菜:

>>> fruits = {"apple", "banana", "tomato"} >>> veggies = {"eggplant", "tomato"} >>> fruits | veggies {'tomato', 'apple', 'eggplant', 'banana'} >>> fruits & veggies {'tomato'} >>> fruits ^ veggies {'apple', 'eggplant', 'banana'} >>> fruits - veggies # Not a bitwise operator! {'apple', 'banana'}

它们共享一个难以分类的共同成员,但它们的其余元素是不相交的。

需要注意的一件事是 immutable frozenset(),它缺少就地更新的方法。但是,当您使用它们的按位运算符对应项时,含义略有不同:

>>> const_fruits = frozenset({"apple", "banana", "tomato"}) >>> const_veggies = frozenset({"eggplant", "tomato"}) >>> const_fruits.update(const_veggies) Traceback (most recent call last): File "", line 1, in const_fruits.update(const_veggies) AttributeError: 'frozenset' object has no attribute 'update' >>> const_fruits |= const_veggies >>> const_fruits frozenset({'tomato', 'apple', 'eggplant', 'banana'})

frozenset()当您使用按位运算符时,它看起来并不是那么一成不变,但问题在于细节。以下是实际发生的情况:

const_fruits = const_fruits | const_veggies

它第二次起作用的原因是您没有更改原始的不可变对象。相反,您创建一个新变量并再次将其分配给同一个变量。

Pythondict仅支持按位 OR,其工作方式类似于联合运算符。您可以使用它来更新字典或将两个字典合并为一个新字典:

>>> fruits = {"apples": 2, "bananas": 5, "tomatoes": 0} >>> veggies = {"eggplants": 2, "tomatoes": 4} >>> fruits | veggies # Python 3.9+ {'apples': 2, 'bananas': 5, 'tomatoes': 4, 'eggplants': 2} >>> fruits |= veggies # Python 3.9+, same as fruits.update(veggies)

按位运算符的增强版本等效于.update()。

第三方模块

许多流行的库,包括NumPy、pandas和SQLAlchemy,都为它们的特定数据类型重载了按位运算符。这是您最有可能在 Python 中找到按位运算符的地方,因为它们不再经常以其原始含义使用。

例如,NumPy 以逐点方式将它们应用于矢量化数据:

>>> import numpy as np >>> np.array([1, 2, 3]) << 2 array([ 4, 8, 12])

这样,您无需手动将相同的按位运算符应用于数组的每个元素。但是你不能用 Python 中的普通列表做同样的事情。

pandas 在幕后使用 NumPy,它还为其DataFrame和Series对象提供了按位运算符的重载版本。但是,它们的行为与您期望的一样。唯一的区别是他们在数字的向量和矩阵上而不是在单个标量上做他们通常的工作。

使用赋予按位运算符全新含义的库,事情会变得更有趣。例如,SQLAlchemy 提供了一种用于查询数据库的简洁语法:

session.query(User) \ .filter((User.age > 40) & (User.name == "Doe")) \ .all()

按位 AND 运算符 ( &) 最终将转换为一段SQL查询。然而,这不是很明显,至少对我的IDE 来说不是,当它在这种类型的表达式中看到它们时,它会抱怨按位运算符的非pythonic使用。它立即建议用&逻辑替换每个出现的and,不知道这样做会使代码停止工作!

这种类型的运算符重载是一种有争议的做法,它依赖于您必须预先了解的隐式魔法。一些编程语言(如 Java)通过完全禁止运算符重载来防止这种滥用。Python 在这方面更加自由,并且相信您知道自己在做什么。

自定义数据类型

要自定义 Python 的按位运算符的行为,您必须定义一个类,然后在其中实现相应的魔术方法。同时,您不能为现有类型重新定义按位运算符的行为。运算符重载仅适用于新数据类型。

以下是让您重载按位运算符的特殊方法的简要概述:

您不需要定义所有这些。例如,要使用稍微更方便的语法将元素附加到deque,仅实现.__lshift__()and就足够了.__rrshift__():

>>> from collections import deque >>> class DoubleEndedQueue(deque): ... def __lshift__(self, value): ... self.append(value) ... def __rrshift__(self, value): ... self.appendleft(value) ... >>> items = DoubleEndedQueue(["middle"]) >>> items << "last" >>> "first" >> items >>> items DoubleEndedQueue(['first', 'middle', 'last'])

这个用户定义的类包装了一个双端队列以重用它的实现,并用两个额外的方法来扩充它,这些方法允许将项目添加到集合的左端或右端。

最不重要的位隐写术

哇,要处理的事情太多了!如果您仍在挠头,想知道为什么要使用按位运算符,那么请不要担心。是时候以有趣的方式展示您可以用它们做什么了。

要跟随本节中的示例,您可以通过单击下面的链接下载源代码:

您将学习隐写术并将此概念应用于在位图图像中秘密嵌入任意文件。

密码学与隐写术

密码学是将一条消息变成只有拥有正确密钥的人才能读取的消息。其他人仍然可以看到加密的消息,但对他们来说没有任何意义。密码学的最初形式之一是替代密码,例如以朱利叶斯·凯撒命名的凯撒密码。

从零开始学python | Python 中的按位运算符 II 从零开始学python | Python 中的按位运算符 I

隐写术类似于密码术,因为它还允许您与所需的受众共享秘密消息。然而,它没有使用加密,而是巧妙地将信息隐藏在不引起注意的介质中。示例包括使用隐形墨水或写离合体,其中每个单词或行的第一个字母形成一个秘密信息。

除非您知道秘密消息被隐藏以及恢复它的方法,否则您可能会忽略运营商。您可以将这两种技术结合起来更安全,隐藏加密消息而不是原始消息。

有很多方法可以在数字世界中走私秘密数据。特别是携带大量数据的文件格式,例如音频文件、视频或图像,非常适合,因为它们为您提供了很大的工作空间。例如,发布受版权保护的材料的公司可能会使用隐写术为单个副本添加水印并追踪泄漏源。

下面,您将把秘密数据注入到一个普通的bitmap 中,这在 Python 中很容易读写,不需要外部依赖。

位图文件格式

位图一词通常指的是Windows 位图( .bmp) 文件格式,它支持几种表示像素的替代方法。为了让生活更轻松,您将假设像素以 24 位未压缩RGB(红色、绿色和蓝色)格式存储。一个像素将具有三个颜色通道,每个通道可以保存从 0 10到 255 10 的值。

每个位图都以文件头开头,其中包含图像宽度和高度等元数据。以下是一些有趣的字段及其相对于标题开头的位置:

您可以从该标头推断相应的位图宽 1,954 像素,高 1,301 像素。它不使用压缩,也没有调色板。每个像素占用 24 位或 3 个字节,原始像素数据从偏移量 122 10开始。

您可以以二进制模式打开位图,寻找所需的偏移量,读取给定的字节数,然后像以前一样使用反序列化它们:struct

from struct import unpack with open("example.bmp", "rb") as file_object: file_object.seek(0x22) field: bytes = file_object.read(4) value: int = unpack("

请注意,位图中的所有整数字段都以 little-endian 字节顺序存储。

您可能已经注意到标头中声明的像素字节数与图像大小导致的像素字节数之间存在微小差异。当您乘以 1,954 像素 × 1,301 像素 × 3 字节时,您会得到一个比 7,629,064 少 2,602 字节的值。

这是因为像素字节用零填充,因此每一行都是四字节的倍数。如果图像的宽度乘以三个字节恰好是四的倍数,则不需要填充。否则,在每一行的末尾添加空字节。

注意:为避免引起怀疑,您需要通过跳过空字节来考虑该填充。否则,对于知道要寻找什么的人来说,这将是一个明显的赠品。

位图倒置存储像素行,从底部而不是顶部开始。此外,每个像素都以有点奇怪的 BGR 顺序而不是 RGB 序列化为颜色通道向量。然而,这与隐藏秘密数据的任务无关。

按位捉迷藏

您可以使用按位运算符将自定义数据分布在连续的像素字节上。这个想法是用来自下一个秘密字节的位覆盖每个中的最低有效位。这将引入最少的噪声,但您可以尝试添加更多位以在注入数据的大小和像素失真之间取得平衡。

注意:使用最低有效位隐写术不会影响生成的位图的文件大小。它将保持与原始文件相同。

在某些情况下,相应的位将相同,导致像素值没有任何变化。然而,即使在最坏的情况下,像素颜色的差异也只有百分之几。这种微小的异常对人眼来说仍然是不可见的,但可以通过使用统计数据的隐写分析检测到。

看看这些裁剪的图像:

左边的一个来自原始位图,而右边的图像描绘了一个处理过的位图,嵌入的视频存储在最低有效位上。您看得出来差别吗?

以下代码将秘密数据编码到位图上:

for secret_byte, eight_bytes in zip(file.secret_bytes, bitmap.byte_slices): secret_bits = [(secret_byte >> i) & 1 for i in reversed(range(8))] bitmap[eight_bytes] = bytes( [ byte | 1 if bit else byte & ~1 for byte, bit in zip(bitmap[eight_bytes], secret_bits) ] )

对于每个字节的秘密数据和相应的八个字节的像素数据,不包括填充字节,它准备一个要传播的位列表。接下来,它使用相关位掩码覆盖八个字节中每个字节中的最低有效位。结果被转换为一个bytes()对象并分配回它最初来自的位图部分。

要从同一个位图解码文件,您需要知道写入了多少秘密字节。您可以在数据流的开头分配几个字节来存储这个数字,或者您可以使用位图标头中的保留字段:

@reserved_field.setter def reserved_field(self, value: int) -> None: """Store a little-endian 32-bit unsigned integer.""" self._file_bytes.seek(0x06) self._file_bytes.write(pack("

这会跳转到文件中的正确偏移量,将 Python 序列化为int原始字节,并将它们写下来。

您可能还想存储机密文件的名称。由于它可以具有任意长度,因此使用空终止字符串对其进行序列化是有意义的,该字符串将位于文件内容之前。要创建这样的字符串,您需要将 Pythonstr对象编码为字节并在末尾手动附加空字节:

>>> from pathlib import Path >>> path = Path("/home/jsmith/café.pdf") >>> path.name.encode("utf-8") + b"\x00" b'caf\xc3\xa9.pdf\x00'

此外,使用pathlib.

补充本文的示例代码将使您可以使用以下命令对给定位图中的机密文件进行编码、解码和擦除:

$ python -m stegano example.bmp -d Extracted a secret file: podcast.mp4 $ python -m stegano example.bmp -x Erased a secret file from the bitmap $ python -m stegano example.bmp -e pdcast.mp4 Secret file was embedded in the bitmap

这是一个可运行的模块,可以通过调用其包含的目录来执行。您还可以根据其内容制作可移植的 ZIP 格式存档,以利用Python ZIP 应用程序支持。

该程序依赖于文章中提到的标准库中的模块以及您以前可能没有听说过的其他一些模块。一个关键模块是mmap,它将 Python 接口暴露给内存映射文件。它们让您可以使用标准文件 API 和序列 API 来操作大文件。就好像文件是一个可以切片的大可变列表。

继续使用附加到支持材料的位图。它包含一个小惊喜给你!

结论

掌握 Python 按位运算符可让您在项目中拥有操作二进制数据的最大自由。您现在知道它们的语法和不同的风格以及支持它们的数据类型。您还可以根据自己的需要自定义他们的行为。

在本教程中,您学习了如何:

使用 Python按位运算符操作单个位

以与平台无关的方式读取和写入二进制数据

使用位掩码将信息打包到单个字节上

在自定义数据类型中重载Python 按位运算符

在数字图像中隐藏秘密信息

您还学习了计算机如何使用二进制系统来表示不同种类的数字信息。您看到了几种流行的解释位的方法,以及如何缓解Python 中缺少无符号数据类型的问题,以及 Python 在内存中存储整数的独特方法。

有了这些信息,您就可以在代码中充分利用二进制数据了。要下载水印示例中使用的源代码并继续试验按位运算符,您可以单击以下链接:

Python 面向对象编程

版权声明:本文内容由网络用户投稿,版权归原作者所有,本站不拥有其著作权,亦不承担相应法律责任。如果您发现本站中有涉嫌抄袭或描述失实的内容,请联系我们jiasou666@gmail.com 处理,核实后本网站将在24小时内删除侵权内容。

上一篇:[Python人工智能] 八.卷积神经网络CNN原理详解及TensorFlow编写CNN 丨【百变AI秀】
下一篇:可能是最详尽的PyTorch动态图解析
相关文章