从浏览器来分析
我们知道大多数时候浏览器都能够正确处理网页的编码,当我们使用浏览器浏览不同编码的中文网站时不会出现“乱码”,但有时候也会出现乱码,这是为什么呢?浏览器如何知道返回来的流是什么编码?为什么有时候浏览器也不能正确处理编码问题?
浏览器很可能是通过两种方式来获得服务器返回流的字符编码。 首先,浏览器会从尝试从Http Header来获得字符串编码,如果得不到(有些服务器返回的Http Header中没有包含CharSet)则尝试从HTML的Meta标签中获得。如果有的网站做得实在太烂,Http Header和Html的Meta中都没有返回编码信息,那就有可能出现乱码。
由于ASP.NET可以通过Response.CharSet和Response.ContentType来控制发送给客户端的字符编码信息,因此浏览器的这些行为是可以验证的。
这篇文章有列出验证实验:CodePage、ContentEncoding、Charset、ContentType、meta charset 有什么区别?
问题的抛出
因此问题就在于如何获得HttpWebResponse返回的流的编码。对于Header里面的CharSet我们直接通过HttpWebResponse的属性CharacterSet即可得到——当然如果服务器没有返回则会得到可能一个不正确的字符编码“ISO-8859-1”。这个时候我们只有从字符串本身包含的meta标签来获得编码信息,如果字符串不包含meta标签或者meta标签里面没有CharSet,那问题就难办了。
如何处理?
原理很简单,上面的问题的抛出已经说明了如何处理。这里直接贴代码:
internal static HttpFormResponse ReadResponse(WebRequest request)
{
const int MaxTry = 3;int tryCount = 0;
var httpFormResponse = new HttpFormResponse();// Sometimes the external site can throw exception so we might
// have to retry few more times
while (string.IsNullOrEmpty(httpFormResponse.Response) && (tryCount < MaxTry))
{
try
{
using (WebResponse response = request.GetResponse())
{
PopulateHeadersAndCookies(response, httpFormResponse);var httpWebResponse = (HttpWebResponse) response;
var charSet = httpWebResponse.CharacterSet;
var buffer = GetBytes(httpWebResponse);
httpFormResponse.Response = GetStringFromBuffer(buffer, charSet);
}
}
catch (WebException)
{
tryCount += 1;
Thread.Sleep(200);
}
}return httpFormResponse;
}private static byte[] GetBytes(WebResponse response)
{
var length = (int)response.ContentLength;
byte[] data;using (var memoryStream = new MemoryStream())
{
var buffer = new byte[0x100];using (var rs = response.GetResponseStream())
{
for (var i = rs.Read(buffer, 0, buffer.Length); i > 0; i = rs.Read(buffer, 0, buffer.Length))
{
memoryStream.Write(buffer, 0, i);
}
}data = memoryStream.ToArray();
}return data;
}private static string GetStringFromBuffer(byte[] buffer, string charSet)
{
if (string.IsNullOrEmpty(charSet) || string.Compare(charSet, "ISO-8859-1") == 0)
{
charSet = GetEncodingFromBody(buffer);
}try
{
var encoding = Encoding.GetEncoding(charSet);
var str = encoding.GetString(buffer);
return str;
}
catch (Exception ex)
{
Log.Exception(ex);
return string.Empty;
}
}private static string GetEncodingFromBody(byte[] buffer)
{
var regex = new Regex(@"<meta(\s+)http-equiv(\s*)=(\s*""?\s*)content-type(\s*""?\s+)content(\s*)=(\s*)""text/html;(\s+)charset(\s*)=(\s*)(?<charset>[a-zA-Z0-9-]+?)""(\s*)(/?)>", RegexOptions.IgnoreCase | RegexOptions.Compiled);
var str = Encoding.ASCII.GetString(buffer);
var regMatch = regex.Match(str);
if (regMatch.Success)
{
var charSet = regMatch.Groups["charset"].Value;
return charSet;
}return Encoding.ASCII.BodyName;
}internal static void PopulateHeadersAndCookies(WebResponse webResponse, HttpFormResponse response)
{
response.Headers.Add(webResponse.Headers);var httpWebResponse = webResponse as HttpWebResponse;
if (httpWebResponse != null)
{
foreach (Cookie cookie in httpWebResponse.Cookies)
{
response.Cookies.Add(cookie.Name, cookie.Value);
}
}
}
其中关键的代码就是这几行:
var httpWebResponse = (HttpWebResponse) response;
var charSet = httpWebResponse.CharacterSet;
var buffer = GetBytes(httpWebResponse);
httpFormResponse.Response = GetStringFromBuffer(buffer, charSet);
我觉得最关键的就是var buffer = GetBytes(httpWebResponse); ,通过这行代码将流拷贝到byte数组中保存起来,当无法从Http Header中获得编码信息是就用ASCII编码从buffer中获得字符串。我们知道HTML的标签都是字母,使用ASCII编码虽然中文或者其他双字节字符会出现乱码,但是HTML标签还是能够解析出来。这样我们就可以检测HTML的meta标签从而获得charset。
这个问题的处理花费了我不少时间,最初是发现有些网站在Header里面返回了编码信息而有的没有,然后就是因为对流的处理不熟悉,不知道如何把流拷贝到内存里面可以多次使用。系统的学习很重要啊。