Membaca file baris demi baris di Go

335

Saya tidak dapat menemukan file.ReadLinefungsi di Go. Saya bisa mencari cara untuk menulisnya dengan cepat, tetapi saya hanya ingin tahu apakah saya mengabaikan sesuatu di sini. Bagaimana cara membaca file baris demi baris?

g06lin
sumber
7
Pada Go1.1, bufio.Scanner adalah cara terbaik untuk melakukan ini.
Malcolm

Jawaban:

133

CATATAN: Jawaban yang diterima benar dalam versi awal Go. Lihat jawaban terpilih tertinggi berisi cara idiomatik terbaru untuk mencapai ini.

Ada fungsi ReadLine dalam paket bufio.

Harap dicatat bahwa jika baris tidak sesuai dengan buffer baca, fungsi akan mengembalikan baris yang tidak lengkap. Jika Anda ingin selalu membaca seluruh baris dalam program Anda dengan satu panggilan ke suatu fungsi, Anda harus merangkum ReadLinefungsi tersebut ke dalam fungsi Anda sendiri yang memanggil ReadLinefor-loop.

bufio.ReadString('\n')tidak sepenuhnya setara dengan ReadLinekarena ReadStringtidak dapat menangani kasus ketika baris terakhir file tidak berakhir dengan karakter baris baru.

Samuel Hawksby-Robinson
sumber
37
Dari dokumen: "ReadLine adalah primitif pembacaan baris tingkat rendah. Sebagian besar penelepon harus menggunakan ReadBytes ('\ n') atau ReadString ('\ n') sebagai gantinya atau menggunakan Scanner."
mdwhatcott
12
@ mdwhatcott mengapa itu penting bahwa itu adalah "primitif membaca garis tingkat rendah"? Bagaimana hal itu sampai pada kesimpulan bahwa "Sebagian besar penelepon harus menggunakan ReadBytes ('\ n') atau ReadString ('\ n') sebagai gantinya atau menggunakan Scanner."?
Charlie Parker
12
@CharlieParker - Tidak yakin, hanya mengutip dokumen untuk menambahkan konteks.
mdwhatcott
11
Dari dokumen yang sama .. "Jika ReadString menemukan kesalahan sebelum menemukan pembatas, itu mengembalikan data yang dibaca sebelum kesalahan dan kesalahan itu sendiri (sering io.EOF)." Jadi Anda bisa memeriksa kesalahan io.EOF dan tahu Anda sudah selesai.
eduncan911
1
Perhatikan bahwa membaca atau menulis dapat gagal karena panggilan sistem terputus, yang menghasilkan kurang dari jumlah byte yang diharapkan yang dibaca atau ditulis.
Justin Swanhart
599

Di Go 1.1 dan yang lebih baru cara paling sederhana untuk melakukan ini adalah dengan a bufio.Scanner. Berikut adalah contoh sederhana yang membaca baris dari file:

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

Ini adalah cara terbersih untuk membaca dari Readerbaris demi baris.

Ada satu peringatan: Pemindai tidak berurusan dengan garis lebih dari 65536 karakter. Jika itu merupakan masalah bagi Anda maka Anda mungkin harus memutar sendiri di atas Reader.Read().

Stefan Arentz
sumber
40
Dan karena OP diminta untuk memindai suatu file, itu akan sepele untuk pertama file, _ := os.Open("/path/to/file.csv")dan kemudian memindai menangani file:scanner := bufio.NewScanner(file)
Evan Plumlee
14
Jangan lupa defer file.Close().
Kiril
13
Masalahnya adalah Scanner.Scan () terbatas dalam ukuran buffer byte 4096 [] per baris. Anda akan mendapatkan bufio.ErrTooLongkesalahan, yaitu bufio.Scanner: token too longjika garis terlalu panjang. Dalam hal ini, Anda harus menggunakan bufio.ReaderLine () atau ReadString ().
eduncan911
5
Hanya $ 0,02 saya - ini adalah jawaban yang paling benar di halaman :)
sethvargo
5
Anda dapat mengonfigurasi Pemindai untuk menangani garis yang lebih panjang menggunakan metode Buffer (): golang.org/pkg/bufio/#Scanner.Buffer
Alex Robinson
78

Menggunakan:

  • reader.ReadString('\n')
    • Jika Anda tidak keberatan salurannya bisa sangat panjang (mis. Gunakan banyak RAM). Itu membuat \npada akhir string dikembalikan.
  • reader.ReadLine()
    • Jika Anda peduli tentang membatasi konsumsi RAM dan tidak keberatan bekerja ekstra menangani kasus di mana garis lebih besar dari ukuran buffer pembaca.

Saya menguji berbagai solusi yang disarankan dengan menulis sebuah program untuk menguji skenario yang diidentifikasi sebagai masalah dalam jawaban lain:

  • File dengan garis 4MB.
  • File yang tidak berakhir dengan jeda baris.

Saya menemukan bahwa:

  • Itu Scanner solusi tidak menangani antrean panjang.
  • The ReadLinesolusi adalah kompleks untuk melaksanakan.
  • The ReadStringsolusi adalah yang paling sederhana dan bekerja untuk garis panjang.

Berikut adalah kode yang menunjukkan setiap solusi, dapat dijalankan melalui 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]
}

Saya diuji pada:

  • go versi go1.7 windows / amd64
  • go versi go1.6.3 linux / amd64
  • go versi go1.7.4 darwin / amd64

Output program pengujian:

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.
ah
sumber
9
The defer file.Close()harus setelah cek error; jika tidak pada kesalahan itu akan panik.
mlg
Solusi pemindai menangani garis panjang jika Anda mengkonfigurasinya seperti itu. Lihat: golang.org/pkg/bufio/#Scanner.Buffer
Inanc Gumus
Anda harus memeriksa kesalahan dengan benar seperti yang terlihat dalam dokumen: play.golang.org/p/5CCPzVTSj6 yaitu jika err == io.EOF {break} else {return err}
Chuque
53

EDIT: Pada go1.1, solusi idiomatik adalah dengan menggunakan bufio.Scanner

Saya menulis cara untuk dengan mudah membaca setiap baris dari file. Fungsi Readln (* bufio.Reader) mengembalikan baris (sans \ n) dari struct bufio.Reader yang mendasarinya.

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

Anda dapat menggunakan Readln untuk membaca setiap baris dari file. Kode berikut membaca setiap baris dalam file dan menampilkan setiap baris ke stdout.

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

Bersulang!

Malcolm
sumber
14
Saya menulis jawaban ini sebelum Go 1.1 keluar. Go 1.1 memiliki paket Scanner di stdlib. yang menyediakan fungsi yang sama dengan jawaban saya. Saya akan merekomendasikan menggunakan Scanner daripada jawaban saya karena Scanner ada di stdlib. Selamat melakukan peretasan! :-)
Malcolm
30

Ada dua cara umum untuk membaca file baris demi baris.

  1. Gunakan bufio.Scanner
  2. Gunakan ReadString / ReadBytes / ... di bufio.Reader

Di ruang uji saya, ~ 250MB, ~ 2.500.000 baris , bufio.Scanner (waktu yang digunakan: 0.395491384s) lebih cepat daripada bufio.Reader.ReadString (time_used: 0.446867622s).

Kode sumber: https://github.com/xpzouying/go-practice/tree/master/read_file_line_by_line

Baca file menggunakan 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
    }
}

Baca file menggunakan 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
    }
}
zouying
sumber
Perlu diketahui bahwa bufio.Readercontoh ini tidak akan membaca baris terakhir dalam file jika tidak diakhiri dengan baris baru. ReadStringakan mengembalikan baris terakhir dan io.EOFdalam hal ini.
konrad
18

Contoh dari intisari ini

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

tetapi ini memberikan kesalahan ketika ada garis yang lebih besar dari buffer Scanner.

Ketika itu terjadi, apa yang saya lakukan adalah menggunakan reader := bufio.NewReader(inFile)create dan concat buffer saya sendiri baik menggunakan ch, err := reader.ReadByte()ataulen, err := reader.Read(myBuffer)

Cara lain yang saya gunakan (ganti os.Stdin dengan file seperti di atas), ini cocok ketika baris panjang (isPrefix) dan mengabaikan baris kosong:


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
}
Kokizzu
sumber
mau menjelaskan kenapa -1?
Kokizzu
Saya pikir itu; sedikit terlalu rumit solusi ini, bukan?
Decebal
10

Anda juga dapat menggunakan ReadString dengan \ n sebagai pemisah:

  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
    }
  }
lzap
sumber
5

bufio.Reader.ReadLine () berfungsi dengan baik. Tetapi jika Anda ingin membaca setiap baris dengan sebuah string, coba gunakan ReadString ('\ n') . Tidak perlu menemukan kembali roda.

kroisse
sumber
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                                                   
}                                    
dunia maya
sumber
1

Dalam kode di bawah, saya membaca minat dari CLI hingga pengguna mengklik masuk dan saya menggunakan 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)
zuzuleinen
sumber
0

Saya suka solusi Lzap, saya baru di Go, saya ingin bertanya ke lzap tapi saya tidak bisa melakukannya saya belum 50 poin .. Saya mengubah sedikit solusi Anda dan menyelesaikan kode ...

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

Saya tidak yakin mengapa saya perlu menguji 'err' lagi, tetapi dengan cara apa pun kita bisa melakukannya. Tapi, pertanyaan utamanya adalah .. mengapa Go tidak menghasilkan kesalahan dengan kalimat => line, err: = r.ReadString (10), di dalam loop? Ini didefinisikan lagi dan lagi setiap kali loop dijalankan. Saya menghindari situasi itu dengan perubahan saya, ada komentar? Saya mengatur kondisi EOF di 'untuk' yang mirip dengan Sementara juga. Terima kasih

Jose.mg
sumber
0
import (
     "bufio"
     "os"
)

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

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

Berikut ini adalah contoh dengan fungsi ReadFromStdin()itu seperti fmt.Scan(&name)tetapi mengambil semua string dengan spasi kosong seperti: "Hello My Name Is ..."

var name string = ReadFromStdin()

println(name)
0DAYanc
sumber
0

Metode lain adalah dengan menggunakan io/ioutildan stringspustaka untuk membaca seluruh byte file, mengubahnya menjadi string dan membaginya menggunakan karakter " \n" (baris baru) sebagai pembatas, misalnya:

import (
    "io/ioutil"
    "strings"
)

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

Secara teknis Anda tidak membaca file baris demi baris, namun Anda dapat mengurai setiap baris menggunakan teknik ini. Metode ini berlaku untuk file yang lebih kecil. Jika Anda mencoba mem-parsing file besar gunakan salah satu teknik yang membaca baris demi baris.

pythoner
sumber