先说编码
1.为什么需要编码
- 计算机是英语国家做出来的玩意儿,所以它只能认识英文字母符号,阿拉伯数字等,其他所有的语言要想在计算机中显示就必须进行编码,翻译。而计算机中表示数据信息的最小单元为一个字节,即8个bit,可以表示0~255个字符,世界上的各种语言千奇百怪,需要多个字节来表示,多个字节表示的数据要存储在计算机中或者要在网络间传输必须转化为计算机认识的byte,如何转换,那就是编码。
2.常见的编码
- ascii码
只占用一个字节,第一位为0,用低7位来编码。
- ISO-88591系列(ISO-88591-1 到ISO-88591-15)
这玩意儿也只占用一个字节,欧洲一些国家的字符用ascii没法表示出来,所以把ascii编码的首位也用来编码。
- GB2312
可以表示汉字的编码,两个字节(兼容ascii码单字节形式),能表示6000多个汉字(区位码,高位字节存当前汉字在哪个区,低位字节存在区中第几位),相对算是挺少的。。。
其实在GB2312编码里,并不是所有的字符都会用两个字节来表示的。为了能清晰说明这个这个问题,我用二进制编码来解释一下。 首先,ASCII编码虽然说是用一个字节来表示字符,但是它其实只用了后7位,第1位永远是0。它的编码范围,从00000000到01111111,都是以0开头的。 而GB2312编码,就是在ASCII编码的基础上进行扩充的,它规定了:ASCII的字符完整地包含在GB2312里,编码不变,仍然是以0开头,用一个字节来表示一个字符;对于ASCII没有的字符,就用1开头来区分,用两个字节合起来表示一个字符。 这样,在解码的时候,遇到字节是以0开头的,就知道这一个字节就表示了一个字符;遇到字节是以1开头的,就知道要加上下一个字节合起来表示一个字符。这样就在GB2312中既把ASCII的字符包含了进来,又能将它们区分出来,能达到兼容的效果了。
- GBK
也是两个字节,但是可以表示的汉字有几万个,完全是在gb2312的基础上进行扩展,完全兼容GB2312编码,所以基本可以忘记有GB2312这个编码,直接使用GBK替代他就好。
tips:其实gbk和GB2312也算是变长编码,根据码表查询到码位置大小来进行判断用一个字节还是双字节。
=====================
以下两个unicode编码的实现..
- UTF-16
tips:最最最高效的编码
他的编码理念是常见的普通字符都用两个字节存储,而且不需要查码表什么的,因为unicode规范里,世界上任何一个符号,字符都有一个唯一编码对应,所以utf-16直接将对应的字符的unicode编码分别放在两个字节里即可,单字节就能表示的字符它以高位补0方式存储。(以不同的处理器可能会以高位在前或者高位在后的方式解析,所以编码解码时需要指定到底是高位在前还是低位在前)
对于UCS-4辅助平面内的字符,采用四字节存储:
Unicode码位值为2AEAB,减去0x10000得到1AEAB(二进制值为0001 1010 1110 1010 1011),前10位加上D800得到D86B,后10位加上DC00得到DEAB。于是该字的UTF-16编码值为D86BDEAB(该值为大端表示,小端为6BD8ABDE)。
- UTF-8
任何一个程序员都知道的屌屌的编码,它对utf-16编码的优化:
1.变长编码,高位为0,表示它是一个ascii编码字符(ascii编码本身就是首位为0,所以utf-8完全兼容ascii编码),大于一个字节的编码,会在首位以连续的1的个数来标识是几个字节。后面字节的前两位一律设为10。
2.变长带来的好处:相对节约空间,安全(utf-16是每两个挨着的字节表示一个字符,如果在网络上传输时丢失了某个字节,那么该字节后面的所有编码将会全部乱掉,而utf-8编码将不会应用后续的编码)
uftf-8编码示意:
Unicode符号范围 | UTF-8编码方式
(十六进制) | (二进制)
--------------------+---------------------------------------------
0000 0000-0000 007F | 0xxxxxxx
0000 0080-0000 07FF | 110xxxxx 10xxxxxx
0000 0800-0000 FFFF | 1110xxxx 10xxxxxx 10xxxxxx
0001 0000-0010 FFFF | 11110xxx 10xxxxxx 10xxxxxx 10xxxxxx
3.编码选择
- 首先,中文不能使用ascii或者iso系列,单字节编码不可以表示中文
- 其次,gbk和gb2312必须选gbk,不用多说(现在已经流行GB18030了)
- utf-8和utf-16之间:java虚拟机使用utf-16进行编码,追求编码解码速度(字节固定好查找),字节只在机器本地的磁盘和内存之间流动,不走网络,所以也不会丢失。 在web开发中utf-8是最佳最佳选择,空间小,容错率高~~~
4.深入javaweb书上提到的关键点
看一段字符到底会占多少内存,不是看string.length有多长,而是要看其用的什么编码,(作者提到他在做项目时对cookie做过各种压缩,但是最终并木有任何成效)
java的string使用的编码是unicode,但是,当string存在于内存中时(也就是当程序运行时、你在代码中用string类型的引用对它进行操作时、也就是string没有被存在文件中且也没有在网络中传输(序列化)时),是“只有编码而没有编码格式的”,所以java程序中的任何String对象,说它是gbk还是utf-8都是错的,gbk和utf-8是编码格式而不是编码,String在内存中不需要“编码格式”(记住编码格式是在存文件或序列化的时候使用的), 它只是一个unicode的字符串而已
作者:温悦
链接:https://www.zhihu.com/question/20361462/answer/14899233
来源:知乎
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
5.书中的其他知识点(与本文的东西无关)
http请求的编码:
- 首先,urlPath路径上的字符一般是通过utf-8编码,而querystring中则是使用gbk编码(为什么url中的中文编码后会有一大堆%,因为url编码规范规定转换为16进制表示后需要在每个16进制位前加上%),<b>浏览器对url中path和query的编码不一样</b>
- tomcat对path的编码定义<Connector URIEncoding="UTF-8"/>这里不定义将默认采用iso编码。
- 对于query的编码解码,是在后端代码中调用request.getParameter("xx")时进行的。解码时会先查看http header的contenttype重定义的charset(需要在tomcat重配置<Connector URIEncoding="UTF-8" useBodyEncodingForURI="true"/>)
tips:<b>在平时的开发中,尽量避免在uri或者path中传中文,很容易乱码,因为不同的浏览器编码规则不一样,后端开发者没办法完全掌控</b> - 对于header的编码解码,没地方可以配置,所以http header不支持传入中文,一定要传入中文需要先编码。解码触发时机为request.getHeader("xx"),解码方式固定以:iso
- 对于http postbody内容,浏览器端会先根据contentType设置的charset来进行编码,服务端接收到数据也将会以contentType中的charset进行解码。
utf-8再稍微多说一点
utf-8:https://en.wikipedia.org/wiki/UTF-8#Description
从上个小节得出结论,utf-8这种变长的表示方式其实可以表示8个字节以内的大小的编码(因为它是以首字母的连续1的个数来判断是几个字节的),其实utf-8在目前的最大可表示字节数为4.对应如下:
数据库的字符集utf8与utf8mb4
使用show variables like ‘%character%’;
可以查看mysql当前的字符集。
utf8:标准的 UTF-8 字符集编码是可以用 1~4 个字节去编码21位字符,这几乎包含了是世界上所有能看见的语言了。然而在MySQL里实现的utf8最长使用3个字节,也就是只支持到了 Unicode 中的U+0000至U+FFFF),包含了控制符、拉丁文,中、日、韩等绝大多数国际字符。重要的事情说三遍:<b>特智能表示3个字节以内的编码字符</b>,<b>特智能表示3个字节以内的编码字符</b>,<b>特智能表示3个字节以内的编码字符</b>
utf8mb4:新版本的mysql才支持,简单说 utf8mb4 是 utf8 的超集并完全兼容utf8,能够用四个字节存储更多的字符。(当然这样空间会比较浪费一点)
如果当前db的字符集为utf8,插入的数据为4个字节编码时,将会抛出:incorrect string value
异常
nested exception is org.apache.ibatis.exceptions.PersistenceException: \n### Error updating database. Cause: ERR-CODE: [TDDL-4601][ERR_EXECUTOR] Incorrect string value: '\\xF2\\xBC\\xAF\\xBA\\xEF\\xBF...' for column 'config_value' at row 1 More: [http:\/\/middleware.alibaba-inc.com\/faq\/faqByFaqCode.html?faqCode=TDDL-4601]\n### The error may involve com.alibaba.cornerstone.dao.output.OutputAppConfigDAO.addBatch-Inline\n### The error occurred while setting parameters\n### SQL: insert into output_appconfig ( app_name, component_id, config_value, config_type, finished ) values ( ?, ?, ?, ?, ? ) , ( ?, ?, ?, ?, ? ) , ( ?, ?, ?, ?, ? )\n### Cause: ERR-CODE: [TDDL-4601][ERR_EXECUTOR] Incorrect string value: '\\xF2\\xBC\\xAF\\xBA\\xEF\\xBF...' for column 'config_value' at row 1
插入数据库的数据是一堆mysql建表语句,为什么会出现4个字节编码的字符
- 写了个main方法,从磁盘读入这个ddl.sql文件,挨个字符读取并判断其编码字节数,最终找到这样一串字符(因为直接粘贴过来就变样了,所以截图)
这里的第四个字符编码为4位的(我本来还以为是ddl.sql文件里有四个字节的utf-8编码才能表示的中文汉子呢~~~~~~~~~),原来是因为建表语句的comment里有乱码,而这些乱码里有4字节utf-8字符。
- 为什么从公司内部的db导出的建表与存在这样奇怪的乱码。(还没搞清楚。。。。)
解决方式
- 修改数据库字符集为utf8mb4,在java连接mysql时加上
set names utf8mb4;
,但是公司的dba很拽的,db字符集不支持自助修改,dba整天整天的没反应。。。。。蛋疼 - 在java端过滤掉这些4字节字符(这种乱码本来就没啥用,过滤掉岂不更好。。。~~)
正则表达式之unicode匹配
都知道正则表达式可以用[a-zA-Z]这样的方式匹配字母,那我们的中文字符也想这样匹配怎么办呢,用unicode匹配吧,世界上任何一个字符都可以用unicode来表示。
-
\u
开头表示直接匹配unicode编码
2E80~33FFh:中日韩符号区。收容康熙字典部首、中日韩辅助部首、注音符号、日本假名、韩文音符,中日韩的符号、标点、带圈或带括符文数字、月份,以及日本的假名组合、单位、年号、月份、日期、时间等。
3400~4DFFh:中日韩认同表意文字扩充A区,总计收容6,582个中日韩汉字。
4E00~9FFFh:中日韩认同表意文字区,总计收容20,902个中日韩汉字。
A000~A4FFh:彝族文字区,收容中国南方彝族文字和字根。
AC00~D7FFh:韩文拼音组合字区,收容以韩文音符拼成的文字。
F900~FAFFh:中日韩兼容表意文字区,总计收容302个中日韩汉字。
FB00~FFFDh:文字表现形式区,收容组合拉丁文字、希伯来文、阿拉伯文、中日韩直式标点、小符号、半角符号、全角符号等。
-
-p
表示匹配unicode编码的属性(unicode属性有很多,类似中文标点,汉字什么什么的)
\p{xx}
表示一个有属性 xx 的字符,可以在左花括号 { 后面增加 ^ 表示取反。比如: \p{^Lu} 就等同于 \P{Lu}。
-
-P
表示匹配没有unicode编码的属性
\P{xx}
表示一个没有属性 xx 的字符
最后,用unicode正则表达式替换掉所有4个字节的utf-8编码字符
前面讲过utf-8的3个字节以内能表示的字符的unicode编码范围,所以直接过滤掉不在这个范围的字符
public static final String filterCodeLargerThan3Byte(String s) {
if (s == null) {
return s;
}
return s.replaceAll("[^\\u0000-\\u007F\\u0080-\\u07FF\\u0800-\\uFFFF]", "");
}