Skip to content

Instantly share code, notes, and snippets.

Embed
What would you like to do?

Date: 2015-12-17 Title: Emoji表情传输和保存:对非BMP范围的Unicode字符的处理 Category: PHP Tags: Emoji, Unicode, PHP, Lua Slug: php_emoji_to_unicode

参考:

UTF-16

Emoji

十分钟搞清字符集和字符编码

4 byte unicode character in Java

Emoji与Unicode、UTF8

Emoji是一种特殊的字符,而不是像QQ表情一样的普通字符的转义表示。在Unicode编码中,占用了U+1F300U+1F64F中的部分范围

Emoji字符的特殊之处在于,其使用的Unicode字符超出了通常使用的三字节UTF-8编码的Unicode范围,即BMP范围U+0000U+FFFF。按照UTF-8编码规范,Emoji字符属于辅助平面范围,通常对应4字节的UTF-8编码。

Android上Emoji的特殊表示

在Android上显示Emoji出现问题根源在于Java的char长度是两个字节,因此不能直接表示BMP范围外的Unicode字符,包括Emoji。对于BMP范围外的字符,Java没有直接编码的方案,而是采用一种替代手段,使用两个char来表示一个字符,称为high surrogate和low surrogate。其中high surrogate使用的是U+D800U+DBFF,low surrogate使用的是U+DC00-U+DFFF,这两个范围都是Unicode编码的保留范围,专门用于表示surrogate字符。

举个栗子,Unicode编码为U+1F602的Emoji符号。

Face With Tears of Joy

在Java中看一下对它的存储和编码:

char[] tear = Character.toChars(0x1F602); // Face with Tears of Joy
final String s = new String(tear);
int length = s.length();
int byteLength = s.getBytes(StandardCharsets.UTF_8).length;
String escape = StringEscapeUtils.escapeJava(s);

Log.i(TAG, "s " + s);
Log.i(TAG, "length " + length);
Log.i(TAG, "byteLength " + byteLength);
Log.i(TAG, "escape " + escape);

输出如下:

12-18 14:37:11.437 28246-28246/info.x7res.myapplication I/MainActivity: s ��
12-18 14:37:11.437 28246-28246/info.x7res.myapplication I/MainActivity: length 2
12-18 14:37:11.437 28246-28246/info.x7res.myapplication I/MainActivity: byteLength 4
12-18 14:37:11.437 28246-28246/info.x7res.myapplication I/MainActivity: escape \uD83D\uDE02

String对象的长度为2,编码为UTF-8字节数组后长度为4,单个字符unicode-escape编码结果为\uD83D\uDE02。因此,虽然Java不能支持直接用单个Unicode字符表示Emoji表情,但是通过使用两个surrogate字符的替代方案也能很好的支持Emoji表情的保存和编码显示。

Lua对Emoji字符串的处理

Lua的字符串只能是ANSI编码,不支持Unicode字符的字符串。本节下文是在触动精灵框架下,在Android上的Lua解释器测试得到的结论。

在Lua中,一个Emoji字符是6个字节长度,而不是直接UTF-8编码得到的4个字节。这是在Java两个surrogate字符表示一个Emoji字符的基础上,又对这两个surrogate字符进行标准UTF-8编码,得到的6个字节的字符串。

PHP对Emoji字符的接收和保存

对于Android或者Lua经过urlencode上传的Emoji字符,在服务器的PHP上经过自动的urldecode会得到6个字节长度的2个字符,这里的表现是与Android相匹配的。

问题出现在数据保存,虽然MySQL提供的utf8mb4编码支持4字节的UTF-8字符,但是要做匹配的另一批数据保存到了MongoDB,中文字符保存在成json格式时自动转为了unicode-escape编码,不能支持多字节的UTF-8字符或非BMP的unicode字符。因此为了与其他平台的数据做匹配,要求按照unicode-escape编码数据,即要求将U+1F602编码为\uD83D\uDE02

PHP并没有提供Python一样便利的编解码,而且也并不认为现unicode-escape是一种编码格式,因此没有直接encode字符串的方法。对于普通的BMP范围内的字符,比如中文字符,可以通过json_encode方法实现unicode-escape编码,与Python的处理方式类似。

$nickname_unicode = trim(json_encode($nickname), '"');

然而,与Python不同的是,PHP(5.6)的json_encode方法认为U+D800-U+DFFF的范围是非法的,不能处理带有上述6字节Emoji符号的字符串,这样会得到空的结果。所以要实现保留字段的unicode-escape编码,需要重写json_encode方法。这里参考了一个开源项目的一部分代码

重写的json_encode代码放在了我的gist,注意与原本的参考代码不同的是,173-175行的mb_convert_encoding被注释掉,否则将使用PHP自带的mb_convert_encoding方法,对于Unicode保留字符同样不能正常编码。

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment