我是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())
}
}
我很确定这样的东西会起作用,但它仍然会绕过我们已经读过的行。请建议一个更好的方法。
答案 0 :(得分:18)
如果你不想阅读但只是跳过你之前读过的行,你需要获得你离开的位置。
不同的解决方案以函数的形式呈现,该函数将输入读取并从起始位置(字节位置)开始读取行,例如:
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上找到。
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()
创建一个新的split函数,所以我们甚至不必实现它的逻辑,只需使用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
在此解决方案中,我们使用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
的{{1}}。
测试代码首先调用string
和withScanner()
传递withReader()
开始位置: clean start 。在下一轮中,我们将传递0
的起始位置,这是3行的位置,因此我们不会看到处理(打印)的前2行: resume 模拟
start = 14
输出:
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)
}
}
尝试Go Playground上的解决方案和测试代码。
答案 1 :(得分:4)
不使用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)
移动到您离开的位置。
答案 2 :(得分:3)
如果您想使用扫描仪,您可以通过文件的乞讨直到找到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())
}
或者,您可以在计数器中存储偏移量而不是行号,但请记住,使用扫描程序时终止令牌被剥离,对于新行,令牌为{{1} }(regexp表示法)所以不清楚你是否应该在文本长度上加1或2:
\r?\n
因此,最好使用以前的解决方案,或者根本不使用扫描仪。
答案 3 :(得分:2)
其他答案中有很多单词,并且它们不是真正可重复使用的代码,所以这里是一个可重复使用的功能,它可以寻找给定的行号和数字。返回它和行开始的偏移量。 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
}