Чтение файла построчно в Go

335

Я не могу найти file.ReadLineфункцию в Go. Я могу понять, как быстро написать один, но мне просто интересно, что я здесь пропускаю. Как читать файл построчно?

g06lin
источник
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
Обратите внимание, что чтение или запись может завершиться ошибкой из-за прерванного системного вызова, что приводит к тому, что число читаемых или записываемых байтов меньше ожидаемого.
Джастин Суонхарт
599

В Go 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
И поскольку ОП попросил просканировать файл, сначала было бы тривиально, 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 МБ.
  • Файл, который не заканчивается переводом строки.

Я нашел это:

  • 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 версия go1.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()Должно быть после проверки ошибок; в противном случае при ошибке он будет паниковать.
млг
Решение для сканера обрабатывает длинные строки, если вы настроите его так. См. Golang.org/pkg/bufio/#Scanner.Buffer
Инан Гумус
Вы должны проверить ошибку должным образом, как показано в документах: 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 для чтения каждой строки из файла. Следующий код читает каждую строку в файле и выводит каждую строку в стандартный вывод.

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 вместо моего ответа, так как Scanner находится в stdlib. Счастливого взлома! :-)
Малькольм
30

Существует два распространенных способа чтения файла построчно.

  1. Используйте bufio.Scanner
  2. Используйте ReadString / ReadBytes / ... в bufio.Reader

В моем тестовом примере ~ 250 МБ, ~ 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
    }
}
zouying
источник
Имейте в 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)create и concat моего собственного буфера, используя 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
}
Kokizzu
источник
Не хочешь объяснить, почему -1?
Kokizzu
Я думаю, это слишком сложное решение, не так ли?
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
    }
  }
lzap
источник
5

bufio.Reader.ReadLine () работает хорошо. Но если вы хотите прочитать каждую строку по строке, попробуйте использовать ReadString ('\ n') . Не нужно изобретать велосипед.

kroisse
источник
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)
zuzuleinen
источник
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
    }
}

Я не уверен, почему мне нужно снова проверять 'err', но в любом случае мы можем это сделать. Но главный вопрос заключается в том, почему Go не выдает ошибку с строкой предложения =>, err: = r.ReadString (10) внутри цикла? Он определяется снова и снова каждый раз, когда выполняется цикл. Я избегаю этой ситуации с моим изменением, любой комментарий? Я установил условие EOF в 'for' так же, как и в то время как. Спасибо

Jose.mg
источник
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)
0DAYanc
источник
0

Другой способ заключается в использовании io/ioutilи stringsбиблиотек для чтения байт всего файла целиком, в преобразовать их в строку и разделить их с помощью « \n» (перевода строки) символа в качестве разделителя, например:

import (
    "io/ioutil"
    "strings"
)

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

Технически вы не читаете файл построчно, однако вы можете анализировать каждую строку, используя эту технику. Этот метод применим к файлам меньшего размера. Если вы пытаетесь проанализировать массивный файл, используйте один из методов, который читает построчно.

pythoner
источник