程序员常见的编码和转义

xo87x3z9 8年前
   <p>作为一名天朝程序员,除了看惯了乱码之外,在日常的工作中经常会碰到编码和转义。如果能掌握这块领域的一些常识,就可以在开发和支持时游刃有余。</p>    <p><strong>编码(encoding)</strong></p>    <p><strong>ASCII & EASCII & ISO-8859-1</strong></p>    <p>要聊编码,就需要从ASCII开始。众所周知,计算机的世界里,数据都是0和1这样的二进制。用它们的组合来表示字母、数字和常用符号的最通用编码标准就是ASCII(American Standard Code for Information Interchange,美国信息交换标准代码)。完整的ASCII编码可以从 这里 找到。Mac或Linux可以用以下命令来查看所有的ASCII字符:</p>    <pre>  <code class="language-javascript">man ascii</code></pre>    <p>其中,00000000到00011111的前32位字符和01111111是控制字符,00100000到01111110之间的都是可显示字符,一个字符占8位(bit),第1位总是0,这样能够支持2的7次方即128个符号编码。虽然ASCII编码能搞定美国大多数的应用场景,但是对于其它发达国家的语言来说就无能为力了。于是在其上发展出了EASCII(extended ASCII),通过扩展最前面的一位为1来提供多达256个符号编码的支持。可是这样又带来了两个问题:一来即使是256个编码,对于世界范围尤其是像中日这样的汉字国家来说还是远远不够;二来各个国家规定的EASCII编码都不一样,比如对于希腊来说EASCII表示的就是希腊字母,而对于法国来说可能就是某个带有注音符号的字母。这样的背景下,ISO(International Standard Organization,国际标准化组织)设计了ISO/IEC 8859字符集(不包含ASCII),力图一统拉丁语系。其实现的编码表ISO-8859-1(包含ASCII)应用得非常广泛。</p>    <p><strong>GB2312 & GBK & GB18030 & ANSI</strong></p>    <p>本节介绍的是解决EASCII带来的第一个问题的方法。对于中文来说,8位的编码远远不够,于是就会想到用两个8位来表示一个汉字。为了与ASCII码兼容,如果碰到0~127的字符,需要认定为ASCII编码字符。只有当两个大于127的字符连在一起时,才表示一个汉字。前一个字符称为高字节,后一个称为低字节,这样就诞生了GB2312编码。每一个双字节字符就称为一个全角字符,而单字节字符就称为半角字符。再后来,发现编码还是不够用,干脆就允许低字节也使用0~127的字符,反正用高字节就能判断是否是汉字,这样就诞生了GBK(K表示“扩展”)编码。GBK里甚至还包含了日语的假名和俄语字母。GB2312和GBK这两种编码都是单字节(表示ASCII)和双字节(表示汉字)混合使用的编码。我国最新的汉字编码国标是GB18030,这是一种类似下文UTF-8那样的变长编码。</p>    <p>虽然中国解决了中文问题,但是世界各国都搞出了一套自己的编码系统,还是不能轻易相互转化。例如台湾用BIG5,日本用Shift-JIS。要想解决EASCII的第二个问题,还需要另寻他途。Window系统的记事本里,默认编码为ANSI,即根据系统语言的不同,而选用不同的编码。</p>    <p><strong>Unicode & UFT-8</strong></p>    <p>本节说的是解决EASCII带来的第二个问题的方法。ISO带来了一个囊括全球所有文字的编码:Unicode。它最初规定了所有的字符(包括ASCII)都使用两个字节来表示,这个版本称为UCS-2(Universal Multiple-Octet Coded Character Set)或UTF-16。对于ASCII码来说,在它的前面加上00000000作为高字节即可。这样的好处是,由于高低字节可以同时包含0~256,能表示的字符数量就更多了,理论上可以达到256×256=65536个。即使如此,也只能说是基本上够用,要囊括所有文明的文字,还需要更多的字节。目前最多支持4个字节代表一个字符,称为UCS-4或UTF-32,它的最高位规定必须为0,可以表示65536×65536÷2=2147483648个字符(这样是不是统一银河系也够用了)。与此同时,它包含的字符集也在不断的增加,甚至收录了emoji(绘文字),大大增加了文字符号的表现力,看看????????????????????????????????,是不是增加了很多乐趣呢。</p>    <p>Unicode就像是“书同文、车同轨”,极大地方便了各国的交流。可是它也有自身的缺点。一个问题是它与各国自身的标准不兼容(例如GB18030),但是这个问题貌似无解,因为各国的标准本来就是排斥的。另一个问题是随着Unicode标准的发展,出现了4个字节的字符。但是当设计Java的时候,是 将unicode当做2个字节的定长字符来看待 的。这样就导致Java里需要用两个char来表示一个4字节的字符,如emoji(????=\uD83D\uDE02)。Java平台中的增补字符就是Oracle官方写来专门解决长字节Unicode的。打开链接就会看到一堆的乱码,说明编码问题还真是普遍存在并难以解决的啊。好在还有 英文版 可供阅读。还有一个问题就是对于英文来说,用高字节为固定值的两个字节来保存数据,就会使原来一个字节的数据量翻倍,对于传输和存储来说都是较大负担。</p>    <p>解决上面这个问题的办法就是UTF-8。它是一种变长的编码方式。如果是ASCII码的字符,就用一个字节表示。否则就在前面增加一个高位字节(但是在8个bit之内)。这回英文符号是满意了,但是中文字符可能就会因为增加的高位字节从Unicode的占用两个字节变成UTF-8的占用三个字节。没有两全其美的事啊!这也是为什么GB2312和GBK今天仍被广泛使用的原因之一,我们也不想增加传输和存储的负担呀。</p>    <p>如果要打开一个文本文件,首先需要知道它的编码。位于文件头的BOM(Byte order mark,字节顺序标记)可以用来标记文件的编码类型。它分为BE(big-endian,大端序)和LE(little-endian,小端序),指的是高字节的位置在前还是在后。但是在类Unix系统中,它很可能因为无法被程序识别而带来一系列问题。所以一般的纯文本文件还是建议保存为不带BOM形式的编码。Window系统的记事本里,如果输入联通保存,便会将其保存为无BOM的GB格式,再次用记事本打开此文件时,因为没有BOM信息,记事本就需要自己推断这个文件的编码是什么。显然window是上这个推断很有问题,误认为是UTF-8格式(可以从文件菜单里的“另存为”看出来)。而mac上默认的文本编辑表现还是不错的。如果用word来打开它,便可以在一系列的编码中,自行寻找合适的编码来打开。如果用记事本另存为UTF-8格式,便不会有问题。Sublime Text可以支持用许多不同的编码来打开或是保存,光是UTF系列的就不少,如下图:</p>    <p style="text-align:center"><img src="https://simg.open-open.com/show/35c85c3297a267d1686f8403d499bedf.png"></p>    <p>sublime-text-encoding</p>    <p>对于Java来说,内部的String编码默认为UTF-16,但如果由于用不着而觉得浪费内存的话,可以在JVM打开-XX:+UseCompressedStrings,就会变成ISO-8859-1了。Intellij IDEA的Preference里,有两个关于encoding的选项:</p>    <p style="text-align:center"><img src="https://simg.open-open.com/show/1c61496a3e77a53040d2564cadbb4fef.jpg"></p>    <p>Intellij-IDEA-encoding</p>    <p>可以通过Project Encoding来指定项目的JVM里String的内部编码,默认为UTF-8。可以通过下面这两个表达式来看到,它们的编码是完全一致的:</p>    <pre>  <code class="language-javascript">"懒".getBytes()  "懒".getBytes("UTF-8")</code></pre>    <p>Java里可以用Integer.toHexString来看到汉字的unicode编码:</p>    <pre>  <code class="language-javascript">System.out.println("\\u" + Integer.toHexString('懒'));  System.out.println("\u61d2");</code></pre>    <p>通过下面的语句,可以将字节数组byte[]还原为原先的字符串。如果指定错了编码,就会看到乱码产生啦:</p>    <pre>  <code class="language-javascript">System.out.println(new String("懒".getBytes("UTF-8"), "UTF-8"));  // 正常  System.out.println(new String("懒".getBytes("UTF-8"), "UTF-16")); // 乱码:??  System.out.println(new String("懒".getBytes("UTF-16"), "UTF-8")); // 乱码:??a?</code></pre>    <p>读文件、流也是一样的道理,知道了它们的编码才能正确地读取,否则只好像微软的记事本那样去猜啦。Java还提供了一个小工具native2ascii,可以把本地编码的文件转换为各种格式:</p>    <pre>  <code class="language-javascript">echo 懒程序员 > ggg.txt  native2ascii -encoding UTF-8 ggg.txt out.txt  cat out.txt  native2ascii -reverse -encoding UTF-8 out.txt  base64 & UTF-7</code></pre>    <h3><strong>base64 & UTF-7</strong></h3>    <p>Base64是一种在网络上传递信息时常见的编码。它相当于是一张64条记录的映射表,键从000000到111111,值就是64个不同的字符。编码时,如果原字符的bit数正好能被6整除,那就查表得到每6个bit所对应的值,合起来就是base64编码的结果。如果不能被6整除,那就在末尾用0补足。每补两个0,就在最终结果的后面加一个=号。所以如果一段数据以等号结尾,那十有八九就是base64编码。Mac或Linux可以用以下命令来进行base64编码及解码:</p>    <pre>  <code class="language-javascript">echo -n A | base64  echo -n AB | base64  echo -n ABC | base64  echo -n QQ== | base64 --decode  echo -n QUI= | base64 --decode  echo -n QUJD | base64 --decode</code></pre>    <p>UTF-7理论上也属于一种base64编码,只不过它的64行映射表不一样罢了。过去的SMTP协议仅能接受7个bit(ASCII)的字符,Unicode无法直接传输。所以通过UTF-7编码的方式,将Unicode字符转换为7个bit以内的字符。UTF-7本身并不是Unicode的标准,现在也已经由于邮件和传输都支持UTF-8而退出历史舞台了。</p>    <p>写到这里感觉得收一下了,不然MD5、SHA什么的都要出来了。</p>    <p><strong>转义(escaping)</strong></p>    <p><strong>html & url</strong></p>    <p>下面说说转义,不少人都把它与编码混而一谈,以至于它也算作编码的一部分了。从最简单的html聊起吧。在html里,如果只写上一些文本,那当我们用浏览器打开这个html时,就会完完整整地显示这些文本的内容。我们也知道,html里无论输入多少个空格,只会显示一个空格。因为在html里,把空格当成了特殊字符。在这种情况下,如果想要在html里放上空格,就需要对空格编码,也就是大家熟知的 。其中nbsp大名唤作Non-Breaking Space(不换行空格),除了名字以外,它也有自己的编码: 。除了空格,常见的还有代表标签的 <和> 。完整的html转义可以从这里找到。奇怪的是这么常用的转义,js居然没有原生的函数支持。如果要转义可以使用下面这条语句来得到 <code>&lt;div&gt;</code>:</p>    <pre>  <code class="language-javascript">function htmlEncode(html) {      return document.createElement('a').appendChild(document.createTextNode(html)).parentNode.innerHTML;  };  htmlEncode('<div>');</code></pre>    <p>解码的话,这样做:</p>    <pre>  <code class="language-javascript">function htmlDecode(html) {      var a = document.createElement('a');      a.innerHTML = html;      return a.textContent;  };  htmlDecode('&lt;div&gt;');</code></pre>    <p>如果使用jQuery,思路一致,但是代码可以稍微短一点:</p>    <pre>  <code class="language-javascript">function htmlEncode(value){    return $('<div/>').text(value).html();  }  htmlEncode('<div>');    function htmlDecode(value){    return $('<div/>').html(value).text();  }  htmlDecode('&lt;div&gt;');</code></pre>    <p>可惜的是上面的函数并不能解决空格和<code>&nbsp;</code>之间的转换。想要个万能的?也许只好使用replace一个个地慢慢替换了。</p>    <p>想要请求一个html,需要先输入一个url。这里就涉及到了url转义。因为url里可能会有类似<code>?name=ggg</code>这样的参数,所以起码就需要对<code>?</code>和<code>=</code>进行转义。转义之后分别为<code>%3F</code>和<code>%3D</code>,这与ASCII码是相对应的。完整的url编码可以从这里找到。这回js终于有原生的函数支持了:</p>    <pre>  <code class="language-javascript">encodeURI('http://qinghua.github.io?name=g gg');  encodeURIComponent('http://qinghua.github.io?name=g gg');</code></pre>    <p>用<code>encodeURI</code>函数的网址,不会去碰<code>http://</code>,所以编码后还是一个合法的网址。而<code>encodeURIComponent</code>会将一切都进行编码,网址也就不是网址了。不过它很适合将网址作为参数来使用。解码的话,这样做:</p>    <pre>  <code class="language-javascript">decodeURI('http://qinghua.github.io?name=g%20gg');  decodeURIComponent('http%3A%2F%2Fqinghua.github.io%3Fname%3Dg%20gg');</code></pre>    <p>在Java里可以用以下语句来完成url的转义:</p>    <pre>  <code class="language-javascript">URLEncoder.encode("懒", "UTF-8");  URLDecoder.decode("%E6%87%92", "UTF-8");</code></pre>    <p><strong>XML & YAML & JSON & CSV</strong></p>    <p>在这些数据格式中,对xml的转义基本上跟html差不多,这里就不再赘述了。对于yaml来说,规则如下:</p>    <ol>     <li>在一个单引号标注的字符串中,一个单引号需要转义成两个单引号</li>     <li>在一个双引号标注的字符串中,大部分符号都需要用反斜杠来转义</li>     <li>如果字符串中有控制字符(如<code>\0</code>、<code>\n</code>等),需要用双引号来标注</li>     <li>如果字符串看起来像下面的样子,需要用引号(无所谓哪种)来标注:      <ul>       <li><code>true</code>或<code>false</code></li>       <li><code>null</code>或<code>~</code></li>       <li>看起来像数字,如<code>2</code>,<code>14.9</code>,<code>12e7</code>等</li>       <li>看起来像日期,如<code>2014-12-31</code></li>      </ul> </li>    </ol>    <p>对与json来说,需要转义的字符如下图:</p>    <p style="text-align:center"><br> <a href="https://simg.open-open.com/show/c8222bd57e962c5281e5449aefa76748.gif" rel="article0" title="json string escape"><img alt="程序员常见的编码和转义" src="https://simg.open-open.com/show/c8222bd57e962c5281e5449aefa76748.gif" width="598" height="413"></a>json string escape</p>    <p>对于csv来说,转义的规则只有两条:</p>    <ol>     <li>如果值里有逗号、换行或是双引号,需要用双引号来标注</li>     <li>如果值里有双引号,需要把它转义成两个双引号<code>""</code></li>    </ol>    <p><strong>Java & .NET & JS & SQL</strong></p>    <p>对于大部分的编程语言,例如Java、.NET还有JavaScript,甚至C、GO、Ruby等等来说,通常的转义都是通过反斜杠<code>\</code>来实现的。一般都包括如下几项:</p>    <ul>     <li>退格: <code>\b</code></li>     <li>换行: <code>\n</code></li>     <li>制表符: <code>\t</code></li>     <li>回车: <code>\r</code></li>     <li>换页: <code>\f</code></li>     <li>双引号: <code>\"</code></li>     <li>反斜杠: <code>\\</code></li>    </ul>    <p>不过C和C++支持的16进制<code>\x</code>,在java里不被支持。所以<code>\x61\xd2</code>的这个“懒”字,在java中可以通以下这两个表达式来得到真实的字符:</p>    <pre>  <code class="language-javascript">"\u61d2"  new String(new byte[] {(byte) 0x61, (byte) 0xd2}, "unicode")</code></pre>    <p>SQL有些不一样。它从语法层面支持模糊查询,所以即使在完全匹配中使用了<code>%</code>也不需要转义。但是代表字符串的单引号<code>'</code>还是不得不转义成两个单引号<code>''</code>。</p>    <p> </p>    <!--和-->    <p> </p>    <p>来自:http://www.cocoachina.com/ios/20161206/18301.html</p>    <p> </p>