【いつもわからなくなるシリーズ】UTF-8とSJISの変換で文字化けする仕組み

# はじめに
- 「～」などの文字が SJIS と UTF-8 で変換すると文字化けをします。
- 調べると【なんとなく】わかるのですが、しばらくすると、いつもわからなくなりこんがらがるので、まとめておきます。


# 文字化けする
文字化けする理由はいくつかあります。その中でも、SJIS と UTF-8 の変換によってで文字化けすることがあります。
これは、SJIS の 文字コード には「Shift_JIS」や「MS932」「Windows-31J」などあります。
この変換によって、文字化けします。

これ　→　「～」　"から" ですね。
この文字が化けることがあります。（ほかにもありますが判りにくいので、一旦この文字で検証！）

まず、この文字のコードを「Shift_JIS」「Windows-31J (MS932)」で確認しましょう。
| 文字 | コード |
| --- | --- |
| ～ | 0x8160 |

※「Windows-31J」と「MS932」は名前が違うが同じもの

この文字を「Shift_JIS」「Windows-31J」のそれぞれで「UTF-8」に変換しましょう。

Java でコードを書いて検証しましょう。


```java
public static void readWrite(String inputFile, String inputCharsetName, String outputFile, String outputCharsetName) {
    FileInputStream    fileInputStream    = null;
    InputStreamReader  inputStreamReader  = null;

    FileOutputStream   fileOutputStream   = null;
    OutputStreamWriter outputStreamWriter = null;

    System.out.println("-----------");
    System.out.println("inputFile:" + inputFile);
    System.out.println("inputCharsetName:" + inputCharsetName);
    System.out.println("outputFile:" + outputFile);
    System.out.println("outputCharsetName:" + outputCharsetName);
    System.out.println("");

    try {
        fileInputStream    = new FileInputStream(inputFile);
        fileOutputStream   = new FileOutputStream(outputFile);

        inputStreamReader  = new InputStreamReader(fileInputStream, inputCharsetName);
        outputStreamWriter = new OutputStreamWriter(fileOutputStream, outputCharsetName);

        int ch;
        while ((ch = inputStreamReader.read()) != -1) {
            System.out.print(Integer.toHexString(ch) + " ");

            outputStreamWriter.write(ch);
        }

    } catch (IOException e) {
        System.out.println(e);

    } finally {
        if (outputStreamWriter != null) {
            try {
                outputStreamWriter.close();
            } catch (IOException e) {}
        }
        if (inputStreamReader != null) {
            try {
                inputStreamReader.close();
            } catch (IOException e) {}
        }
        if (fileOutputStream != null) {
            try {
                fileOutputStream.close();
            } catch (IOException e) {}
        }
        if (fileInputStream != null) {
            try {
                fileInputStream.close();
            } catch (IOException e) {}
        }
    }
    System.out.println("");

}
```

```java
        readWrite("./in/SJIS.txt", "Windows-31J", "./out/Windows-31J_to_UTF8.txt", "UTF-8");
        readWrite("./in/SJIS.txt", "Shift_JIS", "./out/Shift_JIS_to_UTF8.txt", "UTF-8");
```

```text:/in/SJIS.txt (入力のファイル)
～
```

```text:/out/Windows-31J_to_UTF8.txt(出力した結果のファイル)
～
```

```text:/out/Shift_JIS_to_UTF8.txt (出力した結果のファイル)
〜
```

```text:実行結果
-----------
inputFile:./in/SJIS.txt
inputCharsetName:Windows-31J
outputFile:./out/Windows-31J_to_UTF8.txt
outputCharsetName:UTF-8

ff5e 
-----------
inputFile:./in/SJIS.txt
inputCharsetName:Shift_JIS
outputFile:./out/Shift_JIS_to_UTF8.txt
outputCharsetName:UTF-8

301c 
```

なんと、元が「～」から「文字コード」によって UTF-8 上違う文字・コードになりました。

| 文字コード | コード |
| --- | :---: |
| Shift_JIS | U+301C |
| Windows-31J | U+FF5E |

UTF-8 から改めて、元の文字コードを利用して戻すと、「～」に戻りますが、
- Windows-31J で読み込み、UTF-8 に変換、Shift_JIS で出力
- Shift_JIS で読み込み、UTF-8 に変換、Windows-31J で出力
のように異なる文字コードにすると、文字化けします。

| SJIS上の文字 | 読むときの文字コード | → | UTFの文字 | → | 書き込むときの文字コード | できたSJIS上の文字 |
| :---: | --- | --- | :---: | --- | --- | --- |
| ～ (0x8160) | Windows-31J | → | ～ (U+FF5E) | → | Windows-31J | ～ (0x8160) |
| ～ (0x8160) | Shift_JIS | → | 〜 (U+301C) | → | Shift-JIS | ～ (0x8160) |
| ～ (0x8160) | Windows-31J | → | ～ (U+FF5E) | → | Shift-JIS | 文字化け |
| ～ (0x8160) | Shift_JIS | → | 〜 (U+301C) | → | Windows-31J | 文字化け |

さて、読むときと書くときで、違う文字コードになることはあるのでしょうか。

それは、「クライアントが Windows、サーバが Unix系 でデータをやり取りする場合」です。

以下のような対処が必要になってきます。
クライアント Windows から サーバ Unix系 へデータを送信する場合
　・Windows-31J で読み込み、UTF-8 に変換。
　・「～ (U+FF5E)」を「〜 (U+301C)」に変換。
　・サーバ側 Unix系にデータを送信。

サーバ Unix系 から クライアント Windows へデータを受信する場合
　・サーバ側 Unix系からデータを受信。
　・「〜 (U+301C)」を「～ (U+FF5E)」に変換。
　・Windows-31J で書き込む。

| SJIS上の文字 | 読むときの文字コード | UTFの文字 | 書き込むときの文字コード |
| --- | --- | --- | --- |
| ～ (0x8160) | Windows-31J | ～ (U+FF5E) | Windows-31J | ～ (0x8160) |
| ～ (0x8160) | Shift_JIS | 〜 (U+301C) | Shift-JIS | ～ (0x8160) |
| ～ (0x8160) | Windows-31J | ～ (U+FF5E)から「〜 (U+301C)」に変換 | Shift-JIS | 文字化け |
| ～ (0x8160) | Shift_JIS | 〜 (U+301C)から「～ (U+FF5E)」に変換 | Windows-31J | 文字化け |

このような対処が必要になる文字として、以下の文字があります。
UTF-8 上で、文字の変換が必要になります。

| SJIS | Windows-31J で読み込んでUTF-8へ変換 | Shift-JIS で読み込んでUTF-8へ変換 |
| :---: | :---: | :---: |
| ～ (0x8160) | ～ (U+FF5E) | 〜 (U+301C) |
| ∥ (0x8161) | ∥ (U+2225) | ‖ (U+2016) |
| － (0x817C) | － (U+FF0D) | − (U+2212) |
| ￠ (0x8191) | ￠ (U+FFE0) | ¢ (U+00A2) |
| ￡ (0x8192) | ￡ (U+FFE1) | £ (U+00A3) |
| ￢ (0x81CA) | ￢ (U+FFE2) | ¬ (U+00AC) |
| ― (0x81CA) | ― (U+2015) | — (U+2014) |


では、その変換を入れたコードにしてみましょう。

```java
readWriteForWindows31JtoShiftJIS("./in/SJIS.txt", "Windows-31J", "./out/Windows-31J_to_Shift_JIS.txt", "Shift_JIS");
readWriteForShiftJIStoWindows31J("./in/SJIS.txt", "Shift_JIS", "./out/Shift_JIS_to_Windows-31J.txt", "Windows-31J");
```

```text:SJIS.txt
～∥－￠￡￢―
```

```java:readWriteForWindows31JtoShiftJISメソッド
public static void readWriteForWindows31JtoShiftJIS(String inputFile, String inputCharsetName, String outputFile, String outputCharsetName) {
    FileInputStream    fileInputStream    = null;
    InputStreamReader  inputStreamReader  = null;

    FileOutputStream   fileOutputStream   = null;
    OutputStreamWriter outputStreamWriter = null;

    System.out.println("-----------");
    System.out.println("inputFile:" + inputFile);
    System.out.println("inputCharsetName:" + inputCharsetName);
    System.out.println("outputFile:" + outputFile);
    System.out.println("outputCharsetName:" + outputCharsetName);
    System.out.println("");

    try {
        fileInputStream    = new FileInputStream(inputFile);
        fileOutputStream   = new FileOutputStream(outputFile);

        inputStreamReader  = new InputStreamReader(fileInputStream, inputCharsetName);
        outputStreamWriter = new OutputStreamWriter(fileOutputStream, outputCharsetName);

        int ch;
        while ((ch = inputStreamReader.read()) != -1) {
            System.out.print(Integer.toHexString(ch) + " ");

            switch (ch) {
            case 0xff5e:
                ch = 0x301c;
                break;
            case 0x2225:
                ch = 0x2016;
                break;
            case 0xff0D:
                ch = 0x2212;
                break;
            case 0xFFE0:
                ch = 0x00A2;
                break;
            case 0xFFE1:
                ch = 0x00A3;
                break;
            case 0xFFE2:
                ch = 0x00AC;
                break;
            case 0x2015:
                ch = 0x2014;
                break;
            }

            outputStreamWriter.write(ch);

        }

    } catch (IOException e) {
        System.out.println(e);

    } finally {
        if (outputStreamWriter != null) {
            try {
                outputStreamWriter.close();
            } catch (IOException e) {}
        }
        if (inputStreamReader != null) {
            try {
                inputStreamReader.close();
            } catch (IOException e) {}
        }
        if (fileOutputStream != null) {
            try {
                fileOutputStream.close();
            } catch (IOException e) {}
        }
        if (fileInputStream != null) {
            try {
                fileInputStream.close();
            } catch (IOException e) {}
        }
    }
    System.out.println("");

}
```

```java:readWriteForShiftJIStoWindows31Jメソッド
public static void readWriteForShiftJIStoWindows31J(String inputFile, String inputCharsetName, String outputFile, String outputCharsetName) {
    FileInputStream    fileInputStream    = null;
    InputStreamReader  inputStreamReader  = null;

    FileOutputStream   fileOutputStream   = null;
    OutputStreamWriter outputStreamWriter = null;

    System.out.println("-----------");
    System.out.println("inputFile:" + inputFile);
    System.out.println("inputCharsetName:" + inputCharsetName);
    System.out.println("outputFile:" + outputFile);
    System.out.println("outputCharsetName:" + outputCharsetName);
    System.out.println("");

    try {
        fileInputStream    = new FileInputStream(inputFile);
        fileOutputStream   = new FileOutputStream(outputFile);

        inputStreamReader  = new InputStreamReader(fileInputStream, inputCharsetName);
        outputStreamWriter = new OutputStreamWriter(fileOutputStream, outputCharsetName);

        int ch;
        while ((ch = inputStreamReader.read()) != -1) {
            System.out.print(Integer.toHexString(ch) + " ");

            switch (ch) {
            case 0x301c:
                ch = 0xff5e;
                break;
            case 0x2016:
                ch = 0x2225;
                break;
            case 0x2212:
                ch = 0xff0D;
                break;
            case 0x00A2:
                ch = 0xFFE0;
                break;
            case 0x00A3:
                ch = 0xFFE1;
                break;
            case 0x00AC:
                ch = 0xFFE2;
                break;
            case 0x2014:
                ch = 0x2015;
            }

            outputStreamWriter.write(ch);

        }

    } catch (IOException e) {
        System.out.println(e);

    } finally {
        if (outputStreamWriter != null) {
            try {
                outputStreamWriter.close();
            } catch (IOException e) {}
        }
        if (inputStreamReader != null) {
            try {
                inputStreamReader.close();
            } catch (IOException e) {}
        }
        if (fileOutputStream != null) {
            try {
                fileOutputStream.close();
            } catch (IOException e) {}
        }
        if (fileInputStream != null) {
            try {
                fileInputStream.close();
            } catch (IOException e) {}
        }
    }
    System.out.println("");

}
```


#### ご了承ください
- 自分が何とか学習した内容を記載しており、間違ってはいないと思いますが、そのあたりは生暖かくご覧いただければ、嬉しいです。

#### 参考にさせていただいたページ
参考にさせていただきました、ありがとうございます。
- UTF-8 → cp932(Shift_JIS)変換表
    - https://sites.google.com/site/fudist/Home/vim-nihongo-ban/mojibake/utf8-cp932conv
- とほほのJava入門 - 入出力
    - https://www.tohoho-web.com/java/file.htm