在Go语言中逐行读取文件

548

我无法在 Go 语言中找到 file.ReadLine 函数。

如何逐行读取文件?


9
从Go1.1开始,bufio.Scanner是最好的方法来实现这个。 - Malcolm
13个回答

931
在 Go 1.1 及更新版本中,最简单的方法是使用 bufio.Scanner。以下是一个从文件中读取行的简单示例:
package main

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

func main() {
    file, err := os.Open("/path/to/file.txt")
    if err != nil {
        log.Fatal(err)
    }
    defer file.Close()

    scanner := bufio.NewScanner(file)
    // optionally, resize scanner's capacity for lines over 64K, see next example
    for scanner.Scan() {
        fmt.Println(scanner.Text())
    }

    if err := scanner.Err(); err != nil {
        log.Fatal(err)
    }
}

这是从Reader逐行读取的最干净的方法。

有一个注意点:如果行的长度超过65536个字符,扫描器会出错。如果您知道您的行长度大于64K,请使用Buffer()方法增加扫描器的容量:

...
scanner := bufio.NewScanner(file)

const maxCapacity int = longLineLen  // your required line length
buf := make([]byte, maxCapacity)
scanner.Buffer(buf, maxCapacity)

for scanner.Scan() {
...

47
因为OP要求扫描一个文件,所以首先打开文件:file, _ := os.Open("/path/to/file.csv"),然后扫描文件句柄:scanner := bufio.NewScanner(file),这很容易实现。 - Evan Plumlee
26
问题是Scanner.Scan()在每行中有4096个[]byte的缓冲区大小限制。如果行太长,则会出现bufio.ErrTooLong错误,即bufio.Scanner:token too long。这种情况下,您将不得不使用bufio.ReaderLine()或ReadString()。 - eduncan911
12
只是我的个人意见——这是页面上最正确的答案 :) - sethvargo
11
从源代码来看,现在限制已经从4KB增加到了64KB。详见:http://golang.org/src/bufio/scan.go?#L71 - Kokizzu
11
您可以使用Scanner的Buffer()方法来配置它以处理更长的行:https://golang.org/pkg/bufio/#Scanner.Buffer - Alex Robinson
显示剩余3条评论

192

注意:在Go的早期版本中,接受的答案是正确的。请参阅得票最高的答案包含了更近期的惯用方法来实现这一点。

bufio包中有一个名为ReadLine的函数。

请注意,如果行不适合读取缓冲区,该函数将返回不完整的行。如果您希望通过单个函数调用始终读取整行,请将ReadLine函数封装到自己的函数中,并在for循环中调用ReadLine

bufio.ReadString('\n')ReadLine并不完全等价,因为ReadString无法处理文件的最后一行不以换行符结尾的情况。


48
文档中的内容:“ReadLine是一种低级别的读取行的原始操作。大多数调用者应该使用ReadBytes('\n')或者ReadString('\n')替代它,或者使用Scanner。” - Michael Whatcott
14
为什么它是“低级别的行读取原语”很重要?这如何导致结论:“大多数调用者应该使用ReadBytes('\n')或ReadString('\n')而不是使用Scanner。”? - Charlie Parker
16
@CharlieParker - 我不确定,只是引用文档以增加背景信息。 - Michael Whatcott
13
从同一份文档中可以找到这样的内容:"如果ReadString在找到分隔符之前遇到错误,则会返回错误发生前读取的数据和错误本身(通常是io.EOF)。" 因此,您只需检查io.EOF错误即可知道操作已完成。 - eduncan911
2
当某个东西说它是低级别的,大多数调用者应该使用其他高级别的东西,这是因为高级别的东西可能更容易使用,正确地实现了低级别的东西,并提供了一个抽象层,以便将对低级别的事物的破坏性更改的影响限制在通过高级别的事物修复使用,而不是直接调用低级别的事物的所有用户代码上。 - Davos
显示剩余4条评论

64

编辑:从go1.1开始,惯用的解决方案是使用bufio.Scanner

我写了一个简单的方法来从文件中轻松读取每行内容。Readln(*bufio.Reader)函数从基础的bufio.Reader结构返回一行内容(不包括\n)。

// Readln returns a single line (without the ending \n)
// from the input buffered reader.
// An error is returned iff there is an error with the
// buffered reader.
func Readln(r *bufio.Reader) (string, error) {
  var (isPrefix bool = true
       err error = nil
       line, ln []byte
      )
  for isPrefix && err == nil {
      line, isPrefix, err = r.ReadLine()
      ln = append(ln, line...)
  }
  return string(ln),err
}

您可以使用 Readln 从文件中读取每一行。以下代码将读取文件中的每一行并输出到标准输出。

f, err := os.Open(fi)
if err != nil {
    fmt.Printf("error opening file: %v\n",err)
    os.Exit(1)
}
r := bufio.NewReader(f)
s, e := Readln(r)
for e == nil {
    fmt.Println(s)
    s,e = Readln(r)
}

干杯!


23
我在 Go 1.1 发布之前撰写了这篇答案。Go 1.1 中的 stdlib 包含有一个名为 Scanner 的包,提供了与我的答案相同的功能。我建议使用 Scanner 而不是我的答案,因为 Scanner 包含在 stdlib 中。祝你愉快地编程! :-) - Malcolm

45

读取文件的两种常见方法:

  1. 使用 bufio.Scanner
  2. 使用 bufio.Reader 中的 ReadString/ReadBytes/...

在我的测试用例中,对于 ~250MB、~2,500,000 行的文件,bufio.Scanner (时间使用:0.395491384s) 比 bufio.Reader.ReadString (时间使用:0.446867622s) 更快。

源代码:https://github.com/xpzouying/go-practice/tree/master/read_file_line_by_line

使用 bufio.Scanner 读取文件:

func scanFile() {
    f, err := os.OpenFile(logfile, os.O_RDONLY, os.ModePerm)
    if err != nil {
        log.Fatalf("open file error: %v", err)
        return
    }
    defer f.Close()

    sc := bufio.NewScanner(f)
    for sc.Scan() {
        _ = sc.Text()  // GET the line string
    }
    if err := sc.Err(); err != nil {
        log.Fatalf("scan file error: %v", err)
        return
    }
}

使用bufio.Reader读取文件,

func readFileLines() {
    f, err := os.OpenFile(logfile, os.O_RDONLY, os.ModePerm)
    if err != nil {
        log.Fatalf("open file error: %v", err)
        return
    }
    defer f.Close()

    rd := bufio.NewReader(f)
    for {
        line, err := rd.ReadString('\n')
        if err != nil {
            if err == io.EOF {
                break
            }

            log.Fatalf("read file line error: %v", err)
            return
        }
        _ = line  // GET the line string
    }
}

2
请注意,如果文件的最后一行没有以换行符结尾,那么此 bufio.Reader 示例将无法读取该行。在这种情况下,ReadString 将同时返回最后一行和 io.EOF - konrad
代码使用bufio.Reader会丢失文件的最后一行。如果err == io.EOF,则不能直接中断,此时该行是文件的最后一行。 - Justin

21

这里有一个来自于 Gist 的示例。

func readLine(path string) {
  inFile, err := os.Open(path)
  if err != nil {
     fmt.Println(err.Error() + `: ` + path)
     return
  }
  defer inFile.Close()

  scanner := bufio.NewScanner(inFile)
  for scanner.Scan() {
    fmt.Println(scanner.Text()) // the line
  }
}

但是当有一行大于Scanner缓冲区时会导致错误。

当发生这种情况时,我所做的是使用reader := bufio.NewReader(inFile)创建并连接自己的缓冲区,可以使用ch, err := reader.ReadByte()len, err := reader.Read(myBuffer)

另一种我使用的方法(用文件代替os.Stdin),当行很长(isPrefix)时连接,忽略空行:


func readLines() []string {
  r := bufio.NewReader(os.Stdin)
  bytes := []byte{}
  lines := []string{}
  for {
    line, isPrefix, err := r.ReadLine()
    if err != nil {
      break
    }
    bytes = append(bytes, line...)
    if !isPrefix {
      str := strings.TrimSpace(string(bytes))
      if len(str) > 0 {
        lines = append(lines, str)
        bytes = []byte{}
      }
    }
  }
  if len(bytes) > 0 {
    lines = append(lines, string(bytes))
  }
  return lines
}

解释一下为什么是“-1”? - Kokizzu
我认为这个解决方案有点过于复杂了,你觉得呢? - Decebal

13
您还可以使用带有 \n 分隔符的 ReadString:
  f, err := os.Open(filename)
  if err != nil {
    fmt.Println("error opening file ", err)
    os.Exit(1)
  }
  defer f.Close()
  r := bufio.NewReader(f)
  for {
    path, err := r.ReadString(10) // 0x0A separator = newline
    if err == io.EOF {
      // do something here
      break
    } else if err != nil {
      return err // if you return error
    }
  }

将字节转换为字符串使用哪种编码? - Thomas S.

7

另一种方法是使用 io/ioutilstrings 库来读取整个文件的字节,将它们转换成字符串并使用一个 "\n" (换行符) 作为分隔符进行拆分,例如:

import (
    "io/ioutil"
    "strings"
)

func main() {
    bytesRead, _ := ioutil.ReadFile("something.txt")
    fileContent := string(bytesRead)
    lines := strings.Split(fileContent, "\n")
}

严格来说,你并不是逐行读取文件,但是你可以使用这种技术来解析每一行。这种方法适用于小文件。如果你试图解析大型文件,请使用逐行读取的技术之一。


7
像这样将整个文件读入内存,然后再解压缩,对于大型文件来说可能非常昂贵。 - donatJ
2
os.ReadFile() 似乎做相同的事情。 - user2233706

6

4
// strip '\n' or read until EOF, return error if read error  
func readline(reader io.Reader) (line []byte, err error) {   
    line = make([]byte, 0, 100)                              
    for {                                                    
        b := make([]byte, 1)                                 
        n, er := reader.Read(b)                              
        if n > 0 {                                           
            c := b[0]                                        
            if c == '\n' { // end of line                    
                break                                        
            }                                                
            line = append(line, c)                           
        }                                                    
        if er != nil {                                       
            err = er                                         
            return                                           
        }                                                    
    }                                                        
    return                                                   
}                                    

2
在下面的代码中,我使用Readline从CLI读取兴趣直到用户按下回车键:
interests := make([]string, 1)
r := bufio.NewReader(os.Stdin)
for true {
    fmt.Print("Give me an interest:")
    t, _, _ := r.ReadLine()
    interests = append(interests, string(t))
    if len(t) == 0 {
        break;
    }
}
fmt.Println(interests)

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接