Đọc một dòng tệp theo dòng trong Go


332

Tôi không thể tìm thấy file.ReadLinechức năng trong Go. Tôi có thể tìm ra cách để nhanh chóng viết một cái, nhưng tôi chỉ tự hỏi liệu tôi có đang nhìn gì đó ở đây không. Làm thế nào để một người đọc một dòng tập tin theo dòng?


7
Kể từ Go1.1, bufio.Scanner là cách tốt nhất để làm điều này.
Malcolm

Câu trả lời:


133

LƯU Ý: Câu trả lời được chấp nhận là chính xác trong các phiên bản đầu của Go. Xem câu trả lời được bình chọn cao nhất chứa cách thành ngữ gần đây hơn để đạt được điều này.

Có chức năng ReadLine trong gói bufio.

Xin lưu ý rằng nếu dòng không vừa với bộ đệm đọc, hàm sẽ trả về một dòng không đầy đủ. Nếu bạn muốn luôn luôn đọc toàn bộ một dòng trong chương trình của mình bằng một cuộc gọi đến một chức năng, bạn sẽ cần phải đóng gói ReadLinechức năng đó vào chức năng của chính bạn, gọi ReadLinetrong một vòng lặp for.

bufio.ReadString('\n')không hoàn toàn tương đương ReadLineReadStringkhông thể xử lý trường hợp khi dòng cuối cùng của tệp không kết thúc bằng ký tự dòng mới.


37
Từ các tài liệu: "ReadLine là một nguyên thủy đọc dòng cấp thấp. Hầu hết người gọi nên sử dụng ReadBytes ('\ n') hoặc ReadString ('\ n') hoặc sử dụng Máy quét."
mdwhatcott

12
@mdwhatcott tại sao vấn đề của nó là "nguyên thủy đọc dòng cấp thấp"? Làm thế nào mà đi đến kết luận rằng "Hầu hết người gọi nên sử dụng ReadBytes ('\ n') hoặc ReadString ('\ n') thay vào đó hoặc sử dụng Máy quét."?
Charlie Parker

12
@CharlieParker - Không chắc chắn, chỉ trích dẫn các tài liệu để thêm ngữ cảnh.
mdwhatcott

11
Từ cùng một tài liệu .. "Nếu ReadString gặp lỗi trước khi tìm dấu phân cách, nó sẽ trả về dữ liệu đã đọc trước lỗi và chính lỗi đó (thường là io.EOF)." Vì vậy, bạn chỉ có thể kiểm tra lỗi io.EOF và biết bạn đã hoàn thành.
eduncan911

1
Lưu ý rằng việc đọc hoặc ghi có thể thất bại do cuộc gọi hệ thống bị gián đoạn, dẫn đến ít hơn số byte dự kiến ​​được đọc hoặc ghi.
Justin Swanhart

597

Trong Go 1.1 và mới hơn, cách đơn giản nhất để làm điều này là với a bufio.Scanner. Đây là một ví dụ đơn giản đọc các dòng từ một tệp:

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)
    for scanner.Scan() {
        fmt.Println(scanner.Text())
    }

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

Đây là cách sạch nhất để đọc từ một Readerdòng theo dòng.

Có một cảnh báo: Máy quét không xử lý tốt các dòng dài hơn 65536 ký tự. Nếu đó là một vấn đề cho bạn thì có lẽ bạn nên tự mình lăn lộn Reader.Read().


40
Và vì OP yêu cầu quét qua một tệp, trước tiên, nó sẽ là tầm thường file, _ := os.Open("/path/to/file.csv")và sau đó quét qua phần xử lý tệp:scanner := bufio.NewScanner(file)
Evan Plumlee

14
Đừng quên defer file.Close().
Kiril

13
Vấn đề là Scanner.Scan () bị giới hạn ở kích thước bộ đệm byte 4096 [] trên mỗi dòng. Bạn sẽ gặp bufio.ErrTooLonglỗi, đó là bufio.Scanner: token too longnếu dòng quá dài. Trong trường hợp đó, bạn sẽ phải sử dụng bufio.ReaderLine () hoặc ReadString ().
eduncan911

5
Chỉ 0,02 đô la của tôi - đây là câu trả lời đúng nhất trên trang :)
sethvargo

5
Bạn có thể định cấu hình Máy quét để xử lý các dòng thậm chí dài hơn bằng phương thức Buffer (): golang.org/pkg/bufio/#Scanner.Buffer
Alex Robinson

77

Sử dụng:

  • reader.ReadString('\n')
    • Nếu bạn không nhớ rằng dòng có thể rất dài (tức là sử dụng nhiều RAM). Nó giữ \nở cuối chuỗi trả về.
  • reader.ReadLine()
    • Nếu bạn quan tâm đến việc hạn chế mức tiêu thụ RAM và đừng bận tâm đến việc xử lý thêm trường hợp đường truyền lớn hơn kích thước bộ đệm của trình đọc.

Tôi đã thử nghiệm các giải pháp khác nhau được đề xuất bằng cách viết chương trình để kiểm tra các tình huống được xác định là sự cố trong các câu trả lời khác:

  • Một tệp có dòng 4MB.
  • Một tập tin không kết thúc bằng ngắt dòng.

Tôi thấy rằng:

  • Các Scanner giải pháp không xử lý xếp hàng dài.
  • Các ReadLinegiải pháp phức tạp để thực hiện.
  • Các ReadStringgiải pháp là đơn giản nhất và làm việc cho đường dây dài.

Đây là mã trình bày từng giải pháp, nó có thể được chạy qua go run main.go:

package main

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

func readFileWithReadString(fn string) (err error) {
    fmt.Println("readFileWithReadString")

    file, err := os.Open(fn)
    defer file.Close()

    if err != nil {
        return err
    }

    // Start reading from the file with a reader.
    reader := bufio.NewReader(file)

    var line string
    for {
        line, err = reader.ReadString('\n')

        fmt.Printf(" > Read %d characters\n", len(line))

        // Process the line here.
        fmt.Println(" > > " + limitLength(line, 50))

        if err != nil {
            break
        }
    }

    if err != io.EOF {
        fmt.Printf(" > Failed!: %v\n", err)
    }

    return
}

func readFileWithScanner(fn string) (err error) {
    fmt.Println("readFileWithScanner - this will fail!")

    // Don't use this, it doesn't work with long lines...

    file, err := os.Open(fn)
    defer file.Close()

    if err != nil {
        return err
    }

    // Start reading from the file using a scanner.
    scanner := bufio.NewScanner(file)

    for scanner.Scan() {
        line := scanner.Text()

        fmt.Printf(" > Read %d characters\n", len(line))

        // Process the line here.
        fmt.Println(" > > " + limitLength(line, 50))
    }

    if scanner.Err() != nil {
        fmt.Printf(" > Failed!: %v\n", scanner.Err())
    }

    return
}

func readFileWithReadLine(fn string) (err error) {
    fmt.Println("readFileWithReadLine")

    file, err := os.Open(fn)
    defer file.Close()

    if err != nil {
        return err
    }

    // Start reading from the file with a reader.
    reader := bufio.NewReader(file)

    for {
        var buffer bytes.Buffer

        var l []byte
        var isPrefix bool
        for {
            l, isPrefix, err = reader.ReadLine()
            buffer.Write(l)

            // If we've reached the end of the line, stop reading.
            if !isPrefix {
                break
            }

            // If we're just at the EOF, break
            if err != nil {
                break
            }
        }

        if err == io.EOF {
            break
        }

        line := buffer.String()

        fmt.Printf(" > Read %d characters\n", len(line))

        // Process the line here.
        fmt.Println(" > > " + limitLength(line, 50))
    }

    if err != io.EOF {
        fmt.Printf(" > Failed!: %v\n", err)
    }

    return
}

func main() {
    testLongLines()
    testLinesThatDoNotFinishWithALinebreak()
}

func testLongLines() {
    fmt.Println("Long lines")
    fmt.Println()

    createFileWithLongLine("longline.txt")
    readFileWithReadString("longline.txt")
    fmt.Println()
    readFileWithScanner("longline.txt")
    fmt.Println()
    readFileWithReadLine("longline.txt")
    fmt.Println()
}

func testLinesThatDoNotFinishWithALinebreak() {
    fmt.Println("No linebreak")
    fmt.Println()

    createFileThatDoesNotEndWithALineBreak("nolinebreak.txt")
    readFileWithReadString("nolinebreak.txt")
    fmt.Println()
    readFileWithScanner("nolinebreak.txt")
    fmt.Println()
    readFileWithReadLine("nolinebreak.txt")
    fmt.Println()
}

func createFileThatDoesNotEndWithALineBreak(fn string) (err error) {
    file, err := os.Create(fn)
    defer file.Close()

    if err != nil {
        return err
    }

    w := bufio.NewWriter(file)
    w.WriteString("Does not end with linebreak.")
    w.Flush()

    return
}

func createFileWithLongLine(fn string) (err error) {
    file, err := os.Create(fn)
    defer file.Close()

    if err != nil {
        return err
    }

    w := bufio.NewWriter(file)

    fs := 1024 * 1024 * 4 // 4MB

    // Create a 4MB long line consisting of the letter a.
    for i := 0; i < fs; i++ {
        w.WriteRune('a')
    }

    // Terminate the line with a break.
    w.WriteRune('\n')

    // Put in a second line, which doesn't have a linebreak.
    w.WriteString("Second line.")

    w.Flush()

    return
}

func limitLength(s string, length int) string {
    if len(s) < length {
        return s
    }

    return s[:length]
}

Tôi đã thử nghiệm trên:

  • đi phiên bản go1.7 windows / amd64
  • đi phiên bản go1.6.3 linux / amd64
  • đi phiên bản go1.7.4 darwin / amd64

Các đầu ra chương trình thử nghiệm:

Long lines

readFileWithReadString
 > Read 4194305 characters
 > > aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
 > Read 12 characters
 > > Second line.

readFileWithScanner - this will fail!
 > Failed!: bufio.Scanner: token too long

readFileWithReadLine
 > Read 4194304 characters
 > > aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
 > Read 12 characters
 > > Second line.

No linebreak

readFileWithReadString
 > Read 28 characters
 > > Does not end with linebreak.

readFileWithScanner - this will fail!
 > Read 28 characters
 > > Does not end with linebreak.

readFileWithReadLine
 > Read 28 characters
 > > Does not end with linebreak.

9
Các defer file.Close()nên sau khi kiểm tra lỗi; nếu không có lỗi nó sẽ hoảng loạn.
mlg

Giải pháp quét sẽ xử lý các dòng dài nếu bạn cấu hình nó như vậy. Xem: golang.org/pkg/bufio/#Scanner.Buffer
Inanc Gumus 25/03/19

Bạn nên kiểm tra lỗi đúng như đã thấy trong các tài liệu: play.golang.org/p/5CCPzVTSj6 tức là nếu err == io.EOF {break} khác {return err}
Chuque

53

EDIT: Kể từ go1.1, giải pháp thành ngữ là sử dụng bufio.Scanner

Tôi đã viết lên một cách để dễ dàng đọc từng dòng từ một tập tin. Hàm Readln (* bufio.Reader) trả về một dòng (sans \ n) từ cấu trúc bufio.Reader bên dưới.

// 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
}

Bạn có thể sử dụng Readln để đọc mọi dòng từ một tệp. Đoạn mã sau đọc từng dòng trong một tệp và xuất từng dòng thành thiết bị xuất chuẩn.

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)
}

Chúc mừng!


14
Tôi đã viết câu trả lời này trước khi Go 1.1 xuất hiện. Go 1.1 có gói Scanner trong stdlib. cung cấp các chức năng tương tự như câu trả lời của tôi. Tôi khuyên bạn nên sử dụng Máy quét thay vì câu trả lời của mình vì Máy quét nằm trong stdlib. Chúc mừng hack! :-)
Malcolm

30

Có hai cách phổ biến để đọc từng dòng tệp.

  1. Sử dụng bufio.Scanner
  2. Sử dụng ReadString / ReadBytes / ... trong bufio.Reader

Trong thử nghiệm của tôi, ~ 250MB, ~ 2.500.000 dòng , bufio.Scanner (thời gian sử dụng: 0.395491384s) nhanh hơn bufio.Reader.ReadString (time_use: 0.446867622s).

Mã nguồn: https://github.com/xpzouying/go-practice/tree/master/read_file_line_by_line

Đọc tệp sử dụng 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
    }
}

Đọc tệp sử dụng 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
    }
}

Xin lưu ý rằng bufio.Readerví dụ này sẽ không đọc dòng cuối cùng trong một tệp nếu nó không kết thúc bằng một dòng mới. ReadStringsẽ trả lại cả dòng cuối cùng và io.EOFtrong trường hợp này.
konrad

18

Ví dụ từ ý chính này

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
  }
}

nhưng điều này gây ra lỗi khi có một dòng lớn hơn bộ đệm của Scanner.

Khi điều đó xảy ra, những gì tôi làm là sử dụng reader := bufio.NewReader(inFile)tạo và ghép bộ đệm của riêng tôi bằng cách sử dụng ch, err := reader.ReadByte()hoặclen, err := reader.Read(myBuffer)

Một cách khác mà tôi sử dụng (thay thế os.Stdin bằng tệp như trên), cách này sẽ che giấu khi các dòng dài (isPrefix) và bỏ qua các dòng trống:


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
}

quan tâm để giải thích tại sao -1?
Kokizzu

Tôi nghĩ rằng nó, một chút quá phức tạp giải pháp này, phải không?
Decebal

10

Bạn cũng có thể sử dụng ReadString với \ n làm dấu phân cách:

  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
    }
  }


3
// 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                                                   
}                                    

1

Trong mã dưới đây, tôi đọc các sở thích từ CLI cho đến khi người dùng nhấn enter và tôi đang sử dụng Readline:

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)

0

Tôi thích giải pháp Lzap, tôi mới tham gia Go, tôi muốn hỏi về lzap nhưng tôi không thể làm được. Tôi chưa có 50 điểm .. Tôi thay đổi một chút giải pháp của bạn và hoàn tất mã ...

package main

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

func main() {
    f, err := os.Open("archiveName")
    if err != nil {
        fmt.Println(err)
        os.Exit(1)
    }
    defer f.Close()
    r := bufio.NewReader(f)
    line, err := r.ReadString(10)    // line defined once 
    for err != io.EOF {
        fmt.Print(line)              // or any stuff
        line, err = r.ReadString(10) //  line was defined before
    }
}

Tôi không chắc tại sao tôi lại phải kiểm tra 'err', nhưng dù sao thì chúng tôi cũng có thể làm được. Nhưng, câu hỏi chính là .. tại sao Go không tạo ra lỗi với câu => line, err: = r.ReadString (10), bên trong vòng lặp? Nó được định nghĩa lặp đi lặp lại mỗi lần vòng lặp được thực thi. Tôi tránh tình huống đó với sự thay đổi của tôi, bất kỳ bình luận? Tôi cũng đặt điều kiện EOF trong 'for' tương tự như While. Cảm ơn


0
import (
     "bufio"
     "os"
)

var (
    reader = bufio.NewReader(os.Stdin)
)

func ReadFromStdin() string{
    result, _ := reader.ReadString('\n')
    witl := result[:len(result)-1]
    return witl
}

Dưới đây là một ví dụ với chức năng ReadFromStdin()giống như vậy fmt.Scan(&name)nhưng nó lấy tất cả các chuỗi có khoảng trắng như: "Xin chào Tên tôi là ..."

var name string = ReadFromStdin()

println(name)
Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.