<ポエム>
言うことは分かっている
やることも分かっている
ただ確認したかっただけ
寿司の絵文字が'\xF0\x9F\x8D\xA3'になるまで
諸事情で MySQL5.7 をまだ利用しています。
そして、character set もやんごとなくて utf8mb4 になっていません。
そんな環境でアプリを動かしていると、🍣や🍺などの4バイト文字を、アプリ層で個別に対応してあげる必要があります。
なので、うっかり対応が漏れていたりするとエラーメッセージが投げられるのですが、、、
ActiveRecord::StatementInvalid: Mysql2::Error: Incorrect string value: '\xF0\x9F\x8D\xA3...
などと言われても「4バイト文字のあれ・・だよね」と自信がなかったりします。(わたしは)
POSTされた値を見るなりして確認しているわけですが、変換の過程が分かっていないので調べてみました。
🍣 → '\xF0\x9F\x...'の過程
1. 絵文字から数値文字参照1の値(コードポイント)を取得する
絵文字は"数値文字参照"というUnicodeの文字番号(= コードポイント)により管理されています。
たとえば、あ
とhtmlやQiitaの記事中に書くとブラウザが「あ」に変換してくれます。
&
と書くと「&」にブラウザが変換してくれるのに非常に似ています。
数値文字参照と同じ仕組みを使いつつ、ampやlt,gtなど人が判別しやすいコードをふっているので"文字実体参照"と別の名前がついているそうです。2
JavaScriptであればメソッドが用意されていて、次のように取得できます
'🍣'.codePointAt() // 127843
ただしいコードが取得できているかどうかは、たとえば次のサイトで確認することができます。
HTMLの絵文字 文字コード表
2. コードポイントの値の16進数表記、2進数表記を取得
次の工程で、"UTF-8の符号化"という変換を行います。
2進数表記は変換元となる値。16進数表記は変換方法の判別にそれぞれ利用します。
let codePoint = '🍣'.codePointAt() // 127843
let binaryNumber = '0000' + codePoint.toString(2) // '000011111001101100011'
let hexadecimal = codePoint.toString(16) // '1f363'
3. UTF-8の符号化
詳しい説明は参考記事3をご覧いただくとして、、、
ここでは4バイト文字の変換にフォーカスしています。
(hexadecimal は参考記事を読む際にお使いください。以下の符号化作業では用いません)
// NOTE:
// 4バイト文字用の符号化ビット列(1111 0xxx 10xx xxxx 10xx xxxx 10xx xxxx)の'x'に binaryNumber の数値を当てはめていく
// binaryNumberの桁数と'x'の数が一致するよう、前段でprefix'0000'をつけてある
// 必要な桁数ごとに区切って配列におさめる
let bin = [
binaryNumber.substring(0,3), // 0xxx
binaryNumber.substring(3,5), // 10xx
binaryNumber.substring(5,9), // xxxx
binaryNumber.substring(9,11), // 10xx
binaryNumber.substring(11,15), // xxxx
binaryNumber.substring(15,17), // 10xx
binaryNumber.substring(17,21), // xxxx
]
// '1111 0xxx 10xx xxxx 10xx xxxx 10xx xxxx' の穴埋め
// 過程を見やすくするため、スペースを入れてあるが本来は不要。
let encodedBinaryNumber = `1111 0${bin[0]} 10${bin[1]} ${bin[2]} 10${bin[3]} ${bin[4]} 10${bin[5]} ${bin[6]}`
// console.log(encodedBinaryNumber)
// '1111 0000 1001 1111 1000 1101 1010 0011'
// 不要なスペースを除去しつつ、10進数に戻す
let decimalNumber = parseInt(encodedBinaryNumber.replaceAll(' ', ''), 2)
// 10進数→16進数
decimalNumber.toString(16) // 'f09f8da3' ← !!!
4. 出ましたね
エラメッセージでは1バイトごとにエスケープシーケンス\x
が入っていますが、それら除去した'F09F8DA3'
は最後の行で出力された'f09f8da3'
と同じことが確認できました。
まとめ
- コードポイントの値(10進数)を取得して2進数に変換
- 🍣 →
'000011111001101100011'
- 🍣 →
- encode(符号化)
-
'000011111001101100011'
→'11110000100111111000110110100011'
-
- 16進数にする
'f09f8da3'
絵文字からエラーメッセージに出ていたUnicode文字に変換できました。
たんに変換したい時
上記の作業をすればokですが、いちいち面倒ですね。
モダンな言語であればきっと標準装備されていると思うのですが未調査なのでRubyの例だけ紹介。
# unicodeから絵文字
irb(main):001:0> "\xF0\x9F\x8D\xA3".unicode_normalize
=> "🍣"
# 絵文字からunicode
irb(main):009:0> '🍣'.unpack('H*')
=> ["f09f8da3"]
参考記事
参考にさせていただきました。ありがとうございます
文字コード全般
符号化
Rubyと文字コード
絵文字とコードの対応
-
HTMLの絵文字 文字コード表
- どの絵文字がどういう合成をしているのか分かりやすい
-
Emoji List
- コードの組合わせが改行で表記されている分コードが判別しやすい
-
数値文字参照 : 「表記したい文字を、Unicode/ISO 10646の文字番号(コードポイント)で表す方式」(引用元: IT用語辞典 e-Words) ↩
-
文字実体参照 : 出典は"数値文字参照"に同じ ↩
-
Qiita: UnicodeをUTF-8やUTF-16に変換する方法 ↩