DotNetNuke作为开源项目很多地方为我们提供了优良的示范得以一窥前人的智慧前几日因为研究一个DNN的BUG对文件编码和文件编码相关方面的处理有一些认识
我们经常需要把一个Text文件(如XMLSQL Script)上传到服务器然后进行处理(如显示或者执行)这里就涉及到文本文件编码的问题了
什么是文件编码?
首先我们来复习一下编码的基本概念由于历史原因Text文件存在ASCIIUnicodeUTFUTF等等编码方式对于中文还有GB对于Unicode还有UnicodeUnicode对于Unicode又分为Unicode Little EndianUnicode Big Endian要把所有的编码方式列举出来是相当的复杂想仔细的研究一下各种编码的规则和由来可以参考一下这篇文章编码charset乱码unicodeutf与net简单释义我们读取一个文本文件时总是使用某一种编码方式去解码这个文本文件如果我们使用的解码方式和文本文件本身的编码方式不一致最后的结果就是得到一个乱码的文件
我可以不用关心这个麻烦的文件编码吗?
大致了解了什么是文件编码我们来看看在DNN里为什么要和文件编码打交道这么麻烦我们不能绕开它吗?
在DNN里人们可以制作和上传皮肤模块语言包的就拿模块包说吧模块包里包含各种文本文件比如定义模块的dnn文件数据库的SQL 脚本文件等等因为DNN是一个开源软件世界上任何一个地方的人群都可能使用它所以这些文本文件可能以各种编码格式存储你无法强制别人只用某一种格式来储存我们只能侦测每一个遇到文本文件的编码方式并做对应的解码
这里要强调的一点是对于DNN对文本文件的编码方式做了一些限制那就是一定要使用带有BOM的Unicode格式其它格式都一律按不支持处理所以DNN的代码并不是一个彻底的解决方案但事情总是取一个平衡为%的应用在多做%的工作有时候是没必要的
如何解决文件编码转换的问题?
回到我们的问题对于一个上传到服务器的Text文件我们要解决的问题就是如何得知这个文件的编码方式并用正确的方式解码得到 文本文件中的内容
如何得知这个文件的编码方式?
首先我们来看看如何得知文本文件的编码方式为了简化问题我们只讨论Unicode编码这种形式(实际上DNN里也只针对Unicode做了处理)对于其它各种编码的判别方式我们不做讨论
BOM
这里涉及到一个BOM(Byte Order Mark) 的概念简单的讲在Unicode标准中为了标示文本文件的编码类型可以在文本文件的开始插入几个特殊的byte通过这几个特殊的byte应用程序就可以鑒别文本文件使用的是那种编码了那几个特殊的byte也被称之为BOM(参考 )
对于Unicode几种编码的BOM如下
UTF bigendian 文件的前个byte是 FE FF
UTF littleendian文件的前个byte是FF FE
UTF bigendian文件的前个byte是FE FF
UTF littleendian文件的前个byte是FF FE
UTF文件的前个byte是EF BB BF
UTF的规律特殊一点不是前几个byte而是所有的byte转换为十进制都小于
判定文件编码方式
知道了这一点你也应该能想到如何判定一个文本文件的编码方式了吧读取文件的前面几个字节跟上面的表对比就可以知道这个文件使用的哪一种编码了
看看DNN的代码这个函数在DotNetNukeModulesAdminResourceInstaller命名空间下的PaFile类里
GetTextEncodingType
Private Function GetTextEncodingType()Function GetTextEncodingType(ByVal Buffer As Byte()) As PaTextEncoding
UTF = No byte higher than
UTF = first three bytes EF BB BF
UTFBigEndian = first two bytes FE FF
UTFLittleEndian = first two bytes FF FE
Lets do the easy ones first
If Buffer() = And Buffer() = Then
Return PaTextEncodingUTFLittleEndian
End If
If Buffer() = And Buffer() = Then
Return PaTextEncodingUTFBigEndian
End If
If Buffer() = And Buffer() = And Buffer() = Then
Return PaTextEncodingUTF
End If
This does a simple test to verify that there are no bytes with a value larger than
which would be invalid in UTF encoding
Dim i As Integer
For i = To
If Buffer(i) > Then
Return PaTextEncodingUnknown
End If
Next
Return PaTextEncodingUTF
End Function
代码很好懂PaTextEncoding是一个枚举类型枚举各种编码格式唯一要注意的就是对于UTF编码采用了一种比较简单的判定方式——只检查了前个byte是否小于
SystemText
知道了编码方式接下来的工作就是解码了这里我们要用到Net的SystemText命名空间下的一些类 Encoding
UnicodeEncoding
ASCIIEncoding
UTFEncoding
UTFEncoding
UTFEncoding
等等
Encoding是基类UnicodeEncodingASCIIEncoding UTFEncodingUTFEncodingUTFEncoding等类继承自Encoding类专门用来处理各种编码
使用EncodingConvert (Encoding Encoding Byte[])方法可以把字节数组从一种编码的转换为另一种编码
使用GetString(Byte[])方法比如UTFEncodingGetString(Byte[])就可以把UTF编码得到字节数组还原成一个String
复习了SytemText下关于编码转换的一些类回到我们的问题你也许已经在想判断完文件编码的类型后只需要调用相应的GetString()函数就可以解码了如下
PaTextEncoding EncodingType = GetTextEncodingType(Buffer);
string DecodedString = ;
switch (EncodingType)
{
case PaTextEncodingUTFLittleEndian:
DecodedString = SystemTextEncodingUnicodeGetString(buffer);
break;
case PaTextEncodingUTFBigEndian:
DecodedString = SystemTextEncodingBigEndianUnicodeGetString(buffer);
break;
case PaTextEncodingUTF:
DecodedString = SystemTextEncodingUTFGetString(buffer);
break;
case PaTextEncodingUTF:
DecodedString = SystemTextEncodingUTFGetString(buffer);
break;
case PaTextEncodingUnknown:
throw new Exception(Unkonw Encoding);
break;
}
想法是没错的但有一个小小的问题之前我们提到过BOM不同的编码文件前面几个字节会有不同的BOM标示这几个字节唯一的作用就是指明编码类型在解码时应该去掉这几个字节但问题是GetString()函数不会自动去掉这几个字节如果直接把所有的字节数组传给GetString()函数因为BOM的影响解码得到的字符串前面几个字是乱码
DNN里用了一个比较巧妙的办法首先侦测字节数组的编码方式之后把所有的字节数组都转换为ASCII编码方式的字节数组最后通过ASCIIEncoding的GetString()函数得到字符串因为BOM的影响转换得到的ASCII字符串前面会有一些?字符查找这些字符并去掉即可代码如下
这一部分代码在DNN的DotNetNukeModulesAdminResourceInstaller命名空间下的PaDnnInstallerBase类里
GetAsciiString()函数实现转换为ASCII编码并解码为String Protected Function GetAsciiString()Function GetAsciiString(ByVal Buffer As Byte() ByVal SourceEncoding As Encoding) As String
Create two different encodings
Dim TargetEncoding As Encoding = EncodingASCII
Perform the conversion from one encoding to the other
Dim asciiBytes As Byte() = EncodingConvert(SourceEncoding TargetEncoding Buffer)
Convert the new byte[] into an ascii string
Dim asciiString As String = SystemTextEncodingASCIIGetString(asciiBytes)
Return asciiString
End Function
根据不同的编码方式传入不同的参数 Dim strScript As String =
Select Case sqlFileEncoding
Case PaTextEncodingUTFLittleEndian
strScript = GetAsciiString(sqlFileBuffer SystemTextEncodingUnicode) SystemTextEncodingUnicodeGetString(sqlFileBuffer)
Case PaTextEncodingUTFBigEndian
strScript = GetAsciiString(sqlFileBuffer SystemTextEncodingBigEndianUnicode) SystemTextEncodingBigEndianUnicodeGetString(sqlFileBuffer)
Case PaTextEncodingUTF
strScript = GetAsciiString(sqlFileBuffer SystemTextEncodingUTF) SystemTextEncodingUTFGetString(sqlFileBuffer)
Case PaTextEncodingUTF
strScript = GetAsciiString(sqlFileBuffer SystemTextEncodingUTF) SystemTextEncodingUTFGetString(sqlFileBuffer)
Case PaTextEncodingUnknown
Throw New Exception(StringFormat(SQL_UnknownFile sqlFileName))
End Select
This check needs to be included because the unicode Byte Order mark results in an extra character at the start of the file
The extra character ? causes an error with the database
If strScriptStartsWith(?) Then
strScript = strScriptSubstring()
End If
最后的一点问题
DNN里这种避免BOM影响解码的方法有一个问题那就是它把所有的文件都转为ASCII编码而ASCII编码是不支持双字节的也就是说如果文件中包含中文中文在解码后就成为乱码了具体现象可以参考这个文章SQL SERVER EXPRESS与出现中文变成问号的奇怪问题很可能不是通常的utf编码问题
我想解决方案是把所有的文件都转为UTF编码针对BOM影响编码的问题使用UTFEncodingGetString(buffer bufferlength)跳过字节数组的前三个字节