65.9K
CodeProject 正在变化。 阅读更多。
Home

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

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.88/5 (72投票s)

2007 年 1 月 17 日

公共领域

7分钟阅读

viewsIcon

512927

downloadIcon

23384

检测没有 BOM(字节顺序标记)的文本编码,并选择最佳编码用于文本的持久化或网络传输

Sample Image - DetectEncoding.gif

引言

在某些情况下,您需要知道最佳代码页(编码)是什么,以便在互联网上传输文本或将其存储在文本文件中。有人可能会说 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 RoederReflector,我查看了签名:

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

Detection Perfomance

所有样本都已正确检测。

使用 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:更新了源文件和演示项目
© . All rights reserved.