一个讨厌的编码问题(续)

昨天说了编码问题,也找到了正确的打包方法。但有个问题,许多用修改前的代码打的zip包,如何修复?

最直观的方法,是先解压,再重新打包,代码大致是:

                        string folderName = "";
                        using (ZipFile zip = ZipFile.Read("foo.zip"))
                        {
                              foreach (ZipEntry entry in zip)
                             {
                                 if (entry.IsDirectory)
                                 {
                                      folderName = entry.FileName.Replace("/", "");
                                 }
                                 entry.Extract(rootFolder); 
                             }
                        }
                        byte[] bytes = Encoding.GetEncoding("gb2312").GetBytes(folderName.ToCharArray());
                        var newName = Encoding.GetEncoding("IBM437").GetString(bytes);                        
                         using (ZipFile zip = new ZipFile(Encoding.UTF8))
                        {
                            zip.CompressionMethod = Ionic.Zip.CompressionMethod.Deflate;
                            zip.AlternateEncoding = Encoding.GetEncoding("IBM437");
                            var entry = zip.AddDirectory(Path.Combine(rootFolder, folderName), newName);
                            zip.Save(Path.Combine(rootFolder, folderName + ".uvz"));
                        }                                       

这样确实能行,但是因为解压,重压大量的IO操作,性能比较差。发现如果用7-zip打开有问题的zip包,然后修改一下里面的文件夹名,就能用UnicornViewer打开了。那么是不是有可能不重新压缩,而通过改名的方法,避免IO操作(改名的本质大概是先解压到内存,然后重新压缩,不操作磁盘。这只是猜想,懒得去看代码验证。),而达到同样的效果呢?

DotNetZip允许修改已有zip包里的文件名,所以开始写了如下的代码:

                        using (ZipFile zip = ZipFile.Read("foo.uvz"))
                        {
                              var list = zip.ToList();//因为要修改zipentry的属性,用foreach会出错,所以这里先转成list后再遍历
                              for (int i = 0; i < list.Count; i++)
                             {
                                 string oldName = list[i].FileName;
                                 byte[] bytes = Encoding.GetEncoding("gb2312").GetBytes(oldName.ToCharArray());
                                 var newName = Encoding.GetEncoding("IBM437").GetString(bytes);
                                 list[i].FileName = newName;
                                 list[i].AlternateEncoding = Encoding.GetEncoding("IBM437");
                              }
                              zip.Save();
                        }

修改后,果然能用UnicornViewer打开了,但是有个问题,如果用7-zip打开这个修改后的zip包,发现里面的文件夹名是乱码。

试了不少方法都没用,最后还是老办法,把7-zip打包的zip和代码打包的zip分别读出来,然后比较zipentry的属性,终于发现了问题。7-zip打包的zip,zipEntry的BitField属性是0,而代码打包的zip,这个属性是2048。但是试图用代码修改BitField时,却提示是个只读属性,不可修改。

查了Dotnetzip的文档,这样说:

The bitfield for the entry as defined in the zip spec. You probably never need to look at this.

.........

You probably do not need to concern yourself with the contents of this property, but in case you do:

下面解释了每个位的意义,其中和这里的问题有关的是第11位:

11 Language encoding flag (EFS). If this bit is set, the filename and comment fields for this file must be encoded using UTF-8. This library currently does not support UTF-8.

估计就是这个在作怪。属性只读怎么办?好在Dotnetzip是开源的,从Github上找到源码,下载后,找到相关代码,把只读修改为可写(原来只有get,加个set,搞过.net的应该很好理解),然后重新编译。编译时还出了点小问题。首先是提示找不到密钥,这好办,修改项目属性,去掉“签名”一项就可以了。然后说某个vbs执行出错。查了下,原来prebuild里定义了一个vbs(VBScript代码文件),找到这个vbs,手工执行,提示找不到目录什么的,修改路径后成功执行。既然vbs已经执行了,就把prebuild里的命令行去掉,重新编译成功。然后在自己的项目里重新引用这个新的Ionic.Zip.dll,并将上面的代码加了一句,修改成:

                        using (ZipFile zip = ZipFile.Read("foo.uvz"))
                        {
                              var list = zip.ToList();//因为要修改zipentry的属性,用foreach会出错,所以这里先转成list后再遍历
                              for (int i = 0; i < list.Count; i++)
                             {
                                 string oldName = list[i].FileName;
                                 byte[] bytes = Encoding.GetEncoding("gb2312").GetBytes(oldName.ToCharArray());
                                 var newName = Encoding.GetEncoding("IBM437").GetString(bytes);
                                 list[i].FileName = newName;
                                 list[i].AlternateEncoding = Encoding.GetEncoding("IBM437");
                                 list[i].BitField = 0;
                              }
                              zip.Save();
                        }

重试通过,果然性能大大提高。

写这个程序主要的收获是对乱码多了点理解。所谓乱码,就是编码和解码用了不同的Encoding方式。所以要找到原来的Encoding和正确的解码Encoding,然后用上面的转换,就可以了。比如对于乱码"╩└╜τ╡┌╥╗╬╗┴∙╣┌═⌡╒╘╣·╚┘╩╡╒╜╫¿╝¡",可以用下面的代码得到正确的中文字符:

string strWrong = "╩└╜τ╡┌╥╗╬╗┴∙╣┌═⌡╒╘╣·╚┘╩╡╒╜╫¿╝¡";
byte[] bytes = Encoding.GetEncoding("IBM437").GetBytes(strWrong);
string strRight = Encoding.GetEncoding("gb2312").GetString(bytes);

问题在于,难以知道原来是用什么编码的,又应该用什么去解码。就象stackoverflow上说的:

Notice: as already pointed out "determine encoding" makes sense only for byte streams. If you have a string it is already encoded from someone along the way who already knew or guessed the encoding to get the string in the first place. (https://stackoverflow.com/questions/1025332/determine-a-strings-encoding-in-c-sharp)

也就是说只有对字节流才能检查编码。如果已经编码好了,比如上面的代码片断,那只有一个个Encoding地试了。当然,人比机器智能,不需要遍历所有的编码组合,而试几个可能的编码方式就行了。比如中文乱码,解码一般是gb2312/gbk,big5,hz,utf-8什么的,编码多是IBM437,IBM852什么的。

posted @ 2020-01-02 01:12  平静寄居者  阅读(212)  评论(0编辑  收藏  举报