二进制文件 “二进制”文件和“文本”文件之间的区别

2025-06-07

二进制文件“二进制”文件和“文本”文件之间的区别

本文探讨了“二进制”文件和“文本”文件。这两者之间有什么区别(如果有的话)?对于“二进制”文件和“文本”文件的构成,有明确的定义吗?

我们从两个候选文件开始我们的旅程,我们直观地将其内容分别归类为“文本”和“二进制”数据:


 bash
echo "hello 🌍" > message
convert -size 1x1 xc:white png:white


Enter fullscreen mode Exit fullscreen mode

我们创建了两个文件:一个名为“hello 🌍”的文件message包含Unicode 符号“Earth Globe Europe-Africa”),以及一个名为 的包含单个白色像素的 PNG 图像white。文件扩展名被故意省略了。

为了证明某些程序区分“文本”和“二进制”文件,请查看grep其行为如何改变:



▶ grep -R hello            
message:hello 🌍

▶ grep -R PNG
Binary file white matches


Enter fullscreen mode Exit fullscreen mode

diff做类似的事情:



▶ echo "hello world" > other-message
▶ diff other-message message 
1c1
< hello world
---
> hello 🌍

▶ convert -size 1x1 xc:black png:black
▶ diff black white
Binary files black and white differ


Enter fullscreen mode Exit fullscreen mode

这些程序如何区分“文本”和“二进制”文件?

在回答这个问题之前,我们先来尝试给出一个定义。显然,在文件系统的基本层面上,每个文件都只是字节的集合,因此可以被视为二进制数据。另一方面,区分“文本”和“非文本”(以下简称“二进制”)数据似乎对像grep或 这样的程序很有帮助diff,哪怕只是为了不弄乱终端仿真器的输出。

因此,我们可以先定义“文本”数据。首先将文本抽象为一系列Unicode 代码点,这似乎很合理。代码点的示例包括字符(例如käא),以及特殊符号(例如%或)🙈。要将给定的文本存储为字节序列,我们需要选择一种编码。如果我们想要表示整个 Unicode 范围,通常会选择 UTF-8,有时也会选择 UTF-16 或 UTF-32。从历史上看,仅支持当今部分 Unicode 的编码也很重要。最著名的是 US-ASCII 和 Latin1(ISO 8859-1),但还有更多。所有这些编码在字节级别上看起来都不同。

因此,仅给出文件的内容(而不是其创建历史),我们可以尝试以下定义:

如果文件的内容由 Unicode 代码点的编码序列组成,则该文件被称为“文本文件”。

这个定义有两个实际问题。首先,我们需要一个包含所有可能编码的列表。其次,为了测试文件内容是否以给定的编码进行编码,我们必须解码整个文件内容并查看是否成功¹。整个过程会非常缓慢。

事实证明,有一种更快的方法来区分文本和二进制文件,但这是以精度为代价的。

为了了解其工作原理,让我们回到两个候选文件并探索它们的字节级内容。我使用的hexyl是十六进制查看器,但您也可以使用hexdump -C

“消息”和“白色”的二进制内容

请注意,这两个文件都包含 ASCII 范围(00… )内和范围外的字节。例如,文件中的7f四个字节是 Unicode 码位(🌍)的 UTF-8 编码版本。另一方面,图像开头的字节是字符 ² 的简单 ASCII 编码版本f0 9f 8c 8dmessageU+1F30D50 4e 47whitePNG

显然,查看 ASCII 范围之外的字节不能用作检测“二进制”文件的方法。然而,这两个文件之间存在差异。图像文件包含大量 NULL 字节(00),而短文本消息则没有。事实证明,这可以转化为一种简单的启发式方法来检测二进制文件,因为许多编码的文本数据不包含任何 NULL 字节(即使它可能是合法的)。

事实上,这正是diff和用来检测“二进制”文件的方法。s源代码grep中包含以下宏( diffsrc/io.c



#define binary_file_p(buf, size) (memchr (buf, 0, size) != 0)


Enter fullscreen mode Exit fullscreen mode

这里,memchr(const void *s, int c, size_t n)函数用于size在从 开始的内存区域的初始字节中搜索buf字符0。为了进一步加快此过程,通常只将文件的前几个字节buf(例如 1024 个字节)读入缓冲区。总结一下,grepdiff使用以下启发式方法:

如果文件内容的前 1024 个字节不包含任何 NULL 字节,则文件很可能是“文本文件”。

请注意,存在一些反例,表明此方法会失效。例如,即使可能性不大,UTF-8 编码的文本可以合法地包含 NULL 字节。相反,某些特定的二进制格式(例如二进制PGM)不包含 NULL 字节。此方法通常也会将 UTF-16 和 UTF-32 编码的文本归类为“二进制”,因为它们使用 NULL 字节编码常见的 Latin-1 码位:



▶ iconv -f UTF-8 -t UTF-16 message > message-utf16
▶ hexdump -C message-utf16 
00000000  ff fe 68 00 65 00 6c 00  6c 00 6f 00 20 00 3c d8  |..h.e.l.l.o. .<.|
00000010  0d df 0a 00                                       |....|
00000014
▶ grep . message-utf16                            
Binary file message-utf16 matches


Enter fullscreen mode Exit fullscreen mode

尽管如此,这种启发式方法非常有用。我用 Rust 编写了一个小型库,它使用了这个方法的稍微改进版本,可以快速判断给定文件包含的是“二进制”数据还是“文本”数据。在我的程序中,它被用来bat防止“二进制”文件被转储到终端:

bat,检测二进制文件

脚注


¹ 请注意,有些编码会在文件开头写入所谓的字节顺序标记ff fe 00 00(BOM),以指示编码类型。例如,UTF-32 的小端字节序变体使用。这些 BOM 有助于解决第二点问题,因为我们无需解码文件的全部内容。遗憾的是,添加 BOM 是可选的,很多编码并没有指定 BOM。


²是 PNG 格式的魔数 (magic number)50 4e 47的一部分。魔数类似于 BOM,许多二进制格式在文件开头使用魔数来指示其类型。该工具使用魔数来检测某些类型的“二进制”文件file

文章来源:https://dev.to/sharkdp/what-is-a-binary-file-2cf5
PREV
Python中的垃圾收集
NEXT
使用 TypeScript 和 Next.JS 的 Context API