Note:我优化和改进了以下解决方案,并将其作为库发布在这里:github.com/icza/backscanner https://github.com/icza/backscanner
bufio.Scanner https://golang.org/pkg/bufio/#Scanner使用一个io.Reader https://golang.org/pkg/io/#Reader作为其源,它不支持从任意位置查找和/或读取,因此它无法从末尾扫描行。bufio.Scanner
只有在读取了输入之前的所有数据后,才能读取输入的任何部分(也就是说,如果它首先读取文件的所有内容,则只能读取文件的末尾)。
因此我们需要一个定制的解决方案来实现此类功能。幸运的是os.File https://golang.org/pkg/os/#File确实支持从任意位置读取,因为它实现了io.Seeker https://golang.org/pkg/io/#Seeker and io.ReaderAt https://golang.org/pkg/io/#ReaderAt(其中任何一个都足以满足我们的需要)。
从末尾开始向后返回行的扫描仪
让我们构建一个Scanner
扫描线backward,从最后一行开始。为此,我们将利用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: %s\n", pos, line)
}
}
const src = `Start
Line1
Line2
Line3
End`
输出(尝试一下去游乐场 https://play.golang.org/p/Wujr0QcL3P):
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
Notes:
- 以上
Scanner
不限制行的最大长度,它可以处理所有行。
- 以上
Scanner
处理两者\n
and \r\n
行结尾(由dropCR()
功能)。
- 您可以传递任何起始位置,而不仅仅是尺寸/长度,列表行将从那里开始执行(继续)。
- 以上
Scanner
不重用缓冲区,总是在需要时创建新缓冲区。 (预)分配 2 个缓冲区并明智地使用它们就足够了。实施将变得更加复杂,并且会引入最大线路长度限制。
与文件一起使用
要使用这个Scanner
对于文件,您可以使用os.Open()
打开一个文件。注意*File
实施io.ReaderAt()
。那么你可以使用File.Stat() https://golang.org/pkg/os/#File.Stat获取有关文件的信息(os.FileInfo https://golang.org/pkg/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() https://golang.org/pkg/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: %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
}
}
输出(尝试一下去游乐场 https://play.golang.org/p/ixMatyaSMA):
Line start: 24, line: End
Line start: 18, line: Line3
Line start: 12, line: Line2
Found "ine2" at line position: 1, global position: 13