最近在研究如何利用爬虫下载档案,有次在爬一个 Big5 编码的网站时,发生一件令我困惑的事。
该网站的回传 Header 大致如下,使用 Chrome 浏览器。
Cache-Control: privateContent-Disposition: attachment;filename=�������.csvContent-Length: 13Content-Type: application/octet-stream;charset=Big5Date: Sun, 12 Aug 2018 18:16:08 GMT
下载回来的档案名称是正确的 中文测试.csv
,不过 Content-Disposition 看到的乱码 �������.csv
无法解回中文,因此我就很好奇浏览器是如何解码的。
接着想到可以用 Fiddler 拦截封包,看看有什么线索,Fiddler 的 Headers 视窗和浏览器看到的一样,不过 Hexview 就不同了,档名变成了 �������.csv
。
看到这个我又更困惑了,这应该是另一种编码,不过试了好久也是解不回来。
且这时发现 Requset 经过 Fiddler 后,从浏览器下载回来的档案名称变成了乱码 �������.csv
,关掉 Fiddler 后又正常。
到这里我已经彻底混乱了...,换用 HttpWebRequest 来看吧...,看到的是 ¤¤¤å´ú¸Õ.csv
,我的老天鹅,试了三种方法,结果都不一样...
attachment;filename=¤¤¤å´ú¸Õ.csv
不过我感觉这个比较接近答案了,只要知道 Header 是用什么解码的,就能还原回去,Google 后发现原来 Http Header 内容必需以 ISO-8859-1
编码,马上试一下,果然被我猜对了,正确解回来了。
var fileName = "¤¤¤å´ú¸Õ.csv";//先以 ISO-8859-1 解码回 Big5 编码的位元组阵列var bytes = Encoding.GetEncoding("ISO-8859-1").GetByte(fileName);//再用 Big5 编码取出字串var str = Encoding.GetEncoding("Big5").GetString(bytes);//中文测试.csv
不过还是无法解释 Fiddler 看到的,由上面得出的结果,我猜测 HexView 看到的应该也是经过 ISO-8859-1 解码过的文字,那就将所有编码的结果列出来看看。
var fileName = "�������.csv";//先以 ISO-8859-1 编码var bytes = Encoding.GetEncoding("ISO-8859-1").GetByte(fileName);//测试所有编码var builder = new StringBuilder();foreach(var ei in Encoding.GetEncodings()){ builder.AppendFormat("xxx => {0} : {1} \r\n", ei.Name, ei.GetEncoding().GetString(bytes));}//另存文字档File.WriteAllText($"{AppDomain.CurrentDomain.BaseDirectory}test.txt", builder.ToString());
在文字档内看到了熟悉的符号 �������.csv
,看到这个符号我就知道原因了,Big5 编码的档案名称被 Fiddler 用 UTF-8 解码,然后又再用 UTF-8 编码一次,HexView 显示时再用 ISO-8859-1 解码,所以才会看到 �������.csv
。
证实一下。
var fileName = "中文测试.csv";//将档案名称以 Big5 编码var big5Bytes = Encoding.GetEncoding("Big5").GetBytes(fileName;//将 Big5 编码用 UTF-8 解码var utf8 = Encoding.GetEncoding("UTF-8").GetString(big5Bytes);//再以 UTF-8 编码一次var utf8Bytes = Encoding.GetEncoding("UTF-8").GetBytes(utf8);//以 ISO-8859-1 解码var str = Encoding.GetEncoding("ISO-8859-1").GetStrin(utf8Bytes);//�������.csv
这样就能解释为什么 Requset 经过 Fiddler 后,档案名称就会变成乱码,因为 Fiddler 用 UTF-8 错误解码,然后再编码一次传给 Chrome,不过这个动作只发生在 Header,Body 内容不受影响,我也不知道 Fiddler 为什么要这样做 ...
接着我就有个疑问,浏览器如何判断 Content-Disposition 要用 Big5 解码呢,是依据 Content-Type 吗,因此我把 Content-Type 换成 UTF-8 看看。
Content-Type: application/octet-stream;charset=utf-8
结果还是正确的中文,所以和 Content-Type 无关,浏览器应该有其他的判断方式,现在各家浏览器在处理 Content-Disposition 都有各自的做法,并没有依照标準实作。
我测试了四种浏览器:
Chrome:中文测试.csv
FireFox: 中文测试.csv
Edge: ¤¤¤å´ú¸Õ.csv
IE11: 中文测试.csv
没想到 IE11 正确 Edge 竟然乱码,不过这也能说明只有 Edge 遵循标準。
Server 端程式
public class file : IHttpHandler{ public void ProcessRequest(HttpContext context) { context.Response.ContentEncoding = Encoding.GetEncoding("Big5"); context.Response.HeaderEncoding = Encoding.GetEncoding("Big5"); context.Response.ContentType = "application/octet-stream;charset=Big5"; var data = "标题\n中文测试"; context.Response.Write(data); context.Response.AddHeader("Content-Disposition", "attachment;filename=中文测试.csv"); } public bool IsReusable { get { return false; } }}
爬虫程式
var request = (HttpWebRequest)HttpWebRequest.Create( "http://localhost:1651/file.ashx");//测试时设定 Proxy 到 Fiddler 上//request.Proxy = new WebProxy("127.0.0.1", 8888);var response = (HttpWebResponse)request.GetResponse();//取得 Content-Dispositionvar disposition = response.Headers["Content-Disposition"];var fileName = disposition.Split('=')[1];//将 filename 以 ISO-8859-1 编码var bytes = Encoding.GetEncoding("ISO-8859-1").GetBytes(fileName);//再以 Big5 解码var result = Encoding.GetEncoding("Big5").GetString(bytes);//另存档案using (var fs = new FileStream( $"{AppDomain.CurrentDomain.BaseDirectory}{result}", FileMode.Create, FileAccess.Write)){ using (var stream = response.GetResponseStream()) { stream.CopyTo(fs); }}
结语
最后觉得被 Fiddler 害得好惨,一开始测试 HttpWebRequest 时,因为有设 Proxy 到 Fiddler 上,所以看到的结果和浏览器一样,完全不知道 Fiddler 已经从中做了手脚,后来试到很晚,满脑子都是编码解码超级混乱,只好放弃去睡觉,睡醒后头脑比较清楚才找出原因,这篇虽然内容不多不过前后花了不少时间,感谢大家观看。
参考文章
正确处理下载文档时HTTP头的编码问题(Content-Disposition)
下载文件设置header的filename要用ISO8859-1编码的原因
Fiddler2中文乱码问题