gbk编码引起的一个问题

Context

最近在一个项目中,由于转义的问题,导致页面出现报错。检查以后,发现是单引号引起的,但是转义其实已经做了如下处理:

1
<div ng-init='content="{%$data.content|replace:"'":'\"'|replace:'"':'\"'%}"'> ... </div>

奇怪的是,内容中的多个单引号,有一个单引号未被替换掉。

Analysis

代码用到的是PHP的smarty模块,这里的replace等同于PHP函数的 str_replace(),即:

1
str_replace('\'', '\\\'', $data.content);

这里是没有任何问题,那问题在哪儿呢?事出反常必有妖。那一定是某个特定环境的问题,如是,我想到的是项目中的GBK编码所致。

首先,gbk编码是采用双字节:

GBK字符集范围

GB2312字符集

  • 作用:国家简体中文字符集,兼容ASCII。
  • 位数:使用2个字节表示,能表示7445个符号,包括6763个汉字,几乎覆盖所有高频率汉字。
  • 范围:高字节从A1到F7, 低字节从A1到FE。将高字节和低字节分别加上0XA0即可得到编码。

GBK字符集

  • 作用:它是GB2312的扩展,加入对繁体字的支持,兼容GB2312。
  • 位数:使用2个字节表示,可表示21886个字符。
  • 范围:高字节从81到FE,低字节从40到FE。
分区 高位 低位 说明
GBK/1 A1~A9 A1~FE GB2312非汉字符号
GBK/2 B0~F7 A1~FE GB2312汉字
GBK/3 81~A0 40~FE 扩充汉字
GBK/4 AA~FE 40~A0 扩充汉字
GBK/5 A8~A9 40~A0 扩充非汉字

PS:1和2对应的GB2312字符集

ascii编码

  • 作用:表语英语及西欧语言。
  • 位数:ASCII是用7位表示的,能表示128个字符;其扩展使用8位表示,表示256个字符。
  • 范围:ASCII从00到7F,扩展从00到FF

GB2312的编码范围为2121H-777EH,与ASCII有重叠。

str_replace

str_replace的原理是按照ascii编码进行查找替换,不是多字节安全的;GBK编码下的一个经典问题,就是字符替换后的乱码问题;一个例子:

1
2
3
4
5
6
header("Content-type:text/html;charset=GBK");
$str = "叫你姨";
$str = iconv('UTF-8','GBK', $str);
$rs= iconv('UTF-8','GBK', '心');
$str = str_replace($rs, '', $str);
echo $str;

猜猜会输出什么?

过程
  1. 通过bin2hex($str)转码:
  2. 叫你姨的GBK编码为BDD0 C4E3 D2CC
  3. 的GBK编码为D0C4
  4. 经过str_replace的替换,结果就变成了BDE3 D2CC
  5. echo pack(‘N’, hexdec(‘BDE3D2CC’));

结果是:姐姨

continue

回到这个问题,替换前的转码为:

1
3c703ed7f7ceaab7f0bdccd2d5caf5c6b7a3acccc6bfa8d3d0c7e5bebbb5c4cff3d5f7d2e2d2e5a3acbeadb9fd266c6471756f3bbfaab9e226726471756f3bb5c4ccc6bfa8d4dab1b3baf3bfc9bfb4b5bd2220cecb22a1a22220b0a122a1a22220bae422a3a8d2f4d2eba3a9c8fdb8f6e8f3cec4d7d6a3acbfaab9e2baf3b5c4ccc6bfa8d2d4b9a9b7eed4dacbc2d4babbf2bcd2d6d0b7f0ccc3ceaad2cba3bbceb4beadbfaab9e2b5c4ccc6bfa8bfc9d7f7ceaad2d5caf5c6b7b0dab9d2d4dabcd2cda5b8c9bebbb4a6a3acc8e7bfcdccfca1a2cae9b7bfa3accac7c7e5bebbc9edd0c4a1a2c6b7ceb6b8dfd1c5b5c4d2d5caf5bcd1c6b7a1a33c6272202f3e4173204275646468697374206172742c205468616e676b612068617320636c65616e20616e6420707572652073796d626f6c697a6174696f6e2e20416674657220226f70656e696e67206c6967687422207468726565205661746963616e207465787420266d646173683b266d646173683b224f6d222c20224168222c2022486f6e672220287472616e736c697465726174696f6e292063616e206265207365656e20696e20746865206261636b67726f756e64206f66207468616e676b612e205468656e207468616e676b612073686f756c6420626520617070726f7072696174656c7920776f727368697070656420696e207468652074656d706c65206f7220612066616d696c792068616c6c20666f7220776f727368697070696e67204275646468612e20576974686f7574206265696e67206f70656e6564206c696768742c207468616e676b612063616e2062652068616e67656420696e206120636c65616e20706c6163652c2073756368206173206c6976696e6720726f6f6d206f722073747564792e205468616e676b612069732074686520617274206f6620656c6567616e742074617374652077686963682063616e20616c736f2068656c7020707572696679206d656e74616c6974792e3c2f703e

替换后的转码为:

1
3c703ed7f7ceaab7f0bdccd2d5caf5c6b7a3acccc6bfa8d3d0c7e5bebbb5c4cff3d5f7d2e2d2e5a3acbeadb9fd266c6471756f3bbfaab9e226726471756f3bb5c4ccc6bfa8d4dab1b3baf3bfc9bfb4b5bd5c2220cecb5c22a1a25c2220b0a15c22a1a25c2220bae422a3a8d2f4d2eba3a9c8fdb8f6e8f3cec4d7d6a3acbfaab9e2baf3b5c4ccc6bfa8d2d4b9a9b7eed4dacbc2d4babbf2bcd2d6d0b7f0ccc3ceaad2cba3bbceb4beadbfaab9e2b5c4ccc6bfa8bfc9d7f7ceaad2d5caf5c6b7b0dab9d2d4dabcd2cda5b8c9bebbb4a6a3acc8e7bfcdccfca1a2cae9b7bfa3accac7c7e5bebbc9edd0c4a1a2c6b7ceb6b8dfd1c5b5c4d2d5caf5bcd1c6b7a1a33c6272202f3e4173204275646468697374206172742c205468616e676b612068617320636c65616e20616e6420707572652073796d626f6c697a6174696f6e2e204166746572205c226f70656e696e67206c696768745c22207468726565205661746963616e207465787420266d646173683b266d646173683b5c224f6d5c222c205c2241685c222c205c22486f6e675c2220287472616e736c697465726174696f6e292063616e206265207365656e20696e20746865206261636b67726f756e64206f66207468616e676b612e205468656e207468616e676b612073686f756c6420626520617070726f7072696174656c7920776f727368697070656420696e207468652074656d706c65206f7220612066616d696c792068616c6c20666f7220776f727368697070696e67204275646468612e20576974686f7574206265696e67206f70656e6564206c696768742c207468616e676b612063616e2062652068616e67656420696e206120636c65616e20706c6163652c2073756368206173206c6976696e6720726f6f6d206f722073747564792e205468616e676b612069732074686520617274206f6620656c6567616e742074617374652077686963682063616e20616c736f2068656c7020707572696679206d656e74616c6974792e3c2f703e

即将22替换为5c22,替换结果中确实有一处的22未被替换。而这个22就是导致报错的问题了,为啥没有替换,猜想难道是22在str_replace的时候被前后分开了,但并没有…

Why?

在本地环境中,通过对上面的编码,进行翻转,未能复现线上的效果,这是啥原因呢?

1
2
3
4
5
6
<?php
header("Content-type: text/html; charset=gbk");
$content = file_get_contents('./content.txt');
$str = pack("H*", $content) ;
$str = str_replace('"','\"',$str);
echo $str;

解决方案

这类的问题解决方案其实也很多,比如:

  1. 将内容转换为UTF-8,进行替换,替换完了,再转成GBK;
  2. 使用双字节可靠的mb_ereg_replace进行替换;