2

Emoji表情处理二

 1 year ago
source link: https://www.haorooms.com/post/emoji_two
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
neoserver,ios ssh client

Emoji表情处理二

2023年5月6日 89次浏览

两个emoji表情可以拼接成一个emoji,这就给我们处理和截取字符串增加了难度,之前我有文章,截取emoji表情,具体可以看一下。

拼接规则如下:

👩 + ✈️ → 👩‍✈️
👨 + 💻 → 👨‍💻
👰 + ♂️ → 👰‍♂️
🐻 + ❄️ → 🐻‍❄️
🏴 + ☠️ → 🏴‍☠️
🏳️ + 🌈 → 🏳️‍🌈
👨 + 🦰 → 👨‍🦰 (有意思的是:发色是通过 ZWJ 组合基础 emoji 实现,而肤色则是用肤色修饰符实现)
👨🏻 + 🤝 + 👨🏼 → 👨🏻‍🤝‍👨🏼
👨 + ❤️ + 👨 → 👨‍❤️‍👨
👨 + ❤️ + 💋 + 👨 → 👨‍❤️‍💋‍👨
👨 + 👨 + 👧 → 👨‍👨‍👧
👨 + 👨 + 👧 + 👧 → 👨‍👨‍👧‍👧

具体可以看https://unicode.org/Public/emoji/15.0/emoji-zwj-sequences.txt

可惜,有些 emoji 不是通过 ZWJ 组全 emoji 实现的,可能是因为没有赶上 ZWJ 定义的时机

🌂 + 🌧 ≠ ☔️
💄 + 👄 ≠ 💋
🐴 + 🌈 ≠ 🦄
👁 + 👁 ≠ 👀
👨 + 💀 ≠ 🧟
👩 + 🔍 ≠ 🕵️‍♀️
🦵 + 🦵 + 💪 + 💪 + 👂 + 👂 + 👃 + 👅 + 👀 + 🧠 ≠ 🧍

emoji 有特殊的修饰、拼接等规则

通过上面的描述,我们可以看到,在某些 emoji 字符后增加一个肤色修饰符就可以改变 emoji 的肤色、可以将多个 emoji 通过连接符拼接成一个emoji,这些特殊规则使得在代码中判定 emoji 的长度、截取和对 emoji 做其他处理都比较困难。需要澄清的是:用一个 Unicode 字符修饰前一个字符不是 emoji 独有的,其他 Unicode 字符也存在,如:Ü,由大写字母U(U+0055),后面跟一个连音符号(U+0308)组成。

基本 emoji 和常用 Unicode 字符毫无区别

每个 emoji 用对应一个 Unicode 码位,如:🌔 U+1F314 (对应 JS 中 UTF-16 编码是:"\uD83C\uDF14"),汉字 𠇔 U+201D4,对应 JS 中的 UTF-16 编码是"\uD840\uDDD4"

emoji 引入的问题

'中国人123'.length = 6,'工作中👨‍💻'.length = 8

emoji 给编程带来的主要问题是视觉上看到的字符长度(后文称作视觉 length)与代码中获取的长度(后文称作技术 length)不相同,使得字符串截取等操作返回非预期内的结果,如:

'工作中👨‍💻'.substr(0,5) => '工作中👨','工作中👨‍💻'.substr(5)' =>  '‍💻'

本质上在 emoji 出现之前 Unicode 编码就遇到了该问题,只不过 emoji 的普及让该问题更普遍。有的 emoji 长度为 1,有的长度可以达到 15。问题的根源是 Unicode 中可以用多个码点表示一个 emoji,如果所有 emoji 都用一个 Unicode 码点表示就不存在该问题。

相关正则及代码

上一篇文章已经列举了emoji 的正则,本文补充一些。

/**
 * 字素簇截取、拆分
 */
// 是基于unicode15.0中的emoji标准,返回识别字符串中emoji的正则表达式
const regionalIndicator = '\uD83C[\uDDE6-\uDDFF]';
// 地区匹配
const emojiFlagSequence = `${regionalIndicator}${regionalIndicator}`;
// emoji匹配
const emojiCharacter = '[\u231A-\u231B]|[\u23E9-\u23EC]|\u23F0|\u23F3|[\u25FD-\u25FE]|[\u2614-\u2615]|[\u2648-\u2653]|\u267F|\u2693|\u26A1|[\u26AA-\u26AB]|[\u26BD-\u26BE]|[\u26C4-\u26C5]|\u26CE|\u26D4|\u26EA|[\u26F2-\u26F3]|\u26F5|\u26FA|\u26FD|\u2705|[\u270A-\u270B]|\u2728|\u274C|\u274E|[\u2753-\u2755]|\u2757|[\u2795-\u2797]|\u27B0|\u27BF|[\u2B1B-\u2B1C]|\u2B50|\u2B55|\uD83C[\uDC04]|\uD83C[\uDCCF]|\uD83C[\uDD8E]|\uD83C[\uDD91-\uDD9A]|\uD83C[\uDE01]|\uD83C[\uDE1A]|\uD83C[\uDE2F]|\uD83C[\uDE32-\uDE36]|\uD83C[\uDE38-\uDE3A]|\uD83C[\uDE50-\uDE51]|\uD83C[\uDF00-\uDF20]|\uD83C[\uDF2D-\uDF35]|\uD83C[\uDF37-\uDF7C]|\uD83C[\uDF7E-\uDF93]|\uD83C[\uDFA0-\uDFCA]|\uD83C[\uDFCF-\uDFD3]|\uD83C[\uDFE0-\uDFF0]|\uD83C[\uDFF4]|\uD83C[\uDFF8-\uDFFF]|\uD83D[\uDC00-\uDC3E]|\uD83D[\uDC40]|\uD83D[\uDC42-\uDCFC]|\uD83D[\uDCFF-\uDD3D]|\uD83D[\uDD4B-\uDD4E]|\uD83D[\uDD50-\uDD67]|\uD83D[\uDD7A]|\uD83D[\uDD95-\uDD96]|\uD83D[\uDDA4]|\uD83D[\uDDFB-\uDE4F]|\uD83D[\uDE80-\uDEC5]|\uD83D[\uDECC]|\uD83D[\uDED0-\uDED2]|\uD83D[\uDED5-\uDED7]|\uD83D[\uDEDC-\uDEDF]|\uD83D[\uDEEB-\uDEEC]|\uD83D[\uDEF4-\uDEFC]|\uD83D[\uDFE0-\uDFEB]|\uD83D[\uDFF0]|\uD83E[\uDD0C-\uDD3A]|\uD83E[\uDD3C-\uDD45]|\uD83E[\uDD47-\uDDFF]|\uD83E[\uDE70-\uDE7C]|\uD83E[\uDE80-\uDEBD]|\uD83E[\uDEBF-\uDEC5]|\uD83E[\uDECE-\uDEDB]|\uD83E[\uDEE0-\uDEE8]|\uD83E[\uDEF0-\uDEF8]';
// 肤色匹配
const emojiModifier = '\uD83C[\uDFFB-\uDFFF]';
// 匹配变形符号
const emojiPresentationSelector = '\uFE0F';
// 匹配键帽
const keycapSymbol = '\u20E3';
// 匹配标签符号
const tagSymbol = '(\uDB40[\uDC20-\uDC7E])';
// 匹配结束标签符号
const termTagSymbol = '\uDB40\uDC7F';
// 匹配零宽字符
const zeroWidthJoiner = '\u200D';

// 键帽变体序列
const kVSequence = `[#*0-9]\uFE0F?\u20E3`;
// 变体基础字符
const variationSequence = '\u00A9|\u00AE|\u203C|\u2049|\u2122|\u2139|\u2194|\u2195|\u2196|\u2197|\u2198|\u2199|\u21A9|\u21AA|\u2328|\u23CF|\u23ED|\u23EE|\u23EF|\u23F1|\u23F2|\u23F8|\u23F9|\u23FA|\u24C2|\u25AA|\u25AB|\u25B6|\u25C0|\u25FB|\u25FC|\u2600|\u2601|\u2602|\u2603|\u2604|\u260E|\u2611|\u2618|\u261D|\u2620|\u2622|\u2623|\u2626|\u262A|\u262E|\u262F|\u2638|\u2639|\u263A|\u2640|\u2642|\u265F|\u2660|\u2663|\u2665|\u2666|\u2668|\u267B|\u267E|\u2692|\u2694|\u2695|\u2696|\u2697|\u2699|\u269B|\u269C|\u26A0|\u26A7|\u26B0|\u26B1|\u26C8|\u26CF|\u26D1|\u26D3|\u26E9|\u26F0|\u26F1|\u26F4|\u26F7|\u26F8|\u26F9|\u2702|\u2708|\u2709|\u270C|\u270D|\u270F|\u2712|\u2714|\u2716|\u271D|\u2721|\u2733|\u2734|\u2744|\u2747|\u2763|\u2764|\u27A1|\u2934|\u2935|\u2B05|\u2B06|\u2B07|\u3030|\u303D|\u3297|\u3299|\uD83C[\uDD70-\uDD71]|\uD83C[\uDD7E-\uDD7F]|\uD83C[\uDE02]|\uD83C[\uDE37]|\uD83C[\uDF21]|\uD83C[\uDF24-\uDF2C]|\uD83C[\uDF36]|\uD83C[\uDF7D]|\uD83C[\uDF96-\uDF97]|\uD83C[\uDF99-\uDF9B]|\uD83C[\uDF9E-\uDF9F]|\uD83C[\uDFCB-\uDFCE]|\uD83C[\uDFD4-\uDFDF]|\uD83C[\uDFF3]|\uD83C[\uDFF5]|\uD83C[\uDFF7]|\uD83D[\uDC3F]|\uD83D[\uDC41]|\uD83D[\uDCFD]|\uD83D[\uDD49-\uDD4A]|\uD83D[\uDD6F]|\uD83D[\uDD70]|\uD83D[\uDD73-\uDD79]|\uD83D[\uDD87]|\uD83D[\uDD8A-\uDD8D]|\uD83D[\uDD90]|\uD83D[\uDDA5]|\uD83D[\uDDA8]|\uD83D[\uDDB1]|\uD83D[\uDDB2]|\uD83D[\uDDBC]|\uD83D[\uDDC2-\uDDC4]|\uD83D[\uDDD1-\uDDD3]|\uD83D[\uDDDC-\uDDDE]|\uD83D[\uDDE1]|\uD83D[\uDDE3]|\uD83D[\uDDE8]|\uD83D[\uDDEF]|\uD83D[\uDDF3]|\uD83D[\uDDFA]|\uD83D[\uDECB]|\uD83D[\uDECD-\uDECF]|\uD83D[\uDEE0-\uDEE5]|\uD83D[\uDEE9]|\uD83D[\uDEF0]|\uD83D[\uDEF3]';

const emojiZwjElement = `(${emojiCharacter}|${variationSequence})(${emojiModifier}|${emojiPresentationSelector}${keycapSymbol}?|${tagSymbol}+${termTagSymbol}?)?`;
const myRegExp = `${emojiFlagSequence}|${kVSequence}|${emojiZwjElement}(${zeroWidthJoiner}${emojiZwjElement})*`;

/**
 * 识别emoji的正则
 * @param {string} modifier 正则修饰符,比如: 'g'
 * @returns 一段正则。
 *
 * const reg1 = getRegExpEmoji(); // 正则:/\p{emoji}/
 * reg1.test('😊'); // true
 *
 * const reg2 = getRegExpEmoji('g'); // 正则:/\p{emoji}/g
 * '😊😊😊'.match(reg2); // ['😊', '😊', '😊']
 */
const getRegExpEmoji = (modifier?: string) => {
  try {
    const _reg = new RegExp(`${myRegExp}`, `${modifier || ''}`);
    return _reg;
  } catch (e) {
    console.error('getRegExpEmoji提示:该系统不支持new RegExp', e);
    return null;
  }
};

/**
 * 判断一段字符串中是否有emoji
 *
 * @param {string} str 需要检查的字符串
 * @returns boolean 是否有emoji,true: 有,false: 没有
 *
 * hasEmoji('123123123'); // false
 * hasEmoji('12312🇭🇰3123'); // true
 */
const hasEmoji = (str: string): boolean => {
  const _reg = new RegExp(`${myRegExp}`);
  return _reg ? _reg.test(str) : false;
};

/**
 * 匹配一段字符串中的emoji
 * @param {string} str 需要匹配的字符串;
 * @param {string} modifier  new RegExp的修饰符,传g的话就会匹配所有的emoji出来,不传的话只会匹配一个
 * @returns 匹配中的emoji。
 *
 * matchEmoji('123123123'); // null
 *
 * matchEmoji('12312🇭🇰3123'); // ['🇭🇰']
 *
 * matchEmoji('12312🇭🇰31🇭🇰23', 'g'); // ['🇭🇰', '🇭🇰']
 */
const matchEmoji = (str: string, modifier?: string) => {
  const _reg = getRegExpEmoji(modifier);
  try {
    return _reg ? str.match(_reg) : null;
  } catch (e) {
    console.error('matchEmoji提示:该系统不支持new RegExp', e);
    return null;
  }
};

https://unicode.org/emoji/charts/full-emoji-list.html

https://unicode.org/Public/emoji/15.0/


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK