こんにちは。ima1zumi です。
私の開発している Rails アプリでは、Excel で読み込めるように 文字コードを Windows-31J に変換して CSV を出力する機能があります。
先日、CSV 出力にて Unicode の波ダッシュ 〜
を Windows-31J に変換しようとして Encoding::UndefinedConversionError
が発生して CSV 出力に失敗したことがありました。なぜエラーになるのか、どうやって対応するのかをまとめました。
まとめ
encode
メソッドの fallback
オプションを使って未定義文字の変換先を定義することで変換できます。
str = "\u{2014 301C 2016 2212 00A2 00A3 00AC}" undefined_signs = { "\u2014" => "\x81\x5C".force_encoding(Encoding::Windows_31J), # — EM DASH "\u301C" => "\x81\x60".force_encoding(Encoding::Windows_31J), # 〜 WAVE DASH "\u2016" => "\x81\x61".force_encoding(Encoding::Windows_31J), # ‖ DOUBLE VERTICAL LINE "\u2212" => "\x81\x7C".force_encoding(Encoding::Windows_31J), # − MINUS SIGN "\u00A2" => "\x81\x91".force_encoding(Encoding::Windows_31J), # ¢ CENT SIGN "\u00A3" => "\x81\x92".force_encoding(Encoding::Windows_31J), # £ POUND SIGN "\u00AC" => "\x81\xCA".force_encoding(Encoding::Windows_31J), # ¬ NOT SIGN } p str.encode(Encoding::Windows_31J, fallback: undefined_signs)
なぜこれらの文字がエラーになるのか
Unicode の WAVE DASH などの文字は Windows-31J に定義されていないからです。
文字コードを変換するとは、ある文字コードの1文字を別の文字コードの1文字に変換するということです。例えば、Unicode の「あ」は Windows-31J では「あ」に対応する、というように、変換元のある文字は変換先のどの文字に対応する、という関係が1対1で紐付けられています。Unicode の 「🥺」は Windows-31J に存在しないように、ある文字コードには存在していても変換先の文字コードには存在しないため変換できない文字もあります。
Unicode の WAVE DASH(U+301C)は Windows-31J には対応する文字がありません。このため、変換しようとすると定義がないため Encoding::UndefinedConversionError
になります。ですが、Unicode の FULLWIDTH TILDE (U+FF5E) は Windows-31J の 〜
に対応しているため、 「~」は Windows-31J に変換できます。
"\u301C".encode(Encoding::Windows_31J) # WAVE DASH # `encode': U+301C from UTF-8 to Windows-31J (Encoding::UndefinedConversionError) "\uFF5E".encode(Encoding::Windows_31J) # FULLWIDTH TILDE # => "\x{8160}"
Windows-31J の波ダッシュは FULLWIDTH TILDE に対応するため、 WAVE DASH は変換できないという対応付けになってしまっています*1が、実務上は形が同じ文字に変換してしまいたいことはあります。そういったときに、Ruby では変換未定義文字に変換テーブルを定義することで対応できます。
fallback
ということで、未定義文字のいくつかを自前で定義します。これは String#encode
のオプションの fallback
を使えます。
fallback
には Hash
, Proc
, Method
を渡すことができます。ここでは Hash
を使います。キーには変換元である Unicode の文字を、変換先には Windows-31J で変換先に指定したい文字のバイト列を定義します。また、作成した String
の encoding
は force_encoding
で変更して、文字コードを揃えておきます。
str = "\u{2014 301C 2016 2212 00A2 00A3 00AC}" undefined_signs = { "\u2014" => "\x81\x5C".force_encoding(Encoding::Windows_31J), # — EM DASH "\u301C" => "\x81\x60".force_encoding(Encoding::Windows_31J), # 〜 WAVE DASH "\u2016" => "\x81\x61".force_encoding(Encoding::Windows_31J), # ‖ DOUBLE VERTICAL LINE "\u2212" => "\x81\x7C".force_encoding(Encoding::Windows_31J), # − MINUS SIGN "\u00A2" => "\x81\x91".force_encoding(Encoding::Windows_31J), # ¢ CENT SIGN "\u00A3" => "\x81\x92".force_encoding(Encoding::Windows_31J), # £ POUND SIGN "\u00AC" => "\x81\xCA".force_encoding(Encoding::Windows_31J), # ¬ NOT SIGN } p str.encode(Encoding::Windows_31J, fallback: undefined_signs)
このように未定義文字に対して変換規則を作ることで対応ができます。
ref: String#encode (Ruby 3.1 リファレンスマニュアル)
UnicodeからWindows-31Jに変換できない文字はどのくらいあるか
Unicode は世界中の文字を収録した文字コードで Unicode 14.0 時点で 144,697 文字が使えます。Windows-31J は主に日本語が使える文字コードで約 7000 文字を収録しています。変換できない文字は非常に多くあります。 その中でもよく出てくる記号は先ほどのような波ダッシュ「〜」で、漢字では「𠀋」「㐂」「𠮷」(つちよし)など*2があります。これらの変換できない文字をすべて対応しようとするのはあまり現実的ではありません。また、記号であれば形が同じものを同じ文字とみなすことはありますが、特に人名において、字形の異なる漢字を同じ漢字としてみなすことには注意が必要です。このように、未定義文字は単純に別の文字で置き換えればいいという問題ではありません。
未定義文字にどう対応するか
対応方針はいくつか考えられますが、どれもメリット・デメリットがありどれがベストということはありません。扱いたい文章の性質によって対応を切り分けるのが良いと思います。
(1) 入力時に変換できない文字がないかチェックし、変換できない文字は入力させない
メリットは変換できない文字がデータとして入ってこないため、安全に扱えるということです。
デメリットは Windows-31J として入力できる文字かどうかのチェックが大変なことです。また、基本は Unicode として扱って一部 Windows-31J に変換したいような文字の場合、入力できない文字が多いことが不便です。
(2) 変換できない文字は ?
のような別の文字に置き換える
メリットは未定義文字があっても変換に成功することです。
デメリットは変換後の文字列から「変換できなかった文字が何であるか」が分からないことです。また、人名や住所など置換すると意味をなさない文字列がある場合、この方法をとるべきではありません。(例:𠮷田が?田になると意味をなさない)
別の文字に置き換える場合、 String#scrub
で置換できます。 String#scrub (Ruby 3.1 リファレンスマニュアル)
(3) 変換できない文字があった場合はエラーとし、処理を中断する
メリットは変換できない文字に対し個別に対応できることです。デメリットは、ユーザーは処理を中断されるため不便になることです。
おまけ: 補足と用語の整理
Unicode
世界中のあらゆる文字を収録することを目標とした符号化文字集合です。
UTF-8
Unicode の符号化方式のうちの1つで、8ビット単位の可変長です。
U+xxxx
Unicode codepoint の表記方法です。
\uxxxx, \u{xxxx xxxx}
Ruby で Unicode codepoint を指定して文字を表現する記法です。 {}
で括ると複数の文字を指定できます。
ref: リテラル (Ruby 3.1 リファレンスマニュアル)
Shift_JIS
JIS X 0201を1バイトで、JIS X 0208を2バイトで符号化する可変幅文字符号化方式です。
Windows-31J, CP932
Shift_JIS のマイクロソフト拡張版文字コードで、Shift_JIS とは収録されている文字が一部異なります。 Ruby では Windows-31J, CP932 は同じ文字コードです。
WAVE DASH
ダッシュ「―」が波打っている記号で、日本語では波ダッシュと呼びます。
TILDE
ダイアクリティカルマーク(発音を区別する記号)の一種です。チルダ単独で使う場合は数学記号やコンピュータ・プログラミング言語において特別な意味を持つ記号として扱われます。
FULLWIDTH TILDE
いわゆる全角チルダで、互換用の文字です。
この符号位置は、1バイト文字と2バイト文字が混在する符号化方式における重複符号化を救済するための互換用に導入された符号位置です。具体的な用途としては、EUC-JPにおいてASCIIのチルダとJIS X 0212のチルダが重複してしまうのを解決するために、JIS X 0212のチルダに対応付けるための互換用として用いるのが妥当です。
引用元: 矢野 啓介 “WEB+DB PRESS plusシリーズ [改訂新版]プログラマのための文字コード技術入門" ページ位置85%
参考資料
- UnicodeのWAVE DASH例示字形が、25年ぶりに修正された理由 - INTERNET Watch Watch
- 波ダッシュはチルダではない
- Shift_JIS と Windows-31J (MS932) の違いを整理してみよう |
- Microsoftコードページ932 - Wikipedia
- Unicode - Wikipedia
- Shift_JIS系文字一覧イメージとSJIS・MS932・CP943・SJIS2004の違い - instant tools
*1:歴史的経緯により Windows-31J の波ダッシュは FULLWIDTH TILDE に対応していますが、適切な変換先ではないと考えられます。 ref: 波ダッシュはチルダではない