Golang實(shí)現(xiàn)按行讀取文件的方法小結(jié)
引言
我們將要介紹的按行讀取文件的方式其實(shí)是非常適合處理超大文件。
按行讀取文件相較于一次性載入,有著很多優(yōu)勢,如內(nèi)存效率高、處理速度快、實(shí)時性高、可擴(kuò)展性強(qiáng)和靈活度高等,特別是當(dāng)遇到處理大文件時,這些優(yōu)勢會更加明顯。
稍微展開說下各個優(yōu)勢吧。
內(nèi)存效率高,因?yàn)槭前葱凶x取,處理完一行就會丟棄,內(nèi)存占用將大大減少。
處理速度快,主要體現(xiàn)在逐行處理時,因?yàn)闊o需等待全量數(shù)據(jù),能更快開始,而且如果無順序要求,還可并行計(jì)算以最大化利用計(jì)算資源,進(jìn)一步提升處理速度。
實(shí)時性高,因?yàn)榘葱凶x取,無需一次加載全量數(shù)據(jù),自然有 實(shí)時性高 的特點(diǎn),這對于處理實(shí)時流數(shù)據(jù),如日志數(shù)據(jù),非常有用。
可擴(kuò)展性強(qiáng),按行讀取這種方式,不僅僅適用于小文件,大文件同樣使用,有了統(tǒng)一的處理方式,即使未來數(shù)據(jù)量膨脹,也易于擴(kuò)展。
靈活度高,因?yàn)槭且恍行械奶幚?,如果想停止,隨時可以。如果繼續(xù)之前的流程,我們只要重新啟動,從之前的位置繼續(xù)處理即可。
按行讀取其實(shí)只是按塊讀取的一種特殊形式(分隔符是 \n),自然地,上述的優(yōu)勢也同樣適用于按塊讀取文件。
本文的重點(diǎn)在于如何使用 GO 實(shí)現(xiàn)按行讀取,基于的是標(biāo)準(zhǔn)庫的 bufio.Reader
和 bufio.Scanner
。
正式進(jìn)入主題吧。
準(zhǔn)備一個文本文件
我們先準(zhǔn)備一個文本文件 example.txt,內(nèi)容如下:
This post covers the Golang Interface. Let's dive into it. Duck Typing To understand Go's interfaces, it's crucial to grasp the Duck Typing concept. So, what's Duck Typing?
基于 bufio.Reader
Go 中的按行讀取文件,首先可通過 bufio
提供的 Reader
類型實(shí)現(xiàn)。
使用 Reader.ReadLine
Reader
中有一個名為 ReadLine
的方法,顧名思義,它的作用就是按行讀取文件的。
演示代碼:
file, err := os.Open("example.txt") if err != nil { panic(err) } defer func() { _ = file.Close() }() reader := bufio.NewReader(file) for { line, _, err := reader.ReadLine() // 按行讀取文件 if err == io.EOF { // 用于判斷文件是否讀取到結(jié)尾 break } if err != nil { panic(err) } fmt.Printf("%s\n", line) }
重點(diǎn)就是那句 line, _, err := reader.ReadLine()
,返回值的第一值是讀取的內(nèi)容,第三個值是錯誤信息。
執(zhí)行與輸出:
$ go run main.go
This post covers the Golang Interface. Let’s dive into it.
Duck Typing
To understand Go’s interfaces, it’s crucial to grasp the Duck Typing concept.
So, what’s Duck Typing?
和我們預(yù)期的一樣,輸出了完整的文本信息。
要提醒的是,ReadLine
讀取的內(nèi)容不包括行尾符(如 "\r\n" 或 "\n")。也就是說,當(dāng)讀取到一行數(shù)據(jù)時,要自行處理可能的行尾符差異,尤其是在處理來自不同操作系統(tǒng)的文本數(shù)據(jù)時。
還有,ReadLine
省略的第二個參數(shù),名為 isPrefix
,它表示是否是前綴的意思,如果 isPrefix
為 true 表示返回的 line
被截?cái)嗔?,而截?cái)嘣蚝芸赡苁切械膬?nèi)容大小大于緩沖區(qū)。我們可以在初始化時通過 bufio.NewReaderSize(rd io.Reader, size int)
調(diào)整默認(rèn)緩沖區(qū)大小。
不過,這并非最優(yōu)的解法。
使用 Reader.ReadString
解決大行讀取被截?cái)嗟膯栴},還可用 bufio.Reader
的另外一個方法 ReadString
解決。
它與 ReadLine
類似,不過在單個 buffer 不足以容納單行內(nèi)容時,它會多次讀取,直到找到目標(biāo)分割符,合并多次讀取的內(nèi)容。
示例代碼:
reader := bufio.NewReader(file) for { line, err := reader.ReadString('\n') if err == io.EOF { break } if err != nil { panic(err) } fmt.Printf("%s\n", line) }
重點(diǎn)就是那句 reader.ReadString('\n')
,它的入?yún)⑹欠指罘╠elim),即 '\n',而返回值分別讀取內(nèi)容(line)和錯誤(err)。
相較于 ReadLine
,ReadString
顯然是更加靈活,無大行讀取被截?cái)嗟膯栴},而且分割符也可自定義。但只支持單一字節(jié)的分割符自定義,還不夠完美,如我們想按多個字符(如 .|,
等等)分割文本,或者按照大小分塊讀取,就沒有那么方便了。
我們繼續(xù)引入另一個 Go 標(biāo)準(zhǔn)提供的按行讀取文件的方案,即 bufio.Scanner
。
使用 bufio.Scanner
為了由淺入深地介紹 bufio.Scanner
的使用,我們還是先從 bufio.Scanner
實(shí)現(xiàn)按行讀取講起吧。
一個示例代碼了解 bufio.Scanner
的基本使用。
// 創(chuàng)建文件的掃描器,用于逐行讀取文件 scanner := bufio.NewScanner(file) // 循環(huán),直到文件結(jié)束 for scanner.Scan() { // 處理每行的內(nèi)容:打印 fmt.Println(scanner.Text()) } // 最后,檢查掃描過程中是否有錯誤發(fā)生 if err := scanner.Err(); err != nil { panic(err) }
這個例子中,我們基于打開的文件描述符 file
,創(chuàng)建了一個 bufio.Scanner
變量 scanner
,它通過 scanner.Scan()
逐行掃描文件和 scanner.Text()
從 buffer 中獲取掃描內(nèi)容,直到結(jié)束。
毫無疑問,相對于 bufio.Reader
,以上通過 bufio.Scanner
實(shí)現(xiàn)的代碼簡潔很多,而且,錯誤處理也是集中在 for 循環(huán)完成后統(tǒng)一進(jìn)行。
如何讀取大行
bufio.Scanner
如何處理特別長的行呢?
默認(rèn)情況下,bufio.Scanner
初始緩沖區(qū)是 4KB,而最大 token 大小是 64KB,即無法處理超過 64KB 的行。
來自源碼中的定義,如下所示:
// `MaxScanTokenSize` 可定義 buffer 中 token 的最大 size, // 除非用戶通過 `Scanner.Buffer` 顯式修改 // 緩沖區(qū)初始大小和 token 最大 size, // 實(shí)際的最大標(biāo)記大小可能會更小,因?yàn)? // 緩沖區(qū)可能需要包含例如換行符之類的內(nèi)容。 MaxScanTokenSize = 64 * 1024 // 緩沖區(qū)的初始大小 startBufSize = 4096
bufio.Scanner
中提供了 Scanner.Buffer()
方法可用于調(diào)整默認(rèn)的緩沖區(qū)。
示例代碼:
const maxCapacity = 1024 * 1024 // 例如,1MB,可讀取任何 1MB 的行。 buf := make([]byte, maxCapacity) // 初始緩沖大小 1MB,無需多次擴(kuò)容 scanner.Buffer(buf, maxCapacity)
在 scanner
掃描前,加上這段代碼,會重新設(shè)置緩沖區(qū),將初始緩沖大小和最大容易都設(shè)置為 1MB,這樣就可以處理異常長的大行(size <= 1MB)了,而且由于初始緩沖區(qū)大小就是最大容量,也無需多次擴(kuò)容緩沖。
緩沖區(qū)邏輯
為了更好理解上面的緩沖區(qū)配置,我簡單介紹下 bufio.Scanner
是的 Scan
文件讀取邏輯以及緩沖區(qū)是如何用的。
bufio.Scanner
內(nèi)部有一個 s.buf
緩沖區(qū),當(dāng)我們調(diào)用 scannder.Scan
方法時,它會嘗試用 io.Reader
(即示例中的 file
文件描述符)中讀取一個緩存大小的內(nèi)容。它的具體實(shí)現(xiàn)是在 bufio.Scanner
的 Scan
方法中。如果當(dāng)緩沖區(qū)大小不足以容納一個完整的 token,Scanner 會自動增加緩沖區(qū)的大小。
接下來,讓我們實(shí)現(xiàn) bufio.Scanner
按單詞讀取。
擴(kuò)展思路
如果每次都讀取這么大塊的一整行,和一次載入沒有什么區(qū)別,這明顯已經(jīng)失去了開頭介紹的一行行讀取的優(yōu)勢了。
除了直接讀取整行,是否還有什么更好的方法處理大行呢?
我們可以嘗試解放一些思路,是否還有其他方式定義一次讀取內(nèi)容呢?我們只要保證讀取的內(nèi)容有實(shí)際含義即可,如按一句話,一個單詞或者固定的塊大小的切割,而非是糾結(jié)于是不是一整行。
分割規(guī)則定義
在正式介紹切割規(guī)則前,先說明下什么是完整 token。前面一直在說 token,如 MaxScanTokenSize
定義的就是 token 最大 size。
token 定義其實(shí)就是對一次讀取內(nèi)容的定義,如一行文本,一個單詞,或者一個固定大小的塊。相對于特定分隔符,分割規(guī)則更加靈活,可以定義任意的分割方式。
而 bufio.Scanner
是一個非常靈活的工具,它提供了自定義切割文本規(guī)則的函數(shù) - Scanner.Split
。
// 參數(shù) // data []byte: 未處理數(shù)據(jù)的初始子串,當(dāng)前需要處理的輸入數(shù)據(jù)。 // atEOF bool: 一個標(biāo)志,如果為 true,則表示沒有更多數(shù)據(jù)可處理。 // 返回值 // advance int: 需要在輸入中前進(jìn)多少以到達(dá)下一個標(biāo)記的起始位置。 // token []byte: 要返回給用戶的內(nèi)容(如果有)。 // err error: 掃描過程中遇到的錯誤。 type SplitFunc func(data []byte, atEOF bool) (advance int, token []byte, err error)
它的返回是分別讀取內(nèi)容的長度、讀取的內(nèi)容和錯誤信息。
默認(rèn)情況下,Scanner 按行分割(ScanLines)。
scanner.Split(bufio.ScanLines) // 默認(rèn)配置,按行讀取
我們可以通過自定義的 Split 函數(shù)改變這個默認(rèn)行為,如按單詞分割。
示例代碼:
const input = "This is a test. This is only a test." scanner := bufio.NewScanner(strings.NewReader(input)) // 設(shè)置分割函數(shù)為按單詞分割 scanner.Split(bufio.ScanWords) // 逐個讀取單詞 for scanner.Scan() { fmt.Println(scanner.Text()) } if err := scanner.Err(); err != nil { fmt.Fprintln(os.Stderr, "reading input:", err) }
輸出:
This
is
a
test.
This
is
only
a
test.
現(xiàn)在,無論多大的文件,我們都可以通過巧妙定義切割方式來避免一次性讀取的缺點(diǎn)了。
我之前利用 whispwer 識別油管視頻的字幕,有些視頻的內(nèi)容非常長,超長字幕,都在一行?,F(xiàn)在我就可以通過如句號、問號、感嘆號分割即可。現(xiàn)在,我要做的定義這樣一個 ScanSentences
函數(shù)。
示例代碼:
func ScanSentences(data []byte, atEOF bool) (advance int, token []byte, err error) { // 如果我們處于 EOF 并且有數(shù)據(jù),則返回剩余的數(shù)據(jù) if atEOF && len(data) > 0 { return len(data), data, nil } // 定義一個查找任意句子結(jié)束符的函數(shù) findSentenceEnd := func(data []byte) int { // 檢查每個可能的句子結(jié)束符 endIndex := -1 for _, sep := range []byte{'.', '?', '!'} { if i := bytes.IndexByte(data, sep); i >= 0 { // 選擇最小的 index 作為句子結(jié)尾 if i < endIndex || endIndex == -1 { endIndex = i } } } return endIndex } // 使用新的查找邏輯來查找句子結(jié)束位置 if i := findSentenceEnd(data); i >= 0 { // 返回找到的句子(包括句子結(jié)束符),以及下一個 token 的起始位置 return i + 1, data[:i+1], nil } return 0, nil, nil }
我們寫個 main
函數(shù)測試下 ScanSentences
的正確性吧。
示例代碼:
func main() { const input = "This is a test. This is only a test. Is this a test? \n" + "Wow, what a brilliant test! Thanks for your help." scanner := bufio.NewScanner(strings.NewReader(input)) scanner.Split(ScanSentences) for scanner.Scan() { text := scanner.Text() fmt.Printf("%s\n", strings.TrimSpace(text)) } if err := scanner.Err(); err != nil { panic(err) } }
執(zhí)行輸出:
$ go run main.go
This is a test.
This is only a test.
Is this a test?
Wow, what a brilliant test!
Thanks for your help.
或者按照固定大小分批讀取文件,SplitBatchSize
示例代碼:
// ScanBatchSize 返回一個 bufio.SplitFunc 函數(shù),該函數(shù)按照固定的大小分割數(shù)據(jù)。 // 如果數(shù)據(jù)大小不足一個完整的批次,并且已經(jīng)到達(dá) EOF,則返回剩余的數(shù)據(jù)。 func ScanBatchSize(batchSize int) bufio.SplitFunc { return func(data []byte, atEOF bool) (advance int, token []byte, err error) { // 如果數(shù)據(jù)大小達(dá)到或超過批次大小,或者在 EOF 時有剩余數(shù)據(jù) if len(data) >= batchSize || (atEOF && len(data) > 0) { // 如果當(dāng)前批次大小超過剩余數(shù)據(jù)大小,則只返回剩余數(shù)據(jù) if len(data) < batchSize { return len(data), data[:], nil } // 否則,返回一個完整批次的大小和數(shù)據(jù) return batchSize, data[:batchSize], nil } // 如果沒有足夠的數(shù)據(jù)并且沒有到達(dá) EOF,需要更多數(shù)據(jù)來形成一個完整的批次 if !atEOF { return 0, nil, nil } // 處理到達(dá) EOF 但沒有剩余數(shù)據(jù)的情況 return 0, nil, nil } }
SplitBatchSize
是一個閉包,它的返回值是我們期待的 SplitFunc
。我們可傳遞參數(shù)配置每次讀取內(nèi)容的大小。具體可自行測試,這里就演示了。
不得不說
到這里,我還是想再提一點(diǎn),每次從文件中讀取內(nèi)容大小是由傳入系統(tǒng)調(diào)用 read()
函數(shù)時傳入?yún)?shù) buf
大小決定的,而不是由所謂按行還是按塊確定的。按行按塊是基于讀取出來的二次處理的結(jié)果。
之所以要提這點(diǎn),因?yàn)槲抑翱吹揭恍┪恼抡f,按塊相比按行讀取減少了讀取的次數(shù)。
結(jié)論
本文詳細(xì)介紹了在 Go 中如何使用 bufio.Reader 和 bufio.Scanner 按行或按塊讀取文件,通過利用 GO 的標(biāo)準(zhǔn)庫能力,我們有了更加靈活、高效處理大型文本文件的策略。
以上就是Golang實(shí)現(xiàn)按行讀取文件的方法小結(jié)的詳細(xì)內(nèi)容,更多關(guān)于Go按行讀取文件的資料請關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
Go 并發(fā)編程Goroutine的實(shí)現(xiàn)示例
Go語言中的并發(fā)編程主要通過Goroutine和Channel來實(shí)現(xiàn),本文就來介紹一下Go 并發(fā)編程的實(shí)現(xiàn),文中通過示例代碼介紹的非常詳細(xì),對大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧2024-12-12一文帶你學(xué)會Go?select語句輕松實(shí)現(xiàn)高效并發(fā)
這篇文章主要為大家詳細(xì)介紹了Golang中select語句的用法,文中的示例代碼講解詳細(xì),對我們學(xué)習(xí)Golang有一定的幫助,需要的可以參考一下2023-03-03Golang中由零值和gob庫特性引起B(yǎng)UG解析
這篇文章主要為大家介紹了Golang中由零值和gob庫特性引起B(yǎng)UG解析,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪2023-04-04golang validator參數(shù)校驗(yàn)的實(shí)現(xiàn)
這篇文章主要介紹了golang validator參數(shù)校驗(yàn)的實(shí)現(xiàn),文中通過示例代碼介紹的非常詳細(xì),對大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧2020-10-10深度剖析Golang如何實(shí)現(xiàn)GC掃描對象
這篇文章主要為大家詳細(xì)介紹了Golang是如何實(shí)現(xiàn)GC掃描對象的,文中的示例代碼講解詳細(xì),具有一定的學(xué)習(xí)價(jià)值,需要的小伙伴可以參考一下2023-03-03