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

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

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?

Обновлять

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

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

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


person Amyth    schedule 07.01.2016    source источник
comment
В файле нет метаданных, указывающих, где находится строка 30. если вы где-то не храните смещение байта, вам нужно каждый раз читать с самого начала.   -  person JimB    schedule 07.01.2016
comment
@JimB Спасибо за ваш ответ. Пожалуйста, проверьте обновление в вопросе, это мой лучший выбор? Существуют ли какие-либо другие доступные пакеты, способные это сделать?   -  person Amyth    schedule 07.01.2016
comment
Вызовите scanner.Scan() 30 раз независимо от значения вашего счетчика. Если вы считаете, что это слишком медленно (что сомнительно для файла конфигурации), сохраните смещение и используйте golang.org /pkg/os/#File.ReadAt   -  person kopiczko    schedule 07.01.2016
comment
Насколько велики эти файлы, то есть вы читаете миллионы строк? Если вам нужно получить фактическое положение, не используйте сканер, используйте методы нижнего рычага на bufio.Reader. Вы можете получить позицию, сравнив позицию файла с количеством буферизованных байтов.   -  person JimB    schedule 07.01.2016
comment
@JimB да, это файлы журналов postfix, которые я пытаюсь прочитать на централизованном сервере журналов, так что да, они довольно большие.   -  person Amyth    schedule 07.01.2016


Ответы (4)


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

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

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() для установки функции разделения (логика определения границ токенов/линий). Функция разделения по умолчанию: 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

В этом решении мы используем тип bufio.Reader вместо Scanner. 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() для получения io.ReadSeeker, источником которого является string.

Тестовый код сначала вызывает withScanner() и withReader(), передавая 0 начальную позицию: чистый старт. В следующем раунде мы передадим начальную позицию start = 14, которая является позицией 3. строки, поэтому мы не увидим обработанных (напечатанных) первых 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.

person icza    schedule 07.01.2016
comment
Удивительный ответ и объяснение. Спасибо @icza - person Amyth; 07.01.2016
comment
Ваше регулярное выражение для нескольких типов окончания строки неверно. \r\n, \r и \n — допустимые окончания строк. Правильное регулярное выражение будет /(\r\n|\r|\n)/ - person 0xcaff; 27.01.2017
comment
@caffinatedmonkey bufio.ScanLines() также упоминает/использует \r?\n. - person icza; 27.01.2017

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

person JimB    schedule 07.01.2016
comment
Удивительно, спасибо. Я попробую это и приму ваш ответ. - person Amyth; 07.01.2016
comment
@Amyth: мне только что пришло в голову, что я быстро написал этот пример из комбинации двух других частей, и вам действительно не нужен вызов Seek, чтобы получить позицию при использовании bufio.Reader, просто добавьте прочитанные байты (это будет также сохранить много системных вызовов) - person JimB; 07.01.2016

Если вы хотите использовать Сканер, вам придется пройти через попрошайничество файла, пока вы не найдете 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())
}

В качестве альтернативы вы можете сохранить смещение вместо номера строки в счетчике, но помните, что токен завершения удаляется при использовании сканера, а для новой строки токен равен \r?\n (обозначение регулярного выражения), поэтому он неясно, следует ли добавлять 1 или 2 к длине текста:

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

Так что лучше использовать предыдущее решение или вообще не использовать Scanner.

person kopiczko    schedule 07.01.2016

В других ответах много слов, и они на самом деле не являются повторно используемым кодом, поэтому вот повторно используемая функция, которая ищет заданный номер строки и возвращает его и смещение начала строки. 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
}
person user1034533    schedule 06.06.2018