最終更新:
絵文字の文字数カウント|1 文字に見えて複数文字?仕組みを解説
SNS やメッセージアプリで絵文字を使ったとき、文字数が予想以上に増えた経験はありませんか。全角と半角の文字数の違いと同様に、絵文字の文字数カウントにも独特の仕組みがあります。
絵文字の歴史と Unicode バージョン別の推移
絵文字が世界的に広まる起点となったのは 1999 年、NTT ドコモの栗田穣崇氏が i-mode 向けに作成した 176 種類のセットです。12×12 ピクセルのドット絵として誕生した絵文字は、当初は日本の携帯キャリア各社 (ドコモ・au・ソフトバンク) が独自に実装しており、キャリア間で絵文字が文字化けする問題が日常的に発生していました。この互換性問題を解決するため、Google と Apple が Unicode Consortium に絵文字の標準化を提案し、2010 年の Unicode 6.0 で 722 種類が正式採用されました。
その後も Unicode のバージョンアップに合わせて絵文字は追加されていますが、追加数はバージョンごとに大きく違います。公開資料で追える代表的なバージョンは次のとおりです。
| Unicode バージョン | リリース年 | 追加された絵文字数 |
|---|---|---|
| 6.0 | 2010 | 722 |
| 7.0 | 2014 | 約 250 |
| 8.0 | 2015 | 41 |
| 16.0 | 2024 | 8 |
ここで気をつけたいのは、絵文字の「総数」が数え方の定義で変わることです。単独のコードポイントだけを数えるのか、ZWJ シーケンスや肌の色を指定した派生形まで別個に数えるのかで、同じ時点の集計でも数字が大きく変わります。Unicode Consortium 自身の絵文字集計 (Emoji 17.0 版) では、派生形を含めた総数を 3,953 件としています。
初期の一括採用に比べると、2020 年代の新規追加数は明らかに小さくなっています。Unicode Consortium は「既存の絵文字やシーケンスで表現できてしまうもの」を除外条件として明示しており、既存パーツの組み合わせで代替できる提案は採用されません。提案者の側も、Google 検索や Google トレンド、Google Books Ngram Viewer での使用頻度の証拠と、既存の絵文字と見分けがつく独自性の根拠を示す必要があります。
エンコーディング別のバイトサイズ実測データ
絵文字は Unicode の基本と文字コードに基づいてエンコードされますが、エンコーディング方式によってバイトサイズが大きく異なります。以下は代表的な絵文字の実測データです。
| 絵文字 | 見た目 | コードポイント数 | UTF-8 バイト数 | UTF-16 バイト数 | UTF-32 バイト数 |
|---|---|---|---|---|---|
| 😀 (U+1F600) | 1 文字 | 1 | 4 | 4 | 4 |
| 👍🏻 (U+1F44D U+1F3FB) | 1 文字 | 2 | 8 | 8 | 8 |
| 👨👩👧👦 | 1 文字 | 7 | 25 | 22 | 28 |
| 🇯🇵 (U+1F1EF U+1F1F5) | 1 文字 | 2 | 8 | 8 | 8 |
| 1️⃣ (U+0031 U+FE0F U+20E3) | 1 文字 | 3 | 7 | 6 | 12 |
| 🏳️🌈 | 1 文字 | 4 | 14 | 12 | 16 |
UTF-8 では基本多言語面 (BMP) 外のコードポイントは 1 つあたり 4 バイトを消費します。UTF-16 ではサロゲートペア (2 つの 16 ビットユニット) で表現するため同じく 4 バイトです。UTF-32 は固定長で 1 コードポイント = 4 バイトのため計算は単純ですが、メモリ効率は最も悪くなります。家族絵文字 👨👩👧👦 の場合、UTF-8 で 25 バイトを消費する点は、データベースのカラムサイズ設計で特に注意が必要です。
ZWJ・Variation Selector・サロゲートペアの仕組み
Unicode では、複数のコードポイントを ZWJ (Zero Width Joiner: ゼロ幅接合子、U+200D) で結合して 1 つの絵文字を表現する仕組みがあります。家族の絵文字 👨👩👧👦 は「男性 (U+1F468) + ZWJ + 女性 (U+1F469) + ZWJ + 女の子 (U+1F467) + ZWJ + 男の子 (U+1F466)」の 7 コードポイントで構成されています。
この設計が採用された背景には、絵文字の組み合わせの爆発的な増加があります。たとえば 4 人家族の絵文字を、肌の色 5 段階を人物ごとに選べる形で個別に登録すると、それだけで 5 の 4 乗 = 625 通り、さらに家族構成の違いを掛け合わせれば数千通りに膨れ上がります。ZWJ による合成方式なら、基本パーツの組み合わせで同じ表現力を確保できます。Unicode Consortium はこの方式により、コードポイントの枯渇を防ぎつつ多様性を実現しました。
破線のマスが画面に表示されない ZWJ です。絵文字 4 個は BMP 外のコードポイントなのでサロゲートペア 2 ユニット・UTF-8 で 4 バイトを消費し、ZWJ 3 個は 1 ユニット・3 バイトです。同じ 1 つのアイコンが、数える層を変えるだけで 1 (Swift の .count)、7 (Python 3 の len())、11 (JavaScript の .length) と別の答えになります。UTF-8 で 25 バイトに達するため、3 バイトまでしか扱えない MySQL の utf8 では保存できません。
ZWJ 以外にも、絵文字の表示を制御する不可視のコードポイントが存在します。
- Variation Selector (異体字セレクタ): U+FE0F (絵文字表示) と U+FE0E (テキスト表示) の 2 種類があります。例えば ❤ (U+2764) は、U+FE0F を付けると ❤️ (カラー絵文字)、U+FE0E を付けると ❤︎ (テキスト記号) として表示されます。この不可視文字がバイト数に加算されるため、見た目は同じでもデータサイズが異なるケースが生じます。
- 肌色修飾子 (Skin Tone Modifier): U+1F3FB〜U+1F3FF の 5 段階で、フィッツパトリック・スケール (皮膚科学の肌色分類) に基づいています。修飾子を付けると 1 コードポイント (4 バイト) が追加されます。
- Regional Indicator Symbol (地域指標記号): 国旗絵文字は A〜Z に対応する 26 個の Regional Indicator Symbol (U+1F1E6〜U+1F1FF) を 2 つ組み合わせて表現します。🇯🇵 は U+1F1EF (J) + U+1F1F5 (P) です。ISO 3166-1 の国コードに対応しているため、存在しない組み合わせ (例: U+1F1FF + U+1F1FF) は未定義の表示になります。
JavaScript の内部文字列表現は UTF-16 を採用しているため、BMP 外の絵文字 (U+10000 以上) はサロゲートペアとして 2 つの 16 ビットユニットで表現されます。これが "😀".length が 1 ではなく 2 を返す根本的な理由です。サロゲートペアの上位 (U+D800〜U+DBFF) と下位 (U+DC00〜U+DFFF) を分割してしまうと、不正な文字列が生成されるため、文字列の切り詰め処理では特に注意が必要です。
プラットフォーム別の絵文字カウントと表示差異
- X (Twitter): 絵文字は内部的に一律 2 文字としてカウントされます。日本語などの CJK 文字も 1 文字あたり 2 として数えられるため、絵文字と日本語の重みは同じです。ZWJ で結合された家族絵文字 👨👩👧👦 も、コードポイント数にかかわらず 2 文字扱いです。280 文字制限の中で絵文字を多用する場合、この仕様を把握しておかないと投稿が途中で切れる原因になります。
- Instagram: キャプションの 2,200 文字制限では、絵文字は 1 文字としてカウントされます。上限に余裕があるぶん本文で自由に使えますが、カウント単位が X とは違うため、同じ本文を両方に流用すると X 側だけ収まらないという事故が起きます。
- LINE: テキストメッセージでは絵文字は 1 文字としてカウントされます。ただし LINE 独自のスタンプ絵文字と Unicode 絵文字は内部的に異なる扱いを受けるため、API 経由でメッセージを送信する開発者は注意が必要です。
- Slack: メッセージ本文では絵文字は 1 文字としてカウントされますが、カスタム絵文字はショートコード (例:
:thumbsup:) として扱われ、コロンを含む文字列全体が文字数に加算されます。 - SMS: 絵文字を 1 つでも含めると、エンコーディングが GSM-7 から UCS-2 に切り替わり、1 通あたりの文字数上限が 160 文字から 70 文字に激減します。1 通に収まる分量が 4 割強に減るため、マーケティング SMS で絵文字を 1 つ足しただけでも、同じ本文の送信に必要な通数と費用が跳ね上がります。
同じ絵文字でも、Apple・Google・Samsung・Microsoft の各プラットフォームでデザインが大きく異なる点にも注意が必要です。例えば 🔫 (ピストル) は Apple がおもちゃの水鉄砲デザインに変更した後、他社も追随しましたが、変更のタイミングにはばらつきがありました。マーケティングや UI デザインで絵文字の見た目に依存する場合は、主要プラットフォームでの表示を事前に確認することを推奨します。
プログラミング言語別の文字数カウントの違い
同じ絵文字でも、プログラミング言語によって length の返す値が異なります。これは各言語の内部文字列表現の違いに起因します。
| 言語 / メソッド | "😀" | "👍🏻" | "👨👩👧👦" | カウント単位 |
|---|---|---|---|---|
JavaScript .length | 2 | 4 | 11 | UTF-16 コードユニット |
JavaScript [...str].length | 1 | 2 | 7 | Unicode コードポイント |
Python 3 len() | 1 | 2 | 7 | Unicode コードポイント |
Rust .len() | 4 | 8 | 25 | UTF-8 バイト数 |
Rust .chars().count() | 1 | 2 | 7 | Unicode コードポイント |
Swift .count | 1 | 1 | 1 | 書記素クラスタ |
Go len() | 4 | 8 | 25 | UTF-8 バイト数 |
Java .length() | 2 | 4 | 11 | UTF-16 コードユニット |
Swift だけが書記素クラスタ単位でカウントするため、どの絵文字でも見た目どおり 1 を返します。JavaScript や Java は UTF-16 ベースのため、BMP 外の絵文字はサロゲートペアで 2 としてカウントされます。Rust と Go はバイト数を返すため、絵文字の文字数カウントには不向きです。開発者は自分が使う言語の length が何を返すのかを正確に把握しておく必要があります。
よくある失敗パターンと対策
- JavaScript の
String.lengthで文字数制限を実装する:"👨👩👧👦".lengthは 11 を返しますが、見た目は 1 文字です。フォーム入力の文字数バリデーションでString.lengthを使うと、絵文字を含む入力が不当に制限されます。Intl.Segmenterを使えば、書記素クラスタ単位で正確にカウントできます。 - 文字列の途中で切り詰めて ZWJ シーケンスを破壊する: 家族絵文字 👨👩👧👦 を途中のバイト位置やコードユニット位置で切り詰めると、ZWJ シーケンスが分断され、個別の人物絵文字がバラバラに表示されたり、不正な文字が出力されたりします。文字列の切り詰めは必ず書記素クラスタ境界で行う必要があります。
- MySQL の
utf8で絵文字を保存しようとする: MySQL のutf8文字セットは最大 3 バイトまでしか対応しておらず、4 バイトの絵文字 (BMP 外のコードポイント) を保存できません。絵文字を扱うにはutf8mb4を指定する必要があります。さらに、VARCHAR(255)の 255 は文字数の指定なので、7 コードポイントの家族絵文字は 1 個で 7 文字分を消費します。見た目 36 個ぶんの絵文字で上限に達する計算です。加えて 1 行の合計サイズやインデックスの長さはバイト単位で制限されるため、絵文字を多く含む列では文字数側とバイト数側の両方で余裕を見ておく必要があります。PostgreSQL は最初から UTF-8 の全範囲をサポートしているため、この問題は発生しません。 - 正規表現で絵文字を 1 文字としてマッチさせる: JavaScript の
/./はサロゲートペアの片方だけにマッチします。サロゲートペアを 1 つとして扱うには/./u(Unicode フラグ) を使用します。ただしuでもvでも.が数える単位は 1 コードポイントのままなので、フラグをvに変えるだけで家族絵文字が 1 つにまとまるわけではありません。ZWJ シーケンス全体をひとまとめにマッチさせたい場合は、vフラグ (Unicode Sets、ES2024) と文字列プロパティ\p{RGI_Emoji}を組み合わせます。 - メール件名に絵文字を多用する: メールクライアントによっては絵文字が正しく表示されず、文字化けや空白になることがあります。特にビジネスメールでは、受信者の環境を考慮して絵文字の使用を控えるのが安全です。
開発者向け: 絵文字を正確にカウントする方法
Intl.Segmenterによる書記素クラスタ分割: 国際化 API (ECMA-402) で標準化されたIntl.Segmenterは、書記素クラスタ (ユーザーが「1 文字」と認識する単位) で文字列を分割できます。[...new Intl.Segmenter().segment(str)].lengthで、絵文字を含む文字列の「見た目の文字数」を正確に取得できます。Chrome 87 以降、Safari 14.1 以降、Node.js 16 以降で利用でき、Firefox は 125 以降での対応です (実装状況は 2026 年 8 月時点)。- 正規表現による絵文字検出と除去: Unicode プロパティエスケープ
/\p{Emoji_Presentation}/uを使えば、文字列中の絵文字を検出・除去できます。ただし\p{Emoji}は数字 (0-9) や # なども含むため、絵文字のみを対象にする場合は\p{Emoji_Presentation}または\p{Extended_Pictographic}を使い分ける必要があります。 - テスト用の絵文字セット: 開発時のテストには、以下の 5 カテゴリを最低限カバーすると主要なエッジケースを網羅できます。(1) 基本絵文字 (😀)、(2) 肌色修飾子付き (👍🏻)、(3) ZWJ シーケンス (👨👩👧👦)、(4) 国旗 (🇯🇵)、(5) キーキャップシーケンス (1️⃣)。
- データベース設計のベストプラクティス: 絵文字を含むテキストを保存するカラムは、見た目の文字数を基準にせず、コードポイント数とバイト数の両面でサイズを設計します。MySQL では
utf8mb4を指定し、VARCHAR(N)のNは文字数なので、書記素 1 個が 7 文字分を消費し得る前提で長さを決めます。行サイズやインデックス長の見積もりでは 1 文字あたり最大 4 バイトで換算します。絵文字を多用するチャットアプリなどではTEXT型の使用も検討してください。
まとめ
絵文字の文字数カウントは見た目ほど単純ではありません。文字数カウントスでは絵文字を含むテキストも正確にカウントできるので、SNS 投稿前の確認にご活用ください。