Ответ 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
Попробуйте решения и код тестирования на Перейти к игровой площадке.