在 Linux 运行了 Windows 上写的程序,出现了 Windows 平台没有出现的乱码问题,这里记录一下解决的想法与过程
1. 字符集转换前后都乱码
注: 以下环境和输出都是 utf-8
总结: Java 在获取当前系统的文件夹下所有文件时是依赖与系统实现的。
坑: 目标服务器是 zh_CN.UTF-8
,而设备通过 FTP
上传到服务器的文件名的字符集是 GBK
,这就意味在获取文件名的时候需要注意对文件名进行转码,这看似简单的一步缺暗藏两个玄机。
1-1. 字符集转换不可逆
注: 并非所有字符集的转换都不可逆
首先是第一个小坑,字符集的转换,特别是 GBK
和 UTF-8
的转换,因为他们两个对每个字的字节长度定义不同,一个是两字节的定长长度,一个是一到三字节的不定长长度(汉字一般是三字节),这就使得只有正确的解析才能将两个字符集之间的内容进行转换,一旦错误的解析(例如对字符集为 UTF-8 的内容用字符集 gbk 来进行解析,就基本上回不去了)
这个坑在理解字符集转换的原理之前可能会感到很费解,例如一开始我就知道目标文件的字符集是 GBK,所以当我发现我用 UTF-8 启动的项目拿到文件名就已经是乱码的时候,是意料之中,所以我对它进行了从 GBK 转 UTF-8 的操作,结果是乱码,仔细对比转换之前的输出发现并不是 GBK(输出到日志并用 GBK 对文件名进行解析,发现还是失败),这时候我突然发现我拿到的文件名是 GBK 用 UTF-8 的编码格式转换出来的文件名。就好比在我拿到这个文件名之前,有人对它进行了如下操作:
1 2 3 4 5 6 7 8 9 10 11 12
| public static void main(String[] args) { try { String fileNameGBK = new String("文件名".getBytes("UTF-8"), "GBK"); String resultFileName = new String(fileNameGBK.getBytes("UTF-8"), "GBK"); System.out.println("GBK:" + fileNameGBK); System.out.println("用UTF-8解析后GBK后的UTF-8:" + resultFileName); } catch (UnsupportedEncodingException e) { e.printStackTrace(); } }
|
输出结果如下:
1 2
| GBK: 鏂囦欢鍚� 用UTF-8解析后GBK后的UTF-8: 閺傚洣娆㈤崥锟�
|
当我发现到手的字符集是被处理过的时候,我马上就开始了反向转换,而这时候,我压根就没想到还有数据丢失,大致的流程如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
| public static void main(String[] args) { try { String fileNameGBK = new String("文件名".getBytes("UTF-8"), "GBK"); String resultFileName = new String(fileNameGBK.getBytes("UTF-8"), "GBK"); System.out.println("GBK: " + fileNameGBK); System.out.println("用UTF-8解析后GBK后的UTF-8: " + resultFileName); String gbkTo = new String(resultFileName.getBytes("GBK"), "UTF-8"); String utfTo = new String(gbkTo.getBytes("GBK"), "UTF-8"); System.out.println("逆向转换: " + gbkTo); System.out.println("正确解析: " + utfTo); } catch (UnsupportedEncodingException e) { e.printStackTrace(); } }
|
输出结果如下:
1 2 3 4
| GBK: 鏂囦欢鍚� 用UTF-8解析后GBK后的UTF-8: 閺傚洣娆㈤崥锟� 逆向转换: 鏂囦欢鍚�? 正确解析: 文件�??
|
这里我们可以看到,当进行逆向的转换时,乱码后面出现了一个?号,然后再解析成 utf-8 的时候,不可避免的丢失了最后一个字符,但是这不是绝对的,如果我们把一开始的 文件名
改为 文1件2名3
,这时候就是毁灭性的了,这个时候的输出结果如下:
1 2 3 4
| GBK: 鏂�1浠�2鍚�3 用UTF-8解析后GBK后的UTF-8: 閺傦拷1娴狅拷2閸氾拷3 逆向转换: 鏂�1浠�2鍚�3 正确解析: �?1�?2�?3
|
可以看到的是,当之后中文的时候,只是丢失了最后一个汉字,但是当这段汉字里面夹杂着数字或者字母的时候,毁灭性的一幕就出现了,全部汉字都变成了乱码,真的没救了。
当然,造成这种结果的原因很简单,就在于 GBK 和 UTF-8 的编码格式不一样
GBK 是定长的两字节长度,而 UTF-8 为了节省空间,是不定长的,数字和字母只需要一个字节长度就能装下,但是汉字需要三个字节长度,所以当一个汉字和一个数字一前一后放在一起的时候,GBK 第四个字节的内容里面放的是数字和字母,前面使用一个固定的内容作为标识,所以 utf-8 在解析第四个字节的时候能够成功解析为数字,但是前三个字节就会被解析成两个乱码。并且因为汉字的值在 GBK 里面也是比较大, utf-8 需要把自己扩充为三个字节的长度来适应汉字,所以整个的内容就全变了。
我们不妨把从前到后的四个字段都转成 byte 数组,那么就会明显上很多
我们先转换最初的两个,代码如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22
| public static void main(String[] args) { try { String fileNameGBK = new String("文1件2名3".getBytes("UTF-8"), "GBK"); String resultFileName = new String(fileNameGBK.getBytes("UTF-8"), "GBK"); System.out.println("GBK:" + fileNameGBK); System.out.println("用UTF-8解析后GBK后的UTF-8:" + resultFileName); byte[] fileNameGBKBytes = fileNameGBK.getBytes("GBK"); byte[] resultFileNameBytes = resultFileName.getBytes("GBK"); for(byte b : fileNameGBKBytes) { System.out.print("| " + b + " "); } System.out.println("|"); for (byte b : resultFileNameBytes) { System.out.print("| " + b + " "); } System.out.println("|"); } catch (UnsupportedEncodingException e) { e.printStackTrace(); } }
|
输出结果如下:
1 2 3 4
| GBK: 鏂�1浠�1鍚�1 用UTF-8解析后GBK后的UTF-8: 閺傦拷1娴狅拷1閸氾拷1 | -26 | -106 | 63 | 49 | -28 | -69 | 63 | 50 | -27 | -112 | 63 | 51 | | -23 | -113 | -126 | -17 | -65 | -67 | 49 | -26 | -75 | -96 | -17 | -65 | -67 | 50 | -23 | -115 | -102 | -17 | -65 | -67 | 51 |
|
这里虽然 GBK 的输出看起来是有9个字,但是这个控制台也是 utf-8,所以这个乱码是已经没救的乱码
通过观察第一行 byte[] 可知 gbk 的三个字的确是六个字节的长度,并且可以发现,63|49 和 63|50 和 63|51 分别代表了1、2、3
而第二行长达21个字节长度的乱码,其实用 gbk 已经无法解析了,但是用 utf-8 却可以解析,除了单个字节就能代表的49、50、51分别指代了1、2、3,其他在utf-8里面都是三个字节作为一个乱码存在,所以这里应该是18个字节的汉字,共六个,三个字节的数字,共三个,一共9个字,刚好和 gbk 输出在控制台的乱码一样。
所以我们可以发现这时候如果想转回去,凭空多出来三个字,并且这三个字都是 -17|-65|-67 ,也都恰好在数字的前面。
所以基本上这三个汉字都已经救不回来了。
这里把恢复的过程也放出来,这样就简单明了多了,不过还是要懂得字符集相关的知识理解起来会快上很多。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38
| public static void main(String[] args) { try { String fileNameGBK = new String("文1件2名3".getBytes("UTF-8"), "GBK"); String resultFileName = new String(fileNameGBK.getBytes("UTF-8"), "GBK"); System.out.println("GBK: " + fileNameGBK); System.out.println("用UTF-8解析后GBK后的UTF-8: " + resultFileName); String gbkTo = new String(resultFileName.getBytes("GBK"), "UTF-8"); String utfTo = new String(gbkTo.getBytes("GBK"), "UTF-8"); System.out.println("逆向转换: " + gbkTo); System.out.println("正确解析: " + utfTo); byte[] fileNameGBKBytes = fileNameGBK.getBytes("GBK"); byte[] resultFileNameBytes = resultFileName.getBytes("GBK"); byte[] gbkToBytes = gbkTo.getBytes("UTF-8"); byte[] utfToBytes = utfTo.getBytes("UTF-8"); for(byte b : fileNameGBKBytes) { System.out.print("| " + b + " "); } System.out.println("|"); for (byte b : resultFileNameBytes) { System.out.print("| " + b + " "); } System.out.println("|"); for (byte b : gbkToBytes) { System.out.print("| " + b + " "); } System.out.println("|"); for (byte b : utfToBytes) { System.out.print("| " + b + " "); } System.out.println("|"); } catch (UnsupportedEncodingException e) { e.printStackTrace(); } }
|
输出结果如下:
1 2 3 4 5 6 7 8
| GBK: 鏂�1浠�2鍚�3 用UTF-8解析后GBK后的UTF-8: 閺傦拷1娴狅拷2閸氾拷3 逆向转换: 鏂�1浠�2鍚�3 正确解析: �?1�?2�?3 | -26 | -106 | 63 | 49 | -28 | -69 | 63 | 50 | -27 | -112 | 63 | 51 | | -23 | -113 | -126 | -17 | -65 | -67 | 49 | -26 | -75 | -96 | -17 | -65 | -67 | 50 | -23 | -115 | -102 | -17 | -65 | -67 | 51 | | -23 | -113 | -126 | -17 | -65 | -67 | 49 | -26 | -75 | -96 | -17 | -65 | -67 | 50 | -23 | -115 | -102 | -17 | -65 | -67 | 51 | | -17 | -65 | -67 | 63 | 49 | -17 | -65 | -67 | 63 | 50 | -17 | -65 | -67 | 63 | 51 |
|
可以看到在最后的时候,出现了一模一样的乱码,这种情况是因为 utf-8 里面的乱码在 gbk 里面并没有对应的,所以出现了这个乱码,所以我们基本上可以确定的是,一旦第一步错了,后面就基本没办法修复数据了。
所以在拿到数据发现不是 GBK 的时候,应该反应过来不要尝试反向出来,应该想办法解决拿到就是被处理过的数据的这个问题。
1-2. 系统提供的文件信息
我最开始是使用 Java 原生提供的获取文件目录的方法 java.io.File.getFiles()
,它返回了一个文件数组,但是,这个时候的文件名已经是不可修复的乱码了,所以我们得解决这个问题。
首先我们得知道为什么拿到手的数据就已经是乱码了
这个是因为 Java 获取文件目录并不是虚拟机 jvm 去读取的,所以不管你怎么修改项目的默认字符集,jvm启动时指定任何的字符集,在获取文件列表这个行为上,是 jvm 访问系统,拿到系统返回的数组,这应该是用 Java 自带的 dll 或者是 so 提供的功能,而他们统一使用当前系统的字符集
所以这个时候应该修改当前系统的字符集,这里我是用的是 CentOS7,所以只需要修改 /etc/locale.conf
,将 LANG
这一项改为 zh_SG.gbk
就行了,大致内容如下:
1 2
| #LANG="zh_CN.utf-8" LANG="zh_SG.gbk"
|
再 souce 一下配置文件就可以立即生效了,未生效也可以先退出 ssh 重新登录。
当然,最好先使用 locale -a
查看一下当前系统支持的语言,不支持可以先安装后修改。