web-dev-qa-db-ja.com

スキャナーを使用して特定の行番号からファイルを読み取る方法は?

私はGoを初めて使用し、ファイルを1行ずつ読み取る簡単なスクリプトを作成しようとしています。また、進行状況(つまり、最後に読み取られた行番号)をファイルシステムのどこかに保存して、同じファイルがスクリプトへの入力として再度指定された場合に、中断した行からファイルの読み取りを開始できるようにします。以下は私が始めたものです。

package main

// Package Imports
import (
    "bufio"
    "flag"
    "fmt"
    "log"
    "os"
)

// Variable Declaration
var (
    ConfigFile = flag.String("configfile", "../config.json", "Path to json configuration file.")
)

// The main function that reads the file and parses the log entries
func main() {
    flag.Parse()
    settings := NewConfig(*ConfigFile)

    inputFile, err := os.Open(settings.Source)
    if err != nil {
        log.Fatal(err)
    }
    defer inputFile.Close()

    scanner := bufio.NewScanner(inputFile)
    for scanner.Scan() {
        fmt.Println(scanner.Text())
    }

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

// Saves the current progress
func SaveProgress() {

}

// Get the line count from the progress to make sure
func GetCounter() {

}

スキャナーパッケージに行番号を処理するメソッドが見つかりませんでした。 counter := 0と言う整数を宣言し、行がcounter++のように読み取られるたびにインクリメントできることを知っています。しかし、次回、特定の行から開始するようにスキャナーに指示するにはどうすればよいですか?たとえば、次に同じ入力ファイルでスクリプトを実行するときに30行まで読み取る場合、スキャナーに31行から読み取りを開始させるにはどうすればよいですか。

更新

ここで考えられる解決策の1つは、前述のようにカウンターを使用し、次のようなif条件を使用することです。

    scanner := bufio.NewScanner(inputFile)
    for scanner.Scan() {
        if counter > progress {
            fmt.Println(scanner.Text())
        }
    }

このようなものが機能すると確信していますが、それでも、すでに読んだ行をループします。より良い方法を提案してください。

14
Amyth

読みたくなくて、前に読んだ行をスキップするだけの場合は、中断した位置を取得する必要があります。

さまざまなソリューションは、読み取り元の入力と、行の読み取りを開始する開始位置(バイト位置)を受け取る関数の形式で表示されます。例:

_func solution(input io.ReadSeeker, start int64) error
_

特別な _io.Reader_ 入力が使用されます。これは _io.Seeker_ も実装します。これは、データを読み取らずにスキップできる共通のインターフェイスです。 _*os.File_ はこれを実装しているため、これらの関数に_*File_を渡すことができます。良い。 _io.Reader_と_io.Seeker_の両方の「マージされた」インターフェースは _io.ReadSeeker_ です。

clean start(ファイルの先頭から読み取りを開始する)が必要な場合は、_start = 0_を渡すだけです。前の処理を再開する場合は、最後の処理が停止/中止されたバイト位置を渡します。この位置は、以下の関数(ソリューション)のposローカル変数の値です。

以下のすべての例とそのテストコードは、 Go Playground にあります。

1. _bufio.Scanner_を使用

_bufio.Scanner_ 位置を維持しませんが、位置(読み取りバイト)を維持するために非常に簡単に拡張できるため、次に再起動するときに、この位置を探すことができます。

最小限の労力でこれを行うために、入力をトークン(行)に分割する新しい分割関数を使用できます。 Scanner.Split() を使用して、スプリッター関数(トークン/行の境界がどこにあるかを決定するロジック)を設定できます。デフォルトの分割関数は bufio.ScanLines() です。

分割関数宣言を見てみましょう: _bufio.SplitFunc_

_type SplitFunc func(data []byte, atEOF bool) (advance int, token []byte, err error)
_

前進するバイト数を返します:advance。ファイルの位置を維持するために必要なもの。したがって、組み込みのbufio.ScanLines()を使用して新しい分割関数を作成できるため、ロジックを実装する必要はなく、advanceの戻り値を使用して位置を維持します。

_func withScanner(input io.ReadSeeker, start int64) error {
    fmt.Println("--SCANNER, start:", start)
    if _, err := input.Seek(start, 0); err != nil {
        return err
    }
    scanner := bufio.NewScanner(input)

    pos := start
    scanLines := func(data []byte, atEOF bool) (advance int, token []byte, err error) {
        advance, token, err = bufio.ScanLines(data, atEOF)
        pos += int64(advance)
        return
    }
    scanner.Split(scanLines)

    for scanner.Scan() {
        fmt.Printf("Pos: %d, Scanned: %s\n", pos, scanner.Text())
    }
    return scanner.Err()
}
_

2. _bufio.Reader_を使用

このソリューションでは、Scannerの代わりに _bufio.Reader_ タイプを使用します。 _bufio.Reader_にはすでに ReadBytes() メソッドがあります。これは、_'\n'_バイトを区切り文字として渡す場合の「行の読み取り」機能と非常によく似ています。

このソリューションはJimBのソリューションに似ていますが、すべての有効なラインターミネータシーケンスを処理し、それらを読み取りラインから削除する点が追加されています(必要になることは非常にまれです)。正規表現表記では、_\r?\n_です。

_func withReader(input io.ReadSeeker, start int64) error {
    fmt.Println("--READER, start:", start)
    if _, err := input.Seek(start, 0); err != nil {
        return err
    }

    r := bufio.NewReader(input)
    pos := start
    for {
        data, err := r.ReadBytes('\n')
        pos += int64(len(data))
        if err == nil || err == io.EOF {
            if len(data) > 0 && data[len(data)-1] == '\n' {
                data = data[:len(data)-1]
            }
            if len(data) > 0 && data[len(data)-1] == '\r' {
                data = data[:len(data)-1]
            }
            fmt.Printf("Pos: %d, Read: %s\n", pos, data)
        }
        if err != nil {
            if err != io.EOF {
                return err
            }
            break
        }
    }
    return nil
}
_

注:コンテンツが空の行(行末記号)で終わる場合、このソリューションは空の行を処理します。これが必要ない場合は、次のように簡単に確認できます。

_if len(data) != 0 {
    fmt.Printf("Pos: %d, Read: %s\n", pos, data)
} else {
    // Last line is empty, omit it
}
_

ソリューションのテスト:

テストコードは、さまざまな行の終了を伴う複数の行を含むコンテンツ_"first\r\nsecond\nthird\nfourth"_を使用するだけです。 strings.NewReader() を使用して、ソースがstringである_io.ReadSeeker_を取得します。

テストコードは最初にwithScanner()withReader()を呼び出し、_0_開始位置を渡します:aclean start。次のラウンドでは、3行目の位置である_start = 14_の開始位置を渡すため、最初の2行が処理(印刷)されたことがわかりません:resumeシミュレーション。

_func main() {
    const content = "first\r\nsecond\nthird\nfourth"

    if err := withScanner(strings.NewReader(content), 0); err != nil {
        fmt.Println("Scanner error:", err)
    }
    if err := withReader(strings.NewReader(content), 0); err != nil {
        fmt.Println("Reader error:", err)
    }

    if err := withScanner(strings.NewReader(content), 14); err != nil {
        fmt.Println("Scanner error:", err)
    }
    if err := withReader(strings.NewReader(content), 14); err != nil {
        fmt.Println("Reader error:", err)
    }
}
_

出力:

_--SCANNER, start: 0
Pos: 7, Scanned: first
Pos: 14, Scanned: second
Pos: 20, Scanned: third
Pos: 26, Scanned: fourth
--READER, start: 0
Pos: 7, Read: first
Pos: 14, Read: second
Pos: 20, Read: third
Pos: 26, Read: fourth
--SCANNER, start: 14
Pos: 20, Scanned: third
Pos: 26, Scanned: fourth
--READER, start: 14
Pos: 20, Read: third
Pos: 26, Read: fourth
_

Go Playground でソリューションとテストコードを試してください。

20
icza

Scannerを使用する場合は、GetCounter()エンドラインシンボルが見つかるまで、ファイルの要求を通過します。

scanner := bufio.NewScanner(inputFile)
// context line above

// skip first GetCounter() lines
for i := 0; i < GetCounter(); i++ {
    scanner.Scan()
}

// context line below
for scanner.Scan() {
    fmt.Println(scanner.Text())
}

または、行番号の代わりにoffsetをカウンターに格納することもできますが、Scannerを使用する場合は終了トークンが削除されますであり、新しい行の場合、トークンは\r?\n(regexp表記)したがって、テキストの長さに1または2を追加する必要があるかどうかは明確ではありません。

// Not clear how to store offset unless custom SplitFunc provided
inputFile.Seek(GetCounter(), 0)
scanner := bufio.NewScanner(inputFile)

したがって、以前のソリューションを使用するか、スキャナーをまったく使用しないことをお勧めします。

4
kopiczko

Scannerを使用する代わりに、 _bufio.Reader_ 、具体的にはReadBytesまたはReadStringメソッドを使用します。このようにして、各行末までを読み取り、行末を含む完全な行を受け取ることができます。

_r := bufio.NewReader(inputFile)

var line []byte
fPos := 0 // or saved position

for i := 1; ; i++ {
    line, err = r.ReadBytes('\n')
    fmt.Printf("[line:%d pos:%d] %q\n", i, fPos, line)

    if err != nil {
        break
    }
    fPos += len(line)
}

if err != io.EOF {
    log.Fatal(err)
}
_

ファイルの位置と行番号の組み合わせを任意に保存できます。次に開始するときは、inputFile.Seek(fPos, os.SEEK_SET)を使用して中断したところに移動します。

4
JimB

他の回答には多くの単語があり、それらは実際には再利用可能なコードではないため、指定された行番号を探してそれと行の開始位置のオフセットを返す再利用可能な関数を次に示します。 play.golang

func SeekToLine(r io.Reader, lineNo int) (line []byte, offset int, err error) {
    s := bufio.NewScanner(r)

    var pos int

    s.Split(func(data []byte, atEof bool) (advance int, token []byte, err error) {
        advance, token, err = bufio.ScanLines(data, atEof)
        pos += advance
        return advance, token, err
    })

    for i := 0; i < lineNo; i++ {
        offset = pos

        if !s.Scan() {
            return nil, 0, io.EOF
        }
    }

    return s.Bytes(), pos, nil
}
2
user1034533