检测传入/传出文本的编码






4.88/5 (72投票s)
检测没有 BOM(
引言
在某些情况下,您需要知道最佳代码页(编码)是什么,以便在互联网上传输文本或将其存储在文本文件中。有人可能会说 Unicode 总是能解决问题,但我需要最有效(节省字节)的数据传输方式。
从文本检测代码页是一项非常棘手的任务。但幸运的是,微软提供了 MLang
API,其中 IMultiLang3
接口用于出站编码检测。
类似地,IMultiLang2
接口有一个函数可以检测传入字节数组的编码。这对于检测存储在文件中的文本的代码页,或者需要通过互联网发送的文本非常有用。
EncodingTools
类提供了一些易于使用的函数来确定不同场景下的最佳编码。
背景
问题
我开始着手这项工作,同时还有一个用于构造符合 MIME 标准的电子邮件的组件。电子邮件的正文以 String
的形式传递。用户需要手动提供用于 Transfer-Encoding 的字符集。只要您知道目标字符集或始终假定为 Unicode,这就可以了。但对于具有最终用户 GUI 应用程序(大多数用户甚至不知道“编码”是什么)的情况,这绝对不是一个好的解决方案。
我想知道是否可以从给定的文本中检测出最佳编码。
粗糙的解决方案尝试
我的第一次尝试是一个简单的暴力攻击
- 构建一个合适的编码列表(仅包括 iso 代码页和 unicode)
- 遍历所有考虑的编码
- 使用此编码对文本进行编码
- 将其重新编码为 Unicode
- 比较结果是否存在错误
- 如果没有错误,则记住产生最少字节数的编码
这不仅丑陋,而且根本不起作用。所有单字节编码在它们的编码结果上都是二进制相等的。代码页仅用于将单个字节映射到正确的字符以供显示。
因此,此方法只能区分 ASCII (7位)、单字节 (8位) 以及不同的 Unicode 变体 (UTF-7, UTF8, Unicode 等)。
寻找更好的方法
然后我记起了 IMultiLang2.DetectInputCodepage
方法,该方法随 Internet Explorer 5.0 一起引入。此方法检测文本中使用的编码(Internet Explorer 用于在缺少页面头信息时自动检测代码页)。即使这样也不适合我的问题,而且我想知道自 5.0 版本以来是否有任何开发。EncodingTools
类中提供了 DetectInputCodepage
的包装函数。
Internet Explorer 5.5 引入了一个从 MLang
DLL 导出的新接口:IMultiLang3
。MSDN 关于此接口的说法如下:
此接口扩展了 IMultiLanguage2,并为其添加了出站文本检测功能。
哇!这听起来很有希望!该接口只有两个方法:
DetectOutboundCodePage
(用于string
)DetectOutboundCodePageInIStream
(用于stream
)
我选择了使用第一个。
使用 MLang
MLang.dll 位于 Windows\system32 目录中。除了提供一些导出的函数外,它还提供了一些 COM 类,但不包含类型库。因此,简单的“在 Visual Studio 中添加引用”的方法不起作用。
MLang.idl 是 Platform SDK 的一部分,可以在 include
目录中找到。
要从 IDL 文件创建程序集,请从 Visual Studio 命令提示符执行以下命令:
c:\temp\>midl MLang.idl
C:\temp>midl MLang.idl > null
Microsoft (R) 32b/64b MIDL Compiler Version 6.00.0366
Copyright (c) Microsoft Corporation 1991-2002. All rights reserved.
MLang.idl
unknwn.idl
wtypes.idl
basetsd.h
guiddef.h
oaidl.idl
objidl.idl
oaidl.acf
C:\temp>tlbimp mlang.tlb /silent
这两个命令的结果是一个全新的程序集,名为 MultiLanguage.dll。
使用 Lutz Roeder 的 Reflector,我查看了签名:
MethodImpl(MethodImplOptions.InternalCall,
MethodCodeType=MethodCodeType.Runtime)]
void DetectOutboundCodePage([In] uint dwFlags,
[In, MarshalAs(UnmanagedType.LPWStr)] string lpWideCharStr,
[In] uint cchWideChar,
[In] ref uint puiPreferredCodePages,
[In] uint nPreferredCodePages,
[In] ref uint puiDetectedCodePages,
[In, Out] ref uint pnDetectedCodePages,
[In] ref ushort lpSpecialChar);
我对 puiPreferredCodePages
和 puiDetectedCodePages
参数的 ref uint
不太满意。此外,还缺少 dwFlags
的类型化 enum
。
因此,我首先将生成的程序集导出为 C# 源代码,然后对其进行了一些更改:
[Flags]
public enum MLCPF
{
// Not currently supported.
MLDETECTF_MAILNEWS = 0x0001,
// Not currently supported.
MLDETECTF_BROWSER = 0x0002,
// Detection result must be valid for conversion and text rendering.
MLDETECTF_VALID = 0x0004,
// Detection result must be valid for conversion.
MLDETECTF_VALID_NLS = 0x0008,
// Preserve preferred code page order.
// This is meaningful only if you have set the puiPreferredCodePages
// parameter
// in IMultiLanguage3::DetectOutboundCodePage
// or IMultiLanguage3::DetectOutboundCodePageInIStream.
MLDETECTF_PRESERVE_ORDER = 0x0010,
// Only return one of the preferred code pages as the detection result.
// This is meaningful only if you have set the puiPreferredCodePages
// parameter
// in IMultiLanguage3::DetectOutboundCodePage
// or IMultiLanguage3::DetectOutboundCodePageInIStream.
MLDETECTF_PREFERRED_ONLY = 0x0020,
// Filter out graphical symbols and punctuation.
MLDETECTF_FILTER_SPECIALCHAR = 0x0040,
// Return only Unicode codepages if the euro character is detected.
MLDETECTF_EURO_UTF8 = 0x0080
}
[MethodImpl(MethodImplOptions.InternalCall,
MethodCodeType=MethodCodeType.Runtime)]
void DetectOutboundCodePage([In] MLCPF dwFlags,
[In, MarshalAs(UnmanagedType.LPWStr)] string lpWideCharStr,
[In] uint cchWideChar,
[In] IntPtr puiPreferredCodePages,
[In] uint nPreferredCodePages,
[In] IntPtr puiDetectedCodePages,
[In, Out] ref uint pnDetectedCodePages,
[In] ref ushort lpSpecialChar);
然后我将源文件添加到我的项目中(不再需要 MultiLanguage.dll 程序集)。
使用 IMultiLanguage3::DetectOutboundCodePage
获取实现 IMultiLanguage3
的 COM 类的实例非常简单:
// get the IMultiLanguage3 interface
MultiLanguage.IMultiLanguage3 multilang3 = new
MultiLanguage.CMultiLanguageClass();
if (multilang3 == null)
throw new System.Runtime.InteropServices.COMException(
"Failed to get IMultilang3");
接下来是填充参数。
第一个参数 dwFlags
是 tagMLCPF
标志的组合。我选择始终设置 MLDETECTF_VALID_NLS
,因为结果将用于转换。
MLDETECTF_PRESERVE_ORDER
和 MLDETECTF_PREFERRED_ONLY
根据传递给我的检测方法的参数而使用。
接下来的两个参数(lpWideCharStr
和 cchWideChar
)分别是传递用于检测的 string
及其长度。
通过接下来的两个参数(puiPreferredCodePages
和 nPreferredCodePages
),可以将检测限制在所有代码页的一个子集中。如果您只想返回特定代码页的子集,这将非常有用。
方法成功完成后,最后三个参数包含检测结果。
因此,实际调用如下所示:uint[] preferedEncodings; // array of uint passed as parameter to the
// function
int[] resultCodePages = new int[preferedEncodings.Length]; // result array
// ... call the function
multilang2.DetectInputCodepage(options,0, ref input[0], ref srcLen,
ref detectedEncdings[0], ref scores);
// evaluate the result
if (scores > 0)
{
for (int i = 0; i < scores; i++)
{
// add the result
result.Add(Encoding.GetEncoding((int)detectedEncdings[i].nCodePage));
}
}
最后,应释放 COM 对象。
Marshal.FinalReleaseComObject(multilang3);
使用 IMultiLanguage2::DetectInputCodepage
在能够选择最佳编码以在互联网上传输文本或将其保存到流之后,下一个任务是检测传入文本的最佳编码,如果发送者(或存储者)没有选择最佳编码。
DetectInputCodepage
(至少)有两个实际用途。默认情况下,Windows 以当前默认(UI)编码存储文本文件。例如,在我的系统上是“Windows-1252”。一位来自俄罗斯的用户将使用“Windows-1251”编写文本。这两种代码页都是单字节的,没有任何前导符。因此,文本文件不会包含有关所用代码页的任何信息。
因此,如果您打开一个包含使用不同于当前 UI 代码页的代码页创建的文本的文件,StreamReader
将像该文本存储在 UI 的当前代码页中一样读取它。(StreamReader
的编码检测主要是前导符检查。因此,它几乎无法处理任何非 Unicode 文件(或没有 BOM 的 Unicode 文件)。)
ASCII 字符集之外的大多数字符将显示不正确。
这就是 DetectInputCodepage
派上用场的地方。它的准确性不是 100%,但绝对比 StreamReader
的准确性好。
在演示应用程序中,您可以双击一个编码来测试哪种方法能获得更好的结果(请参阅下面的“测试 DetectInputCodepage
性能”)。
另一个实际用途是检测来自实现不佳的 MIME 邮件程序的电子邮件编码。一些奇怪的邮件程序以 8 位编码发送电子邮件,而不在头部指定任何字符集。在这种情况下,DetectInputCodepage
可以提供很大帮助。
对于 DetectOutboundCodePage
方法,我稍微修改了方法签名并添加了 MLDETECTCP
枚举。生成的代码如下所示:
public enum MLDETECTCP {
// Default setting will be used.
MLDETECTCP_NONE = 0,
// Input stream consists of 7-bit data.
MLDETECTCP_7BIT = 1,
// Input stream consists of 8-bit data.
MLDETECTCP_8BIT = 2,
// Input stream consists of double-byte data.
MLDETECTCP_DBCS = 4,
// Input stream is an HTML page.
MLDETECTCP_HTML = 8,
//Not currently supported.
MLDETECTCP_NUMBER = 16
}
[MethodImpl(MethodImplOptions.InternalCall,
MethodCodeType=MethodCodeType.Runtime)]
void DetectInputCodepage([In] MLDETECTCP flags, [In] uint dwPrefWinCodePage,
[In] ref byte pSrcStr, [In, Out] ref int pcSrcSize,
[In, Out] ref DetectEncodingInfo lpEncoding,
[In, Out] ref int pnScores);
该函数的使用与前面描述的 DetectOutboundCodePage
几乎相同。
int maxEncodings; // parameter specifying how many encodings to return
int srcLen = input.Length; // length of the input
int scores = detectedEncdings.Length; // the number of detected scores
// setup options (none)
MultiLanguage.MLDETECTCP options = MultiLanguage.MLDETECTCP.MLDETECTCP_NONE;
// finally... call to DetectInputCodepage
multilang2.DetectInputCodepage(options,0, ref input[0], ref srcLen,
ref detectedEncdings[0], ref scores);
// get result
if (scores > 0)
{
for (int i = 0; i < scores; i++)
{
// add the result
result.Add(Encoding.GetEncoding((int)detectedEncdings[i].nCodePage));
}
}
我的第一次测试并不那么令人鼓舞。当我尝试检测代码页时,总是会抛出 COMExcpetion
(E_FAIL)。
DetectInputCodepage
在文本太短或没有 BOM(字节顺序标记/编码前导符)的情况下会失败。有两种失败方式。如果输入数据很短(少于 60 字节),则很有可能检测到错误的代码页。如果小于 200 字节,则 DetectInputCodepage
很有可能返回 E_FAIL,因为它无法决定使用哪种代码页。对于后一种问题,我实现了一个粗糙的变通方法。我只是将输入数据乘以最多 256 字节。即使对于短字符串,这似乎也能返回合理的结果。
// expand the string to be at least 256 bytes
if (input.Length < 256)
{
byte[] newInput = new byte[256];
int steps = 256 / input.Length;
for (int i = 0; i < steps; i++)
Array.Copy(input, 0, newInput, input.Length * i, input.Length);
int rest = 256 % input.Length;
if (rest > 0)
Array.Copy(input, 0, newInput, steps * input.Length, rest);
input = newInput;
}
总结
我决定创建一个 static
类来提供对 DetectOutboundCodePage
和 DetectInputCodepage
方法的访问。它包含一些 public
方法,提供不同级别的抽象。
以下是应涵盖大多数使用场景的六个高级方法:
GetMostEfficientEncoding
GetMostEfficientEncodingForStream
DetectInputCodepage
ReadTextFile
OpenTextFile
OpenTextStrem
它还包含三个 public static
预定义代码页集数组:
PreferedEncodings
PreferedEncodingsForStream
AllEncodings
这些数组中的代码页顺序能够返回最佳结果,但不是自然排序。
测试 DetectInputCodepage 性能
下面的截图显示了 StreamReader
编码检测和 EncodingTools
检测的比较。示例文本来自 Unciode.org。
所有样本都已正确检测。
使用 EncodingTools 类
以下代码片段展示了如何使用 EncodingTools
类。
出站编码
检测流的最佳编码
// save the given text using the optimal encoding
private void SaveToStream(string text, string path)
{
// this is all... detect the encoding
Encoding enc = EncodingTools.GetMostEfficientEncodingForStream(text);
// then safe
using (StreamWriter sw = new StreamWriter(path, false, enc))
sw.Write(text);
}
检测电子邮件正文的最佳编码
// save the given text using the optimal encoding
private void SaveToAsEmail(string text, string path)
{
// this is all... detect the encoding
Encoding enc = EncodingTools.GetMostEfficientEncoding(text);
// then safe
using (StreamWriter sw = new StreamWriter(path, false, Encoding.ASCII))
{
sw.WriteLine("Subject: test");
sw.WriteLine("Transfer-Encoding: 7bit");
sw.WriteLine(
"Content-Type: text/plain;\r\n\tcharset=\"{0}\"",
enc.BodyName);
sw.WriteLine("Content-Transfer-Encoding: base64"); // should be QP
sw.WriteLine();
sw.Write(Convert.ToBase64String(enc.GetBytes(text),
Base64FormattingOptions.InsertLineBreaks));
}
}
入站编码
打开文本文件
private void OpenTextFileTest()
{
// read the complete file into a string
string content = EncodingTools.ReadTextFile(@"C:\test\txt");
// create a StreamReader with the guessed best encoding
using (StreamReader sr = EncodingTools.OpenTextFile(@"C:\test\txt"))
{
string fileContent = sr.ReadToEnd();
}
}
从流中读取
private void ReadStreamTest()
{
// create a streamReader from a stream
using (MemoryStream ms = new MemoryStream(
Encoding.GetEncoding("windows-1252").GetBytes("Some umlauts: öäüß")))
{
using (StreamReader sr = EncodingTools.OpenTextStream(ms))
{
string fileContent = sr.ReadToEnd();
}
}
}
参考文献
- MSDN 上的 MLang 文档
历史
- 17/01/2007:初始发布
- 22/01/2007:修复代码,使其在没有警告的情况下编译
- 27/10/2009:更新了源文件和演示项目