標籤:
http://blog.csdn.net/binjly/article/details/47321043
今天,測試給我提了一個BUG,說移動端輸入emoji表情無法提交。很早以前就有思考過,手機IME裡內建的emoji表情,應該是某些特殊字元。既然是字元,那應該都能提交才對,可是為啥會被卡住呢?搜了一下,才發現,原來emoji用到的字元是4位元組的utf-16(utf-16有2位元組和4位元組兩種編碼),而我們的資料庫是採用的utf-8,並且最大隻允許3位元組的字元。這樣衝突就產生了,表單因為這些emoji字元的存在無法提交。
找到原因之後,接下來就要考慮解決方案了。目前考慮到的兩種方案,一是讓幕後處理,把這個utf-16字元做一些轉換(這裡不做討論)。第二種辦法就是在前端直接轉換成實體字元後再提交。這樣,後台不用做任何處理,使用者的提交的資訊也得以保留,是不是一個兩全其美的辦法呢?接下來我們要討論的就是怎麼把emoji表情字元轉換成實體字元。
首先,我們來看看手機IME裡內建的emoji字元是什麼樣。下面截了一張圖,來自 http://computerism.ru/emoji-smiles.htm 。我們看到,每個emoji表情字元對應的實體字元編碼都比較大,如第一行的笑臉,實體字元為😊 。而且,我們注意到,後面還有一個16進位的編碼 D83DDE0A。那這個編碼是幹嘛用的?接著往下看。
一、字元檢測
要想把這些emoji表情字元轉換成實體字元,那麼就要先把它們檢測出來。說到字元檢測,我們的正則這時就該上場了。首先我們得確定這些字元的範圍。前面我們已經知道,emoji表情字元用的是4位元組的utf-16編碼,而4位元組的utf-16編碼不被後台接受。所以,我們的檢測範圍就變成了把所有4位元組的utf-16編碼檢測出來。我們通過搜尋查到,4位元組的utf-16編碼範圍為U+010000到U+10FFFF,那麼,我們的正則是不是可以這麼寫:/[\u010000-\u10FFFF]/g ? NO,你會發現這個正則完全不能按我們預期工作。這是為什麼呢?
上面這個問題,一些童鞋可能已經知道答案了。沒錯,就是javascript的編碼問題引起的。我們知道,javascript採用的是unicode編碼,再準確一點說,是ucs-2編碼。從名字上,我們就已經知道,這種編碼方案是2位元組的。在2位元組的編碼中找4位元組的字元,很顯然並沒那麼簡單。所以,我們得考慮一下,這個utf-16在ucs-2編碼中是如何表示的呢?這裡,我搜到了我們可愛的傳教士——阮老師的一篇文章 《Unicode與JavaScript詳解》(http://www.ruanyifeng.com/blog/2014/12/unicode.html) 。 簡單來說,就是把utf-16的4位元組字元,拆分成兩個ucs-2的2位元組字元。具體演算法可參考阮老師的上述文章,本文就不詳細討論了。從阮老師的文章中,我們已經知道了,4位元組utf-16在js中被用兩個字元來表示,高位範圍為0xD800 - 0xDBFF,低位範圍為0xDC00 - 0xDFFF。那麼我們用於檢測的Regex也就出來了:/[\uD800-\uDBFF][\uDC00-\uDFFF]/g 。現在再回過頭看看我們第一張圖的那串16進位,D83DDE0A、D83DDE03,是不是突然就明白了呢?
二、轉換演算法
現在,我們已經能夠檢測出表單裡的emoji表情字元。那麼,重頭戲來了,我們怎麼把這個字元轉換成實體字元呢?我們知道,實體字元是用來表示單個字元的編碼,而我們的emoji表情,在js裡,卻是用兩個字元來表示的。這可怎麼辦?等等,誰說emoji是兩個字元,說好的4位元組單字元呢?沒錯,一開始emoji就是用utf-16表示的啊 這裡,我又參考了另一篇文章,http://unicode-table.com/cn/sets/emoji/,以下截了一部分圖以做說明。
我們還是以那個笑臉的字元為例,其utf-16編碼為U+1F600,我們轉成十進位看看。
128512不正好是我們的實體編碼😀 嗎?所以,現在問題又變成了怎麼取得emoji表情字元utf-16編碼的問題了。可是,可是,我們剛剛已經知道了,在js裡,emoji表情也是用ucs-2編碼的啊,只不過變成了用兩個字元來表示。那麼,我們的問題最終演變成了怎麼從ucs-2編碼轉換成utf-16編碼的問題。
感謝阮老師,在阮老師的那篇文章中,有提到utf-16轉ucs-2(unicode)的公式
[javascript] view plain copy
- H = Math.floor((c-0x10000) / 0x400)+0xD800 // 高位
-
- L = (c - 0x10000) % 0x400 + 0xDC00 // 低位
可是,這個是utf-16轉ucs-2,我們要的是ucs-2轉utf-16啊?怎麼辦?推導回去唄。我們先看看這兩個公式都做了什麼。首先,高位的公式,把字元C減0x10000,再除0x400,取其商,再加0xD800。而低位則是字元C減0x1000,取除0x400的餘,再加0xDC00。所以這個字元其實被分成了兩部分:商和餘,然後再把處理後的商做為高位,加上處理後的餘做為低位,這樣組合成了ucs-2字元。我們知道,被除數=除數×商+餘數。那麼,我們也可以反過來,求得C/0x400的商,再加上C/0x400的餘,不就能算出C了嗎。為了便於計算,我們用Q表示C的商,用M表示C的餘,那麼就有了以下公式:
[javascript] view plain copy
- H = Q - 0x10000 / 0x400 + 0xD800
- L = M - 0x10000 % 0x400 + 0xDC00
- C = Q * 0x400 + M
- // 因為0x10000 % 0x400 = 0,故推得:
- H = Q - 0x10000 / 0x400 + 0xD800
- L = M + 0xDC00
- C = Q * 0x400 + M
- // 根據C的公式,把H*0x400再加L,得到:
- H * 0x400 + L = Q * 0x400 - 0x10000 / 0x400 * 0x400 + 0xD800 * 0x400 + M + 0xDC00
- // 最後把Q * 0x400 + M換成C,得到:
- H * 0x400 + L = C - 0x10000 + 0xD800 * 0x400 + 0xDC00
- // 移項後,我們最終的公式為:
- C = (H - 0xD800) * 0x400 + 0x10000 + L - 0xDC00
公式出來之後,相信大家已經知道怎麼做了,不過最後還是獻一下醜,把我自己寫的一個處理函數提供給大家參考:
[javascript] view plain copy
- /**
- * 用於把用utf16編碼的字元轉換成實體字元,以供後台儲存
- * @param {string} str 將要轉換的字串,其中含有utf16字元將被自動檢出
- * @return {string} 轉換後的字串,utf16字元將被轉換成&#xxxx;形式的實體字元
- */
- function utf16toEntities(str) {
- var patt=/[\ud800-\udbff][\udc00-\udfff]/g; // 檢測utf16字元正則
- str = str.replace(patt, function(char){
- var H, L, code;
- if (char.length===2) {
- H = char.charCodeAt(0); // 取出高位
- L = char.charCodeAt(1); // 取出低位
- code = (H - 0xD800) * 0x400 + 0x10000 + L - 0xDC00; // 轉換演算法
- return "&#" + code + ";";
- } else {
- return char;
- }
- });
- return str;
- }
運行結果如下:
細心的童鞋,在剛剛看那些參考文章的時候,也許已經發現了,其實並不是所有的emoji表情字元都是utf-16編碼的,也有一部分落在了ucs-2編碼的範圍(即只用了兩個位元組)。不過這都不是重點,重點是,我們已經成功的把utf-16編碼部分的emoji表情轉換為了實體字元。
【轉】移動前端手機IME內建emoji表情字元處理