Emoji的结构比想象的要复杂一些,一个简单的要求是从一段包含Emoji表情符号的文本中提取出Emoji,我以前的做法是拿几千个Emoji按照从长到短排序后循环比对,这样基本上可以实现需求,不过运算量很大,在单个Emoji的HTML页面中循环几千次的耗时还是可以忍受的(与数据库读写时间的零点几到几毫秒、网络传输时间的几十到几百毫秒比起来,PHP或者Python程序字符串循环几千次总的耗时零点几毫秒基本可以忽略),但如果很多句子要这样处理,总的耗时量还是非常大的。
前一阵子我们同事在Emoji标签云中运用人工智能进行升级的过程中,使用了正则表达式来对推文中的Emoji进行匹配,因为推文数量巨大,整个字符串处理的过程耗时非常长,需要好多天时间的不间断运算才能处理好。我问了同事他使用的正则表达式中是有几千个Emoji组成“或”的关系,这样其实也快不起来,正则也需要进行循环比对,我就让同事采取我以前用过的几千个Emoji排序后循环比对的办法试一试,实验结果是比以前的正则匹配快一倍。
但快一倍还是需要好几天的时间来计算一种语言的推文的标签云,我们上周又专门购买了AMD Ryzen9 5900X(五代锐龙9台式机处理器,采用全新ZEN 3微架构,台积电7nm工艺,12核24线程,默认主频3.7GHz,睿频4.8GHz,三级缓存64MB,默认TDP为105W)为CPU的台式机来加快处理速度,这样一台机器相当于我们以前的4线程机器乘以6台,处理一种语言的时间缩短到一天了!
尽管如此,我感觉还有提速潜力,给同事推荐了Python编译加速的办法参考,另外再花了时间专门来梳理数据处理流程,将各个环节的耗时都计算出来,最后目光集中在一个循环上,也就是几千个Emoji循环比对的过程耗时是最多的。硬件、编程语言软件的改进是有帮助的,但最大的潜力还在算法上,我们觉得可以在这方面多想办法。
在网上搜索了很多关于Emoji匹配的文章,中文几乎都没有什么有帮助的,英文的信息也繁杂、无定论。我想到以前看到Unicode官方网站中Emoji的文档中有关于Emoji的定义和正则表达式,于是去查看了Unicode® Technical Standard #51 UNICODE EMOJI,其中 EBNF and Regex正则表达式是这样的:
\p{RI} \p{RI} | \p{Emoji} ( \p{EMod} | \x{FE0F} \x{20E3}? | [\x{E0020}-\x{E007E}]+ \x{E007F} )? (\x{200D} \p{Emoji} ( \p{EMod} | \x{FE0F} \x{20E3}? | [\x{E0020}-\x{E007E}]+ \x{E007F} )? )*
这个写法其实是不能直接使用的,其中有几个\p开头的部分是需要另外引用定义的:
- \p{RI} 地区指示符,参看:ED-14. emoji flag sequence,1F1E6(🇦 regional indicator symbol letter a) - 1F1FF(🇿 regional indicator symbol letter z)
- \p{Emoji} Emoji字符,参看:ED-3. emoji character,emoji-data.txt文件中emoji属性的1404个(14.0)
- \p{EMod} Emoji修饰符,参看:ED-11. emoji modifier,5种skin-tone:1F3FB-1F3FF
,4种hair-style:1F9B0-1F9B0
还有几个\x开头的16进制数的含义我也记录如下:
- \x{FE0F} Emoji变体符号,参看:ED-9. emoji presentation selector
- \x{20E3} 键帽符号,参看:ED-14c. emoji keycap sequence
- \x{E0020}-\x{E007E} 标签符号,参看:ED-14a. emoji tag sequence (ETS)
- \x{E007F} 结束标签符号,参看:ED-14a. emoji tag sequence (ETS)
- \x{200D} 零宽连接符,参看:ED-16. emoji zwj sequence
为了对这个正则表达式理解更清晰,我们对其进行了简化:
- 正则表达式的含义:国旗 1个 | (某种Emoji组合) 1个 (零宽连接 某种Emoji组合) 0-n个
- “某种Emoji组合”的含义:Emoji字符 1个 (Emoji修饰符 1个| Emoji变体符号 1个 键帽符号 0-1个 | 标签符号 1-n个 结束标签符号 1个) 0-1个
分析到这个程度,就可以再结合我们以前对Emoji的了解知识来把正则表达式完整写出来。
我们同事把这个正则表达式写出来后,替换几千个Emoji循环的程序,只需要以前1/10的时间就可以处理同样数量的推特数据,效率提升一个数量级❗️
我先记录一下,等后面有空了,我也再把以前Emoji页面中用到的循环匹配PHP程序改为正则表达式匹配。
2022年7月5日补充,同事在网上找了很多Emoji正则表达式的写法,似乎都有一些缺陷,例如判断不了最新的Emoji 14.0符号,今天干脆花了一些时间把上面的办法进行代码实施,下面是PHP代码:
$regex_regional_indicator = '\x{1F1E6}-\x{1F1FF}'; $regex_emoji_character = '\x{0023}|\x{002A}|\x{0030}-\x{0039}|\x{00A9}|\x{00AE}|\x{203C}|\x{2049}|\x{2122}|\x{2139}|\x{2194}-\x{2199}|\x{21A9}-\x{21AA}|\x{231A}-\x{231B}|\x{2328}|\x{23CF}|\x{23E9}-\x{23EC}|\x{23ED}-\x{23EE}|\x{23EF}|\x{23F0}|\x{23F1}-\x{23F2}|\x{23F3}|\x{23F8}-\x{23FA}|\x{24C2}|\x{25AA}-\x{25AB}|\x{25B6}|\x{25C0}|\x{25FB}-\x{25FE}|\x{2600}-\x{2601}|\x{2602}-\x{2603}|\x{2604}|\x{260E}|\x{2611}|\x{2614}-\x{2615}|\x{2618}|\x{261D}|\x{2620}|\x{2622}-\x{2623}|\x{2626}|\x{262A}|\x{262E}|\x{262F}|\x{2638}-\x{2639}|\x{263A}|\x{2640}|\x{2642}|\x{2648}-\x{2653}|\x{265F}|\x{2660}|\x{2663}|\x{2665}-\x{2666}|\x{2668}|\x{267B}|\x{267E}|\x{267F}|\x{2692}|\x{2693}|\x{2694}|\x{2695}|\x{2696}-\x{2697}|\x{2699}|\x{269B}-\x{269C}|\x{26A0}-\x{26A1}|\x{26A7}|\x{26AA}-\x{26AB}|\x{26B0}-\x{26B1}|\x{26BD}-\x{26BE}|\x{26C4}-\x{26C5}|\x{26C8}|\x{26CE}|\x{26CF}|\x{26D1}|\x{26D3}|\x{26D4}|\x{26E9}|\x{26EA}|\x{26F0}-\x{26F1}|\x{26F2}-\x{26F3}|\x{26F4}|\x{26F5}|\x{26F7}-\x{26F9}|\x{26FA}|\x{26FD}|\x{2702}|\x{2705}|\x{2708}-\x{270C}|\x{270D}|\x{270F}|\x{2712}|\x{2714}|\x{2716}|\x{271D}|\x{2721}|\x{2728}|\x{2733}-\x{2734}|\x{2744}|\x{2747}|\x{274C}|\x{274E}|\x{2753}-\x{2755}|\x{2757}|\x{2763}|\x{2764}|\x{2795}-\x{2797}|\x{27A1}|\x{27B0}|\x{27BF}|\x{2934}-\x{2935}|\x{2B05}-\x{2B07}|\x{2B1B}-\x{2B1C}|\x{2B50}|\x{2B55}|\x{3030}|\x{303D}|\x{3297}|\x{3299}|\x{1F004}|\x{1F0CF}|\x{1F170}-\x{1F171}|\x{1F17E}-\x{1F17F}|\x{1F18E}|\x{1F191}-\x{1F19A}|\x{1F1E6}-\x{1F1FF}|\x{1F201}-\x{1F202}|\x{1F21A}|\x{1F22F}|\x{1F232}-\x{1F23A}|\x{1F250}-\x{1F251}|\x{1F300}-\x{1F30C}|\x{1F30D}-\x{1F30E}|\x{1F30F}|\x{1F310}|\x{1F311}|\x{1F312}|\x{1F313}-\x{1F315}|\x{1F316}-\x{1F318}|\x{1F319}|\x{1F31A}|\x{1F31B}|\x{1F31C}|\x{1F31D}-\x{1F31E}|\x{1F31F}-\x{1F320}|\x{1F321}|\x{1F324}-\x{1F32C}|\x{1F32D}-\x{1F32F}|\x{1F330}-\x{1F331}|\x{1F332}-\x{1F333}|\x{1F334}-\x{1F335}|\x{1F336}|\x{1F337}-\x{1F34A}|\x{1F34B}|\x{1F34C}-\x{1F34F}|\x{1F350}|\x{1F351}-\x{1F37B}|\x{1F37C}|\x{1F37D}|\x{1F37E}-\x{1F37F}|\x{1F380}-\x{1F393}|\x{1F396}-\x{1F397}|\x{1F399}-\x{1F39B}|\x{1F39E}-\x{1F39F}|\x{1F3A0}-\x{1F3C4}|\x{1F3C5}|\x{1F3C6}|\x{1F3C7}|\x{1F3C8}|\x{1F3C9}|\x{1F3CA}|\x{1F3CB}-\x{1F3CE}|\x{1F3CF}-\x{1F3D3}|\x{1F3D4}-\x{1F3DF}|\x{1F3E0}-\x{1F3E3}|\x{1F3E4}|\x{1F3E5}-\x{1F3F0}|\x{1F3F3}|\x{1F3F4}|\x{1F3F5}|\x{1F3F7}|\x{1F3F8}-\x{1F407}|\x{1F408}|\x{1F409}-\x{1F40B}|\x{1F40C}-\x{1F40E}|\x{1F40F}-\x{1F410}|\x{1F411}-\x{1F412}|\x{1F413}|\x{1F414}|\x{1F415}|\x{1F416}|\x{1F417}-\x{1F429}|\x{1F42A}|\x{1F42B}-\x{1F43E}|\x{1F43F}|\x{1F440}|\x{1F441}|\x{1F442}-\x{1F464}|\x{1F465}|\x{1F466}-\x{1F46B}|\x{1F46C}-\x{1F46D}|\x{1F46E}-\x{1F4AC}|\x{1F4AD}|\x{1F4AE}-\x{1F4B5}|\x{1F4B6}-\x{1F4B7}|\x{1F4B8}-\x{1F4EB}|\x{1F4EC}-\x{1F4ED}|\x{1F4EE}|\x{1F4EF}|\x{1F4F0}-\x{1F4F4}|\x{1F4F5}|\x{1F4F6}-\x{1F4F7}|\x{1F4F8}|\x{1F4F9}-\x{1F4FC}|\x{1F4FD}|\x{1F4FF}-\x{1F502}|\x{1F503}|\x{1F504}-\x{1F507}|\x{1F508}|\x{1F509}|\x{1F50A}-\x{1F514}|\x{1F515}|\x{1F516}-\x{1F52B}|\x{1F52C}-\x{1F52D}|\x{1F52E}-\x{1F53D}|\x{1F549}-\x{1F54A}|\x{1F54B}-\x{1F54E}|\x{1F550}-\x{1F55B}|\x{1F55C}-\x{1F567}|\x{1F56F}-\x{1F570}|\x{1F573}-\x{1F579}|\x{1F57A}|\x{1F587}|\x{1F58A}-\x{1F58D}|\x{1F590}|\x{1F595}-\x{1F596}|\x{1F5A4}|\x{1F5A5}|\x{1F5A8}|\x{1F5B1}-\x{1F5B2}|\x{1F5BC}|\x{1F5C2}-\x{1F5C4}|\x{1F5D1}-\x{1F5D3}|\x{1F5DC}-\x{1F5DE}|\x{1F5E1}|\x{1F5E3}|\x{1F5E8}|\x{1F5EF}|\x{1F5F3}|\x{1F5FA}|\x{1F5FB}-\x{1F5FF}|\x{1F600}|\x{1F601}-\x{1F606}|\x{1F607}-\x{1F608}|\x{1F609}-\x{1F60D}|\x{1F60E}|\x{1F60F}|\x{1F610}|\x{1F611}|\x{1F612}-\x{1F614}|\x{1F615}|\x{1F616}|\x{1F617}|\x{1F618}|\x{1F619}|\x{1F61A}|\x{1F61B}|\x{1F61C}-\x{1F61E}|\x{1F61F}|\x{1F620}-\x{1F625}|\x{1F626}-\x{1F627}|\x{1F628}-\x{1F62B}|\x{1F62C}|\x{1F62D}|\x{1F62E}-\x{1F62F}|\x{1F630}-\x{1F633}|\x{1F634}|\x{1F635}|\x{1F636}|\x{1F637}-\x{1F640}|\x{1F641}-\x{1F644}|\x{1F645}-\x{1F64F}|\x{1F680}|\x{1F681}-\x{1F682}|\x{1F683}-\x{1F685}|\x{1F686}|\x{1F687}|\x{1F688}|\x{1F689}|\x{1F68A}-\x{1F68B}|\x{1F68C}|\x{1F68D}|\x{1F68E}|\x{1F68F}|\x{1F690}|\x{1F691}-\x{1F693}|\x{1F694}|\x{1F695}|\x{1F696}|\x{1F697}|\x{1F698}|\x{1F699}-\x{1F69A}|\x{1F69B}-\x{1F6A1}|\x{1F6A2}|\x{1F6A3}|\x{1F6A4}-\x{1F6A5}|\x{1F6A6}|\x{1F6A7}-\x{1F6AD}|\x{1F6AE}-\x{1F6B1}|\x{1F6B2}|\x{1F6B3}-\x{1F6B5}|\x{1F6B6}|\x{1F6B7}-\x{1F6B8}|\x{1F6B9}-\x{1F6BE}|\x{1F6BF}|\x{1F6C0}|\x{1F6C1}-\x{1F6C5}|\x{1F6CB}|\x{1F6CC}|\x{1F6CD}-\x{1F6CF}|\x{1F6D0}|\x{1F6D1}-\x{1F6D2}|\x{1F6D5}|\x{1F6D6}-\x{1F6D7}|\x{1F6DD}-\x{1F6DF}|\x{1F6E0}-\x{1F6E5}|\x{1F6E9}|\x{1F6EB}-\x{1F6EC}|\x{1F6F0}|\x{1F6F3}|\x{1F6F4}-\x{1F6F6}|\x{1F6F7}-\x{1F6F8}|\x{1F6F9}|\x{1F6FA}|\x{1F6FB}-\x{1F6FC}|\x{1F7E0}-\x{1F7EB}|\x{1F7F0}|\x{1F90C}|\x{1F90D}-\x{1F90F}|\x{1F910}-\x{1F918}|\x{1F919}-\x{1F91E}|\x{1F91F}|\x{1F920}-\x{1F927}|\x{1F928}-\x{1F92F}|\x{1F930}|\x{1F931}-\x{1F932}|\x{1F933}-\x{1F93A}|\x{1F93C}-\x{1F93E}|\x{1F93F}|\x{1F940}-\x{1F945}|\x{1F947}-\x{1F94B}|\x{1F94C}|\x{1F94D}-\x{1F94F}|\x{1F950}-\x{1F95E}|\x{1F95F}-\x{1F96B}|\x{1F96C}-\x{1F970}|\x{1F971}|\x{1F972}|\x{1F973}-\x{1F976}|\x{1F977}-\x{1F978}|\x{1F979}|\x{1F97A}|\x{1F97B}|\x{1F97C}-\x{1F97F}|\x{1F980}-\x{1F984}|\x{1F985}-\x{1F991}|\x{1F992}-\x{1F997}|\x{1F998}-\x{1F9A2}|\x{1F9A3}-\x{1F9A4}|\x{1F9A5}-\x{1F9AA}|\x{1F9AB}-\x{1F9AD}|\x{1F9AE}-\x{1F9AF}|\x{1F9B0}-\x{1F9B9}|\x{1F9BA}-\x{1F9BF}|\x{1F9C0}|\x{1F9C1}-\x{1F9C2}|\x{1F9C3}-\x{1F9CA}|\x{1F9CB}|\x{1F9CC}|\x{1F9CD}-\x{1F9CF}|\x{1F9D0}-\x{1F9E6}|\x{1F9E7}-\x{1F9FF}|\x{1FA70}-\x{1FA73}|\x{1FA74}|\x{1FA78}-\x{1FA7A}|\x{1FA7B}-\x{1FA7C}|\x{1FA80}-\x{1FA82}|\x{1FA83}-\x{1FA86}|\x{1FA90}-\x{1FA95}|\x{1FA96}-\x{1FAA8}|\x{1FAA9}-\x{1FAAC}|\x{1FAB0}-\x{1FAB6}|\x{1FAB7}-\x{1FABA}|\x{1FAC0}-\x{1FAC2}|\x{1FAC3}-\x{1FAC5}|\x{1FAD0}-\x{1FAD6}|\x{1FAD7}-\x{1FAD9}|\x{1FAE0}-\x{1FAE7}|\x{1FAF0}-\x{1FAF6}'; $regex_emoji_modifier = '\x{1F3FB}-\x{1F3FF}'; $regex_emoji_presentation_selector = '\x{FE0F}'; $regex_keycap_symbol = '\x{20E3}'; $regex_tag_symbol = '\x{E0020}-\x{E007E}'; $regex_term_tag_symbol = '\x{E007F}'; $regex_zero_width_joiner = '\x{200D}'; $regex_emoji_flag_sequence = "[{$regex_regional_indicator}][{$regex_regional_indicator}]"; $regex_emoji_zwj_element = "[{$regex_emoji_character}]([{$regex_emoji_modifier}]|{$regex_emoji_presentation_selector}{$regex_keycap_symbol}?|[{$regex_tag_symbol}]+{$regex_term_tag_symbol}?)?"; $regex_emoji = "/{$regex_emoji_flag_sequence}|{$regex_emoji_zwj_element}({$regex_zero_width_joiner}{$regex_emoji_zwj_element})*/u";
上面是分了好几个中间步骤来把正则表达式拼起来的,同事进行了测试,纠正了部分写法。
评论2
非常感谢您的辛苦付出,您的劳动成果对我而言真的很有用
非常感谢您的辛苦付出,您的劳动成果对我而言真的很有用。emoji在unicode里实在太散乱了,又没有统一的录入标准,在各个系统上的展示效果五花八门,实在让人劳神我们自己搞这个emoji正则表达式也确实花费不少时间精力
我们自己搞这个emoji正则表达式也确实花费不少时间精力,所以整理出来希望对其他有类似需求的朋友有帮助,欢迎交流🤝