Home [Golang] 如何逐行讀取檔案,或使用shell pipe到go process
Post
Cancel

[Golang] 如何逐行讀取檔案,或使用shell pipe到go process

這篇將示範如何用go的bufio,開啟檔案並且一行一行地讀取資料, 最後示範直接用unix pipe將資料pipe給我們的go程式,做到像下面這樣的事:

1
cat words.txt | go run ./wc.go

bufio.Scanner

我們將使用bufio.Scanner來一行一行地讀取檔案內容,先看看會用到的function們的signature:

  • func NewScanner(r io.Reader) *Scanner
  • func (s *Scanner) Scan() bool
  • func (s *Scanner) Text() string

bufio之所以叫做bufio,就是因為他在io的基礎上,加上了buffer, 這邊我們使用的bufio.Scanner,一樣是從io.Reader裡Read資料出來,但會先將資料存在他的buffer裡面, 呼叫Text()可以拿到資料,呼叫Scan()則是叫他繼續讀取下一筆資料。

預設上,每一次Scan就是讀取一行,如果回傳值為false就代表已經沒有資料了, 除了一次讀取一行之外,還有其他的方式,但今天這裡就不介紹了~

範例程式

以下示範讀取一個檔案(words.txt),算出每一行的字出現幾次,並將結果打印至螢幕上:

words.txt:

apple
banana
orange
apple
apple
orange

wc.go:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
package main

import (
        "bufio"
        "fmt"
        "io"
        "log"
        "os"
)

func main() {
        file, err := os.Open("./words.txt")
        if err != nil {
                log.Fatalf("error when reading file with message: %v", err)
        }

        wc(file)
}

func wc(input io.Reader) {
        scanner := bufio.NewScanner(input)

        count := make(map[string]int)
        for scanner.Scan() {
                word := scanner.Text()

                if _, ok := count[word]; ok {
                        count[word] += 1
                } else {
                        count[word] = 1
                }
        }

        for word, n := range count {
                fmt.Printf("%s: %d\n", word, n)
        }
}

Shell Pipe

既然我們已經可以從檔案中一行一行讀取資料了,那要不要試試看這樣! 利用shell pipe從上一個程式的stdout中讀取資料。

1
cat words.txt | go run ./wc.go

想要達成上面的用法,相當簡單,只要把我們wc function的參數,從使用*os.File改成os.Stdin就行了~ 因為他們都有implement io.Reader,也就是main function變成如下:

1
2
3
func main() {
        wc(os.Stdin)
}

Separate IO from Logic

前面我們將wc算完的結果,直接fmt.Printf到stdout上,但有時如果我們想寫到檔案裡, 或是寫到network socket時,就不太方便。

wc應該只專注在word count一件事上就好, 不需要管input是從哪裡來的,就像前面我們使用*os.File後面用os.Stdin一樣, 也不用管output要寫到哪裡去,所以我們再將程式改寫如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
import (
        "os"
        "fmt"
        "io"
        "bufio"
)

func main() {
        wc(os.Stdin, os.Stdout)
}

func wc(input io.Reader, output io.Writer) {
        scanner := bufio.NewScanner(input)

        count := make(map[string]int)
        for scanner.Scan() {
                word := scanner.Text()

                if _, ok := count[word]; ok {
                        count[word] += 1
                } else {
                        count[word] = 1
                }
        }

        for word, n := range count {
                fmt.Fprintf(output, "%s: %d\n", word, n)
        }
}

wc就只管將結果寫到一個可以寫的地方,至於它是什麼地方,就由呼叫他的人來決定!

By the way, wc這名字取的不好,因為我們實現的是uniq -c這個command,而不是wc😄

Summary

感謝你的閱讀~ 今天學到了如何使用bufio.Scanner來一行一行地讀取檔案,另外也體會到了interface的強大, io.Reader及io.Writer是個很好的例子,並且利用os.Stdin就可以使用shell pipe ( | ), 跟其他linux command一起搭配完成任務!

最後,我也有寫一篇一樣功能的Node.js版本,有興趣的可以看一看:

Nodejs: 如何逐行讀取檔案,或使用Unix pipe與其他process互動

This post is licensed under CC BY 4.0 by the author.

104人力網站爬蟲: 如何只用shell script來抓取資料

密碼學是什麼? 有何用途以及要解決什麼樣的問題?

Comments powered by Disqus.