HHeLiBeXの日記 正道編

日々の記憶の記録とメモ‥

各言語でファイル入出力+文字エンコーディング変換

各言語でファイル入出力と文字エンコーディング変換を書いてみたメモ。

やってる途中で、別々のエントリに分けた方が良かったかもと思ったりもしたが、例えばJavaなんかは内部的には「文字」はUTF-8だったりして入出力と文字エンコーディング変換が深くかかわっていたりするので、まぁいいかということで。

要件は以下の通り。

環境

手元にあるものということで、環境は以下のものに限定する。

Java

java.nio.charsetパッケージのCharsetDecoder / CharsetEncoderの存在を今更知ったので、2パターン書いてみた。

  • パターン1
  • パターン2
    • 読み込んだバイト列をCharsetDecoderで文字列に変換
    • 文字列をCharsetEncoderでバイト列に変換して書き込み

パターン1

import java.io.BufferedReader;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.OutputStreamWriter;
import java.io.PrintWriter;

public class Main {
    public static void main(String[] args) {
        if (args.length != 1) {
            System.err.println("Usage: java Main dirname");
            System.exit(1);
            return;
        }

        File inFile = new File(args[0], "in.txt");
        File outFile = new File(args[0], "out.txt");

        try (BufferedReader in = new BufferedReader(new InputStreamReader(new FileInputStream(inFile), "EUC-JP"));
            PrintWriter out = new PrintWriter(new OutputStreamWriter(new FileOutputStream(outFile), "Windows-31J"))
        ) {
            char[] buf = new char[1024];
            int len;
            while ((len = in.read(buf)) > 0) {
                out.write(buf, 0, len);
            }
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

パターン2

import java.io.BufferedInputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.PrintStream;
import java.nio.ByteBuffer;
import java.nio.CharBuffer;
import java.nio.charset.Charset;
import java.nio.charset.CharsetDecoder;
import java.nio.charset.CharsetEncoder;
import java.nio.charset.CoderResult;

public class Main {
    public static void main(String[] args) {
        if (args.length != 1) {
            System.err.println("Usage: java Main dirname");
            System.exit(1);
            return;
        }

        File inFile = new File(args[0], "in.txt");
        File outFile = new File(args[0], "out.txt");

        CharsetDecoder decoder = Charset.forName("EUC-JP").newDecoder();
        decoder.reset();
        CharsetEncoder encoder = Charset.forName("Windows-31J").newEncoder();
        encoder.reset();

        try (InputStream in = new BufferedInputStream(new FileInputStream(inFile));
            PrintStream out = new PrintStream(new FileOutputStream(outFile))
        ) {
            ByteBuffer inBuf = ByteBuffer.allocate(1024);
            ByteBuffer outBuf = ByteBuffer.allocate(1024);
            CharBuffer tmpBuf = CharBuffer.allocate(1024);
            byte[] buf = new byte[1024];
            int len;
            while ((len = in.read(buf, 0, Math.min(buf.length, inBuf.remaining()))) > 0) {
                inBuf.put(buf, 0, len);
                inBuf.flip();
                tmpBuf.clear();
                CoderResult res = decoder.decode(inBuf, tmpBuf, false);
                if (res.isUnderflow()) {
                    inBuf.compact();
                    tmpBuf.flip();
                    outBuf.clear();
                    encoder.encode(tmpBuf, outBuf, false);
                    outBuf.flip();
                    outBuf.get(buf, 0, outBuf.limit());
                    out.write(buf, 0, outBuf.limit());
                }
            }
            /* flush()するためのダミー処理 */
            inBuf.clear();
            inBuf.flip();
            tmpBuf.clear();
            decoder.decode(inBuf, tmpBuf, true);
            tmpBuf.flip();
            outBuf.clear();
            encoder.encode(tmpBuf, outBuf, true);
            outBuf.flip();
            outBuf.get(buf, 0, outBuf.limit());
            out.write(buf, 0, outBuf.limit());

            /* flush() */
            tmpBuf.clear();
            decoder.flush(tmpBuf);
            tmpBuf.flip();
            outBuf.clear();
            encoder.flush(outBuf);
            outBuf.flip();
            outBuf.get(buf, 0, outBuf.limit());
            out.write(buf, 0, outBuf.limit());
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

なんか、CharsetDecoder / CharsetEncoderの挙動を理解するのにすごく時間が掛かった、というか、そもそもByteBuffer / CharBufferの挙動もよく分からんかった。今でも、上記プログラムでほんとに正しいのか全く自信が無い‥

C

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <iconv.h>
#include <limits.h>
#include <errno.h>

#define BUF_SIZE 1024

int main(int argc, char** argv) {
    if (argc != 2) {
        fprintf(stderr, "Usage: %s dirname\n", argv[0]);
        return 1;
    }

    /* ファイルのオープン */
    char inFile[PATH_MAX];
    sprintf(inFile, "%s/in.txt", argv[1]);
    char outFile[PATH_MAX];
    sprintf(outFile, "%s/out.txt", argv[1]);

    FILE* inFp = fopen(inFile, "r");
    if (!inFp) {
        perror(inFile);
        return 1;
    }
    FILE* outFp = fopen(outFile, "w");
    if (!outFp) {
        perror(outFile);    
        fclose(inFp);
        return 1;
    }

    /* 文字エンコーディング変換の準備 */
    iconv_t iconvHandler = iconv_open("CP932", "EUC-JP");

    /* 入力を読み込んで文字エンコーディング変換して出力 */
    char inBuf[BUF_SIZE];
    size_t inBufLeft = 0;
    char outBuf[BUF_SIZE];
    int len;
    while ((len = fread(inBuf + inBufLeft, sizeof(char), sizeof(inBuf) - inBufLeft, inFp)) + inBufLeft > 0) {
        inBufLeft += len;
        char* inPtr = inBuf;
        char* outPtr = outBuf;
        size_t outBufLeft = sizeof(outBuf);

        int rc = iconv(iconvHandler, &inPtr, &inBufLeft, &outPtr, &outBufLeft);
        if (rc == -1 && (errno == EILSEQ || errno == E2BIG)) {
            perror("iconv");
            break;
        }
        fwrite(outBuf, sizeof(char), sizeof(outBuf) - outBufLeft, outFp);

        if (inBufLeft > 0) {
            strncpy(inBuf, inPtr, inBufLeft);
        }
    }
    iconv_close(iconvHandler);

    /* ファイルのクローズ */
    fclose(inFp);
    fclose(outFp);

    return 0;
}

iconvの挙動を理解するのにすごい苦労した。というか、ネット上に転がっていたサンプルをいくつか試してみたが、入力データにちょっと細工したりするとすぐにテストケースでNGが出たりして使えないということになり、結局manページを熟読して理解したという・・・

C++

C言語で書いたプログラムのうち、ファイル入出力の部分をC++版にしてみたもの。

#include <fstream>
#include <iostream>
#include <string>
#include <cstring>
#include <iconv.h>
#include <limits.h>
#include <errno.h>

#define BUF_SIZE 1024

using namespace std;

int main(int argc, char** argv) {
    if (argc != 2) {
        fprintf(stderr, "Usage: %s dirname\n", argv[0]);
        return EXIT_FAILURE;
    }

    /* ファイルのオープン */
    char inFile[PATH_MAX];
    sprintf(inFile, "%s/in.txt", argv[1]);
    char outFile[PATH_MAX];
    sprintf(outFile, "%s/out.txt", argv[1]);

    ifstream inFs(inFile, ios::binary);
    if (inFs.fail()) {
        perror(inFile);
        return EXIT_FAILURE;
    }
    ofstream outFs(outFile, ios::binary);
    if (outFs.fail()) {
        perror(outFile);    
        return EXIT_FAILURE;
    }

    /* 文字エンコーディング変換の準備 */
    iconv_t iconvHandler = iconv_open("CP932", "EUC-JP");

    /* 入力を読み込んで文字エンコーディング変換して出力 */
    char inBuf[BUF_SIZE / 2];
    size_t inBufLeft = 0;
    char outBuf[BUF_SIZE * 2];
    streamsize len;
    while ((len = inFs.readsome(inBuf + inBufLeft, sizeof(inBuf) - inBufLeft)) + inBufLeft > 0) {
        inBufLeft += len;
        char* inPtr = inBuf;
        char* outPtr = outBuf;
        size_t outBufLeft = sizeof(outBuf);

        int rc = iconv(iconvHandler, &inPtr, &inBufLeft, &outPtr, &outBufLeft);
        if (rc == -1 && (errno == EILSEQ || errno == E2BIG)) {
            perror("iconv");
            break;
        }
        outFs.write(outBuf, sizeof(outBuf) - outBufLeft);

        if (inBufLeft > 0) {
            strncpy(inBuf, inPtr, inBufLeft);
        }
    }
    iconv_close(iconvHandler);

    return EXIT_SUCCESS;
}

PHP

2パターン思いついたので、それぞれ書いてみた。

  • パターン1
  • パターン2

パターン1

<?php

if (count($argv) < 2) {
    file_put_contents('php://stderr', "Usage: php {$argv[0]} dirname" . PHP_EOL);
    exit(1);
}
$dir = $argv[1];
if (!file_exists($dir) || !is_dir($dir)) {
    file_put_contents('php://stderr', "{$dir}: No such directory." . PHP_EOL);
    exit(1);
}

$inFile = "{$dir}/in.txt";
$outFile = "{$dir}/out.txt";

$inFp = fopen($inFile, 'rb');
if (!$inFp) {
    file_put_contents('php://stderr', "{$inFile}: Cannot open file." . PHP_EOL);
    exit(1);
}
$outFp = fopen($outFile, 'wb');
if (!$outFp) {
    file_put_contents('php://stderr', "{$outFile}: Cannot open file." . PHP_EOL);
    fclose($inFp);
    exit(1);
}

while (($line = fgets($inFp))) {
    $line = mb_convert_encoding($line, 'SJIS-win', 'eucJP-win');
    fputs($outFp, $line);
}

fclose($inFp);
fclose($outFp);

パターン2

<?php

if (count($argv) < 2) {
    file_put_contents('php://stderr', "Usage: php {$argv[0]} dirname" . PHP_EOL);
    exit(1);
}

$dir = $argv[1];
if (!is_dir($dir)) {
    file_put_contents('php://stderr', "{$dir}: No such directory." . PHP_EOL);
    exit(1);
}

$inFile = "{$dir}/in.txt";
$outFile = "{$dir}/out.txt";
if (!file_exists($inFile)) {
    file_put_contents('php://stderr', "{$inFile}: No such file." . PHP_EOL);
    exit(1);
}

$str = file_get_contents($inFile);

$str = iconv('eucJP-win', 'SJIS-win', $str);

file_put_contents($outFile, $str);

Python 2

import sys
import os
import codecs

if len(sys.argv) < 2:
    sys.stderr.write("Usage: " + sys.argv[0] + " dirname\n")
    exit(1)

dirname = sys.argv[1]

inFile = dirname + "/in.txt"
outFile = dirname + "/out.txt"
if not os.path.isfile(inFile):
    sys.stderr.write(inFile + ": No such file\n")
    exit(1)

inFp = codecs.open(inFile, 'rb', 'EUC-JP')
outFp = codecs.open(outFile, 'wb', 'CP932')

for line in inFp:
    outFp.write(line)

inFp.close()
outFp.close()

Python 3

import sys
import os
import codecs

if len(sys.argv) < 2:
    sys.stderr.write("Usage: " + sys.argv[0] + " dirname\n")
    exit(1)

dirname = sys.argv[1]

inFile = dirname + "/in.txt"
outFile = dirname + "/out.txt"
if not os.path.isfile(inFile):
    sys.stderr.write(inFile + ": No such file\n")
    exit(1)

inFp = codecs.open(inFile, 'rb', 'EUC-JP')
outFp = codecs.open(outFile, 'wb', 'CP932')

for line in inFp:
    outFp.write(line)

inFp.close()
outFp.close()

Python 2と何ら変わりはない。

Ruby

if ARGV.length != 1
    STDERR.puts('Usage: ' + __FILE__ + ' dirname')
    exit 1
end

dir = ARGV[0]

inFile = dir + '/in.txt'
outFile = dir + '/out.txt'
if !File.file?(inFile)
    STDERR.puts(inFile + ': No such file')
    exit 1
end

inFp = File.open(inFile, mode = 'rb')
outFp = File.open(outFile, mode = 'wb')

inFp.each_line{|line|
    line.encode!('CP932', 'EUC-JP')
    outFp.puts(line)
}

inFp.close()
outFp.close()

Perl

パターン1

if (@ARGV < 1) {
    die("Usage: " . __FILE__ . " dirname");
}

my $dir = $ARGV[0];
my $inFile = $dir . "/in.txt";
my $outFile = $dir . "/out.txt";

open(inFp, "<:encoding(EUC-JP)", $inFile) or die($inFile . ": $!");
open(outFp, ">:encoding(CP932)", $outFile) or die($outFile . ": $!");

while (my $line = <inFp>) {
    print outFp $line;
}

close(inFp);
close(outFp);

パターン2

use Encode;

if (@ARGV < 1) {
    die("Usage: " . __FILE__ . " dirname");
}

my $dir = $ARGV[0];
my $inFile = $dir . "/in.txt";
my $outFile = $dir . "/out.txt";

open(inFp, "<", $inFile) or die($inFile . ": $!");
open(outFp, ">", $outFile) or die($outFile . ": $!");

while (my $line = <inFp>) {
    $line = encode('CP932', decode('EUC-JP', $line));
    print outFp $line;
}

close(inFp);
close(outFp);

Go

package main

import (
    "fmt"
    "os"
    "io"
    "bufio"

    "golang.org/x/text/encoding/japanese"
    "golang.org/x/text/transform"
)

func main() {
    if len(os.Args) < 2 {
        fmt.Fprintln(os.Stderr, "Usage: " + os.Args[0] + " dirname")
        os.Exit(1)
    }

    dir := os.Args[1]

    inFile := dir + "/in.txt"
    outFile := dir + "/out.txt"

    inFp, err := os.Open(inFile)
    if err != nil {
        panic(err)
    }
    outFp, err := os.Create(outFile)
    if err != nil {
        inFp.Close()
        panic(err)
    }

    in := bufio.NewReader(transform.NewReader(inFp, japanese.EUCJP.NewDecoder()))
    out := bufio.NewWriter(transform.NewWriter(outFp, japanese.ShiftJIS.NewEncoder()))

    b := make([]byte, 1024)
    for {
        n, err := in.Read(b)
        if err == io.EOF {
            break
        } else if err != nil {
            inFp.Close()
            outFp.Close()
            panic(err)
        }
        out.Write(b[0:n])
    }
    out.Flush()

    inFp.Close()
    outFp.Close()
}

これをやるには「GOPATH=$(pwd) go get golang.org/x/text/encoding/japanese」しておく必要がある。更に実行時に「GOPATH=$(pwd) go run Main.go」することも忘れずに。

bash

#! /bin/bash

dir="${1}"
if [ -z "${dir}" ]; then
    echo "Usage: $0 dirname" >> /dev/stderr
    exit 1
fi
if [ ! -d "${dir}" ]; then
    echo "${dir}: No such directory." >> /dev/stderr
    exit 1
fi

iconv -f EUC-JP -t CP932 < "${dir}/in.txt" > "${dir}/out.txt"