0%

配置文件格式

简介

  • 不管是移动应用,桌面程序还是后台服务,经常需要从配置文件中读取配置信息,进行程序初始化和改变运行时的状态。以什么样的格式来存储配置信息,这是开发人员需要面临的一个文件。
  • 常用的配置文件格式主要用:
    • 键值对
    • JSON
    • XML
    • YAML
    • TOML

键值对

  • 键值对是一个非常简单易用的配置文件格式。每一个键值对表示一项配置,键值对的分隔符一般使用等号或者冒号。解析时,可以将#号开始的行视为注释行,以达到注释的功能
  • 以键值对为表现形式的配置文件格式常见的用Windows .ini文件和Java中的.properties文件
  • 例如下面是一个使用键值对表示的后台服务配置。
    1
    2
    3
    4
    5
    6
    7
    8
    9
    # This is a comment
    name=UserProfileServer
    maxconns=1000
    queuecap=10000
    queuetimeout=300
    loglevel=ERROR
    logsize=10M
    lognum=10
    logpath=/usr/local/app/log
  • 在解析上面的配置时,可以按行读取,然后放到 map 中。下面以 Go 为例,完成对上面配置文件的解析。
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    package main

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

    func ParseConf(confPath string) (map[string]string, error) {
    if confPath == ""{
    return nil, errors.New("param is ill")
    }

    f, err := os.Open(confPath)
    if err != nil {
    return nil, err
    }
    defer f.Close()

    //store config info
    m := make(map[string]string)

    bfRd := bufio.NewReader(f)
    //read by line, the line terminator is '\n'
    for{
    line, err := bfRd.ReadString('\n')
    if err == nil || err == io.EOF{
    //ignore blank and comment line
    if strings.TrimSpace(line) != "" && strings.TrimSpace(line)[0] != '#'{
    vKV := strings.Split(line, "=")
    if len(vKV) == 2 {
    m[vKV[0]] = vKV[1]
    }
    }
    if err == io.EOF{
    return m, nil
    }
    } else {
    return nil, err
    }
    }
    return m, nil
    }

    func main(){
    mConf, _ := ParseConf("server.kv")
    fmt.Println(mConf)
    }
  • 运行结果:
    1
    2
    3
    4
    5
    6
    7
    8
    9
    map[loglevel:ERROR
    lognum:10
    logpath:/usr/local/app/log
    logsize:10M
    maxconns:1000
    name:UserProfileServer
    queuecap:10000
    queuetimeout:300
    ]

JSON

  • JSON(JavaScript Object Notion),是轻量级的文本数据交换格式,独立于语言,具有自我描述性。JSON类似于XML,但是比XML更小,更快,更易解析

  • JSON语法是JavaScript对象表示法语法的子集:

    • 数据在名称/值对中
    • 数据由逗号分隔
    • 花括号保存对象
    • 方括号保存数组
  • 名称/值包括字段名称(在双引号中),后面写一个冒号,然后是值

    1
    "firstName": "John"
  • 一个合法的JSON可以是:

    • 数字(整数或者浮点数)
    • 字符串(在双引号中)
    • 布尔(true或者 false)
    • 数组(在方括号中)
    • 对象(在花括号中)
    • null
  • 下面以 JSON 表示一个简单的后台服务配置:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    {
    "-name": "UserProfileServer",
    "maxconns": "1000",
    "queuecap": "10000",
    "queuetimeout": "300",
    "loginfo": {
    "loglevel": "ERROR",
    "logsize": "10M",
    "lognum": "10",
    "logpath": "/usr/local/app/log"
    }
    }
  • 其中 -name 表示服务的名称,前面一个横杠表示该值可以转换为 XML 的标签属性。其它的键值对表示服务的各个配置项

XML

  • XML(Extensible Markup Language)是可扩展标记语言,用来传输和存储数据。因为其允许用户自定义标记名称,具有自我描述性,可灵活地用于存储服务配置信息

  • XML文档结构是一种树结构,它从根部开始,然后扩展到枝叶。XML文档必须有一个唯一的根节点,根节点包含所有其他节点。所有节点均可拥有文本内容和属性(名称/值的对)。XML节点也叫做XML元素

YAML

  • YAML(YAML Ain’t a Markup Language)是专门用来写配置文件的语言,简洁强大,相比于JSON和XML,更加便于开发人员读写。

  • YAML配置文件后缀为.yml或者.yaml

  • YAML 的基本语法规则如下:

    • 数据结构采用键值对的形式 key: value。
    • 键冒号后面要加空格(一般为 1 个空格)。
    • 字母大小写敏感。
    • 使用缩进表示层级关系。
    • 缩进只允许使用空格,不允许使用 Tab 键。
    • 缩进空格数可以任意,只要相同层级的元素左侧对齐即可。
    • 字符串值一般不使用引号,必要时可使用。使用双引号表示字符串时,会转义字符串中的特殊字符(例如\n)。使用单引号时不会转义字符串中的特殊字符。
    • 数组中的每个元素单独一行,并以 - 开头。或使用方括号,元素用逗号隔开。注意短横杆和逗号后面都要有空格。
    • 对象中的每个成员单独一行,使用键值对形式。或者使用大括号并用逗号分开。
    • 文档以三个连字符—表示开始,以三个点号…表示结束,二者都是可选的。
    • 文档前面可能会有指令,在这种情况下,需要使用—来表示指令的结束。指令是一个%后跟一个标识符和一些形参。
    • 目前只有两个指令:%YAML指定文档的 YAML 版本,%TAG用于 tag 简写。二者都很少使用。
    • #表示注释,从这个字符一直到行尾,都会被解析器忽略。
  • YAML支持的数据结构有三种:

    • 对象:键值对的集合,又称为映射(mapping),散列(hashes),字典(dictionary)
    • 数组:一组按次序排列的值,又称为序列(sequence),列表(list)
    • 标量:单个不可再分的值
  • 对象

    • 对象的一组键值对,使用冒号结构表示
      1
      name: Steve
    • YAML也允许另一种写法,将所有键值对写成一个行内对象
      1
      who: {name: Steve, age: 18}
    • 当然,如果对象元素太多一行放不下,那么可以换行
      1
      2
      3
      who:
      name: Steve
      age: 18
  • 数组

    • 一组以连字符开头的行,构成一个数组。注意,连字符后需要添加空格
      1
      2
      3
      4
      animals:
      - Cat
      - Dog
      - Goldfish
    • 连字符前可以没有缩进,也就是说下面的这种写法也是OK的,但是还是建议缩进,因为更加易读
      1
      2
      3
      4
      animals:
      - Cat
      - Dog
      - Goldfish
    • 数组也可以采用行内表示法
      1
      animal: [Cat, Dog, Goldfish]
    • 如果数组元素是一个数组,则可以在连字符下面再缩进输入一个数组
      1
      2
      3
      4
      5
      6
      7
      animals:
      -
      - Cat
      - Dog
      -
      - Fish
      - Goldfish
    • 如果是行内表示,则为:
      1
      animals: [[Cat, Dog], [Fish, Goldfish]]
    • 如果数组元素是一个对象,可以写作:
      1
      2
      3
      4
      5
      6
      animals:
      - species: dog
      name: foo
      - species: cat
      name: bar

    • 对应的 JSON 为:
      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      {
      "animals": [
      {
      "species": "dog",
      "name": "foo"
      },
      {
      "species": "cat",
      "name": "bar"
      }
      ]
      }
    • 对象和数组可以结合使用,形成复合结构。
      1
      2
      3
      4
      5
      6
      7
      8
      9
      languages:
      - Ruby
      - Perl
      - Python
      websites:
      YAML: yaml.org
      Ruby: ruby-lang.org
      Python: python.org
      Perl: use.perl.org
    • 对应的 JSON 表示如下:
      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      {
      "languages": [
      "Ruby",
      "Perl",
      "Python"
      ],
      "websites": {
      "YAML": "yaml.org",
      "Ruby": "ruby-lang.org",
      "Python": "python.org",
      "Perl": "use.perl.org"
      }
      }
  • 标量是最基本、不可再分的值。有以下 7 种:

    • 字符串
    • 布尔值
    • 整数
    • 浮点数
    • Null
    • 时间
    • 日期
  • 使用一个例子来快速了解标量的基本使用:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    boolean: 
    - TRUE # true、True 都可以
    - FALSE # false、False 都可以
    float:
    - 3.14 # 数值直接以字面量的形式表示
    - 6.8523015e+5 # 可以使用科学计数法
    int:
    - 123 # 数值直接以字面量的形式表示
    - 0b1010_0111_0100_1010_1110 # 二进制表示
    null:
    nodeName: 'node'
    parent: ~ # 使用~表示 null
    string:
    - hello # 字符串默认不使用引号
    - "Hello world" # 使用双引号或单引号包裹含有空格或特殊字符(如冒号)的字符串
    - newline
    newline1 # 字符串可以拆成多行,每一换行符会被转化成一个空格
    date:
    - 2018-02-17 # 日期必须使用 ISO 8601 格式,即 yyyy-MM-dd
    datetime:
    - 2018-02-17T15:02:31+08:00 # 时间使用 ISO 8601 格式,时间和日期之间使用 T 连接,+08:00 表示时区
  • YAML 字符串有三种表示方式:

    • 无引号
    • 单引号
    • 双引号
  • 字符串默认不需要引号,但是如果字符串包含空格或特殊字符(如冒号),需要加引号。

  • 双引号字符串允许在字符串中使用转义序列来表示特殊字符,例如 \n 表示换行,\t 表示制表符,以及 " 表示双引号。

  • 单引号字符串被视为纯粹的字面字符串,不支持转义序列。

  • 如果字符串含有单引号,可以使用双引号包裹,反之亦然

  • 锚点 & 和别名 *,可以用来完成引用

    1
    2
    3
    4
    5
    6
    7
    defaults: &defaults
    adapter: postgres
    host: localhost

    development:
    database: myapp_development
    <<: *defaults
  • 等同于下面的配置。

    1
    2
    3
    4
    5
    6
    7
    8
    defaults:
    adapter: postgres
    host: localhost

    development:
    database: myapp_development
    adapter: postgres
    host: localhost
  • & 用来建立锚点(defaults),<< 表示合并到当前数据,* 用来引用锚点。

  • 如果想引入多行的文本块,可以使用 |,|+,|-,>,>+,>-

  • 当内容换行时,保留换行符

  • 如果最后一行有多个换行符,只保留一个换行符

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    linefeed1: |
    some
    text
    linefeed2: |
    some
    text

    linefeed3: |
    some

    text
  • 对应的 JSON 为:

    1
    2
    3
    4
    5
    {
    "linefeed1": "some\ntext\n",
    "linefeed2": "some\ntext\n",
    "linefeed3": "some\n\ntext\n"
    }
  • 当内容换行时,保留换行符。

  • 与 | 的区别是,如果最后一行有多个换行符,则保留实际数目。

    1
    2
    3
    4
    5
    {
    "linefeed1": "some\ntext\n",
    "linefeed2": "some\ntext\n\n",
    "linefeed3": "some\n\ntext\n"
    }
  • 当内容换行时,保留换行符,但最后的换行符不保留

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    linefeed1: |-
    some
    text
    linefeed2: |-
    some
    text

    linefeed3: |-
    some

    text
  • 对应的 JSON 为:

    1
    2
    3
    4
    5
    {
    "linefeed1": "some\ntext",
    "linefeed2": "some\ntext",
    "linefeed3": "some\n\ntext"
    }
  • 当内容换行时,替换为空格,但保留最后一行的换行符

  • 如果最后一行有多个换行符,只保留一个换行符

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    linefeed1: >
    some
    text
    linefeed2: >
    some
    text

    linefeed3: >
    some

    text
  • 对应的 JSON 为:

    1
    2
    3
    4
    5
    {
    "linefeed1": "some text\n",
    "linefeed2": "some text\n",
    "linefeed3": "some\ntext\n"
    }
  • 当内容换行时,替换为空格,但保留最后一行的换行符。

  • 与 > 的区别是,如果最后一行有多个换行符,则保留实际数目。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    linefeed1: >+
    some
    text
    linefeed2: >+
    some
    text

    linefeed3: >+
    some

    text
  • 对应的 JSON 为

    1
    2
    3
    4
    5
    {
    "linefeed1": "some text\n",
    "linefeed2": "some text\n\n",
    "linefeed3": "some\ntext\n"
    }
  • -(缺省行为)

  • 当内容换行时,替换为空格,不保留最后一行的换行符。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    linefeed1: >-
    some
    text
    linefeed2: >-
    some
    text

    linefeed3: >-
    some

    text
  • 对应的 JSON 为:

    1
    2
    3
    4
    5
    {
    "linefeed1": "some text",
    "linefeed2": "some text",
    "linefeed3": "some\ntext"
    }
  • 注意:以上 6 个特殊字符,|- 和 >- 用得最多

  • 有时我们需要显示指定某些值的类型,可以使用 !(感叹号)显式指定类型。! 单叹号通常是自定义类型,!! 双叹号是内置类型

    1
    2
    3
    4
    # !!str 指定为字符串
    string.value: !!str HelloWorld!
    # !!timestamp 指定为日期时间类型
    datetime.value: !!timestamp 2021-04-13T02:31:00+08:00
  • 内置的类型如下:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    !!int:整数类型
    !!float:浮点类型
    !!bool:布尔类型
    !!str:字符串类型
    !!binary:二进制类型
    !!timestamp:日期时间类型
    !!null:空值
    !!set:集合类型
    !!omap,!!pairs:键值列表或对象列表
    !!seq:序列
    !!map:散列表类型
  • 一个 yaml 文件可以包含多个 yaml 文档,使用三个连字符—分隔

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    a: 10
    b:
    - 1
    - 2
    - 3
    ---
    a: 20
    b:
    - 4
    - 5
  • 这种情况在 K8S 和 SpringBoot 中非常常见。

  • 比如 SpringBoot 在一个 application.yml 文件中,通过 — 分隔多个不同配置,根据 spring.profiles.active 的值来决定启用哪个配置。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    # 公共配置
    spring:
    profiles:
    active: prod #使用名为 prod 的配置,这里可以切换成 dev。
    datasource:
    url: jdbc:mysql://localhost:3306/test_db?serverTimezone=UTC&useSSL=false&allowPublicKeyRetrieval=true
    password: 123456
    username: root
    ---
    # 开发环境配置
    spring:
    profiles: dev #profiles属性代表配置的名称。

    server:
    port: 8080
    ---
    # 生产环境配置
    spring:
    profiles: prod

    server:
    port: 80
  • 下面以 YAML 表示一个简单的后台服务配置:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    name: UserProfileServer
    maxconns: 1000
    queuecap: 10000
    queuetimeout: 300
    loginfo:
    loglevel: ERROR
    logsize: 10M
    lognum: 10
    logpath: /usr/local/app/log

TOML

  • GitHub 联合创始人 Tom Preston-Werner 觉得 YAML 不够简洁优雅,如缩进要严格对齐,因此和其他几位开发者一起捣鼓了一个 TOML(Tom’s Obvious Minimal Language)。TOML 旨在成为一个语义显著且易于阅读的极简配置文件格式,能够无歧义地转化为哈希表,且能够简单地解析成形形色色语言中的数据结构,用于取代 YAML 和 JSON

  • TOML 的基本语法规则如下:

    • TOML 是大小写敏感的
    • TOML 文件必须是合法的 UTF-8 编码的 Unicode 文档
    • 空白的意思是 Tab(0x09)或空格(0x20)
    • 换行的意思是 LF(0x0A)或 CRLF(0x0D0A)
    • 井号将此行剩下的部分标记为注释

配置文件格式的选择

  • 面对常见配置文件格式,使用时该如何选择呢?这里给几个选择的原则:

    • 支持嵌套结构。仅仅支持 KV 结构的键值对表达能力有点弱;
    • 支持注释。不支持注释的 JSON 是给机器读的,不是给人读的;
    • 支持不同的数据类型,而不仅仅是 string。这一点,键值对和 XML 表现的非常逊色;
    • 最好支持 include 其他配置文件,方便配置模块化。复杂的配置也是无奈之举,但如果支持 include 语法,可以方便的把配置文件模块化。
  • 通过以上几个对配置文件的要求,发现键值对不支持层级关系,JSON 不支持注释,可读性较差,虽然 XML 支持注释和层级结构,且可读性较好,但是因为起始标签一定要有个与之对应的结束标签,文件内容较大,解析时占用较多内存,传输时占用较多带宽。所以这里推荐使用 YAML 和 TOML,很多语言都有其 library 实现,跨语言不成问题

感谢老板支持!敬礼(^^ゞ