ファイルの文字コードを変換するには
Shift_JIS(Windows の cp932)で保存されたテキストファイルを UTF-8 に変換して保存し直す方法を各言語で示す。
cp932 と shift_jis で読める文字が違う点と、改行コードや BOM の扱いまでを扱う。
公開:
選んだ言語は、ほかのサンプルコードを開いたときも最初に表示する。
Python
src = "sjis.txt"
dst = "utf8.txt"
# newline="" を指定すると、改行コード(CRLF)を変換せずそのまま引き継ぐ
with open(src, encoding="cp932", newline="") as f:
text = f.read()
with open(dst, "w", encoding="utf-8", newline="") as f:
f.write(text)
with open(src, "rb") as a, open(dst, "rb") as b:
print(len(a.read()), "->", len(b.read()), "bytes") # 36 -> 52 bytes
print(text.splitlines()[0]) # こんにちは、世界open の encoding に、読み込み側と書き込み側の文字コードをそれぞれ指定するだけで変換できる。
Windows 由来のファイルは shift_jis ではなく cp932 を指定し、newline="" を付けると CRLF の改行がそのまま残る。
JavaScript
import { readFile, writeFile } from "node:fs/promises";
const bytes = await readFile("sjis.txt");
// TextDecoder は Shift_JIS を読める。書き出す文字列は常に UTF-8 になる
const text = new TextDecoder("shift_jis").decode(bytes);
await writeFile("utf8.txt", text, "utf8");
const out = await readFile("utf8.txt");
console.log(bytes.length, "->", out.length, "bytes"); // 36 -> 52 bytes
console.log(text.split("\r\n")[0]); // こんにちは、世界TextDecoder に shift_jis を渡して読み、文字列を UTF-8 で書き出す。
TextDecoder は Shift_JIS を読めるが、TextEncoder は UTF-8 にしか書き出せないため、この方法は UTF-8 から Shift_JIS へ戻す向きには使えない。
TypeScript
import { readFile, writeFile } from "node:fs/promises";
async function convertToUtf8(src: string, dst: string, fromEncoding: string): Promise<void> {
const bytes = await readFile(src);
// fatal: true にすると、変換できないバイトがあったときに U+FFFD で埋めず例外にする
const text = new TextDecoder(fromEncoding, { fatal: true }).decode(bytes);
await writeFile(dst, text, "utf8");
}
async function main(): Promise<void> {
await convertToUtf8("sjis.txt", "utf8.txt", "shift_jis");
const out = await readFile("utf8.txt");
console.log(out.length, "bytes"); // 52 bytes
console.log(out.toString("utf8").split("\r\n")[0]); // こんにちは、世界
}
main();TextDecoder の fatal を true にすると、変換できないバイトが混ざっていたときに U+FFFD で埋めず、TypeError で止まる。
壊れたファイルを黙って変換してしまう事故を避けたい処理では、fatal を付けておく。
Go
// 事前準備: go mod init enc && go get golang.org/x/text
package main
import (
"fmt"
"os"
"golang.org/x/text/encoding/japanese"
"golang.org/x/text/transform"
)
func main() {
src, err := os.ReadFile("sjis.txt")
if err != nil {
panic(err)
}
// ShiftJIS の NewDecoder で UTF-8 に変換する。標準ライブラリだけでは Shift_JIS を読めない
utf8Bytes, _, err := transform.Bytes(japanese.ShiftJIS.NewDecoder(), src)
if err != nil {
panic(err)
}
if err := os.WriteFile("utf8.txt", utf8Bytes, 0o644); err != nil {
panic(err)
}
fmt.Println(len(src), "->", len(utf8Bytes), "bytes") // 36 -> 52 bytes
fmt.Println(string(utf8Bytes[:24])) // こんにちは、世界
}標準ライブラリは Shift_JIS を扱えないため、golang.org/x/text を go get し、japanese.ShiftJIS.NewDecoder を transform.Bytes に渡して変換する。
不正なバイトがあっても err は nil のまま U+FFFD に置き換わるので、壊れたファイルを見つけたいときは変換後の文字列に U+FFFD が混ざっていないかを確かめる。
Rust
// Cargo.toml: encoding_rs = "0.8"
use encoding_rs::SHIFT_JIS;
use std::fs;
fn main() -> std::io::Result<()> {
let src = fs::read("sjis.txt")?;
// 戻り値の 3 つ目は「変換できないバイトがあって U+FFFD に置き換えたか」
let (text, _, had_errors) = SHIFT_JIS.decode(&src);
if had_errors {
eprintln!("変換できないバイトがあった");
}
fs::write("utf8.txt", text.as_bytes())?; // Rust の文字列は常に UTF-8
let out = fs::read("utf8.txt")?;
println!("{} -> {} bytes", src.len(), out.len()); // 36 -> 52 bytes
println!("{}", text.lines().next().unwrap()); // こんにちは、世界
Ok(())
}標準ライブラリには Shift_JIS の変換が無いため、encoding_rs クレートの SHIFT_JIS.decode を使う。
戻り値の 3 つ目の had_errors が、変換できないバイトを U+FFFD に置き換えたかどうかを示す。
Rust の文字列は常に UTF-8 なので、as_bytes をそのまま書き出せば UTF-8 のファイルになる。
Java
import java.io.IOException;
import java.nio.charset.Charset;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
public class Main {
public static void main(String[] args) throws IOException {
Path src = Path.of("sjis.txt");
Path dst = Path.of("utf8.txt");
// Windows 由来のファイルは "Shift_JIS" より "windows-31j"(MS932)で読むと安全
String text = Files.readString(src, Charset.forName("windows-31j"));
Files.writeString(dst, text, StandardCharsets.UTF_8);
System.out.println(Files.size(src) + " -> " + Files.size(dst) + " bytes"); // 36 -> 52 bytes
System.out.println(text.lines().findFirst().get()); // こんにちは、世界
}
}Files.readString と Files.writeString に Charset を渡して変換する。
Windows 由来のファイルは "Shift_JIS" ではなく "windows-31j" を指定する。
前者は丸数字などの機種依存文字を含むファイルで MalformedInputException になる。
C#
using System;
using System.IO;
using System.Text;
// .NET Core 以降は Shift_JIS などのコードページを使う前に、プロバイダーの登録が要る
Encoding.RegisterProvider(CodePagesEncodingProvider.Instance);
var shiftJis = Encoding.GetEncoding(932);
var text = File.ReadAllText("sjis.txt", shiftJis);
File.WriteAllText("utf8.txt", text, new UTF8Encoding(false)); // false = BOM を付けない
Console.WriteLine($"{new FileInfo("sjis.txt").Length} -> {new FileInfo("utf8.txt").Length} bytes"); // 36 -> 52 bytes
Console.WriteLine(text.Split("\r\n")[0]); // こんにちは、世界Encoding.GetEncoding(932) を使う前に、CodePagesEncodingProvider の登録が要る。
登録しないと NotSupportedException(No data is available for encoding 932)になる。
書き出しは new UTF8Encoding(false) で BOM を付けない UTF-8 にしている。
つまずき
テキストファイルには文字コードの情報が入っていないので、変換元は読む側が指定する。
指定を間違えると、言語によって症状が分かれる。
Shift_JIS のファイルを UTF-8 として読むと、Python は「'utf-8' codec can't decode byte 0x82 in position 0: invalid start byte」の UnicodeDecodeError で止まる。
一方、Node.js の TextDecoder(fatal なし)は例外を出さず、�(U+FFFD)が並んだ文字列を返すため、変換後のファイルが壊れていても気づきにくい。
shift_jis と cp932 は読める文字が違う
Windows で作られた Shift_JIS のファイルには、丸数字の ① や全角チルダ(U+FF5E の ~)のような機種依存文字が入っていることがある。
これらは cp932(Windows-31J)の拡張で、Python の shift_jis コーデックは扱えない。
実行すると、shift_jis では ① を含むファイルが「'shift_jis' codec can't decode byte 0x87 in position 6: illegal multibyte sequence」で読めず、cp932 では読めた。
Java も同様で、"Shift_JIS" は MalformedInputException、"windows-31j" は読めた。
JavaScript の TextDecoder、Go、Rust の encoding_rs では、① を含むファイルも問題なく変換できた。
変換後の確認
変換できたかは、バイト数と先頭の文字で確かめられる。
サンプルは全角 16 文字と CRLF 2 組のファイルで、Shift_JIS では 36 バイト、UTF-8 では 52 バイトになった。
全角文字は Shift_JIS で 2 バイト、UTF-8 で 3 バイトなので、16 文字ぶん 16 バイト増える。
改行コードは変換せずに引き継いでいるため、CRLF のまま残る。
BOM を付けたくない場合は、Python なら utf-8、C# なら UTF8Encoding(false) を使う。