一次性搞清楚unicode、codepoint、程式碼點、UTF

最近在處理字元過濾,重新研究了下字元、unicode和程式碼點的相關知識,首先要說一下編碼的基本知識unicode
unicode
unicode是電腦科學領域裡的一項業界標準,包括字符集、編碼方案等。計算機採用八位元一個位元組,一個位元組最大整數是255,還要表示中文一個字也是不夠的,至少需要兩個位元組,為了統一所有的文字編碼,unicode為每種語言中的每個字元設定了統一併且唯一的二進位制編碼,通常用兩個位元組表示一個字元,所以unicode每個平面可以組合出65535種不同的字元,一共17個平面。
由於英文符號只需要用到低8位,所以其高8位永遠是0,因此儲存英文文字時會多浪費一倍的空間。
比如漢子“漢”的unicode,在java中輸出
System.out.println("\u5B57");
UTF-8
unicode在計算機中如何儲存呢,就是用unicode字符集轉換格式,即我們常見的UTF-8、UTF-16等。
UTF-8就是以位元組為單位對unicode進行編碼,對不同範圍的字元使用不同長度的編碼。
| Unicode | Utf-8 || --- | --- || 000000-00007F | 0xxxxxxx || 000080-0007FF | 110xxxxx 10xxxxxx || 000800-00FFFF | 1110xxxx 10xxxxxx 10xxxxxx || 010000-10FFFF | 11110xxx10xxxxxx10xxxxxx10xxxxxx |
Java中的String物件就是一個unicode編碼的字串。
java中想知道一個字元的unicode編碼我們可以通過Integer.toHexString()方法
String str = "編"; StringBuffer sb = new StringBuffer(); char [] source_char = str.toCharArray(); String unicode = null; for (int i=0;i<source_char.length;i++) { unicode = Integer.toHexString(source_char[i]); if (unicode.length() <= 2) { unicode = "00" + unicode; } sb.append("\\u" + unicode); } System.out.println(sb); 輸出\u7f16
對應的utf-8編碼是什麼呢?
7f16在0800-FFFF之間,所以要用3位元組模板:1110xxxx 10xxxxxx 10xxxxxx。
7f16寫成二進位制是:0111 1111 0001 0110
按三位元組模板分段方法分為0111 111100 010110,代替模板中的x,得到11100111 10111100 10010110,即“編”對應的utf-8的編碼是e7 bc 96,佔3個位元組
codepoint
unicode的範圍從000000 - 10FFFF,char的範圍只能是在\u0000到\uffff,也就是標準的 2 位元組形式通常稱作 UCS-2,在Java中,char型別用UTF-16編碼描述一個程式碼單元,但unicode大於0x10000的部分如何用char表示呢,比如一些emoji::grinning:
java的char型別佔兩個位元組,想要表示:grinning:這個表情就需要2個char,看如下程式碼
String testCode = "ab\uD83D\uDE03cd"; int length = testCode.length(); int count = testCode.codePointCount(0, testCode.length()); //length=6 //count=5
第三個和第四個字符合起來代表:grinning:,是一個程式碼點,
如果我們想取到每個程式碼點做一些判斷可以這麼寫
String testCode = "ab\uD83D\uDE03cd"; int cpCount = testCode.codePointCount(0, testCode.length()); for(int index = 0; index < cpCount; ++index) { //這裡的i是字元的位置 int i = testCode.offsetByCodePoints(0, index); int codepoint = testCode.codePointAt(i); } //輸出 i:0 index: 0 codePoint: 97 i:1 index: 1 codePoint: 98 i:2 index: 2 codePoint: 128515 i:4 index: 3 codePoint: 99 i:5 index: 4 codePoint: 100
也就是按照codePointindex取字元,0取到a,1取到b,2取到\uD83D\uDE03也就是:grinning:,3取到c,4取到d;
按照String的index取字元,0取到a,1取到b,2取到\uD83D,3取到\uDE03,4取到c,5取到d。
這就是codePointIndex和char的index的區別。
取到codePoint就可以按照unicode值進行字元的過濾等操作。
如果有個需求是既可以按照unicode值過濾字元,也能按照正則表示式過濾字元,並且還有白名單,應該如何實現呢。
其實unicode過濾和正則表示式過濾並不衝突,自己實現自己的過濾就好了,如果需求加入了過濾白名單就會複雜一些,不能直接過濾,需要先檢驗是否是白名單的index。
我的思路是記錄白名單char的index,正則表示式或其他過濾方式可以獲得違規char的index,unicode黑名單的codepointIndex可以轉換成char的index,在獲取codePont的index時可以判斷當前字元是單char字元還是雙char字元,雙char字元需要新增2個下標,方法如下
//取到unicode值 int codepoint = testCode.codePointAt(i); //將unicode值轉換成char陣列 char[] chars = Character.toChars(codepoint); charIndexs.add(pointIndex); if (chars.length > 1) { //表示不是單char字元,記錄index時同時新增i+1 charIndexs.add(pointIndex + 1); }
//例
String str = "ab\uD83D\uDE03漢字";
想處理emoji,那記錄的下標就是2、3,最後和白名單下表比較後統一刪除
如何區別char是一對還是單個
就之前的例子ab\uD83D\uDE03cd,換種寫法\u0061\u0062\uD83D\uDE0\u0063\u0064
程式是如何將\uD83D\uDE03解析成一個字元的呢。這就需要Surrogate這個概念,來自UTF-16。
UTF-16是16bit最多編碼65536,那大於65536如何編碼?Unicode 標準制定組想出的辦法是,從這65536個編碼裡,拿出2048個,規定他們是「Surrogates」,讓他們兩個為一組,來代表編號大於65536的那些字元。
編號為 U+D800 至 U+DBFF 的規定為「High Surrogates」,共1024個。
編號為 U+DC00 至 U+DFFF 的規定為「Low Surrogates」,也是1024個。
他們組合出現,就又可以多表示1048576中字元。
看一下String.codePointAt這個方法,
static int codePointAtImpl(char[] a, int index, int limit) { char c1 = a[index]; if (isHighSurrogate(c1) && ++index < limit) { char c2 = a[index]; if (isLowSurrogate(c2)) { return toCodePoint(c1, c2); } } return c1; }
其中有兩個方法isHighSurrogate、isLowSurrogate。
第一個方法判斷是否為高代理項程式碼單元,即在'\uD800'與'\uDBFF'之間,
第二個方法判斷是否為低代理項程式碼單元,即在'\uDC00'與'\uDFFF'之間。
codePointAtImpl方法判斷當前char是高代理項程式碼單元,下一個是低代理項程式碼單元,則這兩個char是一個codepoint。
再來看一下unicode轉UTF-16的方法
如果U<0x10000,U的UTF-16編碼就是U對應的16位無符號整數(為書寫簡便,下文將16位無符號整數記作WORD)。
如果U≥0x10000,我們先計算U'=U-0x10000,然後將U'寫成二進位制形式:yyyy yyyy yyxx xxxx xxxx,U的UTF-16編碼(二進位制)就是:110110yyyyyyyyyy 110111xxxxxxxxxx。
還是以U+1F603這個:smiley:為例子,U'=U-0x10000=F603
寫成2進位制就是1111011000000011,不足20位前面補0,
變成0000111101-1000000011,替換y和x就是1101100000111101,1101111000000011,最後UTF-16編碼就是[d83d,de03] 和上面一樣。