開発者のための絵文字
絵文字まわりのバグの大半は、ひとつのズレから生まれます。コードが数えているものと、読み手に見えているものが違うのです。家族の絵文字は見た目は1つですが、コードポイントは7つ、JavaScriptの文字数では11、バイト数では25になります。Unicode公式リストにある全3,963個の絵文字に対して、よく使われる正規表現、セグメンター、長さチェックを実行し、それぞれが実際に何にマッチするかをこのページで公開しています。どの方法を選んだのかをバグ報告で知る前に、用途に合った方法を選べるようにするためです。
同じ絵文字でも、言語ごとに長さが違う
各セルは、その列の先頭にある絵文字に対して式が返す値です。結果は4種類に分かれます:UTF-16 単位、コードポイント、UTF-8 バイト、そして書記素(人が数える単位)。書記素の列はこのサーバー上の Intl.Segmenter で算出しているため、Unicode データが古いランタイムでは最新の絵文字に対してより大きな値が返ることがあります。
| 式 | 数える単位 | 😀 | ❤️ | 👍🏽 | 🇯🇵 | 👩💻 | 👨👩👧👦 | 🧑🏻❤️💋🧑🏼 |
|---|---|---|---|---|---|---|---|---|
JavaScripts.length | UTF-16 単位 | 2 | 2 | 4 | 4 | 5 | 11 | 15 |
JavaScript[...s].length | コードポイント | 1 | 2 | 2 | 2 | 3 | 7 | 10 |
JavaScriptIntl.Segmenter | 書記素 | 1 | 1 | 1 | 1 | 1 | 1 | 1 |
Pythonlen(s) | コードポイント | 1 | 2 | 2 | 2 | 3 | 7 | 10 |
Pythonlen(s.encode('utf-8')) | UTF-8 バイト | 4 | 6 | 8 | 8 | 11 | 25 | 35 |
Javas.length() | UTF-16 単位 | 2 | 2 | 4 | 4 | 5 | 11 | 15 |
Javas.codePointCount(0, s.length()) | コードポイント | 1 | 2 | 2 | 2 | 3 | 7 | 10 |
C#s.Length | UTF-16 単位 | 2 | 2 | 4 | 4 | 5 | 11 | 15 |
C# (.NET 5+)new StringInfo(s).LengthInTextElements | 書記素 | 1 | 1 | 1 | 1 | 1 | 1 | 1 |
Golen(s) | UTF-8 バイト | 4 | 6 | 8 | 8 | 11 | 25 | 35 |
Goutf8.RuneCountInString(s) | コードポイント | 1 | 2 | 2 | 2 | 3 | 7 | 10 |
Rusts.len() | UTF-8 バイト | 4 | 6 | 8 | 8 | 11 | 25 | 35 |
Rusts.chars().count() | コードポイント | 1 | 2 | 2 | 2 | 3 | 7 | 10 |
Swifts.count | 書記素 | 1 | 1 | 1 | 1 | 1 | 1 | 1 |
Swifts.unicodeScalars.count | コードポイント | 1 | 2 | 2 | 2 | 3 | 7 | 10 |
Rubys.length | コードポイント | 1 | 2 | 2 | 2 | 3 | 7 | 10 |
Rubys.grapheme_clusters.length | 書記素 | 1 | 1 | 1 | 1 | 1 | 1 | 1 |
PHPstrlen($s) | UTF-8 バイト | 4 | 6 | 8 | 8 | 11 | 25 | 35 |
PHPmb_strlen($s) | コードポイント | 1 | 2 | 2 | 2 | 3 | 7 | 10 |
PHPgrapheme_strlen($s) | 書記素 | 1 | 1 | 1 | 1 | 1 | 1 | 1 |
MySQLCHAR_LENGTH(s) | コードポイント | 1 | 2 | 2 | 2 | 3 | 7 | 10 |
MySQLLENGTH(s) | UTF-8 バイト | 4 | 6 | 8 | 8 | 11 | 25 | 35 |
SQL ServerLEN(s) on nvarchar | UTF-16 単位 | 2 | 2 | 4 | 4 | 5 | 11 | 15 |
素朴な切り詰めが絵文字に与える影響
家族の絵文字 👨👩👧👦 に対して、n を変えながら s.slice(0, n) を適用した結果です。ほとんどの切り詰めは目立つ形では失敗しません。別の、より小さな家族を返すだけです。この種のバグが気づかれないまま本番に出てしまうのはそのためです。
| n で切り詰め | 残る表示 | 残るコードポイント |
|---|---|---|
| s.slice(0, 1) | �サロゲートペアの片割れ(� と表示) | 0 |
| s.slice(0, 2) | 👨 | 1 |
| s.slice(0, 3) | 👨 | 2 |
| s.slice(0, 4) | 👨�サロゲートペアの片割れ(� と表示) | 2 |
| s.slice(0, 5) | 👨👩 | 3 |
| s.slice(0, 6) | 👨👩 | 4 |
| s.slice(0, 7) | 👨👩�サロゲートペアの片割れ(� と表示) | 4 |
| s.slice(0, 8) | 👨👩👧 | 5 |
| s.slice(0, 9) | 👨👩👧 | 6 |
| s.slice(0, 10) | 👨👩👧�サロゲートペアの片割れ(� と表示) | 6 |
| s.slice(0, 11) | 👨👩👧👦 | 7 |
国が入れ替わる国旗
地域指示文字は文字列の先頭から2つずつペアになります。🇺🇸🇯🇵 の先頭からコードポイントを1つ取り除くと、残りの文字は別の組み合わせでペアになります:🇸🇯🇵。エラーは出ず、ただ別の国になるだけです。
🇺🇸🇯🇵 US + JP → 🇸🇯 SJ (Svalbard & Jan Mayen) + 🇵
どの絵文字正規表現が何にマッチするか
2026-10-20 に、全3,963個の fully-qualified 絵文字を対象に、Unicode 17.0 のデータを搭載した Node 25.2.1 でテストしました。パターンが絵文字全体にマッチした場合のみ一致として数えています。
| パターン | 一致数 | 取りこぼすもの |
|---|---|---|
| /^\p{RGI_Emoji}$/v | 3,944 / 3,963 | エンジンの Unicode データより新しいすべての絵文字:Emoji 18.0 (19) |
| Intl.Segmenter | 3,963 / 3,963 | このランタイムではなし |
| /\p{Extended_Pictographic}/u | 3,692 / 3,963 | 259個の国旗と12個のキーキャップ(絵文字的なコードポイントをまったく含まないため) |
| /^\p{Extended_Pictographic}$/u | 1,193 / 3,963 | 複数のコードポイントからなるものすべて:肌の色、国旗、キーキャップ、ZWJ シーケンス、スタイル選択子付きの絵文字 |
そして避けるべきもの:\p{Emoji} は # * 0 1 2 3 4 5 6 7 8 9 © ® にもマッチします。絵文字の除去に使うと、テキスト中の数字がすべて消えてしまいます。
絵文字の除去:各方法で何が残るか
下の2行は、もとは同じテキストでした。残った文字はほとんどのフォントで見えません。だからこそ、検索、重複排除、文字数制限といった後の工程で問題を起こします。
Great 👍🏽 job ❤️ 👩💻 🇯🇵 #️⃣1
| 方法 | 結果 | 残ったコードポイント |
|---|---|---|
| \p{Extended_Pictographic} を除去 | Great 🏽 job ️ 🇯🇵 #️⃣1 | U+1F3FD U+FE0F U+200D U+1F1EF U+1F1F5 U+FE0F U+20E3 |
| \p{RGI_Emoji} を除去 | Great job 1 | なし |
データベースで絵文字が占める容量
全3,963個の絵文字について、1つあたりの UTF-8 バイト数を示します。30% は4バイト以内に収まり、最長のものは 35 バイト必要です。カラムが文字数ではなくバイト数で制限されている場合は、見た目ではなくこの表をもとにサイズを決めてください。
| UTF-8 バイト | 収まる絵文字 | 割合 |
|---|---|---|
| ≤ 4 | 1,193 | 30% |
| ≤ 8 | 2,346 | 59% |
| ≤ 11 | 2,424 | 61% |
| ≤ 17 | 3,424 | 86% |
| ≤ 28 | 3,868 | 98% |
| ≤ 35 | 3,963 | 100% |
🧑🏻❤️💋🧑🏼 U+1F9D1 U+1F3FB U+200D U+2764 U+FE0F U+200D U+1F48B U+200D U+1F9D1 U+1F3FC
適用する制限に合った数え方を選ぶ
唯一の正しい長さというものはなく、適用するルールに合った長さがあるだけです。バイト数で制限されたデータベースのカラムには UTF-8 バイトが必要です。人に対して50文字までと約束するテキスト欄には書記素が必要で、そうしないと国旗を3つ入力したユーザーに12文字使ったと表示してしまいます。例外は SQL Server です。nvarchar(n) は UTF-16 単位で数えるため、😀 は nvarchar では2文字分ですが、コードポイントで数える MySQL や PostgreSQL の varchar では1文字分です。4つのうちどれを指すのかを決めて変数名に書いておけば、あとは自然に決まります。
インデックスではなく書記素の境界で切り詰める
インデックスによる切り詰めは、どれも絵文字の途中に当たる可能性があります。UTF-16 の最悪のケースはサロゲートペアの片割れで、ほとんどのレンダラーは疑問符入りの四角として表示し、JSON エンコーダーによってはそのまま拒否します。コードポイント単位だと見た目には何も壊れませんが、そのほうが厄介です。家族から子どもが1人消え、肌の色が外れて茶色の四角として現れ、国旗は2つのアルファベットになります。解決策は、先に分割してから書記素を整数個で切ることです。Intl.Segmenter は2024年4月に Firefox 125 が搭載して以来すべての主要ブラウザで使え、Node ではそれ以前から使えます。自前の分割処理を書く理由はもうありません。
そのまま使えるコード例
短く、言語が許す限り依存関係なしで書いています。それぞれが上の表のケースを1つずつ解決します。
読み手に見える数を数える(JavaScript)
const seg = new Intl.Segmenter(undefined, { granularity: 'grapheme' });
const visibleLength = (s) => [...seg.segment(s)].length;
visibleLength('👨👩👧👦 ok'); // 4, while '👨👩👧👦 ok'.length is 14絵文字を分割せずに切り詰める(JavaScript)
function truncate(s, max, ellipsis = '…') {
const parts = [...seg.segment(s)].map((x) => x.segment);
return parts.length <= max ? s : parts.slice(0, max - 1).join('') + ellipsis;
}国旗やキーキャップを含むすべての絵文字を見つける(JavaScript)
// ES2024 'v' flag: Chrome 112, Safari 17, Firefox 116, Node 20
const emoji = /\p{RGI_Emoji}/gv;
'Go 🇯🇵 #️⃣1 👍🏽'.match(emoji); // ['🇯🇵', '#️⃣', '👍🏽']同じ処理を Python で(regex モジュール使用)
import regex # pip install regex; the built-in re has no \X
graphemes = regex.findall(r'\X', '👨👩👧👦 ok')
len(graphemes) # 4, while len('👨👩👧👦 ok') is 10あらゆる絵文字を受け入れるカラム
-- MySQL: utf8mb4 on the column AND the connection
CREATE TABLE posts (body VARCHAR(500) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci);
SET NAMES utf8mb4;
-- PostgreSQL: any UTF8 database already stores every emoji
-- SQL Server: use nvarchar, and remember LEN() counts UTF-16 units完成した正規表現が存在しない理由
正規表現エンジンの絵文字プロパティは、そのエンジンに組み込まれた Unicode データに基づいています。つまりパターンの新しさは、それを実行するランタイム次第です。私たちのテストがそれを示しています:\p{RGI_Emoji} は 3,963 個中 3,944 個にしかマッチしませんでした。最新の 19 個のシーケンスが、Node 25.2.1 の Unicode 17.0 テーブルにまだ含まれていないためです。同じコードでもランタイムを更新すればマッチするようになり、古いブラウザを使うユーザーの環境では取りこぼしたままです。新しい絵文字を登場初日から受け入れる必要があるチェックでは、構造(Extended_Pictographic に修飾子、結合子、選択子を加えたもの)か書記素で判定し、プロパティは偽陰性が問題にならない場面だけで使いましょう。
同じ絵文字に3通りの保存形式がある
Unicode には 3,963 個の fully-qualified 絵文字に加えて、同じ絵柄の minimally-qualified 形式が 1,029 個、unqualified 形式が 243 個あり、その多くはスタイル選択子 U+FE0F を持たない版です。❤ と ❤️ は多くのアプリで同じに見えますが別の文字列なので、一意性チェック、リアクション数、検索インデックスでは別物として扱われます。Unicode 正規化も役に立ちません:NFC も NFKC も異体字セレクタには手を付けません。絵文字を比較したり重複排除したりする場合は、形式を1つに決め(fully-qualified が安全です)、入力時に qualified リストを使って変換してください。U+FE0F をすべて削除すると、1️⃣ のようなキーキャップまで壊れてしまいます。
絵文字 Unicode 変換ツールすべての ZWJ シーケンスとそのサイズ絵文字を正しく数える文字数カウンター絵文字用語集
開発者からよくある絵文字の質問
JavaScript で絵文字の長さが 2 になるのはなぜですか?
JavaScript の文字列は UTF-16 で、U+FFFF を超える文字は16ビット単位を2つ使うサロゲートペアになります。ほぼすべての絵文字は U+FFFF より上にあるため、シンプルな 😀 でも .length は 2 です。コードポイント数には [...str].length を、読み手に見える数には Intl.Segmenter を使ってください。
絵文字にマッチさせる最適な正規表現は何ですか?
最近の JavaScript では、/\p{RGI_Emoji}/v が国旗やキーキャップを含む完全な絵文字シーケンスにマッチしますが、エンジンが知っている Unicode バージョンまでです。\p{Extended_Pictographic} は国旗とキーキャップを取りこぼし、\p{Emoji} は数字や #、* に誤ってマッチします。
絵文字を1文字として数えるには?
書記素クラスタを数えます。JavaScript では Intl.Segmenter を granularity grapheme で使います。Swift では String.count がすでにそうなっています。Python ではサードパーティの regex モジュールの \X を、PHP では intl 拡張の grapheme_strlen を使います。
絵文字1つを保存するのに何バイト必要ですか?
UTF-8 では3〜35バイトです。U+FFFF より上にある単一コードポイントの絵文字は4バイトです。肌の色、国旗、結合シーケンスはその分増えます。私たちのテストでは全絵文字の 59% が8バイトに収まり、最長の、肌の色を2つ指定したキスの絵文字は 35 バイトでした。
データベースで絵文字が ???? になるのはなぜですか?
カラムか接続が、4バイト文字を保持できない文字セットを使っているためです。多くは MySQL の旧来の utf8 です。テーブルと接続の両方を utf8mb4 に切り替えてください。具体的な ALTER TABLE 文は Unicode 変換ツールのページで解説しています。
