开源ASP.NET程序是如何处理文件编码的-从DotNetNuke看过来

简介: DotNetNuke作为开源项目,很多地方为我们提供了优良的示范,得以一窥前人的智慧。前几日,因为研究一个DNN的BUG,对文件编码和文件编码相关方面的处理有一些认识。 我们经常需要把一个Text文件(如XML,SQL Script)上传到服务器,然后进行处理(如显示或者执行),这里就涉及到文本文件编码的问题了。

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!)

相关文章
|
1月前
|
C# Windows
.NET开源免费的Windows快速文件搜索和应用程序启动器
今天大姚给大家分享一款.NET开源(MIT License)、免费、功能强大的Windows快速文件搜索和应用程序启动器:Flow Launcher。
|
18天前
|
存储 安全 Unix
【.Net Core】深入理解IO之文件和目录
【.Net Core】深入理解IO之文件和目录
26 4
|
16天前
|
开发框架 .NET Linux
【.NET Developer】已发布好的.NET Core项目文件如何打包为Docker镜像文件
该文介绍了如何不使用VS2019手动创建ASP.NET Core Blazor项目的Dockerfile并构建Docker镜像。首先,创建名为Dockerfile的文件,并复制提供的Dockerfile内容,该文件指定了基础镜像和工作目录。然后,通过CMD在项目目录下运行`docker build -t 自定义镜像名 .`来生成镜像。最后,使用`docker run`命令启动容器并验证项目运行。此外,文章还提到了将镜像推送到Azure Container Registry (ACR)的步骤。
|
7月前
|
开发框架 自然语言处理 前端开发
基于ASP.NET MVC开发的、开源的个人博客系统
基于ASP.NET MVC开发的、开源的个人博客系统
57 0
|
8月前
|
对象存储
.net core 阿里云接口之拷贝文件
紧接上文, 1)[.net core 阿里云接口之获取临时访问凭证](https://developer.aliyun.com/article/1363447?spm=a2c6h.12873639.article-detail.7.2b0e5b1cpeWbZ5 ".net core 阿里云接口之获取临时访问凭证") 2)[.net core 阿里云接口之将指定的OSS文件下载到流](https://developer.aliyun.com/article/1363886 ".net core 阿里云接口之将指定的OSS文件下载到流") 本文继续阿里云接口调用,将指定的OSS文件下载到流。
41 0
|
8月前
|
C#
.net core 从(本地)服务器获取APK文件并解析APK信息
## 1、apk解析除了使用客户端利用aapt.exe、unzip.exe开发客户端解析外,还可以直接利用服务进行解析 ```csharp /// <summary> /// 从本地服务器获取APK文件并解析APK信息 /// </summary> /// <param name="fileName">APK文件的完整路径</param> /// <returns></returns> [HttpPost, HttpGet, HttpOptions, CorsOptions] public IActionResult DecodeAPK(string fileName) { if(fi
44 0
|
8月前
|
JavaScript 前端开发 关系型数据库
.net core + vue + elementui 删除指定日期段、指定路径下的所有文件
# 1、呈现效果 ![image.png](https://ucc.alicdn.com/pic/developer-ecology/j2ygdazy447va_0782583bbc894c33a079db9e44385acd.png) # 2、后端 ## 1)服务层 ```csharp /// <summary> /// 删除指定修改日期段及指定路径下的所有文件 /// </summary> /// <param name="filepath">指定路径</param> /// <returns>返回删除结果提示</returns> public string DeleteSpecif
49 0
|
8月前
|
C#
.net core 删除指定路径下的所有文件以及文件夹(文件夹建议保留目录)
1、服务层 ```csharp /// <summary> /// 删除指定路径下的所有文件 /// </summary> /// <param name="filepath">指定路径</param> /// <returns></returns> public string DeleteSpecifiedPathAllFile(string filepath) { try { DirectoryInfo info = new DirectoryInfo(filepath); // 去除文件夹的只读属性 info.Attribu
55 0
|
10月前
|
开发框架 前端开发 .NET
asp.net 文件分片上传
asp.net 文件分片上传
121 0
asp.net 文件分片上传
|
9月前
|
开发框架 JSON Kubernetes
.NET Core - 环境变量配置和文件提供程序配置方式详解
.NET Core - 环境变量配置和文件提供程序配置方式详解