注意:
我优化并改进了以下解决方案,并将其作为库发布在这里:
github.com/icza/backscanner
bufio.Scanner使用a
io.Reader作为其源,它不支持从任意位置进行查找和/或读取,因此它无法从头开始扫描线。
bufio.Scanner只有在读取了输入之前的所有数据之后,它才能读取输入的任何部分(也就是说,如果先读取所有文件内容,则只能读取文件的结尾)。
因此,我们需要定制的解决方案来实现这种功能。幸运的
os.File是,它在实现
io.Seeker和时都支持从任意位置读取
io.ReaderAt(它们中的任何一个都足以满足我们的需要)。扫描仪返回从末尾开始向后的行
让我们构造一个从最后一行开始
向后
Scanner扫描行的。为此,我们将使用。下面的实现使用一个内部缓冲区,从输入的末尾开始,按块将数据读取到该缓冲区中。输入的大小也必须传递(这基本上是我们要从其开始读取的位置,不一定必须是结束位置)。
__
io.ReaderAt
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: %sn", pos, line) }}const src = `StartLine1Line2Line3End`
输出(在Go Playground上尝试):
Line start: 24, line: EndLine start: 18, line: Line3Line start: 12, line: Line2Line start: 6, line: Line1Line start: 0, line: StartError: EOF
笔记:
- 上面
Scanner
没有限制最大行长,它可以处理所有行。 - 上面
Scanner
处理行尾n
和rn
行尾(由dropCR()
函数确保)。 - 您可以传递任何起始位置,而不仅仅是大小/长度,并且清单行将从此处开始(继续)。
- 上面
Scanner
没有重用缓冲区,总是在需要时创建新的缓冲区。(预)分配2个缓冲区并明智地使用它们就足够了。实现将变得更加复杂,并且将引入最大行长度限制。
要将其
Scanner与文件一起使用,您可以使用
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(),它返回该行内的子字符串位置,如果找到,则将行起始位置添加到该位置。
假设我们正在寻找
"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: %sn", pos, line) if i := strings.Index(line, what); i >= 0 { fmt.Printf("Found %q at line position: %d, global position: %dn", what, i, pos+i) break }}
输出(在Go Playground上尝试):
Line start: 24, line: EndLine start: 18, line: Line3Line start: 12, line: Line2Found "ine2" at line position: 1, global position: 13
欢迎分享,转载请注明来源:内存溢出
评论列表(0条)