确定文本文件的编码?

22

我需要确定一个文本文件的内容是否等于以下其中一种文本编码:

System.Text.Encoding.ASCII
System.Text.Encoding.BigEndianUnicode ' UTF-L 16
System.Text.Encoding.Default ' ANSI
System.Text.Encoding.Unicode ' UTF16
System.Text.Encoding.UTF32
System.Text.Encoding.UTF7
System.Text.Encoding.UTF8

我不知道如何读取文件的字节标记,我看过一些片段可以做到这一点,但只能确定文件是ASCII还是Unicode,因此我需要更通用的东西。


2
你不能可靠地这样做。 - Daniel Hilgarth
https://dev59.com/bW865IYBdhLWcg3wat6l - adripanico
1
@Daniel Hilgarth,请问您认为为什么会这样呢?虽然我不是专家,但我认为如果无法可靠地完成此操作,则“notepad.exe”无法可靠地知道文件使用的编码类型,但是当您按下“保存”按钮时,记事本始终知道并始终显示这些编码的确切编码。 - ElektroStudios
@Daniel Hilgarth 我知道“encryption”这个词是谷歌翻译的错误翻译,感谢您的评论。 - ElektroStudios
3
我不知道为什么版主会标记这个回答,因为我正在寻求VBNET的解决方案,而那个回答是针对C#的,而且所提供的解决方案也没有起作用... - ElektroStudios
显示剩余6条评论
1个回答

68

首先,将文件作为字节数组加载,而不是字符串。字符串始终以UTF-16编码存储在内存中,因此一旦加载到字符串中,原始编码就会丢失。以下是一种将文件加载到字节数组的简单示例:

Dim data() As Byte = File.ReadAllBytes("test.txt")

自动确定给定字节数组的正确编码方式非常困难。有时,为了方便,数据的作者会在数据开头插入一个称为BOM(字节顺序标记)的东西。如果存在BOM,则检测编码就变得轻而易举,因为每种编码使用不同的BOM。
从BOM自动检测编码的最简单方法是让StreamReader帮你完成。在StreamReader的构造函数中,您可以为detectEncodingFromByteOrderMarks参数传递True。然后,您可以通过访问其CurrentEncoding属性来获取流的编码。但是,在StreamReader读取BOM之前,CurrentEncoding属性无法工作。因此,您必须先读取BOM,然后才能获取编码,例如:
Public Function GetFileEncoding(filePath As String) As Encoding
    Using sr As New StreamReader(filePath, True)
        sr.Read()
        Return sr.CurrentEncoding
    End Using
End Function

然而,这种方法的问题在于MSDN似乎暗示StreamReader只能检测某些类型的编码:

通过检查流的前三个字节,detectEncodingFromByteOrderMarks参数可以检测编码。如果文件以适当的字节顺序标记开头,则它会自动识别UTF-8、小端Unicode和大端Unicode文本。有关更多信息,请参见Encoding.GetPreamble方法。

此外,如果StreamReader无法从BOM中确定编码,或者没有BOM,则它将默认使用UTF-8编码,而不会提示您失败。如果您需要比这更精细的控制,您可以很容易地读取BOM并自己解释它。您所要做的就是将字节数组中的前几个字节与一些已知的、预期的BOM进行比较,看它们是否匹配。以下是一些常见的BOM列表:
  • UTF-8: EF BB BF
  • UTF-16 大端字节序: FE FF
  • UTF-16 小端字节序: FF FE
  • UTF-32 大端字节序: 00 00 FE FF
  • UTF-32 小端字节序: FF FE 00 00

例如,要查看字节数组开头是否存在 UTF-16(小端)BOM,您可以简单地执行以下操作:

If (data(0) = &HFF) And (data(1) = &HFE) Then
    ' Data starts with UTF-16 (little endian) BOM
End If

方便的是,.NET中的Encoding类包含一个名为GetPreamble的方法,该方法返回编码使用的BOM,因此您甚至不需要记住它们都是什么。因此,要检查字节数组是否以Unicode(UTF-16,小端)的BOM开头,您只需执行以下操作:
Function IsUtf16LittleEndian(data() as Byte) As Boolean
    Dim bom() As Byte = Encoding.Unicode.GetPreamble()
    If (data(0) = bom(0)) And (data(1) = bom(1) Then
        Return True
    Else
        Return False
    End If
End Function

当然,上述函数假定数据长度至少为两个字节,并且BOM恰好为两个字节。因此,虽然它尽可能清晰地说明了如何执行操作,但这不是最安全的方法。为了使其能够容忍不同的数组长度,特别是因为BOM长度在不同的编码中可能会有所不同,更安全的方法是执行以下操作:
Function IsUtf16LittleEndian(data() as Byte) As Boolean
    Dim bom() As Byte = Encoding.Unicode.GetPreamble()
    Return data.Zip(bom, Function(x, y) x = y).All(Function(x) x)
End Function

所以,问题变成了如何获取所有编码的列表?幸运的是,.NET 的 Encoding 类也提供了一个名为 GetEncodings 的共享(静态)方法,该方法返回所有支持的编码对象的列表。因此,您可以创建一个方法,循环遍历所有编码对象,获取每个对象的 BOM,并将其与字节数组进行比较,直到找到匹配的编码对象为止。例如:
Public Function DetectEncodingFromBom(data() As Byte) As Encoding
    Return Encoding.GetEncodings().
        Select(Function(info) info.GetEncoding()).
        FirstOrDefault(Function(enc) DataStartsWithBom(data, enc))
End Function

Private Function DataStartsWithBom(data() As Byte, enc As Encoding) As Boolean
    Dim bom() As Byte = enc.GetPreamble()
    If bom.Length <> 0 Then
        Return data.
            Zip(bom, Function(x, y) x = y).
            All(Function(x) x)
    Else
        Return False
    End If
End Function

一旦您创建了这样的函数,那么您就可以像这样检测文件的编码:
Dim data() As Byte = File.ReadAllBytes("test.txt")
Dim detectedEncoding As Encoding = DetectEncodingFromBom(data)
If detectedEncoding Is Nothing Then
    Console.WriteLine("Unable to detect encoding")
Else
    Console.WriteLine(detectedEncoding.EncodingName)
End If

然而,问题仍然存在,当没有BOM时,如何自动检测正确的编码方式?从技术上讲,使用UTF-8时建议不在数据开头放置BOM,并且任何ANSI代码页都没有定义BOM。因此,文本文件可能没有BOM并不是不可能的。如果您处理的所有文件都是英文,则可以安全地假设如果没有BOM,则使用UTF-8就足够了。但是,如果任何文件使用其他编码方式且没有BOM,则这种方法将无法正常工作。

正如您所观察到的,即使没有BOM,仍然有应用程序会自动检测编码,但它们是通过启发式(即教育猜测)来实现的,有时不准确。基本上,它们使用每种编码加载数据,然后查看数据是否“看起来”可读。此页面提供了一些关于记事本自动检测算法内部问题的有趣见解。此页面展示了如何利用基于COM的自动检测算法,该算法由Internet Explorer使用(使用C#)。以下是一些人们编写的C#库列表,尝试自动检测字节数组的编码,您可能会发现它们有帮助:

虽然 this question 是关于C#的,但你也可能会发现它的答案很有用。


1
如果我能在按钮中设置我的最爱答案,那么这将是其中之一,非常感谢! - ElektroStudios
1
谢谢!我添加了如何使用StreamReader的信息,这是一个重要的点需要包括。我认为在那个被标记为重复的其他C#答案中,它失败的原因是因为他们在获取编码之前没有对流进行Read操作。在读取BOM之前,它将仅返回默认的UTF-8编码。 - Steven Doggart
StreamReader方法对ANSI文件返回UTF8,但我仍然更喜欢你写的第一种方法,因为它可以检测到好的UTF8文件,而且如果检测到任何编码,则可以返回“最可能的编码”作为ANSI编码,这对我来说非常有效,可以检测到ANSI文件和UTF文件,但我认为在几行代码中无法使用sr方法完成相同的操作,再次感谢! - ElektroStudios
2
正确,因为ANSI编码的文件从不具有BOM,所以StreamReader将始终假定默认的UTF-8编码。我仍然不明白为什么每个人都投票将其关闭为重复项。另一个答案是错误的,并且是用C#编写的。奇怪。我投票重新打开它。无论如何,很高兴我能帮忙。 - Steven Doggart

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