Viki

Viki 写东西的地方

努力上进且优秀
github
email
x
steam
bilibili
douban

emojiによる文字分割問題が引き起こすemojiの再認識

文章の内容が多いため、解決策を探している場合は直接文末にスクロールしてください。

emoji については皆さんも馴染みがあると思います。これはウェブサイトやチャットで広く使用されている絵文字で、例えば 😂, 😄 などがあります。

emoji は合法的な文字列コンテンツですが、その直感に反する長さやタイプの多様性のため、分割時に予期しない結果が生じることがあります。例えば、以下の例を見てみましょう:

'😃⛔'.split('') // ['\uD83D', '\uDE03', '⛔']

え?どうして二つの記号が分割されて三つになったの?しかも文字化けしてる?

焦らないで、まずは彼らの長さを見てみましょう。

'⛔'.length // 1
'😃'.length // 2
'👦🏾'.length // 4
'🏳️‍🌈'.length // 6
'👨‍👨‍👧‍👧'.length // 11

これはひどい、見なければよかった。見るほどにおかしい。いったいどういうこと?なぜ emoji を分割すると、一部の emoji は文字化けし、他のものはそうならないのか?なぜ emoji の長さは 1 ではないのか?

これらの疑問を持ちながら、さらに見ていきましょう。

emoji の再認識

絵文字(日本語:絵文字 / えもじ)、つまりこの記事で言う emoji は、日本で無線通信に使用される視覚的感情記号です。中国では、emoji は通常「小黄脸」と呼ばれるか、直接 emoji と呼ばれます。Apple が iOS 5 の入力方式に emoji を追加して以来、この絵文字は世界中に広まり、emoji はほとんどの現代のコンピュータシステムで互換性のある Unicode コードに採用され、さまざまな携帯電話のメッセージやソーシャルネットワークで広く使用されています。

2010 年 10 月にリリースされた Unicode 6.0 版では、初めて絵文字のコードが収録され、Unicode ブロックを通じて異なる emoji を区別しています。

これらの一般的な emoji 表情に加えて、Unicode 8.0 では 5 つの修飾子が追加されました: 🏻 🏼 🏽 🏾 🏿。これらは一部の人の emoji 表情の後ろに追加され、人型表情の肌の色を調整するために使用されます。これらは フィッツパトリック修飾子 と呼ばれ、人間の肌の色の分類に対応しています。

例えば:👦 👦🏻 👦🏼 👦🏽 👦🏾 👦🏿 と 👧 👧🏻 👧🏼 👧🏽 👧🏾 👧🏿。

さらに、組み合わせによって生成される emoji もあります。例えば、U+200D ゼロ幅連結子 (ZWJ) を使用して二つの絵文字を連結し、一つの絵文字のように見せることができます(例:👨‍👩‍👧)。これがシステムでサポートされている場合、男性と女性、そして女の子からなる家族の絵文字として表示されますが、サポートされていないシステムではこれら三つの絵文字 (👨👩👧) が順に表示されます。また、男女の組み合わせ emoji もあり、例えば女性の emoji 表情にゼロ幅連結子と ♂ を加えると男性版になります。

Unicode における emoji の規範基準についてはこちらを参照してください。Unicode 絵文字キャラクターとシーケンスの構造を定義し、その構造をサポートするデータを提供しています。

以上のような emoji の多様性のため、通常の文字列として分割処理を行うと、分割結果が直感に反することがあります。

では、この問題を解決する方法は何でしょうか?

解決策初探(文末に最終的な解決策のまとめがあります)

Unicode における emoji の定義に基づいて、正規表現を使用して Unicode に割り当てられた emoji のブロックをマッチさせ、分割し、空または未定義のブロックをフィルタリングしてみましょう。

function emojiStringToArray(str) {
  const reg = /([\uD800-\uDBFF][\uDC00-\uDFFF])/
  return str.split(reg).filter(Boolean)
}

この関数が実際の使用でどのように機能するかをテストしてみましょう:

emojiStringToArray('😴⛔🎠🚓🚇') // ['😴', '⛔', '🎠', '🚓', '🚇']

通常の emoji の効果は良さそうですが、先ほど言及した肌の色 emoji や組み合わせ emoji には少し力不足のようです。例えば、以下の例を見てみましょう。

emojiStringToArray('👨‍👨‍👧‍👧') // ['👨', '‍', '👨', '‍', '👧', '‍', '👧']
emojiStringToArray('👦🏾') // ['👦', '🏾'] ここで四角い疑問符が表示される場合、実際には肌の色の emoji で表示されないかもしれません

おお、最初の 👨‍👨‍👧‍👧 は家族をバラバラにしてしまいました、本当にやるね

待って、さっき filter(Boolean) で空をフィルタリングしたのに、なぜ上記の結果配列に「空文字列」が残っているのか?

まさか...(まさか)

この出力された「空文字列」をテストしてみましょう:

'‍' === '' // false

if ('‍') {
  console.log('This is true!') // 成功して This is true! と表示されます
}

おお、実はこれは空文字列ではありません。

注意深いあなたは、上記の Unicode に関する組み合わせ emoji の説明で、この「空文字列」が実際には先ほど言及した U+200D ゼロ幅連結子 (ZWJ) であることに気づいたかもしれません。見た目には空文字列と変わらないように見えますが、これは全く異なる文字で、特定の emoji を組み合わせて 組み合わせ emoji を形成するために使用されます。

ES6 のスプレッド演算子(spread operator)も試してみましょう。

;[...'😴⛔🎠🚓🚇'] // ['😴', '⛔', '🎠', '🚓', '🚇']
[...'👨‍👨‍👧‍👧'] // ['👨', '‍', '👨', '‍', '👧', '‍', '👧']
[...'👦🏾'] // ['👦', '🏾']

また Array.from() も試してみたところ、実際には同じ状況でした。

Array.from('😴⛔🎠🚓🚇') // ['😴', '⛔', '🎠', '🚓', '🚇']
Array.from('👨‍👨‍👧‍👧') // ['👨', '‍', '👨', '‍', '👧', '‍', '👧']
Array.from('👦🏾') // ['👦', '🏾']

これらの方法は本質的に同じで、問題はありません。問題は、一部の emoji が「単独で存在する」ものではなく、肌の色や組み合わせ emoji などの追加要素を持つ可能性があることです。emoji を正確に判断するには、これら二つの特殊な状況も考慮する必要があります。

最適化案

Intl.Segmenter を使用して分割します。

多くの人は Intl についてあまり知らないかもしれませんし、初めて見るかもしれません。私もほとんど見たことがなく、実際にはあまり使ったことがありません。ここで MDN の Intl に関する説明 を引用します:Intl オブジェクトは ECMAScript 国際化 API の名前空間で、正確な文字列比較、数字のフォーマット、日付と時間のフォーマットを提供します。

Intl.Segmenter オブジェクトは言語に敏感なテキスト分割をサポートし、文字列を意味のある部分(文字、単語、文)に分割することを可能にします。

Intl.Segmenter を試してみましょう。

const splitEmoji = string => {
  const segment = new Intl.Segmenter().segment(string)
  return [...segment].map(e => e.segment)
}

splitEmoji('😴😄😃⛔🎠🚓🚇') // ['😴', '😄', '😃', '⛔', '🎠', '🚓', '🚇']

良いですね、しかし基本的な emoji を分割するには、先ほどの方法でも実現できます。次に、複雑な肌の色 emoji や組み合わせ emoji の状況を見てみましょう。

splitEmoji('👨‍👨‍👧‍👧👦🏾') // ['👨‍👨‍👧‍👧', '👦🏾']

素晴らしい!これが私たちが望んでいた結果ではありませんか?この方法は私たちのニーズを完璧に解決しました。

しかし、このものはあまり聞いたことがないので、互換性はどうでしょうか?生産環境で使用できるのでしょうか?Can I Use で検索して調べてみると、世界の 89.7% のブラウザ(モバイルと PC を含む)が互換性があることがわかります。したがって、実際により広いカバレッジが必要なデバイスの互換性を除けば、基本的に Intl.Segmenter を安心して使用できます。

オープンソースコミュニティの解決策

実際、emoji がこれほど長い間使用されているため、オープンソースコミュニティはすでに似たような問題に直面しているはずです。ここで、コミュニティで比較的成熟した解決策を引用します:graphemer

依存関係をインストールします。

npm i graphemer

基本的な使い方は以下の通りです:

// CommonJS
const Graphemer = require('graphemer').default
const splitter = new Graphemer()
const graphemes = splitter.splitGraphemes('😃⛔👨‍👨‍👧‍👧👦🏾')
console.log(graphemes) // ['😃', '⛔', '👨‍👨‍👧‍👧', '👦🏾']

// または ESM
import Graphemer from 'graphemer'
const splitter = new Graphemer()
const graphemes = splitter.splitGraphemes('😃⛔👨‍👨‍👧‍👧👦🏾')
console.log(graphemes) // ['😃', '⛔', '👨‍👨‍👧‍👧', '👦🏾']

graphemer の目的は Unicode 文字分割器 であり、つまり emoji だけでなく、他の類似の Unicode コードも正しく分割できる素晴らしい解決策です。

解決策のまとめ

  1. Intl.Segmenter の特性を利用して分割します。(詳細は上記を参照)
  2. オープンソースコミュニティで比較的成熟した解決策:graphemer(推奨、詳細は上記を参照)

関連知識

毎年 7 月 17 日は世界絵文字の日(英語:world emoji day)です。これは非公式の記念日で、emoji の広範な使用を祝うために、2014 年から開催されています。通常、この日にいくつかの emoji 活動が行われ、新しい emoji が発表されます。

オーストラリアのクイーンズランド州の車両管理局は新しい規則を導入し、2019 年 3 月 1 日から車両のナンバープレートに emoji 表情を追加することを許可しました。

「😂」(中国語:喜極而泣の表情、英語:Face with Tears of Joy)は、オックスフォード辞典によって 2015 年の年間語彙に選ばれました。

関連サイト

参考文献

読み込み中...
文章は、創作者によって署名され、ブロックチェーンに安全に保存されています。