竈門禰󠄀豆子
をMySQL5.6のテーブルにinsertしようとすると正しく格納できず、竈門禰
となってしまうケースがあるという話を聞き、調べてみました。
実践
まずは試しにやってみます。
mysql> show create table verification\G
*************************** 1. row ***************************
Table: verification
Create Table: CREATE TABLE `verification` (
`name` varchar(100) COLLATE utf8_bin DEFAULT NULL
) ENGINE=InnoDB DEFAULT CHARSET=utf8 COLLATE=utf8_bin
1 row in set (0.01 sec)
mysql> insert into verification (name) values ('竈門禰󠄀豆子');
Query OK, 1 row affected, 2 warnings (0.01 sec)
mysql> select * from verification;
+-----------+
| name |
+-----------+
| 竈門禰 |
+-----------+
1 row in set (0.00 sec)
確かに竈門禰
になっていることがわかりました。
調査
文字コード周りの設定を見てみます。
mysql> SHOW VARIABLES LIKE 'char%';
+--------------------------+----------------------------+
| Variable_name | Value |
+--------------------------+----------------------------+
| character_set_client | utf8 |
| character_set_connection | utf8 |
| character_set_database | utf8 |
| character_set_filesystem | binary |
| character_set_results | utf8 |
| character_set_server | utf8 |
| character_set_system | utf8 |
| character_sets_dir | /usr/share/mysql/charsets/ |
+--------------------------+----------------------------+
8 rows in set (0.01 sec)
テーブルの文字コード設定はutf8です。公式リファレンスにも記載がありますが、MySQLのutf8文字セットは、3byteまでの文字を扱うことができます。
mysql> select * from CHARACTER_SETS where CHARACTER_SET_NAME like 'utf8%';
+--------------------+----------------------+---------------+--------+
| CHARACTER_SET_NAME | DEFAULT_COLLATE_NAME | DESCRIPTION | MAXLEN |
+--------------------+----------------------+---------------+--------+
| utf8 | utf8_general_ci | UTF-8 Unicode | 3 |
| utf8mb4 | utf8mb4_general_ci | UTF-8 Unicode | 4 |
+--------------------+----------------------+---------------+--------+
2 rows in set (0.00 sec)
というわけで、文字が途中で途切れて格納されるのは文字コードの設定がutf8であることが一因のようです。
ただ、素直に考えるなら竈門禰
で途切れているので豆
がutf8で扱えない4byte文字ということになります。こんなメジャーな漢字が4byte文字とは到底思えません。確認してみると、
>>> len('豆'.encode('utf-8'))
3
やっぱり3byteでした。そうなると疑わしいのは禰󠄀
です。
よくよく見てみると、insertしようとした竈門禰󠄀豆子
に対し、実際に登録されたのは竈門禰
となっていて、偏がネ
から示
に変わっていました。何故こんなことが起きるのでしょうか?
禰󠄀
について掘り下げて調べてみます。
>>> len('禰󠄀'.encode('utf-8'))
7
7byte…?テーブル格納後の禰
を見てみると…
>>> len('禰'.encode('utf-8'))
3
3byteになっています。insertのタイミングで4byteの何らかの情報が欠落してテーブルに格納されたようです。
禰󠄀
は一体何者なのか、Unicodeのコードポイントを調べてみます。
>>> ord('禰󠄀')
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
TypeError: ord() expected a character, but string of length 2 found #2文字??
>>> s = '禰󠄀'
>>> list(s)
['禰', '󠄀']
>>> for i in list(s):
... hex(ord(i))
...
'0x79b0'
'0xe0100'
16進表現で0x79b0
と0xe0100
の2文字で構成されていることがわかりました。
0xe0100
を調べてみると、これは異体字セレクタ
というもののようです。Wikipediaによれば、
異体字セレクタ (英: Variation Selector) は、Unicode および ISO/IEC 10646 (UCS) における、文字の字体をより詳細に指定するためのセレクタ (選択子) である。
とあり、禰
を基底文字として偏が異なる文字であることを表現するためのセレクタが含まれていることがわかりました。
まとめ
なぜ竈門禰󠄀豆子
がinsert時に一部欠落するかというと、
- utf8のテーブルには4byte文字を格納できない
-
禰󠄀
は禰
の異体字であり、禰
+ 4byteの異体字セレクタの組み合わせで表現される - したがって、
禰󠄀
を格納しようとすると、4byteである異体字セレクタのみ欠落して格納されてしまう
ということでした。異体字セレクタは今まで全く存在を知らず、文字コードの世界は奥が深い、、、と思わされる結果になりました。
追記
この問題を回避したい場合、4byte文字を扱えるutf8mb4を使いましょう。
(参考: https://dev.mysql.com/doc/refman/5.6/ja/charset-unicode-utf8mb4.html )
なお、MySQL8系ではutf8mb4がデフォルトのようです。
(参考:https://dev.mysql.com/doc/refman/8.0/ja/server-system-variables.html#sysvar_character_set_client )