Как читать файл, начинающийся с определенного номера строки с помощью сканера?

Я новичок в Go, и я пытаюсь написать простой script, который читает файл по строкам. Я также хочу сохранить прогресс (т.е. Последний номер строки, который был прочитан) в файловой системе где-нибудь, чтобы, если тот же самый файл был указан как вход в script снова, он начинает читать файл из строки, где он остался выкл. Следующим является то, с чем я начал.

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() {

}

Я не мог найти никаких методов, которые касаются номеров строк в пакете сканера. Я знаю, что могу объявить целое число say counter := 0 и увеличивать его каждый раз, когда строка читается как counter++. Но в следующий раз, как я скажу сканеру начать с определенной строки? Так, например, если я прочитал до строки 30 в следующий раз, когда я запустил script с тем же входным файлом, как я могу заставить сканер начать чтение из строки 31?

Update

Одно из решений, о котором я могу думать здесь, - использовать счетчик, как я сказал выше, и использовать условие if, подобное следующему.

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

Я уверен, что что-то вроде этого будет работать, но он все равно будет перебирать строки, которые мы уже прочитали. Пожалуйста, предложите лучший способ.

Ответ 1

Если вы не хотите читать, а просто пропустите строки, которые вы читали ранее, вам нужно получить позицию, в которой вы остановились.

Различные решения представлены в виде функции, которая берет ввод для чтения и начальную позицию (положение байта), чтобы начать считывание строк, например:

func solution(input io.ReadSeeker, start int64) error

Используется специальный io.Reader, который также реализует io.Seeker, общий интерфейс, который позволяет пропускать данные без необходимости их считывания. *os.File реализует это, поэтому вам разрешено передавать *File этим функциям. Хорошо. "Объединенный" интерфейс как io.Reader, так и io.Seeker io.ReadSeeker.

Если вам нужен чистый старт (чтобы начать чтение с начала файла), просто пройдите start = 0. Если вы хотите возобновить предыдущую обработку, передайте позицию байта, где последняя обработка была остановлена ​​/прервана. Эта позиция является значением локальной переменной pos в функциях (решениях) ниже.

Все приведенные ниже примеры с их кодом тестирования можно найти на Go Playground.

1. С помощью bufio.Scanner

bufio.Scanner не поддерживает позицию, но мы можем очень легко расширить ее, чтобы сохранить позицию (прочитанные байты), поэтому, когда мы хотим перезапустить следующий, мы можем искать эту позицию.

Чтобы сделать это с минимальными усилиями, мы можем использовать новую функцию разделения, которая разбивает входные данные на токены (линии). Мы можем использовать Scanner.Split(), чтобы установить функцию сплиттера (логика определяет, где находятся границы токенов/строк). Функция split по умолчанию bufio.ScanLines().

Посмотрим на объявление функции split: 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

В этом решении мы используем bufio.Reader вместо Scanner. bufio.Reader уже имеет метод ReadBytes(), который очень похож на функциональность "читать строку", если мы передаем байт '\n' as делиметр.

Это решение похоже на JimB's, с добавлением обработки всех допустимых последовательностей терминатора строки, а также снятие их с строки чтения (это очень редко, они необходимы); в обозначениях регулярных выражений это \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() для получения io.ReadSeeker, источником которого является string.

Первые вызовы тестового кода withScanner() и withReader() передача 0 стартовой позиции: чистый старт. В следующем раунде мы пройдем начальную позицию start = 14, которая является позицией 3. строки, поэтому мы не увидим, что первые 2 строки обработаны (напечатаны): имитация возобновления.

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

Попробуйте решения и код тестирования на Перейти к игровой площадке.

Ответ 2

Если вы хотите использовать 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.

Ответ 3

Вместо использования 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) для перемещения туда, где вы остановились.