我需要判断一个文件是二进制还是文本,判断准确率需达到80%,是否有一种在C#中快速且不太优美的方法来实现?
有一种叫做马尔可夫链的方法。扫描两种模型文件的几个文件,并针对从0到255的每个字节值收集后续值的统计数据(基本上是概率)。这将为您提供一个64Kb(256x256)的简介,您可以将运行时文件与其进行比较(在一个%阈值范围内)。
据说这就是浏览器的自动检测编码功能的工作原理。
我可能会寻找大量控制字符,这些字符通常存在于二进制文件中,而在文本文件中很少出现。二进制文件往往使用0的次数足够多,只测试许多0字节可能就足以捕捉到大多数文件。如果您关心本地化,还需要测试多字节模式。
尽管如此,您始终有可能不幸地得到一个看起来像文本的二进制文件或反之亦然。
分享我的解决方案,希望它能像这些帖子和论坛对我有用一样帮助其他人。
我一直在研究和探索同样的解决方案。然而,我原以为它会很简单或稍微有点扭曲。
然而,大多数尝试在这里以及其他来源都提供了错综复杂的解决方案,并深入到Unicode、UTF系列、BOM、编码和字节顺序中。在此过程中,我还走了弯路,进入了Ascii表和代码页。
无论如何,我已经提出了一个基于流阅读器和自定义控制字符检查的解决方案。
它建立在考虑到论坛和其他地方提供的各种提示和技巧的基础上,例如:
我的目标是:
所提出的解决方案在测试数据(包括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
}
如果您尝试以上解决方案,请告诉我它是否对您有用。
其他有趣和相关的链接:
new FileInfo(path).Length
来获取文件大小。 - Jeremy Cook虽然这不是万无一失的,但这应该检查是否有任何二进制内容。
public bool HasBinaryContent(string content)
{
return content.Any(ch => char.IsControl(ch) && ch != '\r' && ch != '\n');
}
因为如果存在任何控制字符(除了标准的 \r\n
),那么它很可能不是文本文件。
如果这里的真正问题是“能否使用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;
}
}
好问题!我自己也很惊讶,.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);
}
快速且简单的方法是使用文件扩展名并查找常见的文本扩展名,如 .txt。为此,您可以使用 Path.GetExtension 方法。其他任何方法都不会真正被归类为“快速”,尽管它可能很粗略。
一种非常非常不好的方法是构建一个正则表达式,只匹配标准文本、标点符号、符号和空格字符,将文件的部分内容加载到文本流中,然后对其运行该正则表达式。根据您问题域中纯文本文件的定义,没有成功的匹配表示一个二进制文件。
为了考虑Unicode,请确保在流上标记编码。
这真的很次优,但你说要快速且肮脏。
另一种方法是使用UDE检测文件的字符集。如果成功检测到字符集,你可以确定它是文本,否则就是二进制。因为二进制没有字符集。
当然,你也可以使用其他字符集检测库。如果字符集检测库足够好,这种方法可以达到100%的正确性。
http://codesnipers.com/?q=node/68描述了如何使用字节顺序标记(可能出现在文件中)来检测UTF-16与UTF-8。 它还建议循环一些字节,以查看它们是否符合UTF-8多字节序列模式(下面)来确定您的文件是否为文本文件。