UTF-8 と Unicode そのエモジがデータベースを壊した理由
あなたのアプリケーションがエモジを挿入し、MySQLが不正な文字列値を投げました。その理由は——コードポイントとバイト、MySQLのutf8とutf8mb4の誤解、JavaScriptのサブリプレートペア、そして実際に解決する方法です。
あなたのアプリはうまく動いていた。しかし、ユーザーがテキストフィールドにエモジを入力したところ、MySQLが Incorrect string value: '\xF0\x9F\x98\x80' for column 'bio'を投げた。あるいは、エモジが静かに消えた。あるいは、INSERTが完全に失敗し、データが失われた。すべて、データベースカラムが期待していない4バイトの文字によるものだった。
これはMySQLの特徴やPHPのバグではなく、Unicode → UTF-8エンコーディングが実際にどのように機能するかの結果であり、それを理解すれば、もう驚かされることはない。
コードポイントとバイト:実際の違い
Unicodeはすべての文字にコードポイントを割り当てます — 数値です。Aの文字はU+0041。ユーロ記号はU+20AC。😀エモジはU+1F600。これは文字の抽象的な識別です。 UTF-8は
— コードポイントをバイトとして保存する方法です。ポイントは、UTF-8は可変幅であるため、コードポイントの値に応じて1〜4バイトを使用します。これにより、ASCIIと後方互換性を保ちつつ、存在するすべての文字をエンコードできます。 エンコーディング エンコーディングルール:
U+0000 から U+007F(ASCII)→ 1バイト
- U+0080 から U+07FF(ラテン拡張、アラビア語、ヘブライ語など)→ 2バイト
- U+0800 から U+FFFF(多くのCJK文字、記号、記号)→ 3バイト
- U+10000 から U+10FFFF(エモジ、稀な文字、数学記号)→ 4バイト
- これが、😀エモジ(U+1F600)が4バイトを必要とする理由です。そのコードポイントはU+FFFFを超えるからです。
UTF-8のバイトサイズ:参照表
ここに一般的な文字が実際に消費するバイト数を示します:
UTF-8 バイト(16進数)
| 文字 | 説明 | ユニコードコードポイント | ラテン大文字A | バイト数 |
|---|---|---|---|---|
| あ | U+0041 | é | 41 | 1 |
| ラテンeの急傾斜 | U+00E9 | C3 A9 | ユーロ記号 | 2 |
| € | U+20AC | E2 82 AC | 中 | 3 |
| 中国語の「中」 | U+4E2D | E4 B8 AD | 😀 | 3 |
| 笑顔の顔エモジ | U+1F600 | F0 9F 98 80 | 火エモジ | 4 |
| 🔥 | U+1F525 | F0 9F 94 A5 | 𝕳 | 4 |
| 数学のフランクルH | U+1D573 | F0 9D 95 B3 | これを自分で確認するには、 | 4 |
— あなたが貼り付けるテキストに対して、文字数とバイト数を表示します。を貼り付けて、1文字ですが4バイトであることがわかります。 文字列長計算機 MySQLのutf8の誤り 😀 開発者が傷つく場所です。MySQLには
という文字コードがあります。名前は正しいように見えますが、間違っています — MySQLの
は最大3バイトのシーケンスしかサポートしていません。エモジ(4バイト)はサポートされません。 utf8実際のフルUTF-8文字コードは utf8 (MySQL 5.5.3で導入、2010年にリリース)です。もしカラムが
を使用していて、エモジを挿入すると、MySQLはデータを静かにトリムするか、または utf8mb4 を投げます。また、アプリケーションのデータベース接続設定を更新してください。MySQL PDOの場合: utf8 VARCHAR(255)の罠
Incorrect string value: '\xF0\x9F\x98\x80' for column 'bio' at row 1
(バイト)を使用します。Laravelでは
-- Convert the table
ALTER TABLE users CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
-- Or for a specific column
ALTER TABLE users MODIFY bio TEXT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
-- And set your connection charset
SET NAMES utf8mb4;
MySQLでは255
$pdo = new PDO($dsn, $user, $pass, [
PDO::MYSQL_ATTR_INIT_COMMAND => "SET NAMES utf8mb4"
]);
文字
VARCHAR(255) 、ではなく255バイト — しかし、1行のストレージ制限はバイト単位で計算されます。 の場合、各文字は最大4バイトを消費し、カラムは最大1,020バイトを確保します。これは、InnoDBのデフォルトのプレフィックスインデックス制限(767バイト)を適用するvarcharカラムに影響します。 utf8mb4JavaScriptとサルベージペアの問題 VARCHAR(255) JavaScriptは内部でUTF-16を使用しており、UTF-8とは異なります。UTF-16はU+FFFFを超えるコードポイントに対して、独自のマルチユニットエンコーディングを持っています:
-- This fails on older MySQL with default innodb_large_prefix=OFF
CREATE INDEX idx_email ON users (email); -- email is VARCHAR(255) utf8mb4
-- Fix: use a prefix index
CREATE INDEX idx_email ON users (email(191)); -- 191 * 4 = 764 bytes, under 767
-- Or enable large prefixes (MySQL 5.7+, on by default in 8.0)
-- Set innodb_large_prefix = ON in my.cnf
サルベージペア
— 2つの16ビットコードユニットが1つの文字を表します。 これにより、 JavaScriptはUTF-16コードユニットを数え、文字ではなくコードユニットを数えます:
文字操作に必要な場合は、スプレッドオペレーターまたは String.length を使用してください。
'😀'.length // → 2 (two UTF-16 surrogate code units)
[...'😀'].length // → 1 (spread operator uses Unicode code points)
// Checking the character at index 0
'😀'[0] // → '\uD83D' (the high surrogate, not the emoji)
'😀'.codePointAt(0) // → 128512 (0x1F600, correct)
家族エモジの例はここで一時的に止めてください。 Intl.Segmenter:
// Count actual grapheme clusters
const segmenter = new Intl.Segmenter();
const chars = [...segmenter.segment('👨👩👧👦')];
chars.length // → 1 (family emoji is one grapheme cluster)
'👨👩👧👦'.length // → 11 (UTF-16 code units)
はゼロ幅ジョインャー(U+200D)で結合された4つのエモジです。単純な 👨👩👧👦 は11を返します。実際のグラフェムクラスタ:1です。これは、文字制限を実装する場合に、ユーザーがエモジシーケンスを入力するときに、制限が不正確になることを意味します。 .length エンコーディングの確認方法 String.length PHPはバイトを数え、文字を数えません。これは、マルチバイト文字を扱うPHP開発者が常に遭遇する問題です — 10エモジの文字列は長さ40を報告します。文字数を気にする場合は、
を使用してください。
パイソン
s = '😀'
print(len(s)) # 1 (Python 3 counts code points)
print(len(s.encode('utf-8'))) # 4 bytes
print(s.encode('utf-8').hex()) # f09f9880
PHP
$s = '😀';
echo strlen($s); // 4 (bytes, not characters)
echo mb_strlen($s); // 1 (characters)
echo mb_strlen($s, '8bit'); // 4 (bytes, explicit)
strlen() 簡易チェック mb_strlen() 任意のテキストのバイト数と文字数を確認したい場合、コードを書かずとも、
MySQL
-- Check charset of a table
SHOW CREATE TABLE users\G
-- Check charset of a specific column
SELECT CHARACTER_SET_NAME, COLLATION_NAME
FROM INFORMATION_SCHEMA.COLUMNS
WHERE TABLE_NAME = 'users' AND COLUMN_NAME = 'bio';
-- Character count vs byte count
SELECT CHAR_LENGTH('😀'), LENGTH('😀');
-- → 1, 4
は即座に処理します — どんなテキストでも貼り付けて、文字数、語数、バイト数を並列に表示します。
エンコーディングのバグチェックリスト 文字列長計算機 MySQL文字コード:
それは
- ですか?確認方法は MySQL接続:
utf8mb4、ではなくutf8アプリケーションがSHOW CREATE TABLE. - を送信していますか?DSNまたは接続設定を確認してください。 PHP strlen と mb_strlen:
SET NAMES utf8mb4バイト数を数える関数を使用している場合、必要に応じて文字数を数えていませんか? - JavaScript .length: コードユニットを数えている場合、必要に応じてグラフェムクラスタを数えていませんか?
- HTTPヘッダー: レスポンスが
- を送信していますか? ファイルエンコーディング:
Content-Type: text/html; charset=utf-8? - ソースファイルやSQLダンプがUTF-8 without BOMで保存されていますか? UTF-8 と Unicode:そのエモジがデータベースを壊した理由 2
恵 スコアボードが到着しました!
スコアボード ゲームを追跡する楽しい方法です。すべてのデータはブラウザに保存されます。さらに多くの機能がまもなく登場します!
