MAGAZINE
ルーターマガジン
【データ整理】Unicode正規化とMySQL COLLATION
ルーターエンジニアの Sasaokaです。
スクレイピングを行っていると避けて通れないのが、取得したデータのクレンジング処理です。簡単なところで行くと、取得したデータの前後に含まれる空白文字除去から始まり、表記の揺れを如何に吸収してくのかはデータ屋さんをする上では避けては通れません。
今回は表記の揺れの中でも、Unicodeの文字体系に由来するものに着目して、Unicode正規化と MySQLのCollation の話を具体例としてご紹介します。 原理原則の部分は他に譲る形で、チートシート形式で何が変わる/変わらないのかをまとめています。
Unicode正規化
Unicode正規化とは
Unicode正規化とは等価な文字を統一的な内部表現に変換する操作のことです。正規化には4つの形式NFD, NFC, NFKD, NFKCがあります。これらの形式の違いについてはWikipediaを参照してください。
Rubyでは文字列クラスのunicode_normalize
メソッドを使って形式を指定してUnicode正規化を行うことができます。
"a".unicode_normalize(:nfc)
変換表
以下の表に、よく使われる文字が各形式においてどのように変換されるかをまとめました。色をつけたところは正規化によって他の文字に変換されたことを表しています。
変換前 | NFD | NFC | NFKD | NFKC |
---|---|---|---|---|
a [全角] | a [全角] | a [全角] | a [半角] | a [半角] |
a [半角] | a | a | a | a |
A [全角] | A [全角] | A [全角] | A [半角] | A [半角] |
A | A | A | A | A |
は | は | は | は | は |
ぱ [合成済文字] | ぱ [結合文字列] | ぱ [合成済文字] | ぱ [結合文字列] | ぱ [合成済文字] |
ぱ [結合文字列] | ぱ [結合文字列] | ぱ [合成済文字] | ぱ [結合文字列] | ぱ [合成済文字] |
ハ | ハ | ハ | ハ | ハ |
パ [合成済文字] | パ [結合文字列] | パ [合成済文字] | パ [結合文字列] | パ [合成済文字] |
パ [結合文字列] | パ [結合文字列] | パ [合成済文字] | パ [結合文字列] | パ [合成済文字] |
ハ | ハ | ハ | ハ | ハ |
パ | パ | パ | パ | パ |
( [全角] | ( [全角] | ( [全角] | ( [半角] | ( [半角] |
( [半角] | ( | ( | ( | ( |
& [全角] | & [全角] | & [全角] | & [半角] | & [半角] |
& [半角] | & | & | & | & |
「 | 「 | 「 | 「 | 「 |
【 | 【 | 【 | 【 | 【 |
・ | ・ | ・ | ・ | ・ |
〜 | 〜 | 〜 | 〜 | 〜 |
① | ① | ① | 1 | 1 |
㈱ | ㈱ | ㈱ | (株) | (株) |
㌖ | ㌖ | ㌖ | キロメートル | キロメートル |
髙島屋 | 髙島屋 | 髙島屋 | 髙島屋 | 髙島屋 |
福 | 福 | 福 | 福 |
解説
4つの正規化形式(NFD、NFC、NFKD、NFKC)は、NFD・NFCとNFKD・NFKCの2つに大別されます。これらの大きな違いは正準等価性で文字を同一視するか、互換等価性で文字を同一視するかによるものです。上の例では、NFD・NFCの場合は「A」「A」のように全角半角による差を吸収しない一方、NFKD・NFKCでは全角半角の違いは吸収され半角文字に正規化されています。
NFDとNFCの違いは、合成文字を変換した場合に表れます。日本語の場合、「ぱ」のような半濁音の文字は、「結合済文字」で表す方法と「基底文字(は) + 結合文字 (゜)」で表す方法の2通りがあります。NFC(NFKC)の場合、結合済文字への正規化を行なうのに対し、NFD(NFKD)の場合は基底文字+結合文字へと分解した文字への正規化を行います。試しに、Rubyでこれらの文字が同じか判定してみると
irb(main):001:0> "ぱ" == "ぱ"
=> false
となり違う文字であることが確認できます。
また一番下の例は、「福」の異体字ですが、これはUnicode正規化を行なうことで見慣れた「福」の字に正規化されるようです。(ブラウザだとそもそも正規化された結果しか見えないため画像で貼っています。) 一方で、「髙」のようにUnicode正規化では同一視されない文字も有ります。
MySQL COLLATION
MySQLでは文字セット(Charset)の他に照合順序(Collation)を指定することができます。Collationは文字の大小関係を指定するものです。つまり、Collationによって、どの文字を同一視するかが変わります。
文字セットutf8mb4のCollationとしてはutf8mb4_bi, utf8mb4_general_ci, utf8mb4_unicode_ci, utf8mb4_unicode_520_ciの4種類がよく用いられます。以下の表に各Collationで、どの文字が同一視されるかをまとめました。Collationを変えての文字の比較は
MariaDB [(none)]> select '😡' = '😊' collate utf8mb4_unicode_520_ci;
+------------------------------------------+
| '?' = '?' collate utf8mb4_unicode_520_ci |
+------------------------------------------+
| 0 |
+------------------------------------------+
1 row in set (0.000 sec)
のように行います。 1の場合は同じ文字と見做されていて、0の場合は区別されます。
左辺 | 右辺 | utf8mb4_general_ci | utf8mb4_unicode_ci | utf8mb4_unicode_520_ci | utf8mb4_bin |
---|---|---|---|---|---|
'ア' | 'ア' | 0 | 1 | 1 | 0 |
'A' | 'A' | 0 | 1 | 1 | 0 |
'a' | 'A' | 1 | 1 | 1 | 0 |
'は' | 'ば' | 0 | 1 | 1 | 0 |
'は' | 'ハ' | 0 | 1 | 1 | 0 |
'は' | 'バ' | 0 | 1 | 1 | 0 |
'ば' | 'バ' | 0 | 1 | 1 | 0 |
'ハ' | 'ハ' | 0 | 1 | 1 | 0 |
'バ' | 'バ' | 0 | 1 | 1 | 0 |
'パ' | 'バ' | 0 | 1 | 1 | 0 |
'&' | '&' | 0 | 1 | 1 | 0 |
'1' | '1' | 0 | 1 | 1 | 0 |
'①' | '1' | 0 | 1 | 1 | 0 |
'😡' | '😊' | 1 | 1 | 0 | 0 |
utf8mb4_bin はバイナリレベルで文字を比較するため当然全て異なる結果となります。
utf8mb4_general_ci は比較的よく使われるCollationですが、半角アルファベットの大文字小文字の違いが同一視されるのが特徴です。 また絵文字の区別はされないのはよく「寿司ビール問題」として有名ですね。
utf8mb4_unicode_ci は「ア」「ア」が同一されるのは良いですが、「は」「ぱ」「ば」あたりが同一視されるなど、日本語を扱う上では正直使いにくいという印象です。
utf8mb4_unicode_520_ci は、utf8mb4_unicode_ci に対して絵文字が全て同一視されるのを無くしたCollationですが、utf8mb4_unicode_ciと同じく日本語には不向きです。
テーブルのデフォルト Collation
MySQLではテーブルを作るときにCHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci
のようにしてCharasetとCollationを指定できます。
CREATE TABLE `test_unicode_ci` (
`text` varchar(256)
) ENGINE=InnoDB AUTO_INCREMENT=8 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
このテーブルに全角アと半角アをINSERTしてみます。
INSERT INTO test_unicode_ci VALUES ('ア');
INSERT INTO test_unicode_ci VALUES ('ア');
MariaDB [collation_test]> SELECT text FROM test_unicode_ci;
+------+
| text |
+------+
| ア |
| ア |
+------+
2 rows in set (0.000 sec)
全角アと半角アの2行がテーブルに入っていますが、DISTINCT指定をして取り出すと、テーブルのCollationをutf8mb4_unicode_ciにしているため、全角アと半角アが同一判定されて1行のみ取り出されます。
MariaDB [collation_test]> SELECT DISTINCT text FROM test_unicode_ci;
+------+
| text |
+------+
| ア |
+------+
1 row in set (0.003 sec)
UNIQUEキー制約をつけると、そのテーブル(またはカラム)のCollationで同一判定されるレコードをINSERTできません。
CREATE TABLE `test_unique_unicode_ci` (
`text` varchar(256) UNIQUE
) ENGINE=InnoDB AUTO_INCREMENT=8 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
MariaDB [collate_test]> INSERT INTO test_unique_unicode_ci VALUES ('ア');
Query OK, 1 row affected (0.019 sec)
MariaDB [collate_test]> INSERT INTO test_unique_unicode_ci VALUES ('ア');
ERROR 1062 (23000): Duplicate entry 'ア' for key 'text'
最後に
Unicode正規化と MySQL Collation についてまとめましたが、ざっくり言うと
- Unicode正規化は、NKCで正規化するのが良さそう。一部、正規化で潰れてしまう漢字もある。
- MySQL Collation は日本語を使う場合は正直、utf8mb4_general_ci以外は使いづらい。入れる前にある程度正規化しておくのが良い。
CONTACT
お問い合わせ・ご依頼はこちらから