Go語言從INI組態檔中讀取需要的值

2020-07-16 10:05:17
INI 檔案是 Initialization File 的縮寫,即初始化檔案,是 Windows 的系統組態檔所採用的儲存格式,統管 Windows 的各項設定。INI 檔案格式由節(section)和鍵(key)構成,一般用於作業系統、虛幻遊戲引擎、GIT 版本管理中,這種組態檔的副檔名為.ini

下面是從 GIT 版本管理的組態檔中擷取的一部分內容,展示 INI 檔案的樣式。

[core]
repositoryformatversion = 0

filemode = false
bare = false
logallrefupdates = true
symlinks = false
ignorecase = true
hideDotFiles = dotGitOnly
[remote "origin"]
url = https://github.com/davyxu/cellnet
fetch = +refs/heads/*:refs/remotes/origin/*
[branch "master"]
remote = origin
merge = refs/heads/master

INI 檔案的格式

INI 檔案由多行文字組成,整個設定由[ ]拆分為多個“段”(section)。每個段中又以分割為“鍵”和“值”。

INI 檔案以;置於行首視為註釋,注釋後將不會被處理和識別,如下所示:

[sectionl]
key1=value1
;key2=value2
[section2]

從 INI 檔案中取值的函數

熟悉了 INI 檔案的格式後,下面我們建立一個 example.ini 檔案,並將從 GIT 版本管理組態檔中擷取的一部分內容複製到該檔案中。

準備好 example.ini 檔案後,下面我們開始嘗試讀取該 INI 檔案,並從檔案中獲取需要的資料,完整的範例程式碼如下所示:
package main

import (
    "bufio"
    "fmt"
    "os"
    "strings"
)

// 根據檔名,段名,鍵名獲取ini的值
func getValue(filename, expectSection, expectKey string) string {
    // 開啟檔案
    file, err := os.Open(filename)
    // 檔案找不到,返回空
    if err != nil {
        return ""
    }
    // 在函數結束時,關閉檔案
    defer file.Close()
    // 使用讀取器讀取檔案
    reader := bufio.NewReader(file)
    // 當前讀取的段的名字
    var sectionName string
    for {
        // 讀取檔案的一行
        linestr, err := reader.ReadString('n')
        if err != nil {
            break
        }
        // 切掉行的左右兩邊的空白字元
        linestr = strings.TrimSpace(linestr)
        // 忽略空行
        if linestr == "" {
            continue
        }
        // 忽略註釋
        if linestr[0] == ';' {
            continue
        }
        // 行首和尾巴分別是方括號的,說明是段標記的起止符
        if linestr[0] == '[' && linestr[len(linestr)-1] == ']' {
            // 將段名取出
            sectionName = linestr[1 : len(linestr)-1]
            // 這個段是希望讀取的
        } else if sectionName == expectSection {
            // 切開等號分割的鍵值對
            pair := strings.Split(linestr, "=")
            // 保證切開只有1個等號分割的簡直情況
            if len(pair) == 2 {
                // 去掉鍵的多餘空白字元
                key := strings.TrimSpace(pair[0])
                // 是期望的鍵
                if key == expectKey {
                    // 返回去掉空白字元的值
                    return strings.TrimSpace(pair[1])
                }
            }
        }
    }
    return ""
}

func main() {
    fmt.Println(getValue("example.ini", "remote "origin"", "fetch"))
    fmt.Println(getValue("example.ini", "core", "hideDotFiles"))
}
本例並不是將整個 INI 檔案讀取儲存後再獲取需要的欄位資料並返回,這裡使用 getValue() 函數,每次從指定檔案中找到需要的段(Section)及鍵(Key)對應的值。

getValue() 函數的宣告如下:

func getValue(filename, expectSection, expectKey string) string

引數說明如下。
  • filename:INI 檔案的檔名。
  • expectSection:期望讀取的段。
  • expectKey:期望讀取段中的鍵。

getValue() 函數的實際使用例子參考程式碼如下:

func main() {
    fmt.Println(getValue("example.ini", "remote "origin"", "fetch"))
    fmt.Println(getValue("example.ini", "core", "hideDotFiles"))
}

執行上面的範例程式,輸出結果如下:

+refs/heads/*:refs/remotes/origin/*
dotGitOnly

輸出內容中“+refs/heads/*:refs/remotes/origin/*”表示 INI 檔案中[remote "origin"]的 "fetch" 鍵對應的值;dotGitOnly 表示 INI 檔案中[core]中鍵名為 "hideDotFiles" 的值。

注意 main 函數的第 2 行中,由於段名中包含雙引號,所以使用進行跳脫。

getValue() 函數的邏輯由 4 部分組成:即讀取檔案、讀取行文字、讀取段和讀取鍵值組成。接下來分步驟了解 getValue() 函數的詳細處理過程。

讀取檔案

Go語言的 OS 包中提供了檔案開啟函數 os.Open(),檔案讀取完成後需要及時關閉,否則檔案會發生占用,系統無法釋放緩衝資源。參考下面程式碼:
// 開啟檔案
file, err := os.Open(filename)

// 檔案找不到,返回空
if err != nil {
    return ""
}

// 在函數結束時,關閉檔案
defer file.Close()
程式碼說明如下:
  • 第 2 行,filename 是由 getValue() 函數引數提供的 INI 的檔名。使用 os.Open() 函數開啟檔案,如果成功開啟,會返回檔案控制代碼,同時返回開啟檔案時可能發生的錯誤:err。
  • 第 5 行,如果檔案開啟錯誤,err 將不為 nil,此時 getValue() 函數返回一個空的字串,表示無法從給定的 INI 檔案中獲取到需要的值。
  • 第 10 行,使用 defer 延遲執行函數,defer 並不會在這一行執行,而是延遲在任何一個 getValue() 函數的返回點,也就是函數退出的地方執行。呼叫 file.Close() 函數後,開啟的檔案就會被關閉並釋放系統資源。

INI 檔案已經開啟了,接下來就可以開始讀取 INI 的資料了。

讀取行文字

INI 檔案的格式是由多行文字組成,因此需要構造一個迴圈,不斷地讀取 INI 檔案的所有行。Go語言總是將檔案以二進位制格式開啟,通過不同的讀取方式對二進位制檔案進行操作。Go語言對二進位制讀取有專門的程式碼,bufio 包即可以方便地以比較常見的方式讀取二進位制檔案。
// 使用讀取器讀取檔案
reader := bufio.NewReader(file)

// 當前讀取的段的名字
var sectionName string

for {

    // 讀取檔案的一行
    linestr, err := reader.ReadString('n')
    if err != nil {
        break
    }

    // 切掉行的左右兩邊的空白字元
    linestr = strings.TrimSpace(linestr)

    // 忽略空行
    if linestr == "" {
        continue
    }

    // 忽略註釋
    if linestr[0] == ';' {
        continue
    }

    //讀取段和鍵值的程式碼
    //...
}
程式碼說明如下:
  • 第 2 行,使用 bufio 包提供的 NewReader() 函數,傳入檔案並構造一個讀取器。
  • 第 5 行,提前宣告段的名字字串,方便後面的段和鍵值讀取。
  • 第 7 行,構建一個讀取迴圈,不斷地讀取檔案中的每一行。
  • 第 10 行,使用 reader.ReadString() 從檔案中讀取字串,直到碰到n,也就是行結束。這個函數返回讀取到的行字串(包括n)和可能的讀取錯誤 err,例如檔案讀取完畢。
  • 第 16 行,每一行的文字可能會在識別符號兩邊混雜有空格、回車符、換行符等不可見的空白字元,使用 strings.TrimSpace() 可以去掉這些空白字元。
  • 第 19 行,可能存在空行的情況,繼續讀取下一行,忽略空行。
  • 第 24 行,當行首的字元為;分號時,表示這一行是注釋行,忽略一整行的讀取。

讀取 INI 文字檔案時,需要注意各種異常情況。文字中的空白符就是經常容易忽略的部分,空白符在偵錯時完全不可見,需要列印出字元的 ASCII 碼才能辨別。

拋開各種異常情況拿到了每行的行文字 linestr 後,就可以方便地讀取 INI 檔案的段和鍵值了。

讀取段

行字串 linestr 已經去除了空白字串,段的起止符又以[開頭,以]結尾,因此可以直接判斷行首和行尾的字串匹配段的起止符匹配時讀取的是段,如下圖所示。

INI 文件的段名解析
圖:INI 檔案的段名解析