如何在C#中确定文件是二进制还是文本?

61

我需要判断一个文件是二进制还是文本,判断准确率需达到80%,是否有一种在C#中快速且不太优美的方法来实现?


你所说的“二进制”,是指可执行文件还是任何随意的东西? - Vizu
不包括图片、音乐、MS Word、可执行文件、dll等。 - Pablo Retyk
如果你告诉我一个人如何确定差异,我可以帮你处理C#部分 :) - G.Y
11个回答

35

有一种叫做马尔可夫链的方法。扫描两种模型文件的几个文件,并针对从0到255的每个字节值收集后续值的统计数据(基本上是概率)。这将为您提供一个64Kb(256x256)的简介,您可以将运行时文件与其进行比较(在一个%阈值范围内)。

据说这就是浏览器的自动检测编码功能的工作原理。


谢谢,我做了类似的事情,我寻找了一连串的空值。 - Pablo Retyk

25

我可能会寻找大量控制字符,这些字符通常存在于二进制文件中,而在文本文件中很少出现。二进制文件往往使用0的次数足够多,只测试许多0字节可能就足以捕捉到大多数文件。如果您关心本地化,还需要测试多字节模式。

尽管如此,您始终有可能不幸地得到一个看起来像文本的二进制文件或反之亦然。


8
谢谢,我寻找了四个连续的空值“\0\0\0\0”。二进制文件似乎有很多这样的空值,所以我在50个随机文件中进行了测试,测试结果有效。 - Pablo Retyk
2
四个连续的空值无法识别某些 .png 文件为二进制文件,因此我尝试了两个连续的空值,效果更好。 - Adam Bruss
11
如果文本文件是 ASCII 或 UTF-8 编码,找到一个零字节就足以得出它不是UTF-16或UTF-32编码的文件。这种方法对UTF-16和UTF-32编码的文件并不适用,但大多数文本编辑器也无法处理这些文件;-) - John Dvorak

19

分享我的解决方案,希望它能像这些帖子和论坛对我有用一样帮助其他人。

背景

我一直在研究和探索同样的解决方案。然而,我原以为它会很简单或稍微有点扭曲。

然而,大多数尝试在这里以及其他来源都提供了错综复杂的解决方案,并深入到Unicode、UTF系列、BOM、编码和字节顺序中。在此过程中,我还走了弯路,进入了Ascii表和代码页

无论如何,我已经提出了一个基于流阅读器和自定义控制字符检查的解决方案。

它建立在考虑到论坛和其他地方提供的各种提示和技巧的基础上,例如:

  1. 检查大量控制字符,例如查找多个连续的空字符。
  2. 检查UTF、Unicode、编码、BOM、字节顺序等方面。

我的目标是:

  1. 它不应该依赖于字节顺序、编码和其他更复杂的奇特工作。
  2. 它应该相对容易实现和易于理解。
  3. 它应该适用于所有类型的文件。

所提出的解决方案在测试数据(包括mp3、eml、txt、info、flv、mp4、pdf、gif、png、jpg)上对我有效,到目前为止它给出了预期结果。

解决方案如何工作

我依赖StreamReader默认构造函数来尽力确定文件编码相关特征,它默认使用UTF8Encoding

我创建了自己版本的检查自定义控制字符条件,因为Char.IsControl看起来没什么用。它说:

控制字符是格式化和其他不打印字符,例如ACK、BEL、CR、FF、LF和VT。Unicode标准将代码点从\ U0000到\ U001F、\ U007F和从\ U0080到\ U009F分配给控制字符。这些值应该被解释为控制字符,除非它们的用途由应用程序另行定义。它认为LF和CR是控制字符之一等等

这使得它不太有用,因为文本文件至少包括CR和LF。

解决方案

static void testBinaryFile(string folderPath)
{
    List<string> output = new List<string>();
    foreach (string filePath in getFiles(folderPath, true))
    {
        output.Add(isBinary(filePath).ToString() + "  ----  " + filePath);
    }
    Clipboard.SetText(string.Join("\n", output), TextDataFormat.Text);
}

public static List<string> getFiles(string path, bool recursive = false)
{
    return Directory.Exists(path) ?
        Directory.GetFiles(path, "*.*",
        recursive ? SearchOption.AllDirectories : SearchOption.TopDirectoryOnly).ToList() :
        new List<string>();
}    

public static bool isBinary(string path)
{
    long length = getSize(path);
    if (length == 0) return false;

    using (StreamReader stream = new StreamReader(path))
    {
        int ch;
        while ((ch = stream.Read()) != -1)
        {
            if (isControlChar(ch))
            {
                return true;
            }
        }
    }
    return false;
}

public static bool isControlChar(int ch)
{
    return (ch > Chars.NUL && ch < Chars.BS)
        || (ch > Chars.CR && ch < Chars.SUB);
}

public static class Chars
{
    public static char NUL = (char)0; // Null char
    public static char BS = (char)8; // Back Space
    public static char CR = (char)13; // Carriage Return
    public static char SUB = (char)26; // Substitute
}

如果您尝试以上解决方案,请告诉我它是否对您有用。

其他有趣和相关的链接:


getSize函数丢失。感谢提供代码。重要部分已被使用,目前测试似乎进展顺利。 - Atron Seige
我实际上很喜欢这个解决方案不需要读取整个文件。这使得运行一个观察整个目录的工具变得更加容易,该目录可能包含50MB的视频。 - Katana314
@AtronSeige,你可以使用new FileInfo(path).Length来获取文件大小。 - Jeremy Cook
它有助于确认编码。我编写了一个工具,使用您的解决方案来确认编码,该工具可以在 https://marketplace.visualstudio.com/items?itemName=lindexigd.vs-extension-18109 中找到。 - lindexi
谢谢。除了一个情况外,它都有效。我拿了一个XML文件,在记事本中打开,并保存为Unicode(还添加了一些外文字符)。我将文件存储在MySQL数据列的博客或文本字段中,然后稍后将其写回磁盘。 - NealWalters
谢谢,这对我解决了问题。文件保存在网络驱动器上,偶尔会被填充所有空字符。 - Aaron

15

虽然这不是万无一失的,但这应该检查是否有任何二进制内容。

public bool HasBinaryContent(string content)
{
    return content.Any(ch => char.IsControl(ch) && ch != '\r' && ch != '\n');
}

因为如果存在任何控制字符(除了标准的 \r\n),那么它很可能不是文本文件。


2
我会包括 FF 和 VT。(我猜 HT 已经包括了...) - TaW
3
最好也要排除 '\t'。 - Cocowalla
1
无法处理波斯文本。 - Mahmood Dehghan

10

如果这里的真正问题是“能否使用StreamReader/StreamWriter读写此文件而无需进行修改?”,那么答案在这里:

/// <summary>
/// Detect if a file is text and detect the encoding.
/// </summary>
/// <param name="encoding">
/// The detected encoding.
/// </param>
/// <param name="fileName">
/// The file name.
/// </param>
/// <param name="windowSize">
/// The number of characters to use for testing.
/// </param>
/// <returns>
/// true if the file is text.
/// </returns>
public static bool IsText(out Encoding encoding, string fileName, int windowSize)
{
    using (var fileStream = File.OpenRead(fileName))
    {
    var rawData = new byte[windowSize];
    var text = new char[windowSize];
    var isText = true;

    // Read raw bytes
    var rawLength = fileStream.Read(rawData, 0, rawData.Length);
    fileStream.Seek(0, SeekOrigin.Begin);

    // Detect encoding correctly (from Rick Strahl's blog)
    // http://www.west-wind.com/weblog/posts/2007/Nov/28/Detecting-Text-Encoding-for-StreamReader
    if (rawData[0] == 0xef && rawData[1] == 0xbb && rawData[2] == 0xbf)
    {
        encoding = Encoding.UTF8;
    }
    else if (rawData[0] == 0xfe && rawData[1] == 0xff)
    {
        encoding = Encoding.Unicode;
    }
    else if (rawData[0] == 0 && rawData[1] == 0 && rawData[2] == 0xfe && rawData[3] == 0xff)
    {
        encoding = Encoding.UTF32;
    }
    else if (rawData[0] == 0x2b && rawData[1] == 0x2f && rawData[2] == 0x76)
    {
        encoding = Encoding.UTF7;
    }
    else
    {
        encoding = Encoding.Default;
    }

    // Read text and detect the encoding
    using (var streamReader = new StreamReader(fileStream))
    {
        streamReader.Read(text, 0, text.Length);
    }

    using (var memoryStream = new MemoryStream())
    {
        using (var streamWriter = new StreamWriter(memoryStream, encoding))
        {
        // Write the text to a buffer
        streamWriter.Write(text);
        streamWriter.Flush();

        // Get the buffer from the memory stream for comparision
        var memoryBuffer = memoryStream.GetBuffer();

        // Compare only bytes read
        for (var i = 0; i < rawLength && isText; i++)
        {
            isText = rawData[i] == memoryBuffer[i];
        }
        }
    }

    return isText;
    }
}

4
无法处理一个简单的文本文件,其中包含一个带重音符号的法语字母 à。 - Alexis Pautrot

7

好问题!我自己也很惊讶,.NET没有提供一个简单的解决方案。

下面的代码对我来说可以区分图片(png、jpg等)和文本文件。

我只是检查了前512个字节中连续的空值(0x00),根据Ron Warholic和Adam Bruss的建议:

if (File.Exists(path))
{
    // Is it binary? Check for consecutive nulls..
    byte[] content = File.ReadAllBytes(path);
    for (int i = 1; i < 512 && i < content.Length; i++) {
        if (content[i] == 0x00 && content[i-1] == 0x00) {
            return Convert.ToBase64String(content);
        }
    }
    // No? return text
    return File.ReadAllText(path);
}

显然,这是一种快速而简单的方法,但可以通过将文件分成10个512字节的块并检查其中8个连续的空值来轻松扩展它(个人认为,如果其中2或3个匹配,我会得出结论它是二进制文件 - 在文本文件中,null很少见)。这应该可以提供一个相当不错的解决方案。

4

快速且简单的方法是使用文件扩展名并查找常见的文本扩展名,如 .txt。为此,您可以使用 Path.GetExtension 方法。其他任何方法都不会真正被归类为“快速”,尽管它可能很粗略。


4
有时候像我这样的人会将二进制文件的扩展名改为 .txt。 - Kirtan
显然,但他要求便宜而肮脏 - 没有绝对可靠的方法,只能请一个人来阅读它。 - Jeff Yates
很好,不幸的是我没有处理常见扩展名,我正在编写某种所有文件列表并需要将它们分类为二进制或文本,大多数人手动完成,但由于我很懒,我更喜欢编写代码。 - Pablo Retyk
许多人导出扩展名为.xls的“Excel文件”,实际上是csv文件或html文件。 - Tim Schmelter
@TimSchmelter:我说的是快速而不是万无一失和100%有效。 :) - Jeff Yates
@JeffYates:我知道,但大多数人(比如Kirtan)认为扩展名方法只是在有人试图将exe上传为txt或其他格式时才会出现问题。即使在正常情况下,许多文件也不是它们应该的样子。 - Tim Schmelter

2

一种非常非常不好的方法是构建一个正则表达式,只匹配标准文本、标点符号、符号和空格字符,将文件的部分内容加载到文本流中,然后对其运行该正则表达式。根据您问题域中纯文本文件的定义,没有成功的匹配表示一个二进制文件。

为了考虑Unicode,请确保在流上标记编码。

这真的很次优,但你说要快速且肮脏。


1
哦,我不确定对几兆字节的文件进行正则表达式处理是否足够“快”。 - Anton Kraievyi
2
根据"quick"的定义而定。是指运行速度快还是编写速度快?:) - Chad Ruppert

1

另一种方法是使用UDE检测文件的字符集。如果成功检测到字符集,你可以确定它是文本,否则就是二进制。因为二进制没有字符集。

当然,你也可以使用其他字符集检测库。如果字符集检测库足够好,这种方法可以达到100%的正确性。


1

http://codesnipers.com/?q=node/68描述了如何使用字节顺序标记(可能出现在文件中)来检测UTF-16与UTF-8。 它还建议循环一些字节,以查看它们是否符合UTF-8多字节序列模式(下面)来确定您的文件是否为文本文件。

  • 0xxxxxxx ASCII<0x80(128)
  • 110xxxxx 10xxxxxx 2字节>=0x80
  • 1110xxxx 10xxxxxx 10xxxxxx 3字节>=0x400
  • 11110xxx 10xxxxxx 10xxxxxx 10xxxxxx 4字节>=0x10000

如果文件保证是UTF8/16或二进制,则此方法有效。但如果不是呢?如果它是一个文本文件,既不是ASCII也不是UTF-8/16编码的呢?如果它是以Big5代码页或ISO-8859-1编码的呢?这些都没有BOM。那么...如何处理这种情况呢? - Cheeso
如果文件是(美国)ASCII格式,实际上它是UTF-8格式,因为在UTF-8中,7位字符代码的字符被翻译成它们自己,但如果文件是使用某些本地化ANSI代码页创建的,则以上方法仍将其识别为二进制文件。 - mg30rg

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