Читання файлів рядок за рядком у Go


334

Я не можу знайти file.ReadLineфункцію в Go. Я можу зрозуміти, як швидко написати це, але мені просто цікаво, чи я щось тут не помічаю. Як читати файл за рядком?


7
Станом на Go1.1, bufio.Scanner - найкращий спосіб зробити це.
Малькольм

Відповіді:


133

ПРИМІТКА . Прийнята відповідь була правильною у ранніх версіях Go. Дивіться найвищу відповідь містить більш пізній ідіоматичний спосіб досягти цього.

У пакеті є функція ReadLinebufio .

Зауважте, що якщо рядок не вписується в буфер читання, функція поверне неповний рядок. Якщо ви хочете завжди читати цілий рядок у вашій програмі одним викликом функції, вам потрібно буде інкапсулювати цю ReadLineфункцію у свою функцію, яка викликає ReadLineв циклі for.

bufio.ReadString('\n')не повністю еквівалентна ReadLineтому ReadString, що не в змозі обробити випадок, коли останній рядок файлу не закінчується символом нової лінії.


37
З документів: "ReadLine - це примітивний примитив для читання рядків. Більшість абонентів повинні замість цього використовувати ReadBytes ('\ n') або ReadString ('\ n') або використовувати сканер."
mdwhatcott

12
@mdwhatcott, чому це важливо, що це "примітивне читання рядків з низьким рівнем"? Як це доходить висновку, що "Більшість абонентів повинні використовувати ReadBytes ('\ n') або ReadString ('\ n') замість цього або використовувати сканер."
Чарлі Паркер

12
@CharlieParker - Не впевнений, просто цитуючи документи, щоб додати контекст.
mdwhatcott

11
З тих самих документів .. "Якщо ReadString зіштовхується з помилкою, перш ніж знайти роздільник, він повертає прочитані дані перед помилкою та самою помилкою (часто io.EOF)." Тож ви можете просто перевірити наявність помилки io.EOF і знати, що все зроблено.
eduncan911

1
Зауважте, що читання чи запис може не вдатися через перерваний системний виклик, що призводить до меншої, ніж очікуваної кількості байтів, прочитаних або записаних.
Джастін Сванхарт

598

У версії 1.1 та новіших, найпростіший спосіб зробити це за допомогою a 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
А оскільки ОП попросило сканувати файл, спершу було б тривіально, file, _ := os.Open("/path/to/file.csv")а потім сканувати через файловий файл:scanner := bufio.NewScanner(file)
Еван Плюмлі

14
Не забувайте defer file.Close().
Кирило

13
Проблема полягає в тому, що Scanner.Scan () обмежений розміром буфера 4096 [] байтів на рядок. Ви отримаєте bufio.ErrTooLongпомилку, яка є, bufio.Scanner: token too longякщо лінія занадто довга. У такому випадку вам доведеться використовувати bufio.ReaderLine () або ReadString ().
eduncan911

5
Тільки мій 0,02 долара - це найправильніша відповідь на сторінці :)
sethvargo

5
Ви можете налаштувати Сканер для обробки ще довших ліній, використовуючи метод Buffer (): golang.org/pkg/bufio/#Scanner.Buffer
Алекс Робінсон

78

Використання:

  • reader.ReadString('\n')
    • Якщо ви не заперечуєте, що лінія може бути дуже довгою (тобто використовувати багато оперативної пам’яті). Він зберігає \nповернення в кінці рядка.
  • reader.ReadLine()
    • Якщо ви турбуєтесь про обмеження споживання оперативної пам’яті та не заперечуєте над додатковою роботою по обробці випадку, коли лінія перевищує розмір буфера для читання.

Я перевірив різні рішення, запропоновані, написавши програму для тестування сценаріїв, які в інших відповідях визначені як проблеми:

  • Файл із рядком 4 Мб.
  • Файл, який не закінчується розривом рядка.

Я виявив, що:

  • The 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 версія 1.7 windows / amd64
  • go версія go1.6.3 linux / amd64
  • go версія 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, тобто якщо помилка == io.EOF {break} else {return err}
Чук

53

EDIT: Станом на 1.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 є пакет Scanner у stdlib. що забезпечує той же функціонал, що і моя відповідь. Я б рекомендував використовувати Сканер замість моєї відповіді, оскільки Scanner знаходиться у stdlib. Щасливий злом! :-)
Малькольм

30

Існує два поширених способу читання файлів рядок.

  1. Використовуйте bufio.Scanner
  2. Використовуйте ReadString / ReadBytes / ... в bufio.Reader

У моїй тестовій шафі ~ 250 Мб, ~ 2 500 000 рядків , bufio.Scanner (використано час: 0,395491384) швидше, ніж bufio.Reader.ReadString (час_використання: 0,446867622).

Вихідний код: 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в цьому випадку.
конрад

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 на файл, як вище), цей підкреслює, коли рядки довгі (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?
Кокідзу

Я думаю, що це трохи більше ускладнює це рішення, чи не так?
Decebal

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" не створює помилки з рядком речення =>, помилка: = r.ReadString (10), всередині циклу? Він визначається знову і знову щоразу, коли цикл виконується. Я уникаю такої ситуації зі своєю зміною, будь-яким коментарем? Я встановив умову EOF у "для", як аналогічний в той час як. Дякую


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)але вона займає всі рядки з порожніми пробілами на кшталт: "Привіт, мене звуть ..."

var name string = ReadFromStdin()

println(name)
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.