告别乱码,针对GBK、UTF-8两种编码的智能URL解码器的java实现(转)

简介: 效果图     字符 字符是早于计算机而存在,从人类有文明那时起,人们就用一个个符号代表世间万象。如ABC,如“一、二、三”。 字符集 字符集是所有字符的集合。   XXX字符集 给字符集中的每一个字符套上一个序号后的字符集。

效果图


 

 

字符


字符是早于计算机而存在,从人类有文明那时起,人们就用一个个符号代表世间万象。如ABC,如“一、二、三”。

字符集


字符集是所有字符的集合。

 

XXX字符集


给字符集中的每一个字符套上一个序号后的字符集。常见的XXX字符集有ASCLL字符集、Unicode字符集等等,不同种字符集为每个字符编的序号不同,包含的字符数量也不同。

GBK、UTF-8


  GBK、UTF-8是一种编码编码格式。当然,你也可以说unicode是一种编码格式,因为它的的确确为每个字符编了一个码,没错,可是unicode的编码完全没有规律,最多只能把其当映射表用。

  我们知道,计算机只能识别1和0,假如计算机存储中文字符“字”在硬盘,肯定是存储一串二进制串。

  那么问题来了,中文字符【字】在unicode字符集中的序号是23383,那么直接把23383转化成2进制为101101101010111,然后存储在计算机里面,等需要的时候把101101101010111串拿出来,转成23383,再根据unicode映射表,找到中文字符【字】不就好吗?

  答案是否定的,如果是这样的话,那计算机怎么知道多少个1、0才代表一个字符呢?所以我们需要一种编码格式,把23383编码成有规律的1、0串,以便计算机读取。

  而GBK和UTF-8便是两种不同的有规则的编码格式。

  例如:以UTF-8为例子,假如我们所在的环境使用的是unicode字符集,那么“字”在unicode字符集中的序号是23383,转成二进制是101101101010111,使用UTF-8为其编码,以一种特定的算法(下面会具体讲这种算法),把101101101010111转化成11100101 10101101 10010111三个字节的二进制串,再存储到硬盘中,计算机在读取的时候,假如我们指定了让计算机以UTF-8编码格式读取并解码,计算机就会把这三个字节拿出来,倒着转回去,就能得到【字】这个中文字符了。

乱码的根源:

假如我们存储的时候,使用GBK编码格式编码,存储到硬盘,而从硬盘读取出来后,在“倒着转回去”这个步骤却使用UTF-8编码格式转回去,算法不同,那么就可能出现乱码。

如何避免乱码:

以什么编码格式存储,就用什么编码格式解

但是,假如用户A使用GBK编码对“字”进行编码,而用户B并不知情,也没A的联系方式,跟A约定不了,无法得知硬盘中的数据是以什么编码格式编码的,怎么办呢?

解决乱码的思路:

1、随意使用一种编码格式解码,看解码后的字符串是否乱码,如果是乱码,就用另一种编码格式解码。但该方法可能误判。

2、UTF-8编码格式有一定的规律,我们可以通过正则表达式来验证是否是经过UTF-8编码后的。

 

JAVA自带检测乱码


 

1  boolean b = java.nio.charset.Charset.forName("GBK").newEncoder().canEncode(str);

 

当开始接触这种方法时,原以为java能帮我们判断乱码,就可以高枕无忧了,后来发现,该方法的成功率并不高。

但我们可以先用此方法做第一步检测,如果判断不出来,再使用第2种方法。

 

UTF-8的编码规律


UTF-8形式的二进制,当一个字节时,两个字节时,三、四、五、六个字节时,都有一定的格式:

1字节 0xxxxxxx
2字节 110xxxxx 10xxxxxx
3字节 1110xxxx 10xxxxxx 10xxxxxx
4字节 11110xxx 10xxxxxx 10xxxxxx 10xxxxxx
5字节 111110xx 10xxxxxx 10xxxxxx 10xxxxxx 10xxxxxx
6字节 111111x0 10xxxxxx 10xxxxxx 10xxxxxx 10xxxxxx 10xxxxxx

很明显,字节数不一样的话,第一个字节是不同的,所以第一个字节可用用来表示该字符究竟占用了多少个字节。

当计算机读取到以0xxxxxxx开头的字节,那么就代表这个字节独自就已经表示某个字符了,计算机将把这个字节单独拿出来解码。

当计算机读取到以110xxxxx开头的字节,那么就代表两个字节才能表示某个字符,计算机就把这个字节以及它后面的一个字节拿出来,代表一个字符进行解码。

……

而除了第一个字节外,后面的字节都是统一的10xxxxxx格式。

有了上面的有规则的格式,按到理我们就可以使用正则表达式来检测一个二进制串是否是UTF-8编码后的串,但代码中操作二进制并不方便,结合URL为16进制的特点,我们可以用正则表达式判断16进制的串。

 

如何构造正则表达式


 

我们先看看这种编码格式前一个字节的范围:

  二进制 十六进制
1字节 00000000~01111111 00~7f  
2字节 11000000~11011111 c0~df  
3字节 11100000~11101111   e0~df
4字节 11110000~11110111 f0~f7
5字节 11111000~11111011 f8~fb
6字节 11111100~11111101 fc~fd

以上的范围可用计算机自行验证:

后面格式相同的字节10xxxxxx的范围:

10000000~10111111   80~bf

按照这种格式,UTF-8编码格式最多可用用来表示一个1+5*6=31位的二进制串,共使用6个字节。

按照这种规律,我们先练一下手,尝试把“字”转化为UTF-8的十六进制:

java使用的字符集是unicode的,所以我们以unicode为例子。

1、找出“字”在unicdoe字符集中的序号:

?
1
2
3
public static void main(String[] args) {
System.out.println(( int ) '字' );
}

结果为:23383

2、把23383转化二进制:  

23383   101101101010111

可用看出,二进制共15位,按照UTF-8的编码格式,得用3个字节来表示。

我们把101101101010111从后往前分成三组:101,101101,010111

填充到3字节的UTF-8编码格式中为:

1110xxxx 10xxxxxx 10xxxxxx

11100101 10101101 10010111

3、使用计算器把二进制转化为16进制为:

OxE5 OxAD Ox97

4、使用网上的工具验证一下,结果吻合,说明这种规律是正确的。

 

 

上面已经介绍了UTF-8的规律,那么我们借助强大的正则表达式,就可以判断一个URL串是经过什么编码格式编码的了。

先把上面的表复制下来容易观察:

  二进制 十六进制
1字节 00000000~01111111 00~7f  
2字节 11000000~11011111 c0~df  
3字节 11100000~11101111   e0~df
4字节 11110000~11110111 f0~f7
5字节 11111000~11111011 f8~fb
6字节 11111100~11111101 fc~fd

1字节时:[\\x00-\\x7f]---------------------------------1

2字节时:[\\xc0-\\xdf][\\x80-\\xbf]-------------------2

3字节时:[\\xe0-\\xef][\\x80-\\xbf]{2}--------------3

4字节时:[\\xf0-\\xf7][\\x80-\\xbf]{3}--------------4

5字节时:[\\xf8-\\xfb][\\x80-\\xbf]{4}--------------5

6字节时:[\\xfc-\\xfd][\\x80-\\xbf]{5}--------------6

使用或组合在一起就是:^([\\x00-\\x7f]|[\\xc0-\\xdf][\\x80-\\xbf]|[\\xe0-\\xef][\\x80-\\xbf]{2}|[\\xf0-\\xf7][\\x80-\\xbf]{3}|[\\xf8-\\xfb][\\x80-\\xbf]{4}|[\\xfc-\\xfd][\\x80-\\xbf]{5})+$

判断过程是这样子的:例如【字】经过UTF-8编码后,为:%e5 %ad %97,共3个字节,符合第3字节的情况,第一个字节e5在[\\xe0-\\xef]范围内,后两个字节ad和97都在[\\x80-\\xbf]范围内。

所以我们可以说这个字符是经过UTF-8编码的。我们就可以使用UTF-8编码格式对其进行解码了。

java代码如下:

复制代码
1     protected static final Pattern utf8Pattern = Pattern.compile("^([\\x00-\\x7f]|[\\xc0-\\xdf][\\x80-\\xbf]|[\\xe0-\\xef][\\x80-\\xbf]{2}|[\\xf0-\\xf7][\\x80-\\xbf]{3}|[\\xf8-\\xfb][\\x80-\\xbf]{4}|[\\xfc-\\xfd][\\x80-\\xbf]{5})+$");
2                 Matcher matcher = utf8Pattern.matcher(pureValue);
3                 if (matcher.matches()) {
4                     return "UTF-8";
5                 } else {
6                     return "GBK";
7                 }
复制代码

 

缺陷


  使用上面的方法,貌似没什么问题,不过GBK编码后是以两个两个字节呈现的,而UTF-8也有两个字节的情况,所以当一个字符经GBK编码后,转化为16进制,而刚好这个16进制的范围落入UTF-8的两个字节的范围,那么就会被误判成UTF-8,从而导致解码错误。那真的有可能会出现这种情况吗?

答案是会的,我们查看下GBK简体中文编码表

发现有一部分范围落入了UTF-8的二进制范围了。

从:

一直到:

即UTF-8十六进制中两个字节的范围[\\xc0-\\xdf][\\x80-\\xbf],GBK都有。

例如上面表的第二个中文【愧】,愧的GBK十六进制是C0 A0,那么完全符合UTF-8正则表达式中二字节的[\\xc0-\\xdf][\\x80-\\xbf]这个判断,所以会被误认为是UTF-8编码。

注:该缺陷第一次看,是在下方“参考"的第一篇博客里,尝试了一下,的确有缺陷。

 

尝试修复缺陷


根据下面"参考"的第一篇博客,修复的思路是把重复的区域都认为是GBK编码。

我们截取正则表达式的前两种情况(一字节、二字节的情况)来排除:^([\\x01-\\x7f]|[\\xc0-\\xdf][\\x80-\\xbf])+$

假如某个16进制串match该正则表达式,就认为是GBK编码的。

修改后的代码为:

复制代码
 1     protected static final Pattern utf8Pattern = Pattern.compile("^([\\x01-\\x7f]|[\\xc0-\\xdf][\\x80-\\xbf]|[\\xe0-\\xef][\\x80-\\xbf]{2}|[\\xf0-\\xf7][\\x80-\\xbf]{3}|[\\xf8-\\xfb][\\x80-\\xbf]{4}|[\\xfc-\\xfd][\\x80-\\xbf]{5})+$");
 2     protected static final Pattern publicPattern = Pattern.compile("^([\\x01-\\x7f]|[\\xc0-\\xdf][\\x80-\\xbf])+$");
 3 Matcher publicMatcher = publicPattern.matcher(str);
 4                 if(publicMatcher.matches()) {
 5                     return "GBK";
 6                 }
 7                 
 8                 Matcher matcher = utf8Pattern.matcher(str);
 9                 if (matcher.matches()) {
10                     return "UTF-8";
11                 } else {
12                     return "GBK";
13                 }
复制代码

 

又一缺陷


但这样一来,原本是一个字节或两字节,且是UTF-8编码的,就会被误判为GBK。。。

但是,这总比被误判成UTF-8好,因为我们查看Unicode编码表

可以发现,第一个中文是“一”,转化为UTF-8的话已经排到3个字节去了,所以2个字节内不会出现中文。

但是GBK中,中文是两个字节的。

所以,采用上面的修复缺陷的方法,可以保证中文不会乱码。对于某些网站,只需保证中文不会乱码即可,比如说国内的各种中文购物网站。这些网站中商品的标题一般都是中文的,用户一般以中文搜索,我们尽可能保证中文不乱码即可。

所以,该技术还是有一定用处的。

 

参考


1、http://www.cnblogs.com/chengmo/archive/2011/02/19/1958657.html

2、http://www.cnblogs.com/chengmo/archive/2010/10/30/1864004.html

3、unicode编码表

4、GBK简体中文表

http://www.cnblogs.com/xiaoMzjm/p/4648175.html

相关文章
|
1月前
|
Java
【实战演练】JAVA网络编程高手养成记:URL与URLConnection的实战技巧,一学就会!
【6月更文挑战第22天】在Java网络编程中,理解和运用URL与URLConnection是关键。URL代表统一资源定位符,用于标识网络资源;URLConnection则用于建立与URL指定资源的连接。通过构造URL对象并调用openConnection()可创建URLConnection。示例展示了如何发送GET请求读取响应,以及如何设置POST请求以发送数据。GET将参数置于URL,POST将参数置于请求体。练习这些基本操作有助于提升网络编程技能。
|
1月前
|
数据采集 Java 开发者
JAVA网络编程深度探索:URL与URLConnection的精湛技艺
Java网络编程核心在于URL和URLConnection。URL是资源的唯一标识,用于定位网络资源,支持解析、编码解码及参数操作。URLConnection则实现数据交换,允许GET/POST请求,可定制请求头、设置超时,是网络交互的关键。两者结合,适用于网络爬虫等场景,深入学习能提升编程技巧并揭示网络编程奥秘。
|
1月前
|
XML JSON 搜索推荐
【高手过招】JAVA网络编程对决:URL与URLConnection的高级玩法,你敢挑战吗?
【6月更文挑战第22天】在Java网络编程中,URL与URLConnection是核心工具,高手利用它们进行高级操作。从定制请求头(如User-Agent和Authorization)以适应不同场景,到利用POST请求发送复杂数据,甚至是通过设置代理(HTTP或SOCKS)穿越网络障碍,以及运用异步处理和流操作提升效率,每个技巧都是提升网络交互的关键。通过深入学习和实践,开发者可以在网络编程领域不断提升,应对各种挑战。
|
1月前
|
Java
【思维导图】JAVA网络编程思维升级:URL与URLConnection的逻辑梳理,助你一臂之力!
【6月更文挑战第22天】Java网络编程中,URL是资源定位器,用于解析和创建网络地址;URLConnection接口负责建立到URL资源的连接。示例展示了如何使用URL类获取协议、主机、端口和路径,以及如何通过HttpURLConnection进行GET/POST请求,设置超时并处理响应。思维导图概述了从创建URL到设置请求属性、发送请求及处理响应的完整流程,帮助理解两者在网络编程中的作用。
|
1月前
|
缓存 安全 Java
【技术前沿】JAVA网络编程黑科技:URL与URLConnection的创新应用,带你飞越极限!
【6月更文挑战第22天】Java的URL和URLConnection在现代网络编程中扮演关键角色,不仅用于基本HTTP请求,还在微服务(弹性自动化调用)、智能缓存策略、异步处理和安全增强方面展现创新应用。例如,它们支持动态服务发现、HTTP缓存控制、非阻塞I/O和HTTPS加密,助力开发者构建高效、安全的网络解决方案。通过掌握这些技术,可以提升项目性能,应对云计算和大数据时代的挑战。
|
1月前
|
安全 Java 网络安全
【认知革命】JAVA网络编程新视角:重新定义URL与URLConnection,让网络资源触手可及!
【6月更文挑战第22天】JAVA网络编程中,URL代表统一资源定位符,用于表示网络资源地址。通过`new URL("address")`创建URL对象,可解析和访问其组件。URLConnection是与URL建立连接的接口,用于定制HTTP请求,如设置GET/POST、超时及交换数据。
|
25天前
|
JavaScript 前端开发 数据格式
URL编码【详解】——Javascript对URL进行编码解码的三种方式的区别和使用场景,axios请求拦截器中对get请求的参数全部进行URL编码
URL编码【详解】——Javascript对URL进行编码解码的三种方式的区别和使用场景,axios请求拦截器中对get请求的参数全部进行URL编码
22 0
|
28天前
|
安全 JavaScript PHP
URL百分号编码
URL百分号编码
|
1月前
|
安全 Java API
告别文件操作小白,Java大神手把手教你读写、复制、删除!
【6月更文挑战第27天】在Java中进行文件操作是基础且重要的。学习如何读取文件,如使用`BufferedReader`配`FileReader`,写入文件如借助`BufferedWriter`与`FileWriter`,复制文件利用`java.nio.file.Files.copy()`,以及删除文件通过`Files.delete()`,都是提升编程效率的关键。这些示例展示了简洁且安全的文件处理方式。
|
1月前
|
SQL 安全 Java
Java中的安全编码实践
Java中的安全编码实践