.eg 1. 日志文件
从头开始读取文件时,我就能获得Line1的搜索位置。
func getSeekLocation() int64 { start := int64(0) input, err := os.Open(logFile) if err != nil { fmt.Println(err) } if _, err := input.Seek(start, io.SeekStart); err != nil { fmt.Println(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() { if strings.Contains(scanner.Text(), "Line1") { break } } size, err := getFileSize() if err != nil { fmt.Println(err) } return size - pos }
但这不是解决问题的有效方法,因为随着文件大小的增加,获取位置的时间也会增加。我想从EOF位置获取线的位置,我认为这样会更有效。
注意: 我优化并改进了以下解决方案,并将其作为库发布在这里:github.com/icza/backscanner
github.com/icza/backscanner
bufio.Scanner使用a io.Reader作为其源,它不支持从任意位置进行查找和/或读取,因此它无法从头开始扫描线。bufio.Scanner只有在读取了输入之前的所有数据之后,它才能读取输入的任何部分(也就是说,如果先读取所有文件内容,则只能读取文件的结尾)。
bufio.Scanner
io.Reader
因此,我们需要定制的解决方案来实现这种功能。幸运的os.File是,它在实现io.Seeker和时都支持从任意位置读取io.ReaderAt(它们中的任何一个都足以满足我们的需要)。
os.File
io.Seeker
io.ReaderAt
让我们构造一个从最后一行开始 向后Scanner扫描行的。为此,我们将使用。下面的实现使用一个内部缓冲区,从输入的末尾开始,按块将数据读取到该缓冲区中。输入的大小也必须传递(这基本上是我们要从其开始读取的位置,不一定必须是结束位置)。 __io.ReaderAt
Scanner
type Scanner struct { r io.ReaderAt pos int err error buf []byte } func NewScanner(r io.ReaderAt, pos int) *Scanner { return &Scanner{r: r, pos: pos} } func (s *Scanner) readMore() { if s.pos == 0 { s.err = io.EOF return } size := 1024 if size > s.pos { size = s.pos } s.pos -= size buf2 := make([]byte, size, size+len(s.buf)) // ReadAt attempts to read full buff! _, s.err = s.r.ReadAt(buf2, int64(s.pos)) if s.err == nil { s.buf = append(buf2, s.buf...) } } func (s *Scanner) Line() (line string, start int, err error) { if s.err != nil { return "", 0, s.err } for { lineStart := bytes.LastIndexByte(s.buf, '\n') if lineStart >= 0 { // We have a complete line: var line string line, s.buf = string(dropCR(s.buf[lineStart+1:])), s.buf[:lineStart] return line, s.pos + lineStart + 1, nil } // Need more data: s.readMore() if s.err != nil { if s.err == io.EOF { if len(s.buf) > 0 { return string(dropCR(s.buf)), 0, nil } } return "", 0, s.err } } } // dropCR drops a terminal \r from the data. func dropCR(data []byte) []byte { if len(data) > 0 && data[len(data)-1] == '\r' { return data[0 : len(data)-1] } return data }
使用它的示例:
func main() { scanner := NewScanner(strings.NewReader(src), len(src)) for { line, pos, err := scanner.Line() if err != nil { fmt.Println("Error:", err) break } fmt.Printf("Line start: %2d, line: %s\n", pos, line) } } const src = `Start Line1 Line2 Line3 End`
输出(在Go Playground上尝试):
Line start: 24, line: End Line start: 18, line: Line3 Line start: 12, line: Line2 Line start: 6, line: Line1 Line start: 0, line: Start Error: EOF
笔记:
\n
\r\n
dropCR()
要将其Scanner与文件一起使用,您可以使用os.Open()打开文件。注意*File实现io.ReaderAt()。然后,您可以File.Stat()用来获取有关文件(os.FileInfo)的信息,包括文件的大小(长度):
os.Open()
*File
io.ReaderAt()
File.Stat()
os.FileInfo
f, err := os.Open("a.txt") if err != nil { panic(err) } fi, err := f.Stat() if err != nil { panic(err) } defer f.Close() scanner := NewScanner(f, int(fi.Size()))
如果要在一行中查找子字符串,则只需使用上面的代码即可Scanner返回每行的起始位置,并从末尾读取行。
您可以使用来检查每行中的子字符串strings.Index(),它返回该行内的子字符串位置,如果找到,则将行起始位置添加到该位置。
strings.Index()
假设我们正在寻找"ine2"子字符串(这是该"Line2"行的一部分)。这是您可以执行的操作:
"ine2"
"Line2"
scanner := NewScanner(strings.NewReader(src), len(src)) what := "ine2" for { line, pos, err := scanner.Line() if err != nil { fmt.Println("Error:", err) break } fmt.Printf("Line start: %2d, line: %s\n", pos, line) if i := strings.Index(line, what); i >= 0 { fmt.Printf("Found %q at line position: %d, global position: %d\n", what, i, pos+i) break } }
Line start: 24, line: End Line start: 18, line: Line3 Line start: 12, line: Line2 Found "ine2" at line position: 1, global position: 13