Emoji 识别与过滤

后台提了一个需求,要求用户输入上传的内容中不能带 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();
}

参考链接

通过一番探索,虽然对于字符编码相关的知识还不是特别清晰,但至少比以前了解得更多了。在实现过程中参考了诸多的网上的资料,如果我写得你觉得看得不甚了了,可以看看下面这些资料。

附:完整代码

以下是完整的代码,大家可以自行测试是否有问题,发现问题的话也麻烦反馈给我。

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());
    }
}

推荐阅读更多精彩内容