sci

最果て風呂

Go 言語を使ってみる

動機

Twitter の TL で Go 言語について頻繁につぶやかれていたので興味を持っていた。見よう見まねで、手始めに簡易月齢計算である getsurei.go を作ってみたのだけど、C 言語よりは簡単そうな印象だった。

go run getsurei.go

のようにすればコンパイルせずにスクリプト言語感覚で動作確認ができるので、単純なプログラムなら手軽に開発ができそう。

とりあえずの目標として、旧字旧仮名を新字新仮名に変換するスクリプトの mto を Go 言語で実装するところまでとしたい。

道標

辞書データを二次元配列にする

○ファイルを開いて一行ずつ読み込む

内部辞書(二次元配列)を作成するにしても、文字列を変換するにしても、ファイルから文字列を読み込まなければならないのでこの部分から実装する。ファイル名は決め打ちだけど、ファイルを読み込んで標準出力に表示させることが出来た。

import (
    "bufio"
    "fmt"
    "os"
)

func main() {
    inputfile, _ := os.Open("hoge.txt")
    scanner := bufio.NewScanner(inputfile)

    for scanner.Scan() {
        fmt.Println(scanner.Text())
    }
}

正規表現で辞書データを分解して「変換元単語」と「変換先単語」のペアを作成

まずはコメント行を無視して表示させてみる。regexp を import して MatchString で正規表現を作成し判別させる。返り値は true か false の bool 値らしい。コメント行でなければ fmt.Println によって表示する。

aline, _ := regexp.MatchString("^;.*|^$", scanner.Text())
if aline == false {
    fmt.Println(scanner.Text())
}

次はコンスセル(もどき)に整形してみる。PerlRuby で使っているwhitespace の \s は、どうやら [\t\n\f\r ] となるみたい。String の要素 2 つを持つ配列(consセルみたいなもの)が出来る。

re := regexp.MustCompile("[\t\n\f\r ]+;.*")
str := re.ReplaceAllString(scanner.Text(), "")
pairs := regexp.MustCompile(" /").Split(str, 2)

○配列に追加していく(変換辞書になる)

配列への追加方法がわかるまでにかなりの時間がかかってしまったけど、下記のようにすればどんどん追加していける。変数を宣言しなくても使えるようなスクリプト言語を使ってきたので、変数の宣言部分で [][] のところがなかなかピンとこなかったのだわ。

var dicarr = [][]string{}
...
dicarr = append(dicarr, pairs)

dicarr は大域変数になるのかな?その方が都合が良いのだけど。

func DictCreator(jisyo string) {
    ifile, _ := os.Open(jisyo)
    scanner := bufio.NewScanner(ifile)
    re := regexp.MustCompile("[\t\n\f\r ]+;.*")

    for scanner.Scan() {
        line, _ := regexp.MatchString("^;.*|^$", scanner.Text())
        if line == false {
            str := re.ReplaceAllString(scanner.Text(), "")
            pairs := regexp.MustCompile(" /").Split(str, 2)
                dicarr = append(dicarr, pairs)
        }
    }
}

変換したいファイルを読み込んで順次置換する

○ファイルを開いて一行ずつ読み込み

これは辞書を作成する時と同じだわな(と思って同じように実装していたのだけど、下記にあるようにファイルを一気に読み込むようにした)。

○変換辞書の各単語について検索・置換

二次元配列の変換辞書について map 関数を適用したいけど、Go 言語にはあるのかな?each とか foreach みたいなやつ。下記で行けるみたい。index は 要素の添字らしく、必要が無ければ _ にしておけばいい。

for index, element := range dicarr {
}

あとは読み込んだ文字列について破壊的な検索置換操作を適用して出力すれば完成だ。regexp にありそう。

re := regexp.MustCompile(element[0])
str = re.ReplaceAllString(str, element[1])

str についてではなく、re についてあれこれやるのは素直じゃない感じがする。ReplaceAllString はコピーを返すとのことなので、特に何もしなくて良さそう。

とりあえず str に入力(代入)するファイを決め打ちにして動かしてみると、欲しい出力を得ることができた。でもなんか遅い。stringsimport してから

str = strings.Replace(str, element[0], element[1], -1)

のようにして(正規表現を使わずに単純な検索・置換で)も同じだったし、ファイルを一行ずつ処理するのではなく、一気に全体に対してやってみるかなぁ。

io/ioutilimport してから

content, _ := ioutil.ReadFile("hoge.txt")

とすればファイルを一気に読めるらしい。戻り値はどうなっているのかわからないど、文字列として扱うためには string(content) のようにして変換してやらないといけない。

func StringCarReplacer(ifile string) {
    content, err := ioutil.ReadFile(ifile)
        if err != nil {
            fmt.Println("ファイルを開けなかったの(´・ω・`)")
        }
        str := string(content)
        for _, element := range dicarr {
            str = strings.Replace(str, element[0], element[1], -1)
        }
    fmt.Println(str)
}

若干早くなったかも。でも Lua よりも遅い。go build mto.go としてバイナリで実行してみると、Perl よりも遅く、PythonRuby と同等な速度になった。Perl は速いのう。

ここまでの整理

変数に string を使ってしまっていた。こんがらがるので str に変更。

たぶん辞書ファイルから二次元配列を生成する部分に時間がかかってるのだと思う。毎回 re を生成しなくてもいいじゃないかということで re := regexp.MustCompile("[\t\n\f\r ]+;.*")for 文の外に出してみたら若干速くなったような気がする。

現状では変換させる辞書も文章も決め打ちなので、引数処理について調べる必要がある。

オプションを追加

コマンドライン引数は os.Args で得ることができる。これを処理して各種の変換(新旧仮名漢字を双方に変換)をすることが出来るようになった。分岐処理には if〜else 文の例が見付からなかったので、今回は switch〜case 文を使うことにした。

これで当初の目的に到達することが出来たのだけれど、自身が mto/ ディレクトリ以下に居なければならないので都合が悪い。他のスクリプト言語でもそうだったけど、スクリプトファイル自身(今回はバイナリ自身)の場所から辞書ファイルへの相対パスを指定するのが面倒なのであった。

Go 言語では go build mto.go で生成されたバイナリを実行する場合と、go run mto.goスクリプト的に実行する場合では、取得できる自身へのパスが異ってしまうのであった。

バイナリとして実行するのであれば今まで作ってきた言語と同じように実装することができるのだけど、スクリプト的に実行すると一時的に tmp/ 以下に生成されるようなのでうまくいかない。

バイナリで実行することが前提になっているだろうから、そちらに合わせて作っていくようにしようかなぁ。環境変数に辞書へのパスを持たせてそこから取得するのも良い考えだけれども。

あとは指定したファイルの内容を変換するだけでなく、標準入力からのデータも変換できるようにしたい。

結局、環境変数に辞書ファイルのパスを設定してそれを読み込むようにさせた。この場合、'~/work/mto' のような相対パスを設定してもうまくいかないので、ルートからのパスを書くようにした。filepathAbs を使うと絶対パスに変換できるみたいなんだけど、やっぱりうまくいかなかったし。

var (
    kanajisyo string = os.Getenv("MTODIR") + "/dict/kana-jisyo"
    kanjijisyo string = os.Getenv("MTODIR") + "/dict/kanji-jisyo"
)

標準入力からのデータ入力にも対応した。これで一旦終了としよう。