回 帖 发 新 帖 刷新版面

主题:[讨论]判断文件编码方式是否是UTF-8及正确读取的问题

不知道大家有没有遇到过“明明在记事本中显示的是utf-8格式的文件,但却没有BOM头,前3个字节也不是&HEF、&HBB 和 &HBF,甚至也不是&HFF 和 &HFE,而直接是文件的内容(很像ANSI)”的情况。
最近就遇到这么个utf-8文件,怎么检测编码方式都无法检测到是utf-8,但是用utf-8的方式来读取才能得到不是乱码的内容,很让人头疼。
按照一江秋水的帖子 http://bbs.pfan.cn/post-250830.html中的直接检测文件前三个字节内容来判断判断不出来,用 http://topic.csdn.net/u/20090623/16/4149E2C9-28B8-4AC1-9E2F-F534EF9BFDEA.html 中3楼的代码也检测不出是UTF-8。
代码就不再贴了
附上这个诡异的文件,供大家研究。

回复列表 (共10个回复)

沙发

算了,还是附上部分代码,方便大家帮忙测试下
[code=c]
'直接检测文件前三个字节内容来判断是否是UTF-8
Private Function IsUTF8(ByVal strFile As String) As Boolean
Dim fNo As Integer
Dim bytTmp(2) As Byte

fNo = FreeFile()
Open strFile For Binary As fNo
    Get #1, 1, bytTmp()
Close fNo
Debug.Print Hex(bytTmp(0)), Hex(bytTmp(1)), Hex(bytTmp(2))

If bytTmp(0) = &HFF And bytTmp(1) = &HFE Then
    'unicode编码
    IsUTF8 = False
ElseIf bytTmp(0) = &HEF And bytTmp(1) = &HBB And bytTmp(2) = &HBF Then
    IsUTF8 = True
End If

End Function
[/code]

[code=c]

Option Explicit

'mduUnicodeEncodeFormat.bas
'模块:UTF文本文件访问
'作者:zyl910
'版本:1.0
'日期:2006-1-23

'== 说明 ===================================================
'支持Unicode编码的文本文件读写。暂时支持ANSI、UTF-8、UTF-16LE、UTF-16BE这几种编码文本

'== 更新记录 ===============================================
'[V1.0] 2006-1-23
'1.支持最常见的ANSI、UTF-8、UTF-16LE、UTF-16BE这几种编码文本

'## 编译预处理常数 #########################################
'== 全局常数 ===============================================
'IncludeAPILib:引用了API库,此时不需要手动写API声明

'## API ####################################################
#If IncludeAPILib = 0 Then
    '== File ===================================================
    Private Declare Function CreateFile Lib "kernel32" Alias "CreateFileA" (ByVal lpFileName As String, ByVal dwDesiredAccess As Long, ByVal dwShareMode As Long, ByVal lpSecurityAttributes As Long, ByVal dwCreationDisposition As Long, ByVal dwFlagsAndAttributes As Long, ByVal hTemplateFile As Long) As Long
    Private Declare Function CloseHandle Lib "kernel32" (ByVal hObject As Long) As Long
    Private Declare Function ReadFile Lib "kernel32" (ByVal hFile As Long, lpBuffer As Any, ByVal nNumberOfBytesToRead As Long, lpNumberOfBytesRead As Long, ByVal lpOverlapped As Long) As Long
    Private Declare Function WriteFile Lib "kernel32" (ByVal hFile As Long, lpBuffer As Any, ByVal nNumberOfBytesToWrite As Long, lpNumberOfBytesWritten As Long, ByVal lpOverlapped As Long) As Long
    Private Declare Function GetFileSize Lib "kernel32" (ByVal hFile As Long, lpFileSizeHigh As Long) As Long
    Private Declare Function SetFilePointer Lib "kernel32" (ByVal hFile As Long, ByVal lDistanceToMove As Long, lpDistanceToMoveHigh As Long, ByVal dwMoveMethod As Long) As Long

    Private Const INVALID_HANDLE_VALUE = -1

    Private Const GENERIC_READ = &H80000000
    Private Const GENERIC_WRITE = &H40000000

    Private Const FILE_SHARE_READ = &H1
    Private Const FILE_SHARE_WRITE = &H2

    Private Const Create_NEW = 1
    Private Const Create_ALWAYS = 2
    Private Const OPEN_EXISTING = 3
    Private Const OPEN_ALWAYS = 4
    Private Const TRUNCATE_EXISTING = 5

    Private Const FILE_ATTRIBUTE_NORMAL = &H80

    Private Const FILE_BEGIN = 0
    Private Const FILE_CURRENT = 1
    Private Const FILE_END = 2

    '== Unicode ================================================

    Private Declare Function MultiByteToWideChar Lib "kernel32" (ByVal CodePage As Long, ByVal dwFlags As Long, ByRef lpMultiByteStr As Any, ByVal cchMultiByte As Long, ByRef lpWideCharStr As Any, ByVal cchWideChar As Long) As Long
    Private Declare Function WideCharToMultiByte Lib "kernel32" (ByVal CodePage As Long, ByVal dwFlags As Long, ByRef lpWideCharStr As Any, ByVal cchWideChar As Long, ByRef lpMultiByteStr As Any, ByVal cchMultiByte As Long, ByRef lpDefaultChar As Any, ByVal lpUsedDefaultChar As Long) As Long

    Private Const CP_UTF8 As Long = 65001

#End If

'###########################################################

'Unicode编码格式
Public Enum UnicodeEncodeFormat
    UEF_ANSI = 0 'ANSI+DBCS
    UEF_UTF8 'UTF-8
    UEF_UTF16LE 'UTF-16LE
    UEF_UTF16BE 'UTF-16BE
    UEF_UTF32LE 'UTF-32LE
    UEF_UTF32BE 'UTF-32BE

    UEF_Auto = -1 '自动识别编码

    '隐藏项目
    [_UEF_Min] = UEF_ANSI
    [_UEF_Max] = UEF_UTF32BE

End Enum

'ANSI+DBCS方式的文本所使用的代码页。默认为0,表示使用系统当前代码页。可以利用该参数实现读取其他代码编码的文本,比如想在 简体中文平台下 读取 繁体中文平台生成的txt,就将它设为950
Public UEFCodePage As Long

[/code]

板凳

[code=c]
'判断BOM
'返回值:BOM所占字节
'dwFirst:[in]文件最开始的4个字节
'fmt:[out]返回编码类型
Private Function UEFCheckBOM(ByVal dwFirst As Long, ByRef fmt As UnicodeEncodeFormat) As Long

    If dwFirst = &HFEFF& Then
        fmt = UEF_UTF32LE
        UEFCheckBOM = 4
    ElseIf dwFirst = &HFFFE0000 Then
        fmt = UEF_UTF32BE
        UEFCheckBOM = 4
    ElseIf (dwFirst And &HFFFF&) = &HFEFF& Then
        fmt = UEF_UTF16LE
        UEFCheckBOM = 2
    ElseIf (dwFirst And &HFFFF&) = &HFFFE& Then
        fmt = UEF_UTF16BE
        UEFCheckBOM = 2
    ElseIf (dwFirst And &HFFFFFF) = &HBFBBEF Then
        fmt = UEF_UTF8
        UEFCheckBOM = 3
    Else
        fmt = UEF_ANSI
        UEFCheckBOM = 0
    End If

End Function

'生成BOM
'返回值:BOM所占字节
'fmt:[in]编码类型
'dwFirst:[out]文件最开始的4个字节
Private Function UEFMakeBOM(ByVal fmt As UnicodeEncodeFormat, ByRef dwFirst As Long) As Long

    Select Case fmt

        Case UEF_UTF8
            dwFirst = &HBFBBEF
            UEFMakeBOM = 3

        Case UEF_UTF16LE
            dwFirst = &HFEFF&
            UEFMakeBOM = 2

        Case UEF_UTF16BE
            dwFirst = &HFFFE&
            UEFMakeBOM = 2

        Case UEF_UTF32LE
            dwFirst = &HFEFF&
            UEFMakeBOM = 4

        Case UEF_UTF32BE
            dwFirst = &HFFFE0000
            UEFMakeBOM = 4

        Case Else
            dwFirst = 0
            UEFMakeBOM = 0
    End Select

End Function

'判断文本文件的编码类型
'返回值:编码类型。文件无法打开时,返回UEF_Auto
'FileName:文件名
Public Function UEFCheckTextFileFormat(ByVal FileName As String) As UnicodeEncodeFormat
    Dim hFile    As Long
    Dim dwFirst  As Long
    Dim nNumRead As Long

    '打开文件
    hFile = CreateFile(FileName, GENERIC_READ, FILE_SHARE_READ Or FILE_SHARE_WRITE, ByVal 0&, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, ByVal 0&)

    If INVALID_HANDLE_VALUE = hFile Then '文件无法打开
        UEFCheckTextFileFormat = UEF_Auto

        Exit Function

    End If

    '判断BOM
    dwFirst = 0
    Call ReadFile(hFile, dwFirst, 4, nNumRead, ByVal 0&)
    nNumRead = UEFCheckBOM(dwFirst, UEFCheckTextFileFormat)
    'Debug.Print nNumRead

    '关闭文件
    Call CloseHandle(hFile)
    
End Function

'读取文本文件
'返回值:读取的文本。返回vbNullString表示文件无法打开
'FileName:[in]文件名
'fmt:[in,out]使用何种文本编码格式来读取文本。为UEF_Auto时表示自动判断,且在fmt参数返回文本所用编码格式
Public Function UEFLoadTextFile(ByVal FileName As String, Optional ByRef fmt As UnicodeEncodeFormat = UEF_Auto) As String
    Dim hFile      As Long
    Dim nFileSize  As Long
    Dim nNumRead   As Long
    Dim dwFirst    As Long
    Dim CurFmt     As UnicodeEncodeFormat
    Dim cbBOM      As Long
    Dim cbTextData As Long
    Dim CurCP      As Long
    Dim byBuf()    As Byte
    Dim cchStr     As Long
    Dim i          As Long
    Dim byTemp     As Byte

    '判断fmt范围
    If fmt <> UEF_Auto Then
        If fmt < [_UEF_Min] Or fmt > [_UEF_Max] Then
            GoTo FunEnd
        End If
    End If

    '打开文件
    hFile = CreateFile(FileName, GENERIC_READ, FILE_SHARE_READ Or FILE_SHARE_WRITE, ByVal 0&, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, ByVal 0&)

    If INVALID_HANDLE_VALUE = hFile Then '文件无法打开
        GoTo FunEnd
    End If

    '判断文件大小
    nFileSize = GetFileSize(hFile, nNumRead)

    If nNumRead <> 0 Then '超过4GB
        GoTo FreeHandle
    End If

    If nFileSize < 0 Then '超过2GB
        GoTo FreeHandle
    End If
[/code]

3 楼

[code=c]
    '判断BOM
    dwFirst = 0
    Call ReadFile(hFile, dwFirst, 4, nNumRead, ByVal 0&)
    cbBOM = UEFCheckBOM(dwFirst, CurFmt)

    '恢复文件指针
    If fmt = UEF_Auto Then '自动判断
        fmt = CurFmt
        'cbBOM = cbBOM
    Else '手动设置编码

        If fmt = CurFmt Then '若编码相同,则忽略BOM标记
            'cbBOM = cbBOM
        Else '编码不同,那么都是数据
            cbBOM = 0
        End If
    End If

    Call SetFilePointer(hFile, cbBOM, ByVal 0&, FILE_BEGIN)
    cbTextData = nFileSize - cbBOM

    '读取数据
    UEFLoadTextFile = ""

    Select Case fmt

        Case UEF_ANSI, UEF_UTF8
            '判断应使用的CodePage
            CurCP = IIf(fmt = UEF_UTF8, CP_UTF8, UEFCodePage)

            '分配缓冲区
            On Error GoTo FreeHandle

            ReDim byBuf(0 To cbTextData - 1)

            On Error GoTo 0

            '读取数据
            nNumRead = 0
            Call ReadFile(hFile, byBuf(0), cbTextData, nNumRead, ByVal 0&)

            '取得Unicode文本长度
            cchStr = MultiByteToWideChar(CurCP, 0, byBuf(0), nNumRead, ByVal 0&, ByVal 0&)

            If cchStr > 0 Then

                '分配字符串空间
                On Error GoTo FreeHandle

                UEFLoadTextFile = String$(cchStr, 0)

                On Error GoTo 0

                '取得文本
                cchStr = MultiByteToWideChar(CurCP, 0, byBuf(0), nNumRead, ByVal StrPtr(UEFLoadTextFile), cchStr + 1)

            End If

        Case UEF_UTF16LE
            cchStr = (cbTextData + 1) \ 2

            '分配字符串空间
            On Error GoTo FreeHandle

            UEFLoadTextFile = String$(cchStr, 0)

            On Error GoTo 0

            '取得文本
            nNumRead = 0
            Call ReadFile(hFile, ByVal StrPtr(UEFLoadTextFile), cbTextData, nNumRead, ByVal 0&)

            '修正文本长度
            cchStr = (nNumRead + 1) \ 2

            If cchStr > 0 Then
                If Len(UEFLoadTextFile) > cchStr Then
                    UEFLoadTextFile = Left$(UEFLoadTextFile, cchStr)
                End If

            Else
                UEFLoadTextFile = ""
            End If

        Case UEF_UTF16BE

            '分配缓冲区
            On Error GoTo FreeHandle

            ReDim byBuf(0 To cbTextData - 1)

            On Error GoTo 0

            '读取数据
            nNumRead = 0
            Call ReadFile(hFile, byBuf(0), cbTextData, nNumRead, ByVal 0&)

            If nNumRead > 0 Then

                '隔两字节翻转相邻字节
                For i = 0 To nNumRead - 1 - 1 Step 2 '再-1是为了避免最后多出的那个字节
                    byTemp = byBuf(i)
                    byBuf(i) = byBuf(i + 1)
                    byBuf(i + 1) = byTemp
                Next i

                '取得文本
                UEFLoadTextFile = byBuf 'VB允许String中的字符串数据与Byte数组直接转换

            End If

        Case UEF_UTF32LE
            UEFLoadTextFile = vbNullString '暂时不支持

        Case UEF_UTF32BE
            UEFLoadTextFile = vbNullString '暂时不支持

        Case Else
            Debug.Assert False
    End Select

FreeHandle:
    '关闭文件
    Call CloseHandle(hFile)

FunEnd:
End Function
[/code]

4 楼

这样的问题我也经常碰到,因为没有特征码,所以无法判断是否UTF-8编码,只能在发现装入的文本疑似UTF-8编码时,在窗口中进行转换

5 楼

[quote]这样的问题我也经常碰到,因为没有特征码,所以无法判断是否UTF-8编码,只能在发现装入的文本疑似UTF-8编码时,在窗口中进行转换[/quote]

怎么“发现装入的文本疑似UTF-8编码” ?一个一个地检查是不是乱码?

6 楼

http://tonylian.javaeye.com/blog/411558
这篇可以参考

00000000 - 0000007F 0xxxxxxx
00000080 - 000007FF 110xxxxx 10xxxxxx
00000800 - 0000FFFF 1110xxxx 10xxxxxx 10xxxxxx
00010000 - 001FFFFF 11110xxx 10xxxxxx 10xxxxxx 10xxxxxx
00200000 - 03FFFFFF 111110xx 10xxxxxx 10xxxxxx 10xxxxxx 10xxxxxx
04000000 - 7FFFFFFF 1111110x 10xxxxxx 10xxxxxx 10xxxxxx 10xxxxxx 10xxxxxx

没有BOM头的文件还是比较常见的,比如在写PHP时为了保证header能最先输出,通常都会将BOM头给人为去掉

7 楼

参照merry05给的网址中的方法
[code=c]

Private Function IsTextUTF8(ByRef bytSrc() As Byte) As Boolean

    On Error GoTo errHandler

    Dim i    As Long
    Dim AscN As Long
    Dim n    As Long

    n = UBound(bytSrc)

    Do While i <= n

        If bytSrc(i) < 128 Then
            i = i + 1     'ascii字符
            AscN = AscN + 1
        ElseIf (bytSrc(i) And &HE0) = &HC0 And (bytSrc(i + 1) And &HC0) = &H80 Then
            i = i + 2    '2个字节的utf8
        ElseIf (bytSrc(i) And &HF0) = &HE0 And (bytSrc(i + 1) And &HC0) = &H80 And (bytSrc(i + 2) And &HC0) = &H80 Then
            i = i + 3     '3个字节的utf8
        Else
            IsTextUTF8 = False

            Exit Function

            'Err.Raise ERROR_USER, , "未知编码"
        End If

    Loop

    IsTextUTF8 = (AscN <> n)

    Exit Function

errHandler:
    Debug.Print Err.Number, Err.Description
    #If DEBUG_MODE Then
        Stop: Resume
    #End If
    IsTextUTF8 = False
End Function
[/code]

测试:
    Dim fno      As Integer
    Dim strfile  As String
    Dim bytSrc() As Byte
    
    strfile = "F:\焦圣华\VB.Workspace\AspEncode\testutf8.txt"
    
    fno = FreeFile
    Open strfile For Binary As fno
    ReDim bytSrc(LOF(fno) - 1) As Byte
    Get #fno, , bytSrc()
    Close #fno

    Debug.Print IsTextUTF8(bytSrc())
用这个没有BOM头的UTF-8文件、ANSI编码的文件、有BOM头的UTF-8文件测试,均能正常判断。

8 楼

好多网页都是UTF8编码,但是文件头标识被去掉。

9 楼

LZ的代码没必要将整个文本测试完,只要测得其中有UTF-8编码即可退出

10 楼

[quote]LZ的代码没必要将整个文本测试完,只要测得其中有UTF-8编码即可退出[/quote]
好主意。尚未测试

我来回复

您尚未登录,请登录后再回复。点此登录或注册