AndroidJava/文字コード、文字化けの対応

文字コード、文字化けの対応(2019-12-06)


Android、というよりはJavaの話題になると思います。


はじめに、基本的な例

以下、文字コードを指定して
文字列をbyteに(text_org.getBytes(from_code);)
byteを文字列に(new String(b_array, to_code);)
それぞれ変えてみます。
(AndroidStudioで試すなら、適当にExampleUnitTestあたりにベタッとコピペして下さい。)

   @Test
   public void test(){
       example1(); // 詳細は6行ほど後で、
   }
 
   private void log(String string){ // ただのプリント文です。
       System.out.println(string); // 必要に応じてここを Log.d などに置き換えて下さい。
   }
 
   private void example1(){
       String text_org = "はろー"; // オリジナルの文字列
       byte[] b_array;
 
       String[] from_codecs = {"UTF-8", "Shift_JIS"};
       String to_code = "UTF-8"; // from_code と異なる文字コードを指定
 
       for(String from_code : from_codecs) {
           try { // String -> byte[] -> String に変換する
               b_array = text_org.getBytes(from_code);
               String text_after = new String(b_array, to_code);
               log("code:" + from_code + "->" + to_code + ", byte-length:" + b_array.length + ", text:" + text_after);
           } catch (Exception e) {
               log("Exception:" + e);
           }
       }
   }
(実行イメージ)
code:UTF-8->UTF-8, byte-length:9, text:はろー
code:Shift_JIS->UTF-8, byte-length:6, text:??[

1つめは変換してもとに戻す、2つめは文字コード不一致で文字化けする例です。
上記で使っている "UTF-8" やら "Shift_JIS" やらの
文字コードとして指定できる文字列は
各Javaのバージョンにあわせて、Oracleのサポートされているエンコーディング等から確認できます。
例として以下
https://docs.oracle.com/javase/jp/1.5.0/guide/intl/encoding.doc.html

Androidの場合はデフォルトはUTF-8です。
他のOS、たとえばWindowsでなにかメモ帳保存とかするとShift_JISになる場合が多いかと。
Javaで、ファイルから日本語の文書を読み込む時なんかは
byteArrayOutputStream.toString(文字コード); みたいに、
どの文字コードで出力するのかを指定します。

通常はこれで十分かと。
...以下、それでは足りない場合のお話です。

文字化けを確認したり情報を得たりする例

前の例では日本語テキスト全体を変換しましたが、
今度はfor文を使って、1byteずつ、読む範囲を広げていった場合の例になります。
前述の example1 に加えて、ここでは CharsetDecoder の例をメモしています。

   // 前述の example1 をコレに置き換えて下さい。
   private void example2(){
       String text_org = "はろー"; // オリジナルの文字列
       byte[] b_array;
       ByteBuffer b_buff; // これは CharsetDecoder で使います
       CharBuffer c_buff = CharBuffer.allocate(128); // これも CharsetDecoder で使います。128は、よしなに
 
       String from_code ="UTF-8";
       String to_code = "UTF-8";
 
       try { // try で getBytes()と new String() の例外を拾う、が、手抜きしてfor文ごとまるまるtryしています
           b_array = text_org.getBytes(from_code);
           for(int i=1; i<=b_array.length; i++){ // 1バイトずつ読み込む範囲を広げる
               byte[] buffer = new byte[i];
               System.arraycopy(b_array, 0, buffer, 0, i);
               String text_after = new String(buffer, to_code);
               log("---\ncode:" + from_code + "->" + to_code + ", byte-length:" + buffer.length + ", text:" + text_after);
 
               // 以下、CharsetDecoder を使う例
               CharsetDecoder dec = Charset.forName(to_code).newDecoder();
               dec.onUnmappableCharacter(CodingErrorAction.REPORT);
               dec.onMalformedInput(CodingErrorAction.REPORT);
               //--REPORTではなく、CodingErrorAction.REPLACE なら置き換えたい文字を指定--
               // dec.replaceWith("#");
 
               b_buff = ByteBuffer.wrap(b_array, 0, i);
               c_buff.clear();
               CoderResult coderResult = dec.decode(b_buff, c_buff, true);
               c_buff.flip();
 
               // CodingErrorAction.REPORT を指定した場合は情報を得られます
               if(coderResult.isMalformed() || coderResult.isUnmappable()){
                   log("isMalformed:" + coderResult.isMalformed() + ", isUnmappable:" + coderResult.isUnmappable()
                           + ", position:" + b_buff.position()
                           + ", length:" + coderResult.length());
               }
 
               // REPLACE と replaceWith() を使った場合は置き換え文字が指定できます
               // log("dec_text:" + c_buff.toString());
           }
       } catch (Exception e) {
           log("Exception:" + e);
       }
   }
(実行イメージ)
---
code:UTF-8->UTF-8, byte-length:1, text:?
isMalformed:true, isUnmappable:false, position:0, length:1
---
code:UTF-8->UTF-8, byte-length:2, text:?
isMalformed:true, isUnmappable:false, position:0, length:2
---
code:UTF-8->UTF-8, byte-length:3, text:は
---
(以下、省略)

CharBuffer の扱い方(flipやclear)については、それだけで1ページ必要なのでここでは省略させてください。

これについてはWebで先達の皆さま方のサイト(Google:CharBuffer flip clear)を読み漁りました...
一旦、文字コードの件とは分けて、どんなクラスなのかを色々お試しください

  • バッファ内の位置を、flipなどで進めたり戻したりする
  • その位置を起点に、バッファが読み書きされる
  • 先に十分な大きさの箱(バッファ)をallocateで確保して、使いまわす

文字コードの件に戻って、
後半の CharsetDecoder がポイントです。
これを使うと日本語の文字コード変換に失敗した時に、その位置や、失敗箇所の長さを確認したりできます。

CodingErrorActionを指定する箇所で REPLACE を使った場合は置き換え文字を変えることができるのですが、
デフォルトだと変換不可文字を "?"(=0x3F)、あるいはUTF-8の場合は EF BF BD、
といった何か任意の文字で置き換えるところを、
自分で文字を指定したい時に replaceWith() を使います。

この、CharsetDecoder の使い方で苦戦したので今回のメモを残しました。
new String(byte[], 文字コード);
だけでは文字化け箇所の情報が足りずに正しく処理しきれない、といった時に使えそうです。

最後に例として、example3をメモします。
後半でやっていることは同じですが、
前半ではテスト用に適当な壊れた文字列を作っています。
日本語処理を作り込む際は、こんな感じで壊れた文字列を正しく処理できるかどうか、色々試してみて下さい。

   private void example3(){
       String from_code ="UTF-8";
       String to_code = "UTF-8";
 
       String text_org1 = "はろー"; // オリジナルの文字列
       String text_org2 = "ワールド"; // オリジナルの文字列
       byte[] buffer;
 
       //byte[] b_middle = new byte[]{ (byte)0x1b }; // ESC(=0x1b)。ふつうに印字不可文字として扱われるはず
       //byte[] b_middle = new byte[]{ (byte)0x0a }; // 改行(=0x0a)。ふつうに改行が発生するだけ。
       byte[] b_middle = new byte[]{ (byte)0xa0 }; // 文字化けの場合の例、何らかの処理が必要
 
       // テスト用に特殊な文字列を生成する
       try {
           byte[] b_array1 = text_org1.getBytes(from_code);
           byte[] b_array2 = text_org2.getBytes(from_code);
 
           buffer = new byte[b_array1.length + b_middle.length + b_array2.length];
           int n = 0;
           System.arraycopy(b_array1, 0, buffer, n, b_array1.length); n += b_array1.length;
           System.arraycopy(b_middle, 0, buffer, n, b_middle.length); n += b_middle.length;
           System.arraycopy(b_array2, 0, buffer, n, b_array2.length);
 
       } catch (Exception e){
           log("Exception:" + e);
           return;
       }
 
       ByteBuffer b_buff;
       CharBuffer c_buff = CharBuffer.allocate(128); // 十分な値を指定すること。
       try {
           String text_after = new String(buffer, to_code);
           log("---\ncode:" + from_code + "->" + to_code + ", byte-length:" + buffer.length + ", text:" + text_after);
 
           // 文字化けしている箇所を特定したり、置き換え文字を指定したりする場合は CharsetDecoder を使う
           CharsetDecoder dec = Charset.forName(to_code).newDecoder();
           dec.onUnmappableCharacter(CodingErrorAction.REPORT);
           dec.onMalformedInput(CodingErrorAction.REPORT);
           //--REPORTではなく、CodingErrorAction.REPLACE なら置き換えたい文字を指定--
           // dec.replaceWith("#");
 
           b_buff = ByteBuffer.wrap(buffer);
           c_buff.clear();
           CoderResult coderResult = dec.decode(b_buff, c_buff, true);
           c_buff.flip();
 
           // CodingErrorAction.REPORT を指定した場合は情報を得られます
           if(coderResult.isMalformed() || coderResult.isUnmappable()){
               log("isMalformed:" + coderResult.isMalformed() + ", isUnmappable:" + coderResult.isUnmappable()
                       + ", position:" + b_buff.position()
                       + ", length:" + coderResult.length());
           }
 
           // REPLACE と replaceWith() を使った場合は置き換え文字が指定できます
           // log("dec_text:" + c_buff.toString());
 
       } catch (Exception e) {
           log("Exception:" + e);
       }
   }

以上です。
最初はよく分からないかもしれませんが、
色々変換する条件を変えつつ、とりあえず全部デバッグ出力しまくってみると、使い方のヒントになると思います。

余談:受信データの文字コード処理について

ほんとは最初にこの話題なのかもしれませんが、余談として...

前述の例で文字コードをUTF-8やらShift_JISやらに変えると、
byte配列の内容と length が変わるのが確認できると思います。
(おなじ「は」でも、3byteだったり2byteだったりします)
つまり「文字コード」が変わると別のデータになってしまうので、その扱いを間違えると、
メモ帳で書いて、そのファイルを他の端末に移すと、文字化けして読めなくなったり、
Webサイトを作ったのは良いけど、文字化けして読めなくなったり、
サーバに接続したけど、日本語が表示されない、これってどの設定が足りないの?フォント?LANG?アプリの設定?どれ!?
となってしまいます(経験談です [worried]

文字コードとは一体なにものなのか、については
実際にUTF-8やShift_JISのコード一覧表を探して見ると、雰囲気は分かると思います。
(コードによって表の内容が違って、なんかイッパイあるなー、とかが分かるかと)
Google:文字コード一覧表 UTF-8 Shift_JIS EUC_JP

詳しく知るためには、「文字コードの話」の解説がとても参考になりました。
http://euc.jp/i18n/charcode.ja.html

具体的な例、私の場合になりますが、
vttermエミュレータ(有名なのはTeraTerm、など)を 自分で作った時(Googleアプリ) に、
制御コードに加えて、GRやShift-In/Outとかの配慮が必要になってきました。*1

配慮が「不要な」ケース、というのは、
たとえばWebサーバからコンテンツをダウンロードする時なんかは、
コンテンツの大きさと、文字コードは分かっているのだから、*2
全てのコンテンツをダウンロードし終わった後に、
「new String(全てのコンテンツ, 文字コード);」 みたいに一括して処理すれば良いかと。

一方で配慮が必要なケース、というのは、
前述のvttermエミュレータで何か表示する際は、
telnet やら ssh やらでサーバから飛んできた来たデータを順次表示する必要があって、
コンテンツ全体の大きさは分からないと言うか、
飛んで来たこの2byteのデータはこれで一文字分なのか、もう1byteきて(合計3byteで)一文字なのか、
そこへ制御コードが割り込んで入ってくると、どこが文字の区切りなのか分からなかったり、しました。

かといって、自力で文字コードの変換・分岐処理を作るのは、ぜつぼうてきなので、
(前述のOracleのサイトで確認した文字コード一覧、一体いくつの文字コードが存在しているんだって話なので)
意地でもStringクラスにねじ込むぞ、というのが今回のメモに至った経緯です。

日本語って難しいですね...他の国の文字コードもこんなに複雑なんですかね?


*1 私は勉強もかねて自力でやりましたが、ふつうはフリーのvtterm用のライブラリを使えばいいので、自力でどうにかする機会はあまりないかもしれませんが...。商用利用する場合はライブラリのライセンスは確認しましょう。GNUとか。
*2 HTTP応答のContent-Lengthなど、html内の文字コード指定から情報を得られる