后台提了一个需求,要求用户输入上传的内容中不能带 Emoji。网上有一些资料,都提到了过滤 Emoji 的方法,但都存在多过滤或少过滤的情况。我从官方的标准资料入手,希望能解决掉这个问题。
那我们先看看 Emoji 有什么特征。
Emoji 是什么
Emoji 就是可以在文字中输入的表情符。想必大家都用过:
??????????
看到这些图标,不用多说了吧。当前正式标准为 11.0 版本,Emoji 是 Unicode 的一部分,它在 Unicode 中有对应的码点( CodePoint),也就是说,Emoji 符号就是一个文字。
根据 Emoji 维基百科说明,当前版本中共有 1212 个 Emoji ,实际上这指的是单码点的 Emoji,而还有一些 Emoji 是通过多个码点组合而成。
例如"零宽度连接符"(ZERO WIDTH JOINER,缩写 ZWJ)U+200D
。将U+1F468:男人
U+1F469:女人
U+1F467:女孩
这三个码点使用U+200D
连接起来,U+1F468 U+200D U+1F469 U+200D U+1F467
,就会显示为一个 Emoji ????????,表示他们组成的家庭。如果用户的系统不支持这种方法,就还是显示为三个独立的 Emoji ??????。
例如如代表肤色的(U+1F3FB–U+1F3FF): ?? ?? ?? ?? ?? ,发色的(U+1F9B0-U+1F9B3),组合起来后得到同一个表情的不同肤色版本,这一特性在国际大厂的输入法上可以看到,例如 Apple、Google、Samsung 的输入法上都可以输入。
例如 U+1F1E8
U+1F1F3
组合起来成了中国国旗。
由于多码点组合的存在,可以显示的 Emoji 实际上数量多于 1212 个。
Emoji 的识别
一个字符串是否包含了 Emoji?通过上面的描述,我们可以想到,如果字符串中包含了 Emoji 的码点,那不就说明该字符串包含了 Emoji 吗?因此,我们先获取一份完整的码点集合用来判断。
Emoji 所有的码点
那 Emoji 的码点有哪些呢,Unicode 组织的 Unicode? Emoji Charts v11.0 页面中可以找到完整的 Emoji 码点数据:emoji-data.txt ,这个表的内容的解读可见参考资料。
数据经过以下脚本getEmojiData.sh
处理,可以得到一个完整的、有重复 的码表。
#! /bin/bash
cat "$1" |
grep -v ^# |
grep -v ^$ |
while read line
do
echo $line | cut -d \; -f 1
done
$ ./getEmojiData.sh emoji-data.txt > emoji-all-data.txt
得到的格式如下,数量数百行,列出来的码点不重复的有 3000 多个:
0023
002A
0030..0039
00A9
00AE
...
由于这个码点表有重复元素,我们选择将所有码点添加到 Set 集合中。代码如下(使用列编辑模式可快速编辑完成):
public class EmojiUtils {
private static final String TAG = EmojiUtils.class.getSimpleName();
private static Set<Character> emojiSignatureSet = new HashSet<>();
private EmojiUtils() {}
static {
// 省略……
addUnicodeToSet(emojiSignatureSet, 0x2122);
addUnicodeToSet(emojiSignatureSet, 0x2139);
addUnicodeToSet(emojiSignatureSet, 0x2194, 0x2199);
addUnicodeToSet(emojiSignatureSet, 0x21A9, 0x21AA);
// 省略……
}
private static void addUnicodeToSet(Set<Character> set, int code) {
if (set == null) {
return;
}
set.add((char) code);
}
private static void addUnicodeToSet(Set<Character> set, int codeStart, int codeEnd) {
if (set == null) {
return;
}
for (int i = codeStart; i <= codeEnd; i++) {
addUnicodeToSet(set, i);
}
}
}
初试 Emoji 识别
我们有了码表,识别方法就好说了,将字符串拆成单个字符,逐一判断是否是 Emoji 特征码点。
public static boolean isContainEmoji(String s) {
char[] chars = s.toCharArray();
int charsLength = chars.length;
for (int i = 0; i < charsLength; i++) {
char c = chars[i];
if (emojiSignatureSet.contains(c)) {
return true;
}
}
return false;
}
有了识别的方法 EmojiUtils.isContainEmoji(String s)
后,我们来实践一下,过滤掉输入的 Emoji:
etTest.setFilters(new InputFilter[]{new InputFilter() {
@Override
public CharSequence filter(CharSequence source, int start, int end, Spanned dest, int dstart, int dend) {
if (EmojiUtils.isContainEmoji(source.toString())) {
return "";
}
return source;
}
}});
运行起来,你会发现并……没……有……用…… ,想怎么输入就怎么输入。
难道这些码点不对吗?当然,这是 Unicode 标准提供的码点表,不可能不对,那一定是判断时出了问题,我们查看一下输入 Emoji 时输入的是什么字符。
Emoji 的表现形式
通过断点,可以看到,输入一个微笑的 Emoji,其内容实际上是 '\uD83D''\uDE03' ,好像离码点 0x1F603 有点远。实际上,这个输入的编码是特殊的。
Unicode 中计划使用 17 个平面,常用的编码都在第 0 平面中(关于 Unicode 更多知识可以从参考资料进行了解),而在第 0 平面中,有一个特殊的代理区域,不表示任何字符,只用于指向第 1 到第 16 个平面中的字符,这段区域是:D800—DFFF.。其中 0xD800—0xDBFF 是前导代理(lead surrogates),0xDC00—0xDFFF 是后尾代理(trail surrogates)。
它们的代理关系如下图所示:
因此具体的公式是:0x10000 + (前导-0xD800) * 0x400 + (后导-0xDC00) = utf-16编码
我们将微笑 Emoji 的字符串代入计算,结果是:0x10000+(0xD83D - 0xD800)*0x400 + (0xDE03-0xDC00) = 0x1F603
,与码点正好对应上了!
因此,我们需要修改一下判断方法:
public static boolean isContainEmoji(String s) {
char[] chars = s.toCharArray();
int charsLength = chars.length;
for (int i = 0; i < charsLength; i++) {
char c = chars[i];
char realChar = c;
if (c >= 0xD800 && c <= 0xDBFF && ++i < charsLength) {
char nextChar = chars[i];
realChar = (char) (0x10000 + (c - 0xD800) * 0x400 + (nextChar - 0xDC00));
}
if (emojiSignatureSet.contains(realChar)) {
return true;
}
}
return false;
}
修改过后,使用不同的输入法都尝试一下输入 Emoji,果然,全都被过滤了,无法输入。搞定收工!给自己输入一个666
!
嗯?我的 666 呢?这时候,你会发现,无法输入:数字、英文、@、# 等符号。果然没这么简单!
码点表中的奸细
显然,被多过滤掉了字符一定是因为码点集合太多了。仔细查看,终于发现了问题所在。
数字
0023 ; Emoji_Component # 1.1 [1] (#?) number sign
002A ; Emoji_Component # 1.1 [1] (*?) asterisk
0030..0039 ; Emoji_Component # 1.1 [10] (0?..9?) digit zero..digit nine
这些 # * 0~9 这些字符本身是正常的字符,但是它们搭配其他的特征码则变成了 Emoji。
因此,这些字符不能加入特征集合中。数字的问题解决了,还有字母的问题。
字母
根据 Tags (Unicode block) 和 emoji-sequences.txt
E0020 ~ E007F 的使用仅有
1F3F4 E0067 E0062 E0065 E006E E0067 E007F; Emoji_Tag_Sequence; England # 7.0 [1] (??)
1F3F4 E0067 E0062 E0073 E0063 E0074 E007F; Emoji_Tag_Sequence; Scotland # 7.0 [1] (??)
1F3F4 E0067 E0062 E0077 E006C E0073 E007F; Emoji_Tag_Sequence; Wales # 7.0 [1] (??)
而这个范围覆盖了 a~z 及一些符号,如果添加了反而会误判,或者仅添加 E007F 作为 Emoji 特征码
去掉这些奸细,终于可以愉快地输入了……吗?并没有。
还会发现不能输入中文的一些标点符号。再看看还有什么内容没必要添加的。
保留区域
在 emoji-data 中可以看到有一部分码点标记为 reserved,即当前保留着不用,例如<reserved-1F02C>..<reserved-1F02F>
,那么把这些保留区域去除是否就可以了呢,经过实验,去除之后确实就没问题了,特别是最后一行,保留区域1FA6E..1FFFD
个数达 1424 个,去除这个就可以正常输入中文字符了。
Emoji 的过滤
有了上面提到的特征码点集合,过滤和识别其实是一样的。
public static String filterEmoji(String s) {
StringBuilder sb = new StringBuilder();
char[] chars = s.toCharArray();
int charsLength = chars.length;
for (int i = 0; i < charsLength; i++) {
char c = chars[i];
char realChar = c;
if (c >= 0xD800 && c <= 0xDBFF && ++i < charsLength) {
char nextChar = chars[i];
realChar = (char) (0x10000 + (c - 0xD800) * 0x400 + (nextChar - 0xDC00));
}
if (!emojiSignatureSet.contains(realChar)) {
sb.append(c);
}
}
return sb.toString();
}
参考链接
通过一番探索,虽然对于字符编码相关的知识还不是特别清晰,但至少比以前了解得更多了。在实现过程中参考了诸多的网上的资料,如果我写得你觉得看得不甚了了,可以看看下面这些资料。
字符编码的奥秘utf-8, Unicode:Unicode 多平面的理解
iPhone emoji问题牵出的Unicode代理区的思考:代理区的转化识别
Android 准确过滤(禁止) Emoji表情:实现方案的参考来源
-
Unicode Emoji Data Files :对数据文件的解读可看
附:完整代码
以下是完整的代码,大家可以自行测试是否有问题,发现问题的话也麻烦反馈给我。
public class EmojiUtils {
private static final String TAG = EmojiUtils.class.getSimpleName();
private static Set<Character> emojiSignatureSet = new HashSet<>(1801);
private EmojiUtils() {}
public static boolean isContainEmoji(String s) {
char[] chars = s.toCharArray();
int charsLength = chars.length;
char currentChar;
char realChar;
for (int i = 0; i < charsLength; i++) {
currentChar = chars[i];
realChar = currentChar;
if (currentChar >= 0xD800 && currentChar <= 0xDBFF && (i + 1) < charsLength) {
char nextChar = chars[++i];
realChar = (char) (0x10000 + (currentChar - 0xD800) * 0x400 + (nextChar - 0xDC00));
}
if (emojiSignatureSet.contains(realChar)) {
return true;
}
}
return false;
}
public static String filterEmoji(String s) {
StringBuilder sb = new StringBuilder();
char[] chars = s.toCharArray();
int charsLength = chars.length;
char currentChar;
char realChar;
for (int i = 0; i < charsLength; i++) {
currentChar = chars[i];
realChar = currentChar;
if (currentChar >= 0xD800 && currentChar <= 0xDBFF && (i + 1) < charsLength) {
char nextChar = chars[++i];
realChar = (char) (0x10000 + (currentChar - 0xD800) * 0x400 + (nextChar - 0xDC00));
}
if (!emojiSignatureSet.contains(realChar)) {
sb.append(currentChar);
}
}
return sb.toString();
}
private static void addUnicodeToSet(Set<Character> set, int code) {
if (set == null) {
return;
}
set.add((char) code);
}
private static void addUnicodeToSet(Set<Character> set, int codeStart, int codeEnd) {
if (set == null) {
return;
}
for (int i = codeStart; i <= codeEnd; i++) {
addUnicodeToSet(set, i);
}
}
static {
Log.d(TAG, "init start:" + System.currentTimeMillis());
addUnicodeToSet(emojiSignatureSet, 0x007F);
addUnicodeToSet(emojiSignatureSet, 0x00A9);
addUnicodeToSet(emojiSignatureSet, 0x00AE);
addUnicodeToSet(emojiSignatureSet, 0x200D);
addUnicodeToSet(emojiSignatureSet, 0x203C);
addUnicodeToSet(emojiSignatureSet, 0x2049);
addUnicodeToSet(emojiSignatureSet, 0x20E3);
addUnicodeToSet(emojiSignatureSet, 0x2122);
addUnicodeToSet(emojiSignatureSet, 0x2139);
addUnicodeToSet(emojiSignatureSet, 0x2194, 0x2199);
addUnicodeToSet(emojiSignatureSet, 0x21A9, 0x21AA);
addUnicodeToSet(emojiSignatureSet, 0x231A, 0x231B);
addUnicodeToSet(emojiSignatureSet, 0x2328);
addUnicodeToSet(emojiSignatureSet, 0x2388);
addUnicodeToSet(emojiSignatureSet, 0x23CF);
addUnicodeToSet(emojiSignatureSet, 0x23E9, 0x23F3);
addUnicodeToSet(emojiSignatureSet, 0x23F8, 0x23FA);
addUnicodeToSet(emojiSignatureSet, 0x24C2);
addUnicodeToSet(emojiSignatureSet, 0x25AA, 0x25AB);
addUnicodeToSet(emojiSignatureSet, 0x25B6);
addUnicodeToSet(emojiSignatureSet, 0x25C0);
addUnicodeToSet(emojiSignatureSet, 0x25FB, 0x25FE);
addUnicodeToSet(emojiSignatureSet, 0x2600, 0x2605);
addUnicodeToSet(emojiSignatureSet, 0x2607, 0x2612);
addUnicodeToSet(emojiSignatureSet, 0x2614, 0x2685);
addUnicodeToSet(emojiSignatureSet, 0x2690, 0x2705);
addUnicodeToSet(emojiSignatureSet, 0x2708, 0x2712);
addUnicodeToSet(emojiSignatureSet, 0x2714);
addUnicodeToSet(emojiSignatureSet, 0x2716);
addUnicodeToSet(emojiSignatureSet, 0x271D);
addUnicodeToSet(emojiSignatureSet, 0x2721);
addUnicodeToSet(emojiSignatureSet, 0x2728);
addUnicodeToSet(emojiSignatureSet, 0x2733, 0x2734);
addUnicodeToSet(emojiSignatureSet, 0x2744);
addUnicodeToSet(emojiSignatureSet, 0x2747);
addUnicodeToSet(emojiSignatureSet, 0x274C);
addUnicodeToSet(emojiSignatureSet, 0x274E);
addUnicodeToSet(emojiSignatureSet, 0x2753, 0x2755);
addUnicodeToSet(emojiSignatureSet, 0x2757);
addUnicodeToSet(emojiSignatureSet, 0x2763, 0x2767);
addUnicodeToSet(emojiSignatureSet, 0x2795, 0x2797);
addUnicodeToSet(emojiSignatureSet, 0x27A1);
addUnicodeToSet(emojiSignatureSet, 0x27B0);
addUnicodeToSet(emojiSignatureSet, 0x27BF);
addUnicodeToSet(emojiSignatureSet, 0x2934, 0x2935);
addUnicodeToSet(emojiSignatureSet, 0x2B05, 0x2B07);
addUnicodeToSet(emojiSignatureSet, 0x2B1B, 0x2B1C);
addUnicodeToSet(emojiSignatureSet, 0x2B50);
addUnicodeToSet(emojiSignatureSet, 0x2B55);
addUnicodeToSet(emojiSignatureSet, 0x3030);
addUnicodeToSet(emojiSignatureSet, 0x303D);
addUnicodeToSet(emojiSignatureSet, 0x3297);
addUnicodeToSet(emojiSignatureSet, 0x3299);
addUnicodeToSet(emojiSignatureSet, 0xF000, 0xF02B);
addUnicodeToSet(emojiSignatureSet, 0xF030, 0xF093);
addUnicodeToSet(emojiSignatureSet, 0xF0A0, 0xF0AE);
addUnicodeToSet(emojiSignatureSet, 0xF0B1, 0xF0BF);
addUnicodeToSet(emojiSignatureSet, 0xF0C1, 0xF0CF);
addUnicodeToSet(emojiSignatureSet, 0xF0D1, 0xF0F5);
addUnicodeToSet(emojiSignatureSet, 0xF12F);
addUnicodeToSet(emojiSignatureSet, 0xF170, 0xF171);
addUnicodeToSet(emojiSignatureSet, 0xF17E, 0xF17F);
addUnicodeToSet(emojiSignatureSet, 0xF18E);
addUnicodeToSet(emojiSignatureSet, 0xF191, 0xF19A);
addUnicodeToSet(emojiSignatureSet, 0xF1E6, 0xF1FF);
addUnicodeToSet(emojiSignatureSet, 0xF201, 0xF202);
addUnicodeToSet(emojiSignatureSet, 0xF21A);
addUnicodeToSet(emojiSignatureSet, 0xF22F);
addUnicodeToSet(emojiSignatureSet, 0xF232, 0xF23A);
addUnicodeToSet(emojiSignatureSet, 0xF250, 0xF251);
addUnicodeToSet(emojiSignatureSet, 0xF260, 0xF265);
addUnicodeToSet(emojiSignatureSet, 0xF300, 0xF53D);
addUnicodeToSet(emojiSignatureSet, 0xF546, 0xF64F);
addUnicodeToSet(emojiSignatureSet, 0xF680, 0xF6D4);
addUnicodeToSet(emojiSignatureSet, 0xF6E0, 0xF6EC);
addUnicodeToSet(emojiSignatureSet, 0xF6F0, 0xF6F9);
addUnicodeToSet(emojiSignatureSet, 0xF7D5, 0xF7D8);
addUnicodeToSet(emojiSignatureSet, 0xF910, 0xF93A);
addUnicodeToSet(emojiSignatureSet, 0xF93C, 0xF93E);
addUnicodeToSet(emojiSignatureSet, 0xF940, 0xF945);
addUnicodeToSet(emojiSignatureSet, 0xF947, 0xF970);
addUnicodeToSet(emojiSignatureSet, 0xF973, 0xF976);
addUnicodeToSet(emojiSignatureSet, 0xF97A);
addUnicodeToSet(emojiSignatureSet, 0xF97C, 0xF9A2);
addUnicodeToSet(emojiSignatureSet, 0xF9B0, 0xF9B9);
addUnicodeToSet(emojiSignatureSet, 0xF9C0, 0xF9C2);
addUnicodeToSet(emojiSignatureSet, 0xF9D0, 0xF9FF);
addUnicodeToSet(emojiSignatureSet, 0xFA60, 0xFA6D);
addUnicodeToSet(emojiSignatureSet, 0xFE0E, 0xFE0F);
Log.d(TAG, "init end :" + System.currentTimeMillis());
Log.d(TAG, "set size: " + emojiSignatureSet.size());
}
}