如何在golang中逐行读取文件(附代码示例)

1,012 阅读8分钟

今天,我将向你展示如何在golang中逐行读取文件。让我们想象一下,有一个jsonl 文件。什么是jsonl?简单来说就是json行,它是一个文件,其中每一行都代表一个有效的json 对象。因此,如果我们逐行读取文件,我们可以分别对它的每一行进行Marshal/Unmarshal。下面是一个jsonl 文件的例子。

这个文件的每一行都代表一个世界杯的数据:

{"year":"2018","host":"Russia","winner":"France"}
{"year":"2014","host":"Brazil","winner":"Germany"}
{"year":"2010","host":"South Africa","winner":"Spain"}
{"year":"2006","host":"Germany","winner":"Italy"}

使用bufio.Scanner工作

所以,让我们最简单、最方便地逐行读取这个文件。读取文件最简单的方法(至少对我来说)是使用标准库中bufio包中的扫描仪。首先,我们需要用NewScanner 函数创建一个实例,这是在golang中构造结构的一个非常熟悉的方法。这个函数接受一个Reader接口作为输入,好消息是os.File 实现了这个接口。这意味着,我们可以打开一个文件,并将文件的指针传递给bufio.NewScanner。让我们看看它的运行情况:

// first open the file
file, err := os.Open("/Users/Mark/fifa-winners.jsonl")
if err != nil {
    log.Fatalf("could not open the file: %v", err)
}
// don't forget to close the file.
defer file.Close()
// finally, we can have our scanner
scanner := bufio.NewScanner(file)

所以,我们有了扫描仪,我们准备好了......扫描仪有一个名为Scan的函数。这个函数将扫描仪移到下一个标记。我会告诉你这是什么意思,但现在,让我们说,每次调用Scan时,我们都会读取我们文件的一行。因此,如果我们想让扫描器在整个文件中移动,我们就会无限循环地调用扫描函数。问题是我们如何知道,我们可以打破这个循环?这很简单!除非遇到文件的末尾,否则扫描的返回值为真:

for {
    if scanner.Scan() {
        // we have a new line in each iteration
        continue
    }
    // we are done let's break the loop
    break
}
// the rest of our spaghetti

这段代码是有效的,但是你知道吗,我们可以说golang让一个循环一直运行到满足一个特定的条件。上面所有的代码都可以像下面的代码一样简单:

for scanner.Scan() {
    // we have a new line in each iteration
}
// the rest of our spaghetti

好了,让我们回到正题!我们可以通过调用Bytes()Text() 函数,让我们的字节字符串轻松地出现在每一行:

for scanner.Scan() {
  // b is an array of bytes ([]byte)
  b := scanner.Bytes()
  // s is string
  s := scanner.Text()
}

坦率地说,这些函数是一样的!例如,string(scanner.Bytes()) 会给你同样的结果,这正是Text() 函数中发生的事情。

我们读取了我们的文件,那么任务完成了吗?不完全是,因为我们还没有处理任何错误。

扫描仪有另一个函数,叫做Err() 。这个函数给你扫描过程中发生的第一个错误。这意味着,当扫描器试图在文件中移动时,如果发生了不好的事情,扫描函数会返回错误。所以我们的循环立即中断,我们将退出循环。现在我们可以得到这个错误并处理它。

如果我们想知道在文件的每一行发生了什么错误,我们应该使用传统的方法,我们知道扫描器是从文件的开头开始的(第1行),所以我们可以在循环外定义一个变量,代表行号,并在每次迭代中增加它:

lineNumber := 0
for scanner.Scan() {
    lineNumber++
        fmt.Println(scanner.Text())
}
// the rest of our spaghetti
if err := scanner.Err(); err != nil {
    log.Fatalf("something bad happened in the line %v: %v", lineNumber, err)
}

关于Err() ,我们应该考虑的另一件事是,它忽略了io.EOF ,所以如果我们会给出一个错误,那是一个真实的错误!

让我们运行一下:

➜ big-files (main) ✗ go run main.go
{"year":"2018","host":"Russia","winner":"France"}
{"year":"2014","host":"Brazil","winner":"Germany"}
{"year":"2010","host":"South Africa","winner":"Spain"}
{"year":"2006","host":"Germany","winner":"Italy"}

它成功了,那么下一步是什么?

修复bufio.Scanner: token too long错误

我们说过,jsonl文件的每一行都代表一个有效的json,所以它可能太长。我们还说过,Scan 函数将扫描器移到下一个令牌,但问题是下一个令牌在哪里!?

scanner函数还有一个方法,不像它的兄弟姐妹那样有名,Buffer ,它需要一个缓冲区和一个整数作为输入,你可以用这个函数设置缓冲区的最大尺寸。

bufio包有一个最大的令牌大小,相当于64 * 1024 (~65.6kb)。因此,如果我们的某一行大于这个大小,我们就会得到这样的错误token too long error

我们找到了问题的答案。下一个标记是扫描器达到最大尺寸(默认为65kb)的地方**,或者是**该行的末端。

方法1:更大的缓冲区大小

解决这个问题的第一个方法是增加缓冲区的大小。实际上,bufio.MaxScanTokenSize 这个名字有点误导,因为它不是实际的最大,而是默认的最大尺寸。所以我们可以增加它:

buf := []byte{}
scanner := bufio.NewScanner(file)
// increase the buffer size to 2Mb
scanner.Buffer(buf, 2048*1024)

现在我们可以处理jsonl ,行数最多为2Mb。这很好,但如果我们需要更多呢?我们可以随意增加这个数字(可能),但是如果我们的文件有5.000.000行,而其中有一行是100Mb,我们就需要把扫描器增加到这个大小,或者使用另一种方法。

方法2

读取这样一个艰难的文件的下一个方法!是使用另一个函数ReadLine Bufio(buffer-io)给我们提供了比简单的扫描器更多的方法来处理文件,我们必须根据我们的需要和要求选择其中一个。在这种情况下,扫描器不能满足我们的需要,所以让我们看看bufio.Reader 的功能。它比扫描器的水平低一点。一般来说,当你听到lower-level 这个词时,你应该做更多简单的事情,但你有更多的机会和权力!

因此,让我们开始吧。首先,我们需要一个阅读器:

reader := bufio.NewReader(file)

reader,有ReadLine函数,它试图读取整行的内容。就像扫描器一样,我们需要在一个for循环中调用这个函数,但由于我们是在较低的层次上!我们没有一个漂亮的简单的布尔值返回,以知道我们可以打破这个循环。

另一个区别是我们将从ReadLine 函数中给出错误,这也可以是io.EOF 。这对我们来说不会是一个真正的错误,所以我们也必须处理它:

reader := bufio.NewReader(file)
for {
    line, _, err := reader.ReadLine()
    if err != nil {
        if err == io.EOF {
            break
        }
        log.Fatalf("a real error happened here: %v\n", err)
    }
    fmt.Println(string(line))
}

正如你可能已经知道的,到目前为止,我们刚刚读了这个文件,我们实际上已经解决了我们的巨行的问题。

我们忽略了我们在ReadLine函数中给出的第二个参数,而这个参数正是我们解决问题所需要的。它是一个名为isPrefix 的布尔值。如果该行太长,ReadLine 无法将其所有内容放入缓冲区,它将返回已填满的缓冲区,并将 isPrefix 设置为 true,这意味着我们将在下次调用ReadLine 函数时给出该行的下一部分内容。

所以我们只需要调用ReadLine函数,直到isPrefix 变成false ,然后我们就可以去找我们文件的下一行。你可能已经注意到,我们正在谈论一个递归函数。首先,我定义了我们要递归调用的函数:

func read(r *bufio.Reader) ([]byte, error) {
    var (
        isPrefix = true
        err      error
        line, ln []byte
    )

    for isPrefix && err == nil {
        line, isPrefix, err = r.ReadLine()
        ln = append(ln, line...)
    }

    return ln, err
}

isPrefix 在第一处是真,错误也是零,所以我们确保for循环至少运行一次。它的行为类似于do-while循环。我们在循环内重新分配变量,所以我们调用 ,除非我们得到一个错误或者 是假的。在每一个迭代中,我们把从 得到的字节附加到另一个变量上。现在是时候在主函数中调用这个函数了。r.ReadLine isPrefix r.ReadLine()

reader := bufio.NewReader(file)
for {
    line, err := read(reader)
    if err != nil {
        if err == io.EOF {
            break
        }
        log.Fatalf("a real error happened here: %v\n", err)
    }
    fmt.Println(string(line))
}

就这样了!我们解决了这个问题。以下是完整的代码:

package main

import (
    "bufio"
    "fmt"
    "io"
    "log"
    "os"
)

func main() {
    // first open the file
    file, err := os.Open("./fifa-winners.jsonl")
    if err != nil {
        log.Fatalf("could not open the file: %v", err)
    }
    defer file.Close()
    log.Println("******************* READ WITH SCANNER *******************")
    readWithScanner(file)
    log.Println("******************* READ WITH READLINE() *******************")

    // we just reset the offset. because we read this file once
    // imagine the cursor is in the end of the file so we have to get back to the first line and read it again 
    file.Seek(0, 0)
    readWithReadLine(file)

    log.Println("we read a file twice!")
}

// Read with simple scanner

func readWithScanner(file *os.File) {
    // first open the file
    file, err := os.Open("./fifa-winners.jsonl")
    if err != nil {
        log.Fatalf("could not open the file: %v", err)
    }
    // finally, we can have our scanner
    buf := []byte{}
    scanner := bufio.NewScanner(file)
    scanner.Buffer(buf, 2048*1024)
    lineNumber := 1
    for scanner.Scan() {
        fmt.Println(scanner.Text())
        lineNumber++
    }
    // the rest of our spaghetti
    if err := scanner.Err(); err != nil {
        log.Fatalf("something bad happened in the line %v: %v", lineNumber, err)
    }
}

// Read with Readline function

func read(r *bufio.Reader) ([]byte, error) {
    var (
        isPrefix = true
        err      error
        line, ln []byte
    )

    for isPrefix && err == nil {
        line, isPrefix, err = r.ReadLine()
        ln = append(ln, line...)
    }

    return ln, err
}

func readWithReadLine(file *os.File) {
    reader := bufio.NewReader(file)
    for {
        line, err := read(reader)
        if err != nil {
            if err == io.EOF {
                break
            }
            log.Fatalf("a real error happened here: %v\n", err)
        }
        fmt.Println(string(line))
    }
}