Czytanie pliku linia po linii w Go

334

Nie mogę znaleźć file.ReadLinefunkcji w Go. Potrafię wymyślić, jak szybko napisać jeden, ale zastanawiam się, czy coś tutaj przeoczyłem. Jak czytać plik linia po linii?

g06lin
źródło
7
Począwszy od wersji Go1.1 najlepszym sposobem na to jest bufio.Scanner.
Malcolm,

Odpowiedzi:

133

UWAGA: Akceptowana odpowiedź była poprawna we wczesnych wersjach Go. Zobacz najwyżej głosowaną odpowiedź zawiera najnowszy idiomatyczny sposób na osiągnięcie tego.

W pakiecie znajduje się funkcja ReadLinebufio .

Pamiętaj, że jeśli linia nie mieści się w buforze odczytu, funkcja zwróci niekompletną linię. Jeśli chcesz zawsze czytać całą linię w programie za pomocą pojedynczego wywołania funkcji, musisz obudować ReadLinefunkcję we własnej funkcji, która wywołuje ReadLinepętlę for.

bufio.ReadString('\n')nie jest w pełni równoważne, ReadLineponieważ ReadStringnie jest w stanie obsłużyć przypadku, gdy ostatni wiersz pliku nie kończy się znakiem nowej linii.

Samuel Hawksby-Robinson
źródło
37
Z dokumentacji: „ReadLine to podstawowy prymitywny odczyt linii. Większość osób dzwoniących powinna zamiast tego używać ReadBytes ('\ n') lub ReadString ('\ n') lub skanera.”
mdwhatcott,
12
@mdwhatcott, dlaczego ma to znaczenie, że jest to „prymitywne czytanie linii niskiego poziomu”? Jak dochodzi do wniosku, że „Większość dzwoniących powinna zamiast tego używać ReadBytes ('\ n') lub ReadString ('\ n') lub skanera.”?
Charlie Parker,
12
@CharlieParker - Nie jestem pewien, cytuję dokumenty, aby dodać kontekst.
mdwhatcott,
11
Z tych samych dokumentów .. „Jeśli ReadString napotka błąd przed znalezieniem separatora, zwraca dane odczytane przed błędem i sam błąd (często io.EOF).” Możesz więc po prostu sprawdzić, czy nie wystąpił błąd io.EOF i wiedzieć, że gotowe.
eduncan911
1
Należy pamiętać, że odczyt lub zapis może się nie powieść z powodu przerwanego wywołania systemowego, w wyniku czego odczyt lub zapis jest mniejszy niż oczekiwana liczba bajtów.
Justin Swanhart
598

W Go 1.1 i nowszych najprostszym sposobem na to jest użycie bufio.Scanner. Oto prosty przykład, który odczytuje wiersze z pliku:

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

Jest to najczystszy sposób czytania z Readerlinii po linii.

Jest jedno zastrzeżenie: skaner nie radzi sobie dobrze z liniami dłuższymi niż 65536 znaków. Jeśli jest to dla ciebie problem, prawdopodobnie powinieneś rzucić swój własny na Reader.Read().

Stefan Arentz
źródło
40
A ponieważ OP poprosił o zeskanowanie pliku, najpierw byłoby trywialne, file, _ := os.Open("/path/to/file.csv")a następnie zeskanować uchwyt pliku:scanner := bufio.NewScanner(file)
Evan Plumlee
14
Nie zapomnij defer file.Close().
Kiril,
13
Problem polega na tym, że Scanner.Scan () ma rozmiar bufora 4096 [] bajtów na linię. Otrzymasz bufio.ErrTooLongbłąd, to znaczy, bufio.Scanner: token too longjeśli linia jest za długa. W takim przypadku będziesz musiał użyć bufio.ReaderLine () lub ReadString ().
eduncan911,
5
Tylko moje 0,02 $ - to jest najbardziej poprawna odpowiedź na stronie :)
sethvargo
5
Możesz skonfigurować skaner do obsługi nawet dłuższych linii za pomocą jego metody Buffer (): golang.org/pkg/bufio/#Scanner.Buffer
Alex Robinson
78

Posługiwać się:

  • reader.ReadString('\n')
    • Jeśli nie masz nic przeciwko temu, że linia może być bardzo długa (tj. Użyj dużo pamięci RAM). Zachowuje \nzwrócony koniec łańcucha.
  • reader.ReadLine()
    • Jeśli zależy Ci na ograniczeniu zużycia pamięci RAM i nie przeszkadza ci dodatkowa praca związana ze sprawą, w której linia jest większa niż rozmiar bufora czytnika.

Przetestowałem różne rozwiązania sugerowane przez napisanie programu do testowania scenariuszy, które zostały zidentyfikowane jako problemy w innych odpowiedziach:

  • Plik z linią 4 MB.
  • Plik, który nie kończy się podziałem linii.

Znalazłem to:

  • ScannerRozwiązanie nie obsługuje długie linie.
  • ReadLineRozwiązanie jest trudne do wykonania.
  • ReadStringRozwiązanie jest najprostszym i pracuje dla długich kolejkach.

Oto kod demonstrujący każde rozwiązanie, które można uruchomić za pomocą 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]
}

Testowałem na:

  • wersja go go.7.7 windows / amd64
  • wersja go go1.6.3 linux / amd64
  • wersja go go1.7.4 darwin / amd64

Program testowy generuje:

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
źródło
9
defer file.Close()Powinny być po sprawdzeniu błędów; w przeciwnym razie po błędzie wpadnie w panikę.
mlg
Rozwiązanie skanera obsługuje długie linie, jeśli tak je skonfigurujesz. Zobacz: golang.org/pkg/bufio/#Scanner.Buffer
Inanc Gumus
Powinieneś dokładnie sprawdzić błąd, jak widać w dokumentach: play.golang.org/p/5CCPzVTSj6, tj. Jeśli err == io.EOF {break} else {return err}
Chuque
53

EDYCJA: W wersji go1.1 rozwiązaniem idiomatycznym jest użycie bufio.Scanner

Napisałem sposób na łatwy odczyt każdej linii z pliku. Funkcja Readln (* bufio.Reader) zwraca wiersz (sans \ n) z bazowej struktury 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
}

Możesz użyć Readln do odczytania każdej linii z pliku. Poniższy kod czyta każdy wiersz w pliku i wysyła każdy wiersz na standardowe wyjście.

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

Twoje zdrowie!

Malcolm
źródło
14
Napisałem tę odpowiedź, zanim ukazała się wersja Go 1.1. Go 1.1 ma pakiet skanera w stdlib. który zapewnia taką samą funkcjonalność jak moja odpowiedź. Polecam użycie skanera zamiast mojej odpowiedzi, ponieważ skaner jest w standardzie. Miłego hakowania! :-)
Malcolm,
30

Istnieją dwa popularne sposoby odczytu pliku wiersz po wierszu.

  1. Użyj bufio.Scanner
  2. Użyj ReadString / ReadBytes / ... w bufio.Reader

W moim teście ~ 250 MB, ~ 2 500 000 linii , bufio.Scanner (wykorzystany czas: 0,395491384s) jest szybszy niż bufio.Reader.ReadString (wykorzystany czas: 0,446867622s).

Kod źródłowy: https://github.com/xpzouying/go-practice/tree/master/read_file_line_by_line

Czytaj plik użyj 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
    }
}

Czytaj plik użyj 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
źródło
Pamiętaj, że ten bufio.Readerprzykład nie odczyta ostatniego wiersza w pliku, jeśli nie kończy się na nowej linii. ReadStringzwróci zarówno ostatnią linię, jak i io.EOFw tym przypadku.
konrad
18

Przykład z tego 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
  }
}

ale to powoduje błąd, gdy istnieje linia większa niż bufor skanera.

Kiedy tak się stało, używam reader := bufio.NewReader(inFile)tworzenia i łączenia własnego bufora za pomocą ch, err := reader.ReadByte()lublen, err := reader.Read(myBuffer)

Innym sposobem, którego używam (zamień os.Stdin na plik jak wyżej), ten konkatuje, gdy linie są długie (isPrefix) i ignoruje puste linie:


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
źródło
chcesz wyjaśnić, dlaczego -1?
Kokizzu,
Myślę, że to trochę skomplikowało to rozwiązanie, prawda?
Decebal
10

Możesz także użyć ReadString z \ n jako separatorem:

  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
źródło
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                                                   
}                                    
cyber
źródło
1

W poniższym kodzie czytam zainteresowania z interfejsu CLI, dopóki użytkownik nie kliknie Enter i korzystam z 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
źródło
0

Lubię rozwiązanie Lzap, jestem nowy w Go, chciałbym poprosić o lzap, ale nie mogłem tego zrobić Nie mam jeszcze 50 punktów .. Zmieniam trochę twoje rozwiązanie i uzupełniam kod ...

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

Nie jestem pewien, dlaczego muszę ponownie przetestować „błąd”, ale w każdym razie możemy to zrobić. Ale głównym pytaniem jest ... dlaczego Go nie powoduje błędu w zdaniu => wiersz, err: = r.ReadString (10), wewnątrz pętli? Jest definiowany wielokrotnie za każdym razem, gdy pętla jest wykonywana. Unikam tej sytuacji wraz z moją zmianą, jakiś komentarz? Ustawiłem warunek EOF w „for” jako podobny do Chwila też. Dzięki

Jose.mg
źródło
0
import (
     "bufio"
     "os"
)

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

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

Oto przykład z funkcją, ReadFromStdin()która jest podobna, fmt.Scan(&name)ale pobiera wszystkie ciągi znaków z pustymi spacjami, takie jak: „Hello My Name Is ...”

var name string = ReadFromStdin()

println(name)
0DAYanc
źródło