私は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())
}
}
このようなものが機能すると確信していますが、それでも、すでに読んだ行をループします。より良い方法を提案してください。
読みたくなくて、前に読んだ行をスキップするだけの場合は、中断した位置を取得する必要があります。
さまざまなソリューションは、読み取り元の入力と、行の読み取りを開始する開始位置(バイト位置)を受け取る関数の形式で表示されます。例:
_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 にあります。
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()
}
_
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 でソリューションとテストコードを試してください。
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)
したがって、以前のソリューションを使用するか、スキャナーをまったく使用しないことをお勧めします。
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)
を使用して中断したところに移動します。
他の回答には多くの単語があり、それらは実際には再利用可能なコードではないため、指定された行番号を探してそれと行の開始位置のオフセットを返す再利用可能な関数を次に示します。 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
}