HttpClient:检测编码的正确顺序

HttpClient: Correct order to detect encoding

我正在使用 HttpClient 获取一些文件。我把内容放到一个字节数组(bytes)中。现在我需要检测编码。内容类型将为 html、css、JavaScript 或 XML 内容类型。

目前我检查 headers 的字符集,然后检查 BOM(字节顺序标记),最后检查文件的第一部分是否有字符集元标记。 通常这工作正常,因为没有冲突。

但是:这个顺序是否正确(以防冲突)?

我目前使用的代码:

Encoding encoding;
try
{
    encoding = Encoding.GetEncoding(responseMessage.Content.Headers.ContentType.CharSet);
}
catch
{
    using (MemoryStream ms = new MemoryStream(bytes))
    {
        using (StreamReader sr = new StreamReader(ms, Encoding.Default, true))
        {
            char[] chars = new char[1024];
            sr.Read(chars, 0, 1024);
            string textDefault = new string(chars);
            if (sr.CurrentEncoding == Encoding.Default)
            {
                encoding = Global.EncodingFraContentType(textDefault);
            }
            else
            {
                encoding = sr.CurrentEncoding;
            }
        }
    }
}
responseInfo.Text = encoding.GetString(bytes);
Global.EncodingFraContentType 是一个正则表达式,用于查找在 XML 声明或元标记中定义的字符集。

检测的正确顺序是什么charset/encoding?

根据W3C Faq

If you have a UTF-8 byte-order mark (BOM) at the start of your file then recent browser versions other than Internet Explorer 10 or 11 will use that to determine that the encoding of your page is UTF-8. It has a higher precedence than any other declaration, including the HTTP header.

当谈到http-header vs meta BOM优先时,只要meta在前1024内就可以优先,尽管对此没有严格的规定。

正确答案不取决于顺序,而是实际给出正确结果的顺序,这里没有完美的答案。

如果有冲突,那么服务器给了你不正确的东西。因为它不正确,所以不能有 "correct" 命令,因为没有正确的错误方式。而且,也许 header 和嵌入的元数据都是错误的!

没有一点 common-used 编码可以有一些看起来像 BOM 的东西,在开头看起来像 UTF-8 或 UTF-16,并且仍然是您提到的内容类型的有效示例,所以如果有 BOM 则获胜。

(唯一的例外是,如果文档编辑得如此糟糕以至于通过 part-way 切换编码,这并非闻所未闻,但是错误的内容非常错误,以至于没有真正的意思)。

如果内容不包含大于 0x7F 的八位字节,那么这无关紧要,header 和元数据都声称它是 US-ASCII、UTF-8、任何ISO-8859 系列编码,或那些八位字节都映射到相同代码点的任何其他编码,那么你认为它是什么并不重要,因为净值结果是相同的。将其视为元数据所说的任何内容,因为这样您就不需要重写它来正确匹配。

如果它是没有 BOM 的 UTF-16,它可能很快就会很清楚,因为所有这些格式都有很多具有特殊含义的字符,范围在 U+0000 到 U+00FF(确实,通常是 U+0020 到 U+007F),所以你会有很多范围,每隔一个字符一个零字节。

如果它有 0x7F 以上的八位字节并且是有效的 UTF-8,那么它几乎可以肯定是 UTF-8。 (出于同样的原因,如果它不是 UTF-8 并且八位字节高于 0x7F 那么它几乎肯定不会被误认为是 UTF-8)。

最棘手的合理常见情况是,如果您有关于它采用两种不同编码的相互矛盾的说法,这两种编码都是 single-octet-per-character 编码,并且存在 0x80-0xFF 范围内的八位字节。这是你不能确定的情况。如果一种编码是另一种编码的子集(尤其是当 C1 控件被排除在外时),那么您可以选择超集,但这需要存储有关这些编码的知识和大量工作。大多数时候我倾向于抛出一个异常,当它在日志中找到时,看看我是否可以获得源代码来修复他们的错误,或者 special-case 那个源代码,但是如果您正在处理大量您可能没有关系的不同来源。唉,这里没有完美的答案。

还值得注意的是,有时 header 和嵌入的元数据会错误地相互一致。一个常见的情况是 CP-1252 中的内容,但声称在 ISO-8859-1 中。

结论 - 按重要性排序:

  1. 字节顺序标记 (BOM):如果存在,则为 AUTHORATIVE,因为它是 由实际保存文件的编辑器添加(这只能是 出现在 unicode 编码中)。
  2. Content-Type 字符集(在服务器设置的 header 中):对于动态 created/processed 文件,它应该存在(因为 服务器知道),但可能不适用于静态文件(服务器只是 发送那些)。
  3. 内联字符集:对于xml、html和css,可以在文档中指定编码,在xml prolog, html meta tag@charset in css。要阅读,您需要首先解码 文档的一部分使用例如'Windows-1252'编码.
  4. 假定为 utf-8。这是 standard of the web,也是目前最常用的。
  5. 如果找到的编码等于“ISO-8859-1”,请改用“Windows-1252”(html5 中要求 - 在 Wikipedia
  6. 中阅读更多内容

现在尝试使用找到的编码解码文档。如果打开 error handling,那可能会失败!在那种情况下:

  1. 使用'Windows-1252'。这是旧 windows 文件中的标准,上次尝试时效果很好(那里仍然有很多旧文件)。 这将 never throw 错误。然而,它当然可能是错误的。

我做了一个实现这个的方法。我使用的 regex 能够找到指定为的编码:

Xml<?xml version="1.0" encoding="utf-8"?><?xml encoding="utf-8"?>

html<meta charset="utf-8" /><meta http-equiv="Content-Type" content="text/html; charset=utf-8">

css: @charset "utf-8";

(它适用于单引号和双引号)。

您将需要:

using System;
using System.IO;
using System.Net.Http;
using System.Text;
using System.Text.RegularExpressions;
using System.Threading.Tasks;

这里是returns解码字符串的方法(参数是HttpClientUri):

public static async Task<string> GetString(HttpClient httpClient, Uri url)
{
    byte[] bytes;
    Encoding encoding = null;
    Regex charsetRegex = new Regex(@"(?<=(<meta.*?charset=|^\<\?xml.*?encoding=|^@charset[ ]?)[""']?)[\w-]+?(?=[""';\r\n])",
        RegexOptions.IgnoreCase | RegexOptions.CultureInvariant | RegexOptions.ExplicitCapture);

    using (HttpResponseMessage responseMessage = await httpClient.GetAsync(url).ConfigureAwait(false))
    {
        responseMessage.EnsureSuccessStatusCode();
        bytes = await responseMessage.Content.ReadAsByteArrayAsync().ConfigureAwait(false);
        string headerCharset = responseMessage?.Content?.Headers?.ContentType?.CharSet;

        byte[] buffer = new byte[0x1000];
        Array.Copy(bytes, buffer, Math.Min(bytes.Length, buffer.Length));
        using (MemoryStream ms = new MemoryStream(buffer))
        {
            using (StreamReader sr = new StreamReader(ms, Encoding.GetEncoding("Windows-1252"), true, buffer.Length, true))
            {
                string testString = await sr.ReadToEndAsync().ConfigureAwait(false);
                if (!sr.CurrentEncoding.Equals(Encoding.GetEncoding("Windows-1252")))
                {
                    encoding = sr.CurrentEncoding;
                }
                else if (headerCharset != null)
                {
                    encoding = Encoding.GetEncoding(headerCharset, EncoderFallback.ExceptionFallback, DecoderFallback.ExceptionFallback);
                }
                else
                {
                    string inlineCharset = charsetRegex.Match(testString).Value;
                    if (!string.IsNullOrEmpty(inlineCharset))
                    {
                        encoding = Encoding.GetEncoding(inlineCharset, EncoderFallback.ExceptionFallback, DecoderFallback.ExceptionFallback);
                    }
                    else
                    {
                        encoding = new UTF8Encoding(false, true);
                    }
                }
                if (encoding.Equals(Encoding.GetEncoding("iso-8859-1")))
                {
                    encoding = Encoding.GetEncoding("Windows-1252", EncoderFallback.ExceptionFallback, DecoderFallback.ExceptionFallback);
                }
            }
        }
        using (MemoryStream ms = new MemoryStream(bytes))
        {
            try
            {
                using (StreamReader sr = new StreamReader(ms, encoding, false, 0x8000, true))
                {
                    return await sr.ReadToEndAsync().ConfigureAwait(false);
                }
            }
            catch (DecoderFallbackException)
            {
                ms.Position = 0;
                using (StreamReader sr = new StreamReader(ms, Encoding.GetEncoding("Windows-1252"), false, 0x8000, true))
                {
                    return await sr.ReadToEndAsync().ConfigureAwait(false);
                }
            }
        }
    }
}

您应该将方法调用包装在 try/catch 中,因为如果请求失败,HttpClient 会抛出错误。

更新:

.Net Core 中,您没有 'Windows-1252' 编码(恕我直言,大错特错),所以这里您必须使用“ISO-8859-1”来解决。