Rails で MySQL の返した Incorrect string のバイト表記文字列を、読める文字列に戻す


概要

HEX表記の文字列を、UTF8の文字列に変換する方法は、HEX を取り出して pack する。

経緯

MySQL で utf8mb4 を設定しているのに、🎶 という文字の保存がエラーになった。

Mysql2::Error: Incorrect string value: '\xF0\x9F\x8E\xB6' for column 'content' at row 1: UPDATE topics SET content = '🎶', updated_at = '2016-04-22 01:53:02.596188' WHERE topics.id = 17

仕方ないので、Rails でキャッチして、エラーメッセージを返すことにしたが、エラーメッセージの変換に苦労したので、共有する。

コード

  rescue ActiveRecord::StatementInvalid => e
    if e.message.include? "Mysql2::Error: Incorrect string value:"
      message = "扱えない文字が含まれています: "
      m = e.message.match(%r{Incorrect string value: '(.*)' for})
      if m
        m1 = m[1].scan(/../).map { |b| b.to_i(16) }.reject { |x| x == 0 }.pack("C*")
        message += URI.encode(m1[0..3])
      end
      errors[:base] << message
      false
    else
      fail
    end
  end

詳細

MySQL は、エラーメッセージとして、扱えない文字を返すが、それは HEX 表記の文字列だった。

m[1] => "\\xF0\\x9F\\x8E\\xB6"

欲しいのは、UTF8 としての文字列である

"\xF0\x9F\x8E\xB6"

よって、"\x" を削除して、hex を文字列に置き換えれば、UTF-8 のバイト列が取得できる。

[178] pry(main)> a = "\\xF0\\x9F\\x8E\\xB6"
=> "\\xF0\\x9F\\x8E\\xB6"
[179] pry(main)> a.scan(/../).map{ |b| b }
=> ["\\x", "F0", "\\x", "9F", "\\x", "8E", "\\x", "B6"]
[180] pry(main)> a.scan(/../).map{ |b| b.to_i(16) }
=> [0, 240, 0, 159, 0, 142, 0, 182]
[181] pry(main)> a.scan(/../).map{ |b| b.to_i(16) }.reject{|x| x == 0}
=> [240, 159, 142, 182]
[182] pry(main)> a.scan(/../).map{ |b| b.to_i(16) }.reject{|x| x == 0}.pack("C*")
=> "\xF0\x9F\x8E\xB6"
[183] puts a.scan(/../).map{ |b| b.to_i(16) }.reject{|x| x == 0}.pack("C*")
🎶

一回で変換できるものはないのだろうか?

調査中

utf8mb4 を使っているので、 とかは、保存される。
なぜ🎶が保存されないのだろうか。

参考