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"

各言語の標準エラー出力

そういえば今まで意識しなかったな、ということで、各言語の標準エラー出力を使ってみたメモ。

環境

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

Java

import java.io.IOException;

public class Main {
    public static void main(String[] args) throws IOException {
        byte[] buf = new byte[1024];
        int len;
        while ((len = System.in.read(buf)) > 0) {
            System.err.write(buf, 0, len);
        }
    }
}

System.inが標準入力、System.outが標準出力、System.err標準エラー出力を表す。

ちなみに、System.setIn(InputStream)System.setOut(PrintStream)System.setErr(PrintStream)というのがあるので、例えば標準出力や標準エラー出力をファイルに書き出すように切り替えるとかも可能。

C

#include <stdio.h>

int main(int argc, char** argv) {
    char buf[1024];
    while (fgets(buf, sizeof(buf), stdin)) {
        fputs(buf, stderr);
    }
    return 0;
}

FILE*型だと、stdinが標準入力、stdoutが標準出力、stderr標準エラー出力

ちなみに、int型のファイルディスクリプタ(fd)だと、0が標準入力、1が標準出力、2標準エラー出力に関連付いている。 unistd.hを見ると、以下のように書いてある。

/* Standard file descriptors.  */
#define STDIN_FILENO    0       /* Standard input.  */
#define STDOUT_FILENO   1       /* Standard output.  */
#define STDERR_FILENO   2       /* Standard error output.  */

C++

#include <iostream>

using namespace std;

int main(int argc, char** argv) {
    char buf[1024];
    while (cin.getline(buf, sizeof(buf))) {
        cerr << buf << endl;
    }
    return EXIT_SUCCESS;
}

std::cinが標準入力、std::coutが標準出力、std::cerrstd::clog標準エラー出力にそれぞれ関連付いている。 std::cerrstd::clogの違いは、前者がバッファリングされないのに対して、後者がバッファリングされるというもの。 iostreamを見ると、以下のように書いてある。

namespace std _GLIBCXX_VISIBILITY(default)
{
  // (中略)
  extern istream cin;           /// Linked to standard input
  extern ostream cout;          /// Linked to standard output
  extern ostream cerr;          /// Linked to standard error (unbuffered)
  extern ostream clog;          /// Linked to standard error (buffered)

std::clogの存在は、このヘッダファイルを見て初めて知った。

PHP

<?php

$str = file_get_contents('php://stdin');
file_put_contents('php://stderr', $str);

PHPはどちらかというとWebアプリ開発に使われる言語ということで、特に意識せずにprintとかすると標準出力に書き出されるので、標準入出力を意識することは少ないかもしれない。

'php://stdin'が標準入力、'php://stdout'が標準出力、'php://stderr'標準エラー出力を表すので、これをfile_get_contents / file_put_contentsやfopen等にファイル名と同様に指定してやればよい。

Python 2

import sys

while True:
    line = sys.stdin.readline()
    if line == '':
        break
    sys.stderr.write(line)

sys.stdinが標準入力、sys.stdoutが標準出力、sys.stderr標準エラー出力

Python 3

import sys

while True:
    line = sys.stdin.buffer.readline()
    if line == b'':
        break
    sys.stderr.buffer.write(line)

sys.stdinが標準入力、sys.stdoutが標準出力、sys.stderr標準エラー出力

Ruby

while line = STDIN.gets
    STDERR.puts line
end

STDINが標準入力、STDOUTが標準出力、STDERR標準エラー出力

Perl

while ((my $line = <STDIN>)) {
    print STDERR $line;
}

STDINが標準入力、STDOUTが標準出力、STDERR標準エラー出力

Go

package main

import (
    "bufio"
    "io"
    "os"
)

func main() {
    stdin := bufio.NewReader(os.Stdin)
    stderr := bufio.NewWriter(os.Stderr)
    for {
        ch, err := stdin.ReadByte()
        if err == io.EOF {
            break
        }
        stderr.WriteByte(ch)
    }
    stderr.Flush()
}

os.Stdinが標準入力、os.Stdoutが標準出力、os.Stderr標準エラー出力

bash

#! /bin/bash

cat <&0 >&2

0が標準入力、1が標準出力、2標準エラー出力を表すので、標準エラー出力に出したい場合は、>&2のように書いてリダイレクトしてやればよい。

/dev/stdin/dev/stdout/dev/stderrを使う手もあるので、>> /dev/stderrのようにリダイレクトしてもよい。

Awk

{
    print >> "/dev/stderr";
}

"/dev/stdin"が標準入力、"/dev/stdout"が標準出力、"/dev/stderr"標準エラー出力

まぁ、標準入力や標準出力を明示することはほとんどないけど。

DateTimeクラスのdiffメソッドの罠

突然ですが、以下のコードの出力結果はどうなると思いますか?ちなみに「invert」というのは、結果が負の場合に「1」それ以外の場合に「0」になるプロパティ。

<?php
date_default_timezone_set("Asia/Tokyo");

$dt1 = new DateTime("2017-12-01");
$dt2 = new DateTime("2017-11-30");

$diff1 = $dt1->diff($dt2);
$diff2 = date_diff($dt1, $dt2);

var_dump($diff1->invert, $diff2->invert);

$dt1の方が未来なので、直観的にはint(0)になるだろうと思うところだが・・

ドキュメントは以下だけど・・・

明記されていないが、$dt2 - $dt1の結果になる。すなわち、負になるので、int(1)が出力される。

これは罠過ぎるだろう!!!

なので、DateTimeオブジェクト同士の日時の前後を比較したいだけなら、(PHP 5.2.2以降になるが)比較演算子で比較するようにした方が安全かも。

<?php
date_default_timezone_set("Asia/Tokyo");

$dt1 = new DateTime("2017-12-01");
$dt2 = new DateTime("2017-11-30");

var_dump($dt1 == $dt2);
var_dump($dt1 < $dt2);
var_dump($dt1 <= $dt2);
var_dump($dt1 > $dt2);
var_dump($dt1 >= $dt2);

いやそれリークしますからっ!

以下のようなプログラムを見つけて、思わず机を叩き割ろうかと思いましたよ、えぇ‥

Reader reader = null;
try {
    for (int i = 0; i < files.length; ++i) {
        reader = new BufferedReader(new InputStreamReader(new FileInputStream(files[i])));
        // 以下、いろいろ処理(ただしreaderのclose()以外)
    }
} finally {
    if (reader != null) {
        try { reader.close(); } catch (Exception e) {}
    }
}

いや、これだと、最後に開いたファイルのストリームしか閉じないんですけど‥

バッチプログラムで、長くても数分で終了するプログラムだからまだ影響が少なかったが、Webアプリでこれをやったら確実にアウトですよ、アウト‥

参考

各言語でコマンドライン引数を扱う

そういえばやってなかったなということで、各言語でコマンドライン引数を扱うプログラムを書いてみたメモ。

要件は以下の通り。

  • コマンドライン引数として、3個以上の文字列を与える
  • 標準出力に、以下を順に出力
    • 引数として与えられた文字列を改行区切りで出力
    • 引数の個数を出力
    • 引数の3番目の文字列を出力

環境

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

実行例

[各言語でのコマンド実行] a b c d e

出力例

a
b
c
d
e
5
c

Java

public class Main {
    public static void main(String[] args) {
        for (String s : args) {
            System.out.println(s);
        }
        System.out.println(args.length);
        System.out.println(args[2]);
    }
}

C

#include <stdio.h>

int main(int argc, char** argv) {
    for (int i = 1; i < argc; ++i) {
        printf("%s\n", argv[i]);
    }
    printf("%d\n", argc - 1);
    printf("%s\n", argv[3]);

    return 0;
}

C++

#include <iostream>

using namespace std;

int main(int argc, char** argv) {
    for (int i = 1; i < argc; ++i) {
        cout << argv[i] << endl;
    }
    cout << (argc - 1) << endl;
    cout << argv[3] << endl;

    return EXIT_SUCCESS;
}

PHP

<?php

for ($i = 1; $i < count($argv); ++$i) {
    echo $argv[$i] . PHP_EOL;
}
echo (count($argv) - 1) . PHP_EOL;
echo $argv[3] . PHP_EOL;

Python 2

import sys

for i in range(1, len(sys.argv)):
    print sys.argv[i]
print len(sys.argv) - 1
print sys.argv[3]

Python 3

import sys

for i in range(1, len(sys.argv)):
    print(sys.argv[i])
print(len(sys.argv) - 1)
print(sys.argv[3])

Ruby

for a in ARGV
    print a,"\n"
end
print ARGV.length,"\n"
print ARGV[2],"\n"

Perl

for (my $i = 0; $i < @ARGV; ++$i) {
    print $ARGV[$i],"\n";
}
my $argc = @ARGV;
print $argc,"\n";
print $ARGV[2],"\n";

Go

package main

import (
    "fmt"
    "os"
)

func main() {
    for i := 1; i < len(os.Args); i += 1 {
        fmt.Println(os.Args[i])
    }
    fmt.Println(len(os.Args) - 1)
    fmt.Println(os.Args[3]);
}

bash

#! /bin/bash

for c in "$@" ; do
    echo ${c}
done
echo $#
echo $3

まとめ

それぞれの言語ごとの仕様を表にまとめてみる。

言語 変数名 最初の引数の添え字 個数
Java (*1) args 0 args.length
C (*1) argv 1 argc - 1
C++ (*1) argv 1 argc - 1
PHP $argv 1 count($argv) - 1
Python 2/3 sys.argv 1 len(sys.argv) - 1
Ruby ARGV 0 ARGV.length
Perl $ARGV 0 @ARGV (*3)
Go os.Args 1 len(os.Args) - 1
bash $n または ${n} (*2) 1 $#
  • (*1)変数名はmainメソッド/関数の引数の宣言による。
  • (*2)「n」は正の整数。
  • (*3)文脈によっては、「my $argc = @ARGV」などとしてやらないと、個数でなく引数を結合した文字列が得られてしまう。

文字列のマッチング方法によるパフォーマンスの違い

Javaで文字列のパターンマッチをしようと思ったら、以下の3通りに書ける。

String str = "Hello World";
if (str.matches("H.*W")) {
    ...
}
String str = "Hello World";
if (java.util.regex.Pattern.matches("H.*W", str)) {
    ...
}
String str = "Hello World";
java.util.regex.Pattern p = java.util.regex.Pattern.compile("H.*W");
java.util.regex.Matcher m = m.matcher(str);
if (m.matches()) {
    ...
}

まぁ、1番目は内部で2番目の処理を呼び出しているだけだが。

で、3番目のように正規表現コンパイルを事前に行っておく形とそうでない形とでパフォーマンスにどれだけの差が出るのだろうかとふと思ったので計ってみた。

計測に使用したプログラム

import java.util.Scanner;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

public class Main {
    public static void main(String[] args) {
        Scanner sc = new Scanner(System.in);
        String str = sc.nextLine();
        String regex = sc.nextLine();

        int delta = 100000;
        int max = 2000000;
        for (int i = delta; i <= max; i += delta) {
            System.out.printf("%8d,%8d,%8d,%8d%n",
                i,
                test1(i, str, regex),
                test2(i, str, regex),
                test3(i, str, regex));
        }
    }
    private static long test1(int count, String str, String regex) {
        long S = System.currentTimeMillis();
        for (int i = 0; i < count; ++i) {
            str.matches(regex);
        }
        long G = System.currentTimeMillis();
        return G - S;
    }
    private static long test2(int count, String str, String regex) {
        long S = System.currentTimeMillis();
        for (int i = 0; i < count; ++i) {
            Pattern.matches(regex, str);
        }
        long G = System.currentTimeMillis();
        return G - S;
    }
    private static long test3(int count, String str, String regex) {
        long S = System.currentTimeMillis();
        Pattern p = Pattern.compile(regex);
        for (int i = 0; i < count; ++i) {
            Matcher m = p.matcher(str);
            m.matches();
        }
        long G = System.currentTimeMillis();
        return G - S;
    }
}

入力

正規表現コンパイルにかかる時間に着目しているので、正規表現の複雑さの違いということで2種類の正規表現を試してみた。

  • a: [a-z]+
  • b: [A-Z][a-z]* [A-Z][a-z]*

計測結果

N test1-a test2-a test3-a
100000 191 64 20
200000 172 68 22
300000 79 64 26
400000 83 78 21
500000 100 99 25
600000 120 121 31
700000 138 140 36
800000 154 153 39
900000 176 177 45
1000000 205 197 50
1100000 217 217 54
1200000 235 239 61
1300000 258 259 65
1400000 279 274 71
1500000 306 292 76
1600000 331 326 86
1700000 340 348 89
1800000 364 362 91
1900000 390 383 97
2000000 410 392 100

f:id:hhelibex:20171126155414p:plain

正規表現 b

N test1-b test2-b test3-b
100000 313 92 19
200000 217 130 46
300000 164 161 42
400000 213 217 52
500000 273 271 66
600000 325 326 79
700000 379 389 89
800000 434 431 104
900000 482 490 117
1000000 540 539 131
1100000 584 588 143
1200000 646 641 156
1300000 694 703 173
1400000 745 752 187
1500000 808 816 195
1600000 883 873 211
1700000 932 927 226
1800000 992 984 241
1900000 1032 1020 254
2000000 1083 1088 271

f:id:hhelibex:20171126155536p:plain

まぁ予想通りだが、毎回コンパイルすることになるtest1、test2のケースは、test3に比べて断然遅いことが分かる。 また、正規表現の複雑さが増すと、コンパイルやマッチングに時間が掛かることも読み取れる。

各言語でメッセージダイジェスト

手元にある各言語で、メッセージダイジェストを出力してみたメモ。

要件は以下の通り。

  • 標準入力から、0バイト以上の任意のバイト列が入力される
  • 標準出力に、入力に対する以下のハッシュ文字列を改行区切りで出力する
    • MD5
    • SHA-1
    • SHA-224
    • SHA-256
    • SHA-384
    • SHA-512

環境

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

入力

Hello World!

出力

8ddd8be4b179a529afa5f2ffae4b9858
a0b65939670bc2c010f4d5d6a0b3e4e4590fb92b
de9d76f0f6a015ab6629138a42835e7b44571995e4abb291c0817261
03ba204e50d126e4674c005e04d82e84c21366780af1f43bd54a37816b6ab340
07f60df0b95043b3a3717638e7776ab76ebaa4fc705ba659063229cf162980c04a9f7496dcda50de6510d40fde3eba8a
830445e86a0cfafac4e1531002356f384847a11a7456fb8ccb81ab36e37bff28f34fa2c5bfdd347e964c5c5df0fc305de6394368219307b2ceeb0ec84b7c2b31

Java

import java.io.BufferedInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;

public class Main {
    public static void main(String[] args) throws NoSuchAlgorithmException {
        MessageDigest[] mdAry = {
            MessageDigest.getInstance("MD5"),
            MessageDigest.getInstance("SHA-1"),
            MessageDigest.getInstance("SHA-224"),
            MessageDigest.getInstance("SHA-256"),
            MessageDigest.getInstance("SHA-384"),
            MessageDigest.getInstance("SHA-512"),
        };
        try (InputStream in = new BufferedInputStream(System.in)) {
            byte[] b = new byte[1024];
            int len;
            while ((len = in.read(b)) > 0) {
                for (MessageDigest md : mdAry) {
                    md.update(b, 0, len);
                }
            }
        } catch (IOException e) {
            e.printStackTrace();
        }

        for (MessageDigest md : mdAry) {
            byte[] digest = md.digest();
            for (byte b : digest) {
                System.out.printf("%02x", b);
            }
            System.out.println();
        }
    }
}

C

#include <stdio.h>
#include <string.h>
#include <openssl/md5.h>
#include <openssl/sha.h>

int main(int argc, char** argv) {
    MD5_CTX md5_ctx;
    unsigned char md5[MD5_DIGEST_LENGTH];
    SHA_CTX sha1_ctx;
    unsigned char sha1[SHA_DIGEST_LENGTH];
    SHA256_CTX sha224_ctx;
    unsigned char sha224[SHA224_DIGEST_LENGTH];
    SHA256_CTX sha256_ctx;
    unsigned char sha256[SHA256_DIGEST_LENGTH];
    SHA512_CTX sha384_ctx;
    unsigned char sha384[SHA384_DIGEST_LENGTH];
    SHA512_CTX sha512_ctx;
    unsigned char sha512[SHA512_DIGEST_LENGTH];

    if (!MD5_Init(&md5_ctx)) {
        return 1;
    }
    if (!SHA1_Init(&sha1_ctx)) {
        return 1;
    }
    if (!SHA224_Init(&sha224_ctx)) {
        return 1;
    }
    if (!SHA256_Init(&sha256_ctx)) {
        return 1;
    }
    if (!SHA384_Init(&sha384_ctx)) {
        return 1;
    }
    if (!SHA512_Init(&sha512_ctx)) {
        return 1;
    }

    char buf[1024];
    int len;
    while ((len = fread(buf, sizeof(char), sizeof(buf), stdin)) > 0) {
        if (!MD5_Update(&md5_ctx, buf, len)) {
            return 1;
        }
        if (!SHA1_Update(&sha1_ctx, buf, len)) {
            return 1;
        }
        if (!SHA224_Update(&sha224_ctx, buf, len)) {
            return 1;
        }
        if (!SHA256_Update(&sha256_ctx, buf, len)) {
            return 1;
        }
        if (!SHA384_Update(&sha384_ctx, buf, len)) {
            return 1;
        }
        if (!SHA512_Update(&sha512_ctx, buf, len)) {
            return 1;
        }
    }

    if (!MD5_Final(md5, &md5_ctx)) {
        return 1;
    }
    if (!SHA1_Final(sha1, &sha1_ctx)) {
        return 1;
    }
    if (!SHA224_Final(sha224, &sha224_ctx)) {
        return 1;
    }
    if (!SHA256_Final(sha256, &sha256_ctx)) {
        return 1;
    }
    if (!SHA384_Final(sha384, &sha384_ctx)) {
        return 1;
    }
    if (!SHA512_Final(sha512, &sha512_ctx)) {
        return 1;
    }

    for (int i = 0; i < MD5_DIGEST_LENGTH; ++i) {
        fprintf(stdout, "%02x", md5[i]);
    }
    fprintf(stdout, "\n");
    for (int i = 0; i < SHA_DIGEST_LENGTH; ++i) {
        fprintf(stdout, "%02x", sha1[i]);
    }
    fprintf(stdout, "\n");
    for (int i = 0; i < SHA224_DIGEST_LENGTH; ++i) {
        fprintf(stdout, "%02x", sha224[i]);
    }
    fprintf(stdout, "\n");
    for (int i = 0; i < SHA256_DIGEST_LENGTH; ++i) {
        fprintf(stdout, "%02x", sha256[i]);
    }
    fprintf(stdout, "\n");
    for (int i = 0; i < SHA384_DIGEST_LENGTH; ++i) {
        fprintf(stdout, "%02x", sha384[i]);
    }
    fprintf(stdout, "\n");
    for (int i = 0; i < SHA512_DIGEST_LENGTH; ++i) {
        fprintf(stdout, "%02x", sha512[i]);
    }
    fprintf(stdout, "\n");

    return 0;
}

「-lcrypto」を付けてコンパイルする必要がある。

C++

C言語と大して変わらないけど、一応載せておく。

#include <cstdio>
#include <string>
#include <openssl/md5.h>
#include <openssl/sha.h>

int main(int argc, char** argv) {
    MD5_CTX md5_ctx;
    unsigned char md5[MD5_DIGEST_LENGTH];
    SHA_CTX sha1_ctx;
    unsigned char sha1[SHA_DIGEST_LENGTH];
    SHA256_CTX sha224_ctx;
    unsigned char sha224[SHA224_DIGEST_LENGTH];
    SHA256_CTX sha256_ctx;
    unsigned char sha256[SHA256_DIGEST_LENGTH];
    SHA512_CTX sha384_ctx;
    unsigned char sha384[SHA384_DIGEST_LENGTH];
    SHA512_CTX sha512_ctx;
    unsigned char sha512[SHA512_DIGEST_LENGTH];

    if (!MD5_Init(&md5_ctx)) {
        return EXIT_FAILURE;
    }
    if (!SHA1_Init(&sha1_ctx)) {
        return EXIT_FAILURE;
    }
    if (!SHA224_Init(&sha224_ctx)) {
        return EXIT_FAILURE;
    }
    if (!SHA256_Init(&sha256_ctx)) {
        return EXIT_FAILURE;
    }
    if (!SHA384_Init(&sha384_ctx)) {
        return EXIT_FAILURE;
    }
    if (!SHA512_Init(&sha512_ctx)) {
        return EXIT_FAILURE;
    }

    char buf[1024];
    int len;
    while ((len = fread(buf, sizeof(char), sizeof(buf), stdin)) > 0) {
        if (!MD5_Update(&md5_ctx, buf, len)) {
            return EXIT_FAILURE;
        }
        if (!SHA1_Update(&sha1_ctx, buf, len)) {
            return EXIT_FAILURE;
        }
        if (!SHA224_Update(&sha224_ctx, buf, len)) {
            return EXIT_FAILURE;
        }
        if (!SHA256_Update(&sha256_ctx, buf, len)) {
            return EXIT_FAILURE;
        }
        if (!SHA384_Update(&sha384_ctx, buf, len)) {
            return EXIT_FAILURE;
        }
        if (!SHA512_Update(&sha512_ctx, buf, len)) {
            return EXIT_FAILURE;
        }
    }

    if (!MD5_Final(md5, &md5_ctx)) {
        return EXIT_FAILURE;
    }
    if (!SHA1_Final(sha1, &sha1_ctx)) {
        return EXIT_FAILURE;
    }
    if (!SHA224_Final(sha224, &sha224_ctx)) {
        return EXIT_FAILURE;
    }
    if (!SHA256_Final(sha256, &sha256_ctx)) {
        return EXIT_FAILURE;
    }
    if (!SHA384_Final(sha384, &sha384_ctx)) {
        return EXIT_FAILURE;
    }
    if (!SHA512_Final(sha512, &sha512_ctx)) {
        return EXIT_FAILURE;
    }

    for (int i = 0; i < MD5_DIGEST_LENGTH; ++i) {
        fprintf(stdout, "%02x", md5[i]);
    }
    fprintf(stdout, "\n");
    for (int i = 0; i < SHA_DIGEST_LENGTH; ++i) {
        fprintf(stdout, "%02x", sha1[i]);
    }
    fprintf(stdout, "\n");
    for (int i = 0; i < SHA224_DIGEST_LENGTH; ++i) {
        fprintf(stdout, "%02x", sha224[i]);
    }
    fprintf(stdout, "\n");
    for (int i = 0; i < SHA256_DIGEST_LENGTH; ++i) {
        fprintf(stdout, "%02x", sha256[i]);
    }
    fprintf(stdout, "\n");
    for (int i = 0; i < SHA384_DIGEST_LENGTH; ++i) {
        fprintf(stdout, "%02x", sha384[i]);
    }
    fprintf(stdout, "\n");
    for (int i = 0; i < SHA512_DIGEST_LENGTH; ++i) {
        fprintf(stdout, "%02x", sha512[i]);
    }
    fprintf(stdout, "\n");

    return EXIT_SUCCESS;
}

「-lcrypto」を付けてコンパイルする必要がある。

PHP

<?php

$input = file_get_contents('php://stdin');

printf("%s\n", hash('md5', $input));
printf("%s\n", hash('sha1', $input));
printf("%s\n", hash('sha224', $input));
printf("%s\n", hash('sha256', $input));
printf("%s\n", hash('sha384', $input));
printf("%s\n", hash('sha512', $input));

メモリに載りきらないほどのデータが来たら対応できないのが難点。

Python 2

import sys
import hashlib

md5 = hashlib.md5()
sha1 = hashlib.sha1()
sha224 = hashlib.sha224()
sha256 = hashlib.sha256()
sha384 = hashlib.sha384()
sha512 = hashlib.sha512()

while True:
    line = sys.stdin.readline()
    if line == '':
        break
    md5.update(line)
    sha1.update(line)
    sha224.update(line)
    sha256.update(line)
    sha384.update(line)
    sha512.update(line)

print md5.hexdigest()
print sha1.hexdigest()
print sha224.hexdigest()
print sha256.hexdigest()
print sha384.hexdigest()
print sha512.hexdigest()

Python 3

import sys
import hashlib

md5 = hashlib.md5()
sha1 = hashlib.sha1()
sha224 = hashlib.sha224()
sha256 = hashlib.sha256()
sha384 = hashlib.sha384()
sha512 = hashlib.sha512()

while True:
    line = sys.stdin.buffer.readline()
    if line == b'':
        break
    md5.update(line)
    sha1.update(line)
    sha224.update(line)
    sha256.update(line)
    sha384.update(line)
    sha512.update(line)

print(md5.hexdigest())
print(sha1.hexdigest())
print(sha224.hexdigest())
print(sha256.hexdigest())
print(sha384.hexdigest())
print(sha512.hexdigest())

Ruby

require 'openssl'

mdAry = [
    OpenSSL::Digest.new("md5"),
    OpenSSL::Digest.new("sha1"),
    OpenSSL::Digest.new("sha224"),
    OpenSSL::Digest.new("sha256"),
    OpenSSL::Digest.new("sha384"),
    OpenSSL::Digest.new("sha512"),
]

while line = STDIN.gets
    for md in mdAry
        md.update(line)
    end
end

for md in mdAry
    puts md.hexdigest
end

Perl

use Digest::MD5;
use Digest::SHA;

my $md5 = Digest::MD5->new();
my $sha1 = Digest::SHA->new("sha1");
my $sha224 = Digest::SHA->new("sha224");
my $sha256 = Digest::SHA->new("sha256");
my $sha384 = Digest::SHA->new("sha384");
my $sha512 = Digest::SHA->new("sha512");

while (my $line = readline(STDIN)) {
    $md5->add($line);
    $sha1->add($line);
    $sha224->add($line);
    $sha256->add($line);
    $sha384->add($line);
    $sha512->add($line);
}

print $md5->hexdigest(),"\n";
print $sha1->hexdigest(),"\n";
print $sha224->hexdigest(),"\n";
print $sha256->hexdigest(),"\n";
print $sha384->hexdigest(),"\n";
print $sha512->hexdigest(),"\n";

yum install perl-Digest-MD5 perl-Digest-SHA」が必要。

Go

package main

import (
    "fmt"
    "io"
    "os"
    "crypto/md5"
    "crypto/sha1"
    "crypto/sha256"
    "crypto/sha512"
)

func main() {
    aMd5 := md5.New()
    aSha1 := sha1.New()
    aSha224 := sha256.New224()
    aSha256 := sha256.New()
    aSha384 := sha512.New384()
    aSha512 := sha512.New()

    for {
        buf := make([]byte, 1024)
        n, err := os.Stdin.Read(buf)
        if err == io.EOF {
            break
        }
        buf2 := make([]byte, 0);
        buf2 = append(buf2, buf[0:n]...)

        aMd5.Write(buf2)
        aSha1.Write(buf2)
        aSha224.Write(buf2)
        aSha256.Write(buf2)
        aSha384.Write(buf2)
        aSha512.Write(buf2)
    }

    fmt.Printf("%x\n", aMd5.Sum(nil))
    fmt.Printf("%x\n", aSha1.Sum(nil))
    fmt.Printf("%x\n", aSha224.Sum(nil))
    fmt.Printf("%x\n", aSha256.Sum(nil))
    fmt.Printf("%x\n", aSha384.Sum(nil))
    fmt.Printf("%x\n", aSha512.Sum(nil))
}

bash

#! /bin/bash

cat | tee \
    >(sha512sum | cut -d " " -f 1) \
    >(sha384sum | cut -d " " -f 1) \
    >(sha256sum | cut -d " " -f 1) \
    >(sha224sum | cut -d " " -f 1) \
    >(sha1sum | cut -d " " -f 1) \
    >(md5sum | cut -d " " -f 1) \
    >> /dev/null

バイナリデータのテストケースにも対応するためにteeコマンド+コマンド置換を使ったが、期待する順番通りに出力されないことがあるのが難点。

  • man md5sum
  • man sha1sum
  • man sha224sum
  • man sha256sum
  • man sha384sum
  • man sha512sum