Lire un fichier ligne par ligne dans Go

335

Je n'arrive pas à trouver de file.ReadLinefonction dans Go. Je peux comprendre comment en écrire rapidement un, mais je me demande simplement si j'oublie quelque chose ici. Comment lire un fichier ligne par ligne?

g06lin
la source
7
Depuis Go1.1, bufio.Scanner est le meilleur moyen de le faire.
Malcolm

Réponses:

133

REMARQUE: la réponse acceptée était correcte dans les premières versions de Go. Voir la réponse la plus votée contient la manière idiomatique la plus récente d'y parvenir.

Il y a la fonction ReadLine dans le package bufio.

Veuillez noter que si la ligne ne rentre pas dans le tampon de lecture, la fonction renverra une ligne incomplète. Si vous voulez toujours lire une ligne entière dans votre programme par un seul appel à une fonction, vous devrez encapsuler la ReadLinefonction dans votre propre fonction qui appelle ReadLinedans une boucle for.

bufio.ReadString('\n')n'est pas entièrement équivalent à ReadLinecar ReadStringest incapable de gérer le cas lorsque la dernière ligne d'un fichier ne se termine pas par le caractère de nouvelle ligne.

Samuel Hawksby-Robinson
la source
37
D'après les documents: "ReadLine est une primitive de lecture de ligne de bas niveau. La plupart des appelants devraient utiliser ReadBytes ('\ n') ou ReadString ('\ n') à la place ou utiliser un scanner."
mdwhatcott
12
@mdwhatcott pourquoi est-ce important que ce soit une "primitive de lecture de ligne de bas niveau"? Comment cela aboutit-il à la conclusion que «la plupart des appelants devraient plutôt utiliser ReadBytes ('\ n') ou ReadString ('\ n') ou utiliser un scanner."?
Charlie Parker
12
@CharlieParker - Pas sûr, juste en citant les documents pour ajouter du contexte.
mdwhatcott
11
De la même documentation .. "Si ReadString rencontre une erreur avant de trouver un délimiteur, il renvoie les données lues avant l'erreur et l'erreur elle-même (souvent io.EOF)." Ainsi, vous pouvez simplement vérifier l'erreur io.EOF et savoir que vous avez terminé.
eduncan911
1
Notez qu'une lecture ou une écriture peut échouer en raison d'un appel système interrompu, ce qui entraîne une lecture ou une écriture inférieure au nombre d'octets attendu.
Justin Swanhart
599

Dans Go 1.1 et plus récent, le moyen le plus simple de le faire est d'utiliser a bufio.Scanner. Voici un exemple simple qui lit les lignes d'un fichier:

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

C'est la façon la plus propre de lire d'une Readerligne à l'autre.

Il y a une mise en garde: le scanner ne gère pas bien les lignes de plus de 65 536 caractères. Si c'est un problème pour vous, alors vous devriez probablement lancer le vôtre Reader.Read().

Stefan Arentz
la source
40
Et puisque l'OP a demandé de numériser un fichier, il serait trivial de d'abord file, _ := os.Open("/path/to/file.csv")puis de numériser le descripteur de fichier:scanner := bufio.NewScanner(file)
Evan Plumlee
14
N'oubliez pas defer file.Close().
Kiril
13
Le problème est Scanner.Scan () est limité dans une taille de tampon de 4096 [] octets par ligne. Vous obtiendrez une bufio.ErrTooLongerreur, c'est-à-dire bufio.Scanner: token too longsi la ligne est trop longue. Dans ce cas, vous devrez utiliser bufio.ReaderLine () ou ReadString ().
eduncan911
5
Juste mon 0,02 $ - c'est la réponse la plus correcte sur la page :)
sethvargo
5
Vous pouvez configurer Scanner pour gérer des lignes encore plus longues en utilisant sa méthode Buffer (): golang.org/pkg/bufio/#Scanner.Buffer
Alex Robinson
78

Utilisation:

  • reader.ReadString('\n')
    • Si cela ne vous dérange pas que la ligne puisse être très longue (c'est-à-dire utiliser beaucoup de RAM). Il conserve le \nà la fin de la chaîne retournée.
  • reader.ReadLine()
    • Si vous vous souciez de limiter la consommation de RAM et ne vous occupez pas du travail supplémentaire de gestion du cas où la ligne est supérieure à la taille de la mémoire tampon du lecteur.

J'ai testé les différentes solutions proposées en écrivant un programme pour tester les scénarios identifiés comme problèmes dans d'autres réponses:

  • Un fichier avec une ligne de 4 Mo.
  • Un fichier qui ne se termine pas par un saut de ligne.

Je l'ai trouvé:

  • La Scannersolution ne gère pas les longues lignes.
  • La ReadLinesolution est complexe à mettre en œuvre.
  • La ReadStringsolution est la plus simple et fonctionne pour les longues lignes.

Voici le code qui illustre chaque solution, il peut être exécuté via 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]
}

J'ai testé sur:

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

Le programme de test produit:

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
la source
9
Le defer file.Close()devrait être après la vérification des erreurs; sinon, en cas d'erreur, cela paniquera.
mlg
La solution du scanner gère les longues files d'attente si vous la configurez ainsi. Voir: golang.org/pkg/bufio/#Scanner.Buffer
Inanc Gumus
Vous devriez vérifier l'erreur correctement comme vu dans les documents: play.golang.org/p/5CCPzVTSj6 ie if err == io.EOF {break} else {return err}
Chuque
53

EDIT: À partir de go1.1, la solution idiomatique est d'utiliser bufio.Scanner

J'ai écrit un moyen de lire facilement chaque ligne d'un fichier. La fonction Readln (* bufio.Reader) renvoie une ligne (sans \ n) à partir de la structure bufio.Reader sous-jacente.

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

Vous pouvez utiliser Readln pour lire chaque ligne d'un fichier. Le code suivant lit chaque ligne d'un fichier et envoie chaque ligne à 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)
}

À votre santé!

Malcolm
la source
14
J'ai écrit cette réponse avant la sortie de Go 1.1. Go 1.1 a un package Scanner dans le stdlib. qui fournit les mêmes fonctionnalités que ma réponse. Je recommanderais d'utiliser Scanner au lieu de ma réponse car Scanner est dans le stdlib. Bon piratage! :-)
Malcolm
30

Il existe deux façons courantes de lire le fichier ligne par ligne.

  1. Utilisez bufio.Scanner
  2. Utilisez ReadString / ReadBytes / ... dans bufio.Reader

Dans mon testcase, ~ 250 Mo, ~ 2 500 000 lignes , bufio.Scanner (temps utilisé: 0,395491384s) est plus rapide que bufio.Reader.ReadString (time_used: 0,446867622s).

Code source: https://github.com/xpzouying/go-practice/tree/master/read_file_line_by_line

Lire le fichier, utilisez 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
    }
}

Lire le fichier utilise 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
la source
Sachez que cet bufio.Readerexemple ne lira pas la dernière ligne d'un fichier s'il ne se termine pas par une nouvelle ligne. ReadStringretournera à la fois la dernière ligne et io.EOFdans ce cas.
konrad
18

Exemple de cet essentiel

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

mais cela donne une erreur lorsqu'il y a une ligne plus grande que la mémoire tampon du scanner.

Lorsque cela s'est produit, ce que je fais est d'utiliser la reader := bufio.NewReader(inFile)création et la concaténation de mon propre tampon en utilisant ch, err := reader.ReadByte()oulen, err := reader.Read(myBuffer)

Une autre façon que j'utilise (remplacez os.Stdin par un fichier comme ci-dessus), celle-ci concatène lorsque les lignes sont longues (isPrefix) et ignore les lignes vides:


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
la source
voulez-vous expliquer pourquoi -1?
Kokizzu
Je pense que c'est une solution un peu trop compliquée, n'est-ce pas?
Decebal
10

Vous pouvez également utiliser ReadString avec \ n comme séparateur:

  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
la source
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
la source
1

Dans le code ci-dessous, je lis les intérêts de la CLI jusqu'à ce que l'utilisateur frappe et j'utilise 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
la source
0

J'aime la solution Lzap, je suis nouveau dans Go, j'aimerais demander à lzap mais je n'ai pas pu le faire je n'ai pas encore 50 points .. Je change un peu votre solution et complète le code ...

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

Je ne sais pas pourquoi je dois tester à nouveau 'err', mais de toute façon nous pouvons le faire. Mais, la question principale est .. pourquoi Go ne produit pas d'erreur avec la phrase => ligne, err: = r.ReadString (10), à l'intérieur de la boucle? Il est défini encore et encore à chaque exécution de la boucle. J'évite cette situation avec mon changement, un commentaire? J'ai également défini la condition EOF dans le «pour» comme pour un While. Merci

Jose.mg
la source
0
import (
     "bufio"
     "os"
)

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

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

Voici un exemple avec la fonction à laquelle ReadFromStdin()il ressemble, fmt.Scan(&name)mais il prend toutes les chaînes avec des espaces vides comme: "Bonjour, mon nom est ..."

var name string = ReadFromStdin()

println(name)
0DAYanc
la source
0

Une autre méthode consiste à utiliser les bibliothèques io/ioutilet stringspour lire l'intégralité des octets du fichier, les convertir en chaîne et les diviser en utilisant un caractère " \n" (nouvelle ligne) comme délimiteur, par exemple:

import (
    "io/ioutil"
    "strings"
)

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

Techniquement, vous ne lisez pas le fichier ligne par ligne, mais vous pouvez analyser chaque ligne en utilisant cette technique. Cette méthode est applicable aux fichiers plus petits. Si vous essayez d'analyser un fichier volumineux, utilisez l'une des techniques de lecture ligne par ligne.

pythoner
la source