随心 DevOps
首发于随心 DevOps
深入介绍 Golang 中的 bufio.Scanner

深入介绍 Golang 中的 bufio.Scanner

想试试用 Golang 做小说统计字频,然后做一些优化,利用这个过程来提高自己,第一步就卡住了,如何优雅的读取文件呢?找到一篇文章,喜欢的点个赞哦

原文:In-depth introduction to bufio.Scanner in Golang (需科学上网)

译者:临书


Go 自带的软件包,提供了缓冲 I/O技术,用以优化读取或写入操作。对于写入来说,它在临时存储数据之前进行的(如磁盘或套接字)。数据被存储直到达到特定大小。通过这种方式触发的写操作更少,每个操作都为系统调用,操作会很昂贵。 对于读取而言,这意味着在单次操作中检索更多数据。它还减少了 sycall(系统调用)的数量,但还可以使用更高效的方式使用底层硬件,如读取磁盘块中的数据。本文重点介绍由 bufio 包提供的 Scanner 方法。它对处理数据流很有帮助,方式是将数据拆分为 tokens 并删除它们之间的空间。

"foo bar baz"

如果我们只对字母感兴趣:

package main
import (
    "bufio"
    "fmt"
    "strings"
)
func main() {
    input := "foo   bar      baz"
    scanner := bufio.NewScanner(strings.NewReader(input))
    scanner.Split(bufio.ScanWords)
    for scanner.Scan() {
        fmt.Println(scanner.Text())
    }
}

输出:

foo
bar
baz

扫描器在读取流时使用缓冲 I/O - 它将 io.Reader 作为参数。


如果你正在处理像字符串或字节片段这样的内存数据,那么首先可以试一下诸如 bytes.Splitstrings.Split 等实用方法。当不使用数据流时,依靠字节或字符串包中的方法可能更简单。


实际上(under the hood)扫描器使用缓冲区来累积读取数据。当缓冲区不为空或已达到文件末尾(EOF - End of file)时,将调用分割函数(SplitFunc)。在这之前,我们已经看到了预定义的分割函数,但也可以自己定义:

func(data []byte, atEOF bool) (advance int, token []byte, err error)

Split 函数从数据读取到被调用,基本上可以以 3 种不同的方式运行 - 通过返回的值区分

1.Give me more data

传递的数据不足以获得 token。 它通过返回0,nil,nil 来完成。 发生时,扫描器会尝试读取更多数据。 如果缓冲区已满,则在读取之前将其加倍。让我们看看它是如何工作的:

package main
import (
    "bufio"
    "fmt"
    "strings"
)
func main() {
    input := "abcdefghijkl"
    scanner := bufio.NewScanner(strings.NewReader(input))
    split := func(data []byte, atEOF bool) (advance int, token []byte, err error) {
        fmt.Printf("%t\t%d\t%s\n", atEOF, len(data), data)
        return 0, nil, nil
    }
    scanner.Split(split)
    buf := make([]byte, 2)
    scanner.Buffer(buf, bufio.MaxScanTokenSize)
    for scanner.Scan() {
        fmt.Printf("%s\n", scanner.Text())
    }
}

输出:

false	2	ab
false	4	abcd
false	8	abcdefgh
false	12	abcdefghijkl
true	12	abcdefghijkl

上面的 split 方法非常简单而且贪婪 -- 总是想要更多的数据。Scanner 会试图读取更多,但是当然前提条件是缓冲区需要充足的空间。在我们的例子中空间大小从 2 开始:

buf := make([]byte, 2)
scanner.Buffer(buf, bufio.MaxScanTokenSize)

在第一次调用 split 方法后,Scanner 会将缓冲区的大小加倍,读取更多的数据并第二次调用分割函数。第二次次场景将完全相同。可以从输出中看出来 -- 首先调用 split 得到大小为 2 的片段,然后是 4, 8 和最后 12,没有更多的数据了。

缓冲区的默认 size 是 4096

我们看一下 atEOF 这个参数,它被设计用来通知 split 方法是否没有更多的数据可以读取了,如果到达 EOF,或者出现错误,任何一个发生,scanner 就停止不在读取了,这个标记可以返回错误,scanner.Split() 会返回 false 并停止执行。错误后面可以使用 Err 方法来获取:

package main
import (
    "bufio"
    "errors"
    "fmt"
    "strings"
)
func main() {
    input := "abcdefghijkl"
    scanner := bufio.NewScanner(strings.NewReader(input))
    split := func(data []byte, atEOF bool) (advance int, token []byte, err error) {
        fmt.Printf("%t\t%d\t%s\n", atEOF, len(data), data)
        if atEOF {
            return 0, nil, errors.New("bad luck")
        }
        return 0, nil, nil
    }
    scanner.Split(split)
    buf := make([]byte, 12)
    scanner.Buffer(buf, bufio.MaxScanTokenSize)
    for scanner.Scan() {
        fmt.Printf("%s\n", scanner.Text())
    }
    if scanner.Err() != nil {
        fmt.Printf("error: %s\n", scanner.Err())
    }
}

输出:

false   12      abcdefghijkl
true    12      abcdefghijkl
error: bad luck

参数 atEOF 还可以用来处理缓冲区里的内容,例如下面这种

foo
bar
baz

在最后一行的结尾处没有 \n,所以当方法 ScanLines 找不到新的行字符时,它将简单地返回剩余的字符作为最后一个标记:

package main
import (
    "bufio"
    "fmt"
    "strings"
)
func main() {
    input := "foo\nbar\nbaz"
    scanner := bufio.NewScanner(strings.NewReader(input))
    // Not actually needed since it’s a default split function.
    scanner.Split(bufio.ScanLines)
    for scanner.Scan() {
        fmt.Println(scanner.Text())
    }
}

2.Token found

这种情况在 split 方法能检测到 token 时发生,It returns the number of characters to move forward in the buffer and the token itself. The reason to return two values is simply because token doesn’t have to be always equal to the number of bytes to move forward. If input is “foo foo foo” and when goal is to detect words (ScanWords), then split function will also skip over spaces in between:


(4, "foo")
(4, "foo")
(3, "foo")

让我们看看实际情况,这个函数只会找连续的字符串 foo

package main
import (
    "bufio"
    "bytes"
    "fmt"
    "io"
    "strings"
)
func main() {
    input := "foofoofoo"
    scanner := bufio.NewScanner(strings.NewReader(input))
    split := func(data []byte, atEOF bool) (advance int, token []byte, err error) {
        if bytes.Equal(data[:3], []byte{'f', 'o', 'o'}) {
            return 3, []byte{'F'}, nil
        }
        if atEOF {
            return 0, nil, io.EOF
        }
        return 0, nil, nil
    }
    scanner.Split(split)
    for scanner.Scan() {
        fmt.Printf("%s\n", scanner.Text())
    }
}

输出:

F
F
F

3.Error

如果 split 方法返会错误,scanner 停止:

package main
import (
    "bufio"
    "errors"
    "fmt"
    "strings"
)
func main() {
    input := "abcdefghijkl"
    scanner := bufio.NewScanner(strings.NewReader(input))
    split := func(data []byte, atEOF bool) (advance int, token []byte, err error) {
        return 0, nil, errors.New("bad luck")
    }
    scanner.Split(split)
    for scanner.Scan() {
        fmt.Printf("%s\n", scanner.Text())
    }
    if scanner.Err() != nil {
        fmt.Printf("error: %s\n", scanner.Err())
    }
}

输出:

error: bad luck

有一个特殊的错误不会立即停止 Scanner...

ErrFinalToken

Scanner 提供了一个选项来表示所谓的最终 token。 这是一个特殊的 token,不会中断循环(扫描仍然会返回 true),但随后的扫描将立即停止:

func (s *Scanner) Scan() bool {
    if s.done {
  	return false
    }
    ...

输出:

foo
END
io.EOFErrFinalToken 都不被认为是「真正的」错误 -- 如果这两个中的任何一个导致 Scanner 停止,Err 方法将返回 nil。

Maximum token size / ErrTooLong

默认情况下,下面使用的缓冲区的最大长度是 64 * 1024 字节。这意味着找到的 token 不能超过此限制:

package main
import (
    "bufio"
    "fmt"
    "strings"
)
func main() {
    input := strings.Repeat("x", bufio.MaxScanTokenSize)
    scanner := bufio.NewScanner(strings.NewReader(input))
    for scanner.Scan() {
        fmt.Println(scanner.Text())
    }
    if scanner.Err() != nil {
        fmt.Println(scanner.Err())
    }
}

程序返回:bufio.Scanner: token too long ,这个限制可以通过 Buffer 方法来设置,我们已经在 1.Give me more data 部分中的代码中看到了,我们在举个更小的例子:

buf := make([]byte, 10)
input := strings.Repeat("x", 20)
scanner := bufio.NewScanner(strings.NewReader(input))
scanner.Buffer(buf, 20)
for scanner.Scan() {
    fmt.Println(scanner.Text())
}
if scanner.Err() != nil {
    fmt.Println(scanner.Err())
}

输出:

bufio.Scanner: token too long

防止无限循环(Protecting against endless loop)

package main
import (
    "bufio"
    "bytes"
    "fmt"
    "strings"
)
func main() {
    input := "foo|bar"
    scanner := bufio.NewScanner(strings.NewReader(input))
    split := func(data []byte, atEOF bool) (advance int, token []byte, err error) {
        if i := bytes.IndexByte(data, '|'); i >= 0 {
            return i + 1, data[0:i], nil
        }
        if atEOF {
            return len(data), data[:len(data)], nil
        }
        return 0, nil, nil
    }
    scanner.Split(split)
    for scanner.Scan() {
        if scanner.Text() != "" {
            fmt.Println(scanner.Text())
        }
    }
}

输出:

foo
bar
panic: bufio.Scan: too many empty tokens without progressing

当我第一次阅读 Scanner 或 SplitFunc 的文档时,我的脑海中并不是所有情况下的工作原理都很清楚。看源代码并没有太大的帮助,因为 Scanner 乍一看相当复杂。希望这篇文章能让其他人更清楚。


相关解释:

编辑于 2018-06-08

文章被以下专栏收录