อ่านไฟล์ทีละบรรทัดใน Go


334

ฉันไม่พบfile.ReadLineฟังก์ชันใน Go ฉันสามารถหาวิธีเขียนได้อย่างรวดเร็ว แต่ฉันแค่สงสัยว่าฉันมองอะไรบางอย่างที่นี่ หนึ่งจะอ่านไฟล์ทีละบรรทัดได้อย่างไร


7
ในฐานะของ Go1.1 bufio.Scanner เป็นวิธีที่ดีที่สุดในการทำเช่นนี้
Malcolm

คำตอบ:


133

หมายเหตุ:คำตอบที่ยอมรับนั้นถูกต้องใน Go เวอร์ชันก่อนหน้า ดูคำตอบที่ได้รับการโหวตสูงสุดประกอบด้วยวิธีการสำนวนล่าสุดเพื่อให้บรรลุนี้

มีฟังก์ชั่นReadLinebufioในแพคเกจ

โปรดทราบว่าหากบรรทัดไม่พอดีกับบัฟเฟอร์การอ่านฟังก์ชันจะส่งคืนบรรทัดที่ไม่สมบูรณ์ หากคุณต้องการอ่านทั้งบรรทัดในโปรแกรมของคุณด้วยการเรียกใช้ฟังก์ชันเพียงครั้งเดียวคุณจะต้องสรุปReadLineฟังก์ชั่นลงในฟังก์ชั่นของคุณเองซึ่งเรียกใช้ฟังก์ชันReadLinefor-loop

bufio.ReadString('\n')ไม่เทียบเท่าอย่างเต็มที่ReadLineเนื่องจากReadStringไม่สามารถจัดการเคสได้เมื่อบรรทัดสุดท้ายของไฟล์ไม่ได้ลงท้ายด้วยอักขระขึ้นบรรทัดใหม่


37
จากเอกสาร: "ReadLine เป็นพื้นฐานการอ่านบรรทัดระดับต่ำผู้โทรส่วนใหญ่ควรใช้ ReadBytes ('\ n') หรือ ReadString ('\ n') แทนหรือใช้สแกนเนอร์"
mdwhatcott

12
@ mdwhatcott ทำไมมันถึงสำคัญว่ามันเป็น "ดั้งเดิมของการอ่านบรรทัดในระดับต่ำ"? การเข้าถึงถึงข้อสรุปว่า "ผู้โทรส่วนใหญ่ควรใช้ ReadBytes ('\ n') หรือ ReadString ('\ n') แทนหรือใช้สแกนเนอร์"?
Charlie Parker

12
@CharlieParker - ไม่แน่ใจเพียงแค่อ้างอิงเอกสารเพื่อเพิ่มบริบท
mdwhatcott

11
จากเอกสารเดียวกัน .. "ถ้า ReadString พบข้อผิดพลาดก่อนค้นหาตัวคั่นมันจะส่งคืนข้อมูลที่อ่านก่อนข้อผิดพลาดและข้อผิดพลาดเอง (มักจะเป็น io.EOF)" ดังนั้นคุณสามารถตรวจสอบข้อผิดพลาด io.EOF และทราบว่าคุณทำเสร็จแล้ว
eduncan911

1
โปรดทราบว่าการอ่านหรือเขียนอาจล้มเหลวเนื่องจากการเรียกของระบบที่ขัดจังหวะซึ่งส่งผลให้จำนวนไบต์น้อยกว่าที่คาดว่าจะอ่านหรือเขียน
Justin Swanhart

598

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

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

นี่เป็นวิธีที่สะอาดที่สุดในการอ่านจากReaderบรรทัดต่อบรรทัด

มีข้อแม้หนึ่งข้อ: สแกนเนอร์ทำงานได้ไม่ดีกับบรรทัดที่ยาวกว่า 65536 ตัวอักษร ถ้านั่นเป็นปัญหาสำหรับคุณคุณควรจะม้วนตัวเองReader.Read()ออกมา


40
และตั้งแต่ OP ขอให้สแกนมากกว่าไฟล์ก็จะจิ๊บจ๊อยไปก่อนfile, _ := os.Open("/path/to/file.csv")แล้วสแกนมากกว่าการจัดการไฟล์:scanner := bufio.NewScanner(file)
อีวาน Plumlee

14
defer file.Close()อย่าลืมให้
คิริล

13
ปัญหาคือ Scanner.Scan () ถูก จำกัด ในขนาดบัฟเฟอร์ 4096 [] ไบต์ต่อบรรทัด คุณจะได้รับbufio.ErrTooLongข้อผิดพลาดซึ่งbufio.Scanner: token too longถ้าสายยาวเกินไป ในกรณีนี้คุณจะต้องใช้ bufio.ReaderLine () หรือ ReadString ()
eduncan911

5
เพียงแค่ฉัน $ 0.02 - นี่คือคำตอบที่ถูกต้องที่สุดบนหน้า :)
sethvargo

5
คุณสามารถกำหนดค่าเครื่องสแกนเนอร์ให้จัดการกับสายที่ยาวกว่าได้โดยใช้วิธีการบัฟเฟอร์ (): golang.org/pkg/bufio/#Scanner.Buffer
Alex Robinson

78

ใช้:

  • reader.ReadString('\n')
    • หากคุณไม่คิดว่าสายอาจยาวมาก (เช่นใช้ RAM จำนวนมาก) มันทำให้\nตอนท้ายของสตริงกลับมา
  • reader.ReadLine()
    • หากคุณสนใจที่จะ จำกัด ปริมาณการใช้ RAM และไม่สนใจงานพิเศษในการจัดการเคสที่บรรทัดนั้นมีขนาดใหญ่กว่าขนาดบัฟเฟอร์ของเครื่องอ่าน

ฉันทดสอบวิธีแก้ปัญหาต่าง ๆ ที่แนะนำโดยการเขียนโปรแกรมเพื่อทดสอบสถานการณ์ที่ระบุว่าเป็นปัญหาในคำตอบอื่น ๆ :

  • ไฟล์ที่มีบรรทัด 4MB
  • ไฟล์ที่ไม่ได้ลงท้ายด้วยตัวแบ่งบรรทัด

ฉันพบว่า:

  • การScannerแก้ปัญหาไม่ได้จัดการกับสายยาว
  • การReadLineแก้ปัญหามีความซับซ้อนในการใช้งาน
  • การReadStringแก้ปัญหาเป็นวิธีที่ง่ายที่สุดและใช้งานได้กับสายยาว

นี่คือรหัสซึ่งแสดงให้เห็นในแต่ละวิธีการแก้ปัญหาจะสามารถทำงานผ่าน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]
}

ฉันทดสอบเมื่อ:

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

ผลลัพธ์ของโปรแกรมทดสอบ:

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
defer file.Close()ควรจะเป็นหลังจากเช็คข้อผิดพลาด; มิฉะนั้นในข้อผิดพลาดมันจะตกใจ
mlg

โซลูชันเครื่องสแกนเนอร์จัดการกับสายยาวหากคุณกำหนดค่าเช่นนั้น ดู: golang.org/pkg/bufio/#Scanner.Buffer
Inanc Gumus

คุณควรตรวจสอบข้อผิดพลาดอย่างถูกต้องตามที่เห็นในเอกสาร: play.golang.org/p/5CCPzVTSj6 เช่นถ้า err == io.EOF {break} else {return err}
Chuque

53

แก้ไข: ในฐานะของ go1.1, การแก้ปัญหาสำนวนคือการใช้bufio.Scanner

ฉันเขียนวิธีอ่านแต่ละบรรทัดจากไฟล์ ฟังก์ชัน Readln (* bufio.Reader) ส่งคืนบรรทัด (sans \ n) จากโครงสร้าง bufio.Reader

// 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 เพื่ออ่านทุกบรรทัดจากไฟล์ โค้ดต่อไปนี้อ่านทุกบรรทัดในไฟล์และส่งออกแต่ละบรรทัดเป็น 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)
}

ไชโย!


14
ฉันเขียนคำตอบนี้ก่อนที่ Go 1.1 จะออกมา Go 1.1 มีแพ็คเกจสแกนเนอร์ใน stdlib ที่ให้การทำงานเหมือนกับคำตอบของฉัน ฉันอยากจะแนะนำให้ใช้สแกนเนอร์แทนคำตอบของฉันเนื่องจาก Scanner อยู่ใน stdlib แฮ็คมีความสุข! :-)
Malcolm

30

มีวิธีการทั่วไปสองวิธีในการอ่านไฟล์ทีละบรรทัด

  1. ใช้ bufio.Scanner
  2. ใช้ ReadString / ReadBytes / ... ใน bufio.Reader

ใน testcase ของฉัน~ 250MB ~ 2,500,000 บรรทัด bufio.Scanner (เวลาที่ใช้: 0.395491384s) เร็วกว่า bufio.Reader.ReadString (time_used: 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
    }
}

โปรดระวังว่าbufio.Readerตัวอย่างนี้จะไม่อ่านบรรทัดสุดท้ายในไฟล์หากไม่ได้ขึ้นบรรทัดใหม่ ReadStringจะส่งคืนทั้งบรรทัดสุดท้ายและio.EOFในกรณีนี้
Konrad

18

ตัวอย่างจากส่วนสำคัญนี้

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

แต่สิ่งนี้จะให้ข้อผิดพลาดเมื่อมีบรรทัดที่ใหญ่กว่าบัฟเฟอร์ของสแกนเนอร์

เมื่อสิ่งนั้นเกิดขึ้นสิ่งที่ฉันทำคือใช้reader := bufio.NewReader(inFile)สร้างและต่อเชื่อมบัฟเฟอร์ของฉันเองโดยใช้ch, err := reader.ReadByte()หรือlen, err := reader.Read(myBuffer)

อีกวิธีหนึ่งที่ฉันใช้ (แทนที่ os.Stdin ด้วยไฟล์เหมือนด้านบน) อันนี้ concats เมื่อสายยาว (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

ฉันคิดว่ามันเป็นวิธีแก้ปัญหาที่ซับซ้อนมากกว่านี้ใช่ไหม
หลอกลวง

10

คุณยังสามารถใช้ ReadString ด้วย \ n เป็นตัวคั่นได้:

  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

ในซอลเบลโลว์รหัสฉันอ่านความสนใจจาก CLI จนกระทั่งผู้ใช้กดและฉันใช้ 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

ฉันชอบโซลูชัน Lzap ฉันใหม่ใน Go ฉันอยากถาม lzap แต่ฉันไม่สามารถทำได้ฉันยังไม่ได้ 50 คะแนนเลย .. ฉันเปลี่ยนโซลูชันของคุณเล็กน้อยและกรอกรหัส ...

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

ฉันไม่แน่ใจว่าทำไมฉันต้องทดสอบ 'ผิดพลาด' อีกครั้ง แต่เราสามารถทำได้ แต่คำถามหลักคือ .. ทำไม Go จึงไม่เกิดข้อผิดพลาดกับประโยค => line, err: = r.ReadString (10) ภายในวง? มันถูกกำหนดอีกครั้งและอีกครั้งในแต่ละครั้งที่มีการดำเนินการวง ฉันหลีกเลี่ยงสถานการณ์นั้นเมื่อมีการเปลี่ยนแปลงความคิดเห็นใด ๆ ฉันตั้งเงื่อนไข EOF ใน 'for' คล้ายกับในขณะเดียวกัน ขอบคุณ


0
import (
     "bufio"
     "os"
)

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

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

นี่คือตัวอย่างของฟังก์ชั่นที่ReadFromStdin()มันเป็นเหมือนfmt.Scan(&name)แต่มันใช้สายทั้งหมดที่มีช่องว่างเช่น: "Hello My Name Is ... "

var name string = ReadFromStdin()

println(name)
โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.