LoginSignup
43
42

More than 5 years have passed since last update.

すごく長い行を読む場合の注意点

Last updated at Posted at 2015-12-02

Goで 1行 1行読み取って処理をしたいときは, bufio.Scannerを使うのが推奨されています. しかし bufio.Scannerにはひとつ欠点があり, 長すぎる行が読めないという問題があります. 最長で MaxScanTokenSize(64 * 1024)バイトしか読めず, これ以上長い行を読ませようとするとエラーが返ります. この値は constで定義されているため変更できません. また NewScanner的な関数の引数にサイズを与えて独自のバッファサイズを設定するということもできません. そのため長い行を読む場合は今のところ bufio.Scannerを使うことはできません.

追記

開発版には bufio.Scanner.Buffer()メソッドが追加され, ユーザ定義のバッファが Scannerに設定できるようです. これを使えば上限がわかる場合は Scannerだけでも対応できます(任意の長さとなると後述のような方法しかないと思います. Scannerで errorが返ったときさらに大きなバッファを割り当てるという方法もありますが, 同程度に面倒になりそうです).

コメントで @heliac2000 さんに教えていだきました. ありがとうございます.

多くの場合問題にならないと思いますが, すごく長い行を含むファイルが入力として与えられる可能性がある場合は意識しておく必要があるでしょう.

失敗例

標準入力から読み取り, 各行に何バイト含まれているかを出力するプログラムを考えます. 初めに長い行を含むファイルを以下のコマンドで生成しておきます.

% perl -E 'say "x" x 10_000_000' > test.txt

次に bufio.Scannerを使ったコード例を示します.

package main

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

func main() {
    s := bufio.NewScanner(os.Stdin)

    i := 1
    for s.Scan() {
        fmt.Printf("%d line length is %d\n", len(s.Text()))
        i++
    }

    if err := s.Err(); err != nil {
        log.Fatal(err)
    }
}

このプログラムに先ほど生成したファイルを与えてみましょう.

% go run scanner.go < test.txt
2015/12/02 22:50:12 bufio.Scanner: token too long
exit status 1

MaxScanTokenSizeを超えてしまう行を読み込んでしまったため, token too longというエラーを受け取ってしまいました.

bufio.Scannerを使わない方法

bufio.Readerの ReadLineメソッドを使います. ドキュメントには通常であれば代わりに bufio.Scannerを使えとありますが, 使えないのでこちらを使います.

package main

import (
    "bufio"
    "bytes"
    "fmt"
    "io"
    "log"
    "os"
)

func main() {
    r := bufio.NewReader(os.Stdin)
    for i := 1; ; i++ {
        buf, isPrefix, err := r.ReadLine()
        if err != nil {
            if err != io.EOF {
                log.Fatal(err)
            }
            break
        }

        bb := bytes.NewBuffer(buf)

        if isPrefix {
            for {
                b, cont, err := r.ReadLine()
                if err != nil {
                    if err != io.EOF {
                        log.Fatal(err)
                    }
                    break
                }

                if _, err := bb.Write(b); err != nil {
                    log.Fatal(err)
                }

                if !cont {
                    break
                }
            }
        }

        fmt.Printf("Length of %d line is %d\n", i, bb.Len())
    }
}

bufio.Reader.ReadLineメソッドは 3つの戻り値があり, それぞれ読み取ったバッファ, 全部読み取れたかどうか, エラーになります. 注目するのは 2つめの戻り値でこれが trueであれば, 行はさらに続くのでさらに読み取りを行い, 初めに得たバッファに追記していきます. これを 2つめの戻り値が falseになるまで続けます. これでどれだけ長い行でも読めるようになります. 実行結果は以下のようになり. 10,000,000文字分読めていることが確認できます.

% go run test.go < test.txt
Length of 1 line is 10000000

おわりに

Goで長い行を読む場合の注意点について示しました.

明日は @Maki-Daisuke さんです.

43
42
8

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
43
42