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