我們使用內(nèi)存虛擬文件系統(tǒng)的目的主要有兩方面:一是為了提升一些比較頻繁的文件讀寫操作的速度,二是因為頻繁的文件讀寫操作一定程度上對硬盤等物理介質(zhì)有可能造成比較大的損耗,超出一定范疇還可能造成損傷。而內(nèi)存的特點是速度快,并且更適應高頻次的讀寫,唯一的缺點是相對容量較小,但隨著現(xiàn)在內(nèi)存動輒達到16G或更高,對于一般的文件處理已經(jīng)具備了用內(nèi)存來進行的條件。
下面的例子,就是在實際工作中遇到的問題,問題場景是這樣的: 某企業(yè)多少年所有的銷售記錄都保存在一堆Excel文件中,這些Excel文件有幾十個,每個當中包含時間跨度不等的該企業(yè)所有銷售記錄,并且包含所有產(chǎn)品,雖然有產(chǎn)品序號可供標識,但并沒有按產(chǎn)品拆分開?,F(xiàn)在要對其進行整理,希望達到的目標有兩個,第一是希望對數(shù)據(jù)進行匯總,第二是希望按產(chǎn)品序號對銷售數(shù)據(jù)進行拆分,以便以后查詢和統(tǒng)計。
對問題和具體情況進行進一步分析可以看出,整體數(shù)據(jù)量達到了近2個G,匯總和拆分工作用Excel當然也可以直接做,但Excel屬于重量級軟件,本身就慢,疊加數(shù)據(jù)量這么大以后就更難以接受,另外手工處理數(shù)十個大文件也不太好辦。而數(shù)據(jù)量雖大,但并沒有達到我們內(nèi)存無法承載的程度,畢竟內(nèi)存8個G以上現(xiàn)在已經(jīng)基本成了標配。
Gox語言自帶的內(nèi)存虛擬文件系統(tǒng)包,好處之一就是不像一般的RamDisk軟件那樣,需要事先劃分出固定大小的一塊內(nèi)存作為虛擬文件系統(tǒng)(虛擬硬盤),而是可以隨著用量自行擴展占用的內(nèi)存。
因此,我們可以這樣設計:使用內(nèi)存文件系統(tǒng)先將幾十個Excel文件歸并成一個大的CSV文件實現(xiàn)銷售數(shù)據(jù)的匯總,然后再將其按產(chǎn)品序號拆分出一個個的小CSV文件,每個文件對應一個產(chǎn)品的銷售記錄。CSV文件的好處是,其實質(zhì)其實是以行為基礎的純文本文件,因此用計算機程序處理非常方便,但也可以直接用Excel直接打開進行靈活的排序、篩選等處理。
下面第一段代碼就實現(xiàn)了將幾十個Excel文件歸并為一個大的CSV文件。
// 給github.com/360EntSecGroupSkylar/excelize起一個簡稱為excel
excel = github_360EntSecGroupSkylar_excelize
// 給標準庫包path/filepath起一個簡稱
filepath = path_filepath
// 設置文件操作的根目錄,用于保存最后生成的匯總文件
basePathG = `d:\tmpx\sp`
// 從命令行獲取要處理的所有Excel文件所在的文件夾路徑
pathT = getParameter(argsG, 1, `D:\share\銷售數(shù)據(jù)20200701`)
// 生成該目錄下所有的Excel文件的列表
fileListT = tk.GenerateFileListRecursively(pathT, "*.XLSX", false)
// 新建一個內(nèi)存虛擬文件系統(tǒng)
mfs = memfs.NewMemFS()
// 在內(nèi)存文件系統(tǒng)中創(chuàng)建根目錄下的output.csv文件
of, err = mfs.Create("/output.csv")
// 檢查創(chuàng)建文件過程中是否出錯,如果有錯則輸出信息后退出
checkError(err)
// 新建一個CSV文件的writer(寫入器對象)
// 用于寫入?yún)R總的銷售數(shù)據(jù)
w = encoding_csv.NewWriter(of)
// 循環(huán)遍歷所有Excel文件進行處理
for i, v = range fileListT {
// 如果文件名以“~$”開頭,說明是Excel的臨時文件,跳過
if tk.Contains(v, "~$") {
continue
}
// 輸出正在處理第幾行的信息
pl("processing [%v/%v] %v ...", i+1, len(fileListT), v)
// 逐個打開包含銷售數(shù)據(jù)的Excel文件
f, errT = excel.OpenFile(v)
checkError(errT)
// 獲取第一個表所有行列的數(shù)據(jù)
// 結果是一個[][]string類型的二維數(shù)組
rowsT, errT = f.GetRows(f.GetSheetName(0))
checkError(errT)
// 因為所有Excel文件內(nèi)格式都一樣,并且第一行都是表頭
// 因此,只有第一個文件才將表頭(第一行)寫入?yún)R總CSV文件中
// 后面的文件都將跳過第一行,只寫入后面的行
if i == 0 {
w.WriteAll(rowsT)
} else {
w.WriteAll(rowsT[1:])
}
// WriteAll函數(shù)會自己進行Flush操作,所以無需再手動刷新緩存
// 檢查并處理寫入中可能發(fā)生的錯誤
errT = w.Error()
checkErrf("failed to write output csv file: %v", errT)
}
// 確保要關閉輸出文件
of.Close()
// 將內(nèi)存文件中的輸出文件拷貝到真實文件系統(tǒng)中
// 否則程序退出后,內(nèi)存文件系統(tǒng)將不復存在
// 其中的文件當然也會丟失
// CopyFileTo函數(shù)中加入“-force”參數(shù)表示文件存在的話會覆蓋
err = mfs.CopyFileTo(`/output.csv`, path_filepath.Join(basePathG, `saleOutput.csv`), "-force")
plv(err)
// pass函數(shù)一般用于腳本正常結束,保證不輸出多余信息
// 如果是單腳本執(zhí)行,也可以不寫這一句
pass()
這樣,我們就實現(xiàn)了所有銷售數(shù)據(jù)歸并到了一個大的CSV文件中。注意,所有原始的Excel文件格式要完全一致才能如此操作。
下面我們來以匯總的CSV文件為基礎,將其按產(chǎn)品序號拆分成每個產(chǎn)品一個小CSV文件。每個拆分后的CSV文件將以該產(chǎn)品序號作為名稱,再加上“.txt”后綴。
// 設置path/filepath包的簡稱
filepath = path_filepath
// 設置數(shù)據(jù)文件所在的根目錄
basePathG = `d:\tmpx\sp`
// 設置輸出的小CSV文件的目錄
saleDataPathT = filepath.Join(basePathG, "saleData")
// 確保該目錄存在,如果沒有則創(chuàng)建它
tk.EnsureMakeDirs(saleDataPathT)
// 新建內(nèi)存虛擬文件系統(tǒng)
mfs = memfs.NewMemFS()
// 在虛擬文件系統(tǒng)中確保創(chuàng)建根目錄
mfs.EnsureMakeDirs("/")
// 打開上個例子中輸出的匯總文件以備讀取
f, err = os.Open(filepath.Join(basePathG, `saleOutput.csv`))
// 確保之后關閉該文件(defer后的函數(shù)將在本函數(shù)或腳本退出時被執(zhí)行)
defer f.Close()
// 新建一個CSV的讀取器對象用于從匯總文件中讀取一條條的記錄
r = encoding_csv.NewReader(f)
// 將序號清零
i = 0
// AppendLineToFile函數(shù)用于向輸出文件中增加一行銷售記錄
// 參數(shù)中l(wèi)ineA表示要增加的銷售記錄,類型應為[]string,即字符串數(shù)組
// 每一項是一個字段的數(shù)值
// fileA是要追加寫入的文件名稱
func AppendLineToFile(lineA, fileA) {
// 在虛擬文件系統(tǒng)中打開要寫入的產(chǎn)品銷售數(shù)據(jù)文件
// 如果不存在,則創(chuàng)建它
fileT, err := mfs.OpenFile(fileA, os.O_RDWR|os.O_CREATE|os.O_APPEND, 0666)
checkErrf("failed to open file: %v", err)
defer fileT.Close()
// 新建一個寫入器對象并寫入該條記錄
writerT := encoding_csv.NewWriter(fileT)
writerT.Write(lineA)
writerT.Flush() // 由于Write函數(shù)并不自帶Flush,所以要手動進行
}
// 無限循環(huán)(因為不能預知文件有多少行,因此只能文件全部讀取完后退出)
for true {
// 讀取一行
line, err = r.Read()
// 將序號遞增
i ++
// 如果到了文件結尾(文件讀完),則中止循環(huán)
if err == io.EOF {
break
}
// 如果是其他錯誤,則退出程序執(zhí)行
checkErrf("failed to read content: %v, line: %v", err, line)
// 第一行是表頭,跳過
if i == 1 {
continue
}
// 匯總銷售記錄中的第4個字段(字段序號是3)代表了該記錄的產(chǎn)品序號
// 因此,將該條記錄添加到以此為名稱的拆分銷售數(shù)據(jù)文件中去
AppendLineToFile(line, `/`+line[3]+`.txt`)
}
// 最后將虛擬文件系統(tǒng)中的所有拆分銷售數(shù)據(jù)文件拷貝到真實文件系統(tǒng)中
// CopyFilesTo用于將內(nèi)存虛擬文件系統(tǒng)中某個目錄下所有文件(包含子目錄下的)拷貝到真實文件系統(tǒng)中
// 第一個參數(shù)是指定的源目錄,第二個參數(shù)是文件pattern(“*”代表所有文件),第三個參數(shù)是目標路徑,-force參數(shù)表示如果已存在某文件則覆蓋寫入
plvsr(mfs.CopyFilesTo("/", "*", saleDataPathT, "-force"))
至此,整個任務順利完成。實際工作中,匯總文件總數(shù)據(jù)達到了2G,銷售數(shù)據(jù)記錄條數(shù)達到了3000多萬條,拆分文件個數(shù)達到了3萬多個。這個要用真實文件系統(tǒng)來做,估計很多開發(fā)者會心疼自己的硬盤,畢竟3000多萬條記錄意味著最終文件系統(tǒng)讀取和寫入次數(shù)就有至少6000多萬次(實際上還不止,還有一些其他開銷)。而用內(nèi)存操作,用通俗的話說,“它就是干這個的”,內(nèi)存也確實就是適合高頻次的讀寫操作的。最后,將內(nèi)存中的文件復制到真實文件系統(tǒng)中,僅需一次操作,損耗達到了最小,也實現(xiàn)了數(shù)據(jù)的持久化,避免了內(nèi)存斷電后數(shù)據(jù)丟失,完美實現(xiàn)了內(nèi)存與硬盤的各司其職、各盡所長。