28
14

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 5 years have passed since last update.

歴史から知る「なぜ絵文字対応は面倒なのか」

Posted at

はじめに

企画さんから、
「名前入力の際に絵文字を * でエスケープしてほしい(例:ハッピー:tada:→ハッピー*)」
という要望がきたので、それに対応するまでの流れをまとめたいと思います。(対応したとはとは言ってない)

Unicodeに絵文字が導入されるまで

こういった一部の文字をエスケープしたい場合、for文でcharをチェックするなり正規表現で弾くなりすると思いますが、絵文字の場合はそう上手くはいきません。

これには Unicode が絵文字を表示する仕組みが関係します。
絵文字が導入されるまでを時系列で振り返りながら、仕組みを知っていきましょう。

1990年代前半

制作当初、Unicode では1文字を2バイト固定で表現しようと計画されていました。
これによって、収録できる文字が最大65536文字になりますが、

  • Shift_JIS などの経験から可変長文字コードが面倒なことがわかっていたこと
  • 当時、中国・日本・韓国などの文字をまとめても20000個程度だったこと

などが理由で、問題ないだろうと判断されていました。
実際、Unicode 1.0 が策定された際は65536個以内に収まっていました。

これ以降、2バイト固定長、のちに UTF-16 と呼ばれるルールはC#やJavaなどのプログラミング言語に採用されるようになります。(char を2バイトとして扱う)

1990年代後半

当初、2バイト(65536文字)もあれば十分だろうと判断されていた Unicode ですが、技術者だけでなく言語学者も参画してもらったところ、65536文字では足りないことが明らかになります。

そこで、2文字1組(4バイト)として扱うサロゲートペアというルールが定められました。
これによって、扱える文字は増えたわけですが、当初定めていた2バイト固定長が崩れる形となり、UTF-16 は1文字2バイトまたは4バイトの可変長となってしまいます。

この結果、発生したのがUnicodeを使用したプロジェクトでのバグです。
当初、2バイトで1文字を扱っていたことで、サロゲートペアなどの4バイト文字がきた時に対処できなくなってしまいました。
しかし、サロゲートペアで表現する必要がある文字は一般人が知らないようなマイナー文字がほとんどだったため、特に対策されないプロジェクトが存在する形となりました。

2010年代前半

この状況に一石を投じたのが絵文字でした。

もともと日本の携帯電話などで浸透していた絵文字は、その扱いやすさから Emoji として世界でも扱われるようになります。(大統領からも Emoji を生み出したことに感謝の意を示されたらしいです)

絵文字は4バイトで表現されますが、上記の通り世界各国で使用されているため、技術者はサロゲートペアに対応せざるを得なくなりました。
これによって、今まで対応されていなかったマイナー文字も対応される形となり、結果的にバグ対策がなされる形となっていきます。

なぜ絵文字対応は面倒なのか

今回の記事のタイトルでもある、「なぜ絵文字対応は面倒なのか」
これは、

絵文字は4バイトで表現されますが、上記の通り世界各国で使用されているため、技術者はサロゲートペアに対応せざるを得なくなりました。

が理由となっていることがわかりました。
プログラミング言語的に置き換えると、charが2バイトで扱われている場合、絵文字などの4バイトは正しく1文字と判定されなくなるなどの問題が発生します。

なので、絵文字対応をするためには、この4バイト文字を正しく1文字扱いできればいいことがわかります。
幸い、IsSurrogateなどのサロゲートペアを検知する関数が各言語に存在するかと思われるので、そちらで判定すれば、上記の問題は解決します。

これで絵文字対応は完了...ということにはなりませんでした。
これには絵文字が導入されてからの歴史が関係します。

絵文字が導入されてから

もともとは日本の携帯電話だけにあったものが、iPhone の登場を筆頭に絵文字は世界的に人気となりました。
人気のコンテンツはより人の目を集め、それによって起こる問題も存在します。
主な例としては、人種差別やLGBT問題などが挙げられるでしょう。

参考記事:「絵文字に平等をサポートしてください」人種差別の指摘にゆれるUnicode

ただ、こういった「複数の肌色を用意してほしい」「男と男の絵文字も用意してほしい」といった要望は、日本の携帯電話での使用をベースに作られた絵文字には含まれていませんでした。

この解決策として、これまでの経緯を振り返ると、新たに4バイト文字として肌色別の絵文字を追加するという方法が考えられますが、これでは絵文字を検索する際に面倒になってしまいます。

そこで取られたのが、複数の絵文字を組み合わせて一つの文字を作るという方法でした。

複数の絵文字からなる絵文字

これまでの話では、たとえ1文字が2バイトであろうと4バイトであろうと1コードポイントで表現されていました。(異体字セレクタだと2コードポイントになりますが...)

ここでいうコードポイントとは、文字の集合の中でどこに配置されているかの位置になります。
これは、Unicode だと、U+○○○ のような形で表現され、単純計算で U+0000 ~ U+10FFFF が存在します。
例えば、「🎉」だと U+1F389 に該当します。

さて、先ほど挙げた「複数の絵文字からなる絵文字」ですが、これは複数のコードポイントから1文字として扱われます。

具体例を挙げます。
6AA54B84-84B9-403A-9C1F-6E46309D4F35.png

肌色違いなどの絵文字を表現する場合は、人の顔を表現する絵文字に色を表現する絵文字を追加することで表現できます。
また、この画像では2文字の絵文字を結合していますが、3文字4文字を結合して1文字を表現することもあります。(「👨‍👩‍👧‍👦」4人家族など)

この現象は簡単に確認ができます。実際に肌色違いを試してみましょう。

👱+ 🏻=👱🏻

こちらは上記の画像の上の肌色違い生成式を文字に起こしたものです。
「👱」をコピーして検索バーなどの適当の場所に貼り付けた後、「👱」をコピーして貼り付けた後ろに「 🏻」を貼り付けてください。
肌色違いが生成されたでしょうか?

このように、ある決まった文字が連続で続いた場合、1文字として扱う仕組みがあります。
それが書記素クラスタです。

なお、今回は絵文字を例に挙げていますが、書記素クラスタは「が」などにも利用されています。「か」に濁点の文字が結合されているのです。(1コードポイントの「が」も存在します)

# Ruby
irb(main):001:0> "が".length
=> 2

# C#
> WriteLine("が".Length);
2

Rubyはコードポイント、C#は2バイト単位でカウントするので、「が」は2バイト文字が2つ並んでいることがわかります。(U+304B U+3099

つまり、真に絵文字に対応するということは、こういった1コードポイントを超えた文字にも対応することになります。
これが面倒な理由になります。

各言語での挙動

1コードポイントを超えた文字に対応するためには、前述の通り書記素クラスタを用いる必要があります。
これが使われていなかったり、バージョン違いだったりすると正しく対応することができません。

言語別に挙動を見ていきましょう。

Ruby

まずは Ruby 2.3.8 を試してみます。

補足
Ruby のstring.lengthではコードポイントを数えてしまいます(例:肌色違いならU+1F471 U+1F3FBのため2文字扱い)。
なので、結合してできる肌色違いなどの文字を1文字扱いする正規表現\Xを使用しています。

irb(main):001:0> RUBY_VERSION
=> "2.3.8"
irb(main):002:0> "が".scan(/\X/).count
=> 1
irb(main):003:0> "👨‍👩‍👧‍👦".scan(/\X/).count
=> 7

どうやら正しく文字数をカウントすることができなかったようです。

これは Ruby 2.3.8 での正規表現の\Xがこれらの絵文字に対応していないことが原因となります。
\Xが絵文字に対応するのは Ruby 2.4.0 以降になります。

参考:https://docs.ruby-lang.org/ja/latest/doc/news=2f2_4_0.html

そのため、このバージョンで書記素クラスタを用いるためにはActiveSupportを使う必要があります。

irb(main):001:0> require 'active_support/multibyte/unicode'
=> true
irb(main):002:0> ActiveSupport::Multibyte::Unicode.unpack_graphemes("が").count
=> 1
irb(main):002:0> ActiveSupport::Multibyte::Unicode.unpack_graphemes("👨‍👩‍👧‍👦").count
=> 4

今度は、書記素クラスタを用いた文字数カウントになりますが、一部変更はかかっているものの、やはりおかしなカウントがされています。
これは、今回使用した ActiveSupport 5.0 が Unicode 8.0.0 以下のバージョンに基づいて文字数カウントを行なうためです。

正しくカウントするためには、Unicode 9.0.0 以降に対応した Ruby か ActiveSupport が必要になります。
なお、Unicode 9.0.0 に対応したのも Ruby 2.4.0 以降になります。

次は Ruby 2.6.2 での挙動を見ていきましょう。

irb(main):001:0> RUBY_VERSION
=> "2.6.2"
irb(main):002:0> "が".scan(/\X/).count
=> 1
irb(main):002:0> "👨‍👩‍👧‍👦".scan(/\X/).count
=> 1

Ruby 2.6.2 では正規表現\Xが絵文字に対応しており、Unicode 9.0.0 にも対応しているので、正しく絵文字をカウントできているようです。

C#

次は C# の挙動を見ていきましょう。

補足
C# のstring.Lengthでは char の数を数えています。(例:肌色違いならU+1F471 U+1F3FBで、どちらも4バイト文字なので、2+2で4文字扱い)
なので、4バイト文字や結合してできる肌色違いなどの文字を1文字扱いするStringInfoを使用します。

> System.Console.WriteLine(new System.Globalization.StringInfo("が").LengthInTextElements);
1
> System.Console.WriteLine(new System.Globalization.StringInfo("👨‍👩‍👧‍👦").LengthInTextElements);
7

結果が Ruby 2.3.8 で正規表現\Xを使用した時と同じ挙動ですね...
StringInfoのリファレンスを見てみましょう。

リファレンスを見た限り、特に絵文字に関する記述はありません。
ただ、

.NET Framework 4.6.2 では、文字の分類に基づくUnicode 標準、バージョン 8.0.0します。 .NET Framework 4.6.1 から .NET Framework 4 向けに基づくはUnicode 標準、バージョン 6.3.0します。 .NET Core でに基づくはUnicode 標準、バージョン 8.0.0します。

ということなので、どちらにせよStringInfoでも正しく絵文字を取り扱うことが難しそうです。

では、C#ではどうやって絵文字を扱うのか。
厳密にやるためには、外部のライブラリを頼る方法が一番かと思います。

++C++; // 未確認飛行 C さんのサイトで公開されているGraphemeSplitterを使うなどがいいかもしれません。

参考:https://ufcpp.net/blog/2017/10/graphemesplitter/

まとめ

絵文字の歴史と対応方法を紹介しました。
こういった絵文字の記事、閲覧環境によっては正しく表示されないことがあるので辛いところです...

おまけ

冒頭で企画さんから依頼されたもので、

「名前入力の際に絵文字を * でエスケープしてほしい(例:ハッピー:tada:→ハッピー*)」

こちらは最終的に、絵文字は入力を受け付けないという仕様に変更されたため、System.Globalization.UnicodeCategoryにて絵文字関連の文字は弾くという対応になりました。

今回はUnityでの絵文字対応で、最終的にはこういったコードに落ち着きました。

using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.UI;

public class NewBehaviourScript : MonoBehaviour
{
    [SerializeField] InputField _inputField;

    private void Start()
    {
        _inputField.onValidateInput += ValidateInput;
    }

    private char ValidateInput(string text, int charIndex, char addedChar)
    {
        var category = char.GetUnicodeCategory(addedChar);
        switch (category)
        {
            case System.Globalization.UnicodeCategory.Surrogate:
            /*
            case System.Globalization.UnicodeCategory.Control:
            case System.Globalization.UnicodeCategory.OtherNotAssigned:
            など、その他必要なものがあれば追加
            */
                return '\0';
        }

        return addedChar;
    }
}

参考文献

28
14
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
28
14

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?