DNN开发
在DNN里,人们可以制作和上传皮肤,模块,语言包的。就拿模块包说吧,模块包里包含各种文本文件,比如定义模块的.dnn文件,数据库的SQL 脚本文件等等。因为DNN是一个开源软件,世界上任何一个地方的人群都可能使用它,所以这些文本文件可能以各种编码格式存储,你无法强制别人只用某一种格式来储存,我们只能侦测每一个遇到文本文件的编码方式,并做对应的解码。
DotNetNuke作为开源项目,很多地方为我们提供了优良的示范,得以一窥前人的智慧。前几日,因为研究一个DNN的BUG,对文件编码和文件编码相关方面的处理有一些认识。
我们经常需要把一个Text文件(如XML,SQL Script)上传到服务器,然后进行处理(如显示或者执行),这里就涉及到文本文件编码的问题了。
什么是文件编码
首先我们来复习一下编码的基本概念,由于历史原因,Text文件存在ASCII,Unicode,UTF-8,UTF-7等等编码方式;对于中文,还有GB2312;对于Unicode还有Unicode-16,Unicode-32;对于Unicode-16又分为Unicode-16 Little Endian,Unicode-16 Big Endian。要把所有的编码方式列举出来是相当的复杂。想仔细的研究一下各种编码的规则和由来可以参考一下这篇文章:编码,charset,乱码,unicode,utf-8与net简单释义。我们读取一个文本文件时,总是使用某一种编码方式去解码这个文本文件,如果我们使用的解码方式和文本文件本身的编码方式不一致,最后的结果就是得到一个乱码的文件。
我可以不用关心这个麻烦的文件编码吗
大致了解了什么是文件编码,我们来看看在DNN里为什么要和文件编码打交道,这么麻烦,我们不能绕开它吗?
在DNN里,人们可以制作和上传皮肤,模块,语言包的。就拿模块包说吧,模块包里包含各种文本文件,比如定义模块的.dnn文件,数据库的SQL 脚本文件等等。因为DNN是一个开源软件,世界上任何一个地方的人群都可能使用它,所以这些文本文件可能以各种编码格式存储,你无法强制别人只用某一种格式来储存,我们只能侦测每一个遇到文本文件的编码方式,并做对应的解码。
这里要强调的一点是:对于DNN,对文本文件的编码方式做了一些限制,那就是一定要使用带有BOM的Unicode格式,其它格式都一律按不支持处理。所以DNN的代码并不是一个彻底的解决方案,但事情总是取一个平衡,为20%的应用在多做80%的工作,有时候是没必要的。
如何解决文件编码转换的问题
回到我们的问题,对于一个上传到服务器的Text文件,我们要解决的问题就是:“如何得知这个文件的编码方式,并用正确的方式解码,得到 文本文件中的内容。”
如何得知这个文件的编码方式
首先我们来看看如何得知文本文件的编码方式,为了简化问题,我们只讨论Unicode编码这种形式(实际上DNN里也只针对Unicode做了处理),对于其它各种编码的判别方式我们不做讨论。
BOM
这里涉及到一个BOM(Byte Order Mark) 的概念。简单的讲,在Unicode标准中,为了标示文本文件的编码类型,可以在文本文件的开始插入几个特殊的byte,通过这几个特殊的byte,应用程序就可以鉴别文本文件使用的是那种编码了。那几个特殊的byte也被称之为BOM(参考:http://unicode.org/faq/utf_bom.html )。
对于Unicode,几种编码的BOM如下:
- UTF-32, big-endian 文件的前4个byte是:00 00 FE FF
- UTF-32, little-endian文件的前4个byte是:FF FE 00 00
- UTF-16, big-endian文件的前2个byte是:FE FF
- UTF-16, little-endian文件的前2个byte是:FF FE
- UTF-8文件的前3个byte是:EF BB BF
- UTF-7的规律特殊一点,不是前几个byte,而是所有的byte转换为十进制都小于127。
判定文件编码方式
知道了这一点,你也应该能想到如何判定一个文本文件的编码方式了吧。读取文件的前面几个字节,跟上面的表对比,就可以知道这个文件使用的哪一种编码了。
看看DNN的代码:这个函数在DotNetNuke.Modules.Admin.ResourceInstaller命名空间下的PaFile类里。
GetTextEncodingType
1 Private Function GetTextEncodingType()Function GetTextEncodingType(ByVal Buffer As Byte()) As PaTextEncoding
2 'UTF7 = No byte higher than 127
3 'UTF8 = first three bytes EF BB BF
4 'UTF16BigEndian = first two bytes FE FF
5 'UTF16LittleEndian = first two bytes FF FE
6
7 'Lets do the easy ones first
8 If Buffer(0) = 255 And Buffer(1) = 254 Then
9 Return PaTextEncoding.UTF16LittleEndian
10 End If
11 If Buffer(0) = 254 And Buffer(1) = 255 Then
12 Return PaTextEncoding.UTF16BigEndian
13 End If
14 If Buffer(0) = 239 And Buffer(1) = 187 And Buffer(2) = 191 Then
15 Return PaTextEncoding.UTF8
16 End If
17
18 'This does a simple test to verify that there are no bytes with a value larger than 127
19 'which would be invalid in UTF-7 encoding
20 Dim i As Integer
21 For i = 0 To 100
22 If Buffer(i) > 127 Then
23 Return PaTextEncoding.Unknown
24 End If
25 Next
26 Return PaTextEncoding.UTF7
27
28 End Function
29
代码很好懂,PaTextEncoding是一个枚举类型,枚举各种编码格式。唯一要注意的就是对于UTF-7编码,采用了一种比较简单的判定方式——只检查了前101个byte是否小于127。
System.Text
知道了编码方式,接下来的工作就是解码了。这里我们要用到.Net的System.Text命名空间下的一些类。
- Encoding
- UnicodeEncoding
- ASCIIEncoding
- UTF32Encoding
- UTF8Encoding
- UTF7Encoding
- 等等
Encoding是基类,UnicodeEncoding、ASCIIEncoding、 UTF32Encoding、UTF8Encoding、UTF7Encoding等类继承自Encoding类,专门用来处理各种编码。
- 使用Encoding.Convert (Encoding, Encoding, Byte[])方法,可以把字节数组从一种编码的转换为另一种编码
- 使用GetString(Byte[])方法,比如UTF8Encoding.GetString(Byte[])就可以把UTF8编码得到字节数组还原成一个String.
复习了Sytem.Text下关于编码转换的一些类,回到我们的问题,你也许已经在想,判断完文件编码的类型后,只需要调用相应的GetString()函数就可以解码了。如下:
Code
1 PaTextEncoding EncodingType = GetTextEncodingType(Buffer);
2
3 string DecodedString = "";
4
5 switch (EncodingType)
6 {
7 case PaTextEncoding.UTF16LittleEndian:
8 DecodedString = System.Text.Encoding.Unicode.GetString(buffer);
9 break;
10 case PaTextEncoding.UTF16BigEndian:
11 DecodedString = System.Text.Encoding.BigEndianUnicode.GetString(buffer);
12 break;
13 case PaTextEncoding.UTF8:
14 DecodedString = System.Text.Encoding.UTF8.GetString(buffer);
15 break;
16 case PaTextEncoding.UTF7:
17 DecodedString = System.Text.Encoding.UTF7.GetString(buffer);
18 break;
19 case PaTextEncoding.Unknown:
20 throw new Exception("Unkonw Encoding");
21 break;
22 }
23
想法是没错的,但有一个小小的问题,之前我们提到过BOM,不同的编码文件前面几个字节会有不同的BOM标示,这几个字节唯一的作用就是指明编码类型,在解码时应该去掉这几个字节,但问题是,GetString()函数不会自动去掉这几个字节,如果直接把所有的字节数组传给GetString()函数,因为BOM的影响,解码得到的字符串前面几个字是乱码。
DNN里用了一个比较巧妙的办法,首先侦测字节数组的编码方式,之后把所有的字节数组都转换为ASCII编码方式的字节数组,最后通过ASCIIEncoding的GetString()函数得到字符串。因为BOM的影响,转换得到的ASCII字符串前面会有一些”?”字符,查找这些字符并去掉即可。代码如下:
这一部分代码在DNN的DotNetNuke.Modules.Admin.ResourceInstaller命名空间下的PaDnnInstallerBase类里。
GetAsciiString()函数实现转换为ASCII编码,并解码为String
Code
1 Protected Function GetAsciiString()Function GetAsciiString(ByVal Buffer As Byte(), ByVal SourceEncoding As Encoding) As String
2
3 ' Create two different encodings.
4 Dim TargetEncoding As Encoding = Encoding.ASCII
5
6 ' Perform the conversion from one encoding to the other.
7 Dim asciiBytes As Byte() = Encoding.Convert(SourceEncoding, TargetEncoding, Buffer)
8
9 ' Convert the new byte[] into an ascii string.
10 Dim asciiString As String = System.Text.Encoding.ASCII.GetString(asciiBytes)
11
12 Return asciiString
13 End Function
14
根据不同的编码方式,传入不同的参数:
Code
1 Dim strScript As String = ""
2 Select Case sqlFile.Encoding
3 Case PaTextEncoding.UTF16LittleEndian
4 strScript = GetAsciiString(sqlFile.Buffer, System.Text.Encoding.Unicode) 'System.Text.Encoding.Unicode.GetString(sqlFile.Buffer)
5 Case PaTextEncoding.UTF16BigEndian
6 strScript = GetAsciiString(sqlFile.Buffer, System.Text.Encoding.BigEndianUnicode) 'System.Text.Encoding.BigEndianUnicode.GetString(sqlFile.Buffer)
7 Case PaTextEncoding.UTF8
8 strScript = GetAsciiString(sqlFile.Buffer, System.Text.Encoding.UTF8) 'System.Text.Encoding.UTF8.GetString(sqlFile.Buffer)
9 Case PaTextEncoding.UTF7
10 strScript = GetAsciiString(sqlFile.Buffer, System.Text.Encoding.UTF7) 'System.Text.Encoding.UTF7.GetString(sqlFile.Buffer)
11 Case PaTextEncoding.Unknown
12 Throw New Exception(String.Format(SQL_UnknownFile, sqlFile.Name))
13 End Select
14
15 'This check needs to be included because the unicode Byte Order mark results in an extra character at the start of the file
16 'The extra character - '?' - causes an error with the database.
17 If strScript.StartsWith("?") Then
18 strScript = strScript.Substring(1)
19 End If
20
最后的一点问题
DNN里这种避免BOM影响解码的方法有一个问题,那就是它把所有的文件都转为ASCII编码,而ASCII编码是不支持双字节的,也就是说如果文件中包含中文,中文在解码后就成为乱码了。具体现象可以参考这个文章;SQL SERVER 2005 EXPRESS与ASP.net出现中文变成问号的奇怪问题。很可能不是通常的utf-8编码问题。
我想解决方案是,把所有的文件都转为UTF编码,针对BOM影响编码的问题,使用UTF8Encoding.GetString(buffer, 3, buffer.length)跳过字节数组的前三个字节。
参考文献:
编码,charset,乱码,unicode,utf-8与net简单释义
编码,charset,乱码,unicode,utf-8与net简单释义(转载合集版本)
让你知道codepage的重要,关于多语言编码[转载]
Byte Order Mark (BOM) FAQ
Display problems caused by the UTF-8 BOM
字节流编码获取原来这么复杂
Every character has a story #4: U+feff (alternate title: UTF-8 is the BOM, dude!)