Gox語言中使用內(nèi)存虛擬文件系統(tǒng)的實例:Excel歸并CSV與分拆-GX43.2

我們使用內(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)存與硬盤的各司其職、各盡所長。

最后編輯于
?著作權歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務。

友情鏈接更多精彩內(nèi)容