配置文件格式详解之终极无惑
文章目录
- 1.键值对
- 2.JSON
- 2.1 JSON 语法
- 2.2 JSON 实例
- 2.3 JSON 解析
- 3.XML
- 3.1 XML 语法
- 3.2 XML 实例
- 3.3 XML 解析
- 4.YAML
- YAML 语法
- 历史版本
- YAML 数据结构
- 对象
- 数组
- 复合结构
- 标量
- 引用
- 文本块
- 显示指定类型
- 单文件多文档
- 简单示例
- YAML 解析
- 完整示例
- 5.TOML
- 5.1 TOML 语法
- 5.2 TOML 数据结构
- 5.2.1 键值对
- 5.2.2 字符串
- 基础字符串
- 多行基础字符串
- 字面量字符串
- 多行字面量字符串
- 5.2.3 整数
- 5.2.4 浮点数
- 5.2.5 布尔值
- 5.2.6 坐标日期时刻
- 5.2.7 数组
- 5.2.8 表
- 5.2.9 行内表
- 5.2.10 表数组
- 5.3 TOML 实例
- 5.4 TOML 解析
- 6.配置文件格式的选择
- 参考文献
不管是移动应用、桌面程序还是后台服务,经常需要从配置文件中读取配置信息,进行程序初始化和改变运行时的状态。以什么样的格式来存储配置信息,这是开发人员需要面临的一个问题。
常用的配置文件格式主要有:
- 键值对
- JSON
- XML
- YAML
- TOML
下面会详细介绍并给出解析实例。
1.键值对
键值对是一个非常简单易用的配置文件格式。每一个键值对表示一项配置,键值对的分隔符一般使用等号或冒号。解析时,可以将 # 号开始的行视为注释行,以达到注释的功能。
以键值对为表现形式的配置文件格式常见的有 Windows .ini 文件和 Java 中的 .properties 文件。
例如下面是一个使用键值对表示的后台服务配置。
# 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 为例,完成对上面配置文件的解析。
package mainimport("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 infom := 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 lineif 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)
}
运行结果:
map[loglevel:ERRORlognum:10logpath:/usr/local/app/loglogsize:10Mmaxconns:1000name:UserProfileServerqueuecap:10000queuetimeout:300
]
2.JSON
JSON(JavaScript Object Notation) 是轻量级的文本数据交换格式,独立于语言,具有自我描述性。JSON 类似于 XML,但比 XML 更小、更快,更易解析。
2.1 JSON 语法
JSON 语法是 JavaScript 对象表示法语法的子集。
- 数据在名称/值对中
- 数据由逗号分隔
- 花括号保存对象
- 方括号保存数组
名称/值对包括字段名称(在双引号中),后面写一个冒号,然后是值:
"firstName" : "John"
一个合法的 JSON 可以是:
- 数字(整数或浮点数)
- 字符串(在双引号中)
- 逻辑值(true 或 false)
- 数组(在方括号中)
- 对象(在花括号中)
- null
JSON 对象在花括号中书写,对象可以包含多个名称/值对,使用逗号分隔:
{ "firstName":"John" , "lastName":"Doe" }
JSON 数组在方括号中书写,数组可包含多个对象:
{"employees": [{ "firstName":"John" , "lastName":"Doe" },{ "firstName":"Anna" , "lastName":"Smith" },{ "firstName":"Peter" , "lastName":"Jones" }
]
}
2.2 JSON 实例
下面以 JSON 表示一个简单的后台服务配置:
{"-name": "UserProfileServer","maxconns": "1000","queuecap": "10000","queuetimeout": "300","loginfo": {"loglevel": "ERROR","logsize": "10M","lognum": "10","logpath": "/usr/local/app/log"}
}
其中 -name 表示服务的名称,前面一个横杠表示该值可以转换为 XML 的标签属性。其它的键值对表示服务的各个配置项。
2.3 JSON 解析
下面以 Go 为例,利用 Go 自带的 JSON 包 encoding/json 完成对上面服务配置的解析。
第一步,将 JSON 串转换为 Go struct。把上面的 JSON 串粘贴到 Convert JSON to Go struct。
type Server struct {Name string `json:"-name"`Maxconns string `json:"maxconns"`Queuecap string `json:"queuecap"`Queuetimeout string `json:"queuetimeout"`Loginfo struct {Loglevel string `json:"loglevel"`Logsize string `json:"logsize"`Lognum string `json:"lognum"`Logpath string `json:"logpath"`} `json:"loginfo"`
}
第二步,利用 Go 自带的 JSON 包 encoding/json 解析上面以 JSON 串表示的配置信息。
package mainimport("encoding/json""io/ioutil""fmt""os"
)func main() {file, err := os.Open("server.json")if err != nil {fmt.Printf("error:%v\n", err)return}defer file.Close()data, err := ioutil.ReadAll(file)if err != nil {fmt.Printf("error: %v", err)return}v := Server{}err = json.Unmarshal(data, &v)if err != nil {fmt.Printf("error:%v\n", err)return}fmt.Printf("%+v\n", v)
}
运行输出:
{Name:UserProfileServer Maxconns:1000 Queuecap:10000 Queuetimeout:300 Loginfo:{Loglevel:ERROR Logsize:10M Lognum:10 Logpath:/usr/local/app/log}}
3.XML
XML(Extensible Markup Language)是可扩展标记语言,用来传输和存储数据。因为其允许用户自定义标记名称,具有自我描述性,可灵活地用于存储服务配置信息。
3.1 XML 语法
XML 文档结构是一种树结构,它从“根部”开始,然后扩展到“枝叶”。XML 文档必须有一个唯一的根结点,根结点包含所有其它结点。所有结点均可拥有文本内容和属性(名称/值的对)。XML 结点也叫做 XML 元素。
编写 XML 文档时,还需要注意以下几点:
(1)所有 XML 元素都须有关闭标签;
(2)XML 标签对大小写敏感;
(3)XML 的属性值须加引号;
(4)XML 中的特殊字符可以使用实体引用来表示。在 XML 中,有 5 个预定义的实体引用:
实体引用 | 字符 | 名称 |
---|---|---|
<
|
< | 小于 |
>
|
> | 大于 |
&
|
& | 和号 |
'
|
’ | 单引号 |
"
|
" | 引号 |
(5)在 XML 中编写注释的语法与 HTML 的语法很相似: |
<!-- This is a comment -->
(6)XML 元素必须遵循以下命名规则:
- 名称可以含字母、数字以及其他的字符
- 名称不能以数字或者标点符号开始
- 名称不能以字符串 xml(或者 XML、Xml)开始
- 名称不能包含空格
- 名称不能使用保留的字词
3.2 XML 实例
下面以 XML 表示一个简单的后台服务配置:
<?xml version="1.0" encoding="UTF-8"?>
<server name="UserProfileServer"><maxconns>1000</maxconns><queuecap>10000</queuecap><queuetimeout>300</queuetimeout><loginfo><loglevel>ERROR</loglevel><logsize>10M</logsize><lognum>10</lognum><logpath>/usr/local/app/log</logpath></loginfo>
</server>
第一行是 XML 声明,它定义 XML 的版本(1.0)和所使用的编码(UTF-8)。紧接着 server 为根结点,name 为根结点的一个属性,表示服务名称,其他子结点的文本内容表示服务的具体配置项。
3.3 XML 解析
使用 XML 存储服务配置信息,我们如何解析呢?下面以 Go 为例,来解析上面的 XML 格式的服务配置。
第一步,将上面的 XML 配置信息粘贴到 XML to Go struct 快速获取 Go struct 的定义。
type Server struct {Name string `xml:"name,attr"` //标签属性Maxconns string `xml:"maxconns"`Queuecap string `xml:"queuecap"`Queuetimeout string `xml:"queuetimeout"`Loginfo struct {Loglevel string `xml:"loglevel"`Logsize string `xml:"logsize"`Lognum string `xml:"lognum"`Logpath string `xml:"logpath"`} `xml:"loginfo"`
}
第二步,借助 Go 自带的 encoding/xml 包完成 XML 文档解析。
package mainimport("encoding/xml""io/ioutil""fmt""os"
)func main() {file, err := os.Open("server.xml")if err != nil {fmt.Printf("error:%v\n", err)return}defer file.Close()data, err := ioutil.ReadAll(file)if err != nil {fmt.Printf("error: %v", err)return}v := Server{}err = xml.Unmarshal(data, &v) if err != nil {fmt.Printf("error:%v\n", err)return}fmt.Printf("%+v\n", v)
}
运行输出:
{Name:UserProfileServer Maxconns:1000 Queuecap:10000 Queuetimeout:300 Loginfo:{Loglevel:ERROR Logsize:10M Lognum:10 Logpath:/usr/local/app/log}}
4.YAML
YAML(YAML Ain’t a Markup Language)是专门用来写配置文件的语言,简洁强大,相比于 JSON 和 XML,更加便于开发人员读写。
YAML 配置文件后缀为.yml 或 .yaml。
YAML 语法
YAML 的基本语法规则如下:
- 数据结构采用键值对的形式 key: value。
- 键冒号后面要加空格(一般为 1 个空格)。
- 字母大小写敏感。
- 使用缩进表示层级关系。
- 缩进只允许使用空格,不允许使用 Tab 键。
- 缩进空格数可以任意,只要相同层级的元素左侧对齐即可。
- 字符串值一般不使用引号,必要时可使用。使用双引号表示字符串时,会转义字符串中的特殊字符(例如\n)。使用单引号时不会转义字符串中的特殊字符。
- 数组中的每个元素单独一行,并以 - 开头。或使用方括号,元素用逗号隔开。注意短横杆和逗号后面都要有空格。
- 对象中的每个成员单独一行,使用键值对形式。或者使用大括号并用逗号分开。
- 文档以三个连字符
---
表示开始,以三个点号...
表示结束,二者都是可选的。 - 文档前面可能会有指令,在这种情况下,需要使用
---
来表示指令的结束。指令是一个%后跟一个标识符和一些形参。 - 目前只有两个指令:
%YAML
指定文档的 YAML 版本,%TAG
用于 tag 简写。二者都很少使用。 #
表示注释,从这个字符一直到行尾,都会被解析器忽略。
历史版本
版本 | 发布日期 |
---|---|
YAML 1.0 | 29 January 2004 |
YAML 1.1 | 18 January 2005 |
YAML 1.2.0 | 21 July 2009 |
YAML 1.2.1 | 1 October 2009 |
YAML 1.2.2 | 1 October 2021 |
YAML 数据结构
YAML 支持的数据结构有三种:
- 对象:键值对的集合,又称为映射(mapping)、散列(hashes)、字典(dictionary)。
- 数组:一组按次序排列的值,又称为序列(sequence)、列表(list)。
- 标量:单个不可再分的值
下面分别介绍这三种数据结构。
对象
对象的一组键值对,使用冒号结构表示。
name: Steve
YAML 也允许另一种写法,将所有键值对写成一个行内对象。
who: { name: Steve, age: 18 }
当然,如果对象元素太多一行放不下,那么可以换行。
who:name: Steveage: 18
数组
一组以连字符开头的行,构成一个数组。注意,连字符后需添加空格。
animals:- Cat- Dog- Goldfish
连字符前可以没有缩进,也就是说下面这种写法也是 OK 的,但是还是建议缩进,因为更加易读。
animals:
- Cat
- Dog
- Goldfish
数组也可以采用行内表示法。
animal: [Cat,Dog,Goldfish]
如果数组元素是一个数组,则可以在连字符下面再缩进输入一个数组。
animals:-- Cat- Dog-- Fish- Goldfish
如果是行内表示,则为:
animals: [[Cat,Dog],[Fish,Goldfish]]
如果数组元素是一个对象,可以写作:
animals:- species: dogname: foo- species: catname: bar
对应的 JSON 为:
{"animals": [{"species": "dog","name": "foo"},{"species": "cat","name": "bar"}]
}
复合结构
对象和数组可以结合使用,形成复合结构。
languages:- Ruby- Perl- Python
websites:YAML: yaml.org Ruby: ruby-lang.org Python: python.org Perl: use.perl.org
对应的 JSON 表示如下:
{"languages": ["Ruby","Perl","Python"],"websites": {"YAML": "yaml.org","Ruby": "ruby-lang.org","Python": "python.org","Perl": "use.perl.org"}
}
标量
标量是最基本、不可再分的值。有以下 7 种:
- 字符串
- 布尔值
- 整数
- 浮点数
- Null
- 时间
- 日期
使用一个例子来快速了解标量的基本使用:
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 world' # 可以使用双引号或者单引号包裹特殊字符- newlinenewline2 # 字符串可以拆成多行,每一换行符会被转化成一个空格
date:- 2018-02-17 # 日期必须使用ISO 8601格式,即 yyyy-MM-dd
datetime: - 2018-02-17T15:02:31+08:00 # 时间使用 ISO 8601 格式,时间和日期之间使用 T 连接,最后使用 + 代表时区
引用
锚点 & 和别名 *,可以用来完成引用。
defaults: &defaultsadapter: postgreshost: localhostdevelopment:database: myapp_development<<: *defaults
等同于下面的配置。
defaults:adapter: postgreshost: localhostdevelopment:database: myapp_developmentadapter: postgreshost: localhost
& 用来建立锚点(defaults),<< 表示合并到当前数据,* 用来引用锚点。
文本块
如果想引入多行的文本块,可以使用 |
,|+
,|-
,>
,>+
,>-
。
|
当内容换行时,保留换行符。
如果最后一行有多个换行符,只保留一个换行符。
linefeed1: |sometext
linefeed2: |sometextlinefeed3: |sometext
对应的 JSON 为:
{"linefeed1": "some\ntext\n","linefeed2": "some\ntext\n","linefeed3": "some\n\ntext\n"
}
|+
当内容换行时,保留换行符。
与 | 的区别是,如果最后一行有多个换行符,则保留实际数目。
linefeed1: |+sometext
linefeed2: |+sometextlinefeed3: |+sometext
对应的 JSON 为:
{"linefeed1": "some\ntext\n","linefeed2": "some\ntext\n\n","linefeed3": "some\n\ntext\n"
}
|-
当内容换行时,保留换行符,但最后的换行符不保留。
linefeed1: |-sometext
linefeed2: |-sometextlinefeed3: |-sometext
对应的 JSON 为:
{"linefeed1": "some\ntext","linefeed2": "some\ntext","linefeed3": "some\n\ntext"
}
>
当内容换行时,替换为空格,但保留最后一行的换行符。
如果最后一行有多个换行符,只保留一个换行符。
linefeed1: >sometext
linefeed2: >sometextlinefeed3: >sometext
对应的 JSON 为:
{"linefeed1": "some text\n","linefeed2": "some text\n","linefeed3": "some\ntext\n"
}
>+
当内容换行时,替换为空格,但保留最后一行的换行符。
与 > 的区别是,如果最后一行有多个换行符,则保留实际数目。
linefeed1: >+sometext
linefeed2: >+sometextlinefeed3: >+sometext
对应的 JSON 为:
{"linefeed1": "some text\n","linefeed2": "some text\n\n","linefeed3": "some\ntext\n"
}
>-
(缺省行为)
当内容换行时,替换为空格,不保留最后一行的换行符。
linefeed1: >-sometext
linefeed2: >-sometextlinefeed3: >-sometext
对应的 JSON 为:
{"linefeed1": "some text","linefeed2": "some text","linefeed3": "some\ntext"
}
注意:以上 6 个特殊字符,|-
和 >-
用得最多。
显示指定类型
有时我们需要显示指定某些值的类型,可以使用 !(感叹号)显式指定类型。! 单叹号通常是自定义类型,!! 双叹号是内置类型。
# !!str 指定为字符串
string.value: !!str HelloWorld!
# !!timestamp 指定为日期时间类型
datetime.value: !!timestamp 2021-04-13T02:31:00+08:00
内置的类型如下:
!!int:整数类型
!!float:浮点类型
!!bool:布尔类型
!!str:字符串类型
!!binary:二进制类型
!!timestamp:日期时间类型
!!null:空值
!!set:集合类型
!!omap,!!pairs:键值列表或对象列表
!!seq:序列
!!map:散列表类型
单文件多文档
一个 yaml 文件可以包含多个 yaml 文档,使用三个连字符---
分隔。
a: 10
b: - 1- 2- 3
---
a: 20
b: - 4- 5
这种情况在 K8S 和 SpringBoot 中非常常见。
比如 SpringBoot 在一个 application.yml 文件中,通过 — 分隔多个不同配置,根据 spring.profiles.active 的值来决定启用哪个配置。
# 公共配置
spring:profiles:active: prod #使用名为 prod 的配置,这里可以切换成 dev。datasource:url: jdbc:mysql://localhost:3306/test_db?serverTimezone=UTC&useSSL=false&allowPublicKeyRetrieval=truepassword: 123456username: root
---
# 开发环境配置
spring:profiles: dev #profiles属性代表配置的名称。server:port: 8080
---
# 生产环境配置
spring:profiles: prodserver:port: 80
简单示例
下面以 YAML 表示一个简单的后台服务配置:
name: UserProfileServer
maxconns: 1000
queuecap: 10000
queuetimeout: 300
loginfo:loglevel: ERRORlogsize: 10Mlognum: 10logpath: /usr/local/app/log
YAML 解析
因为 Go 并没有提供解析 YAML 的标准库,所以这里基于第三方开源库 go-yaml 来完成对 YAML 文件的解析。
第一步,将 YAML 配置文件的内容在 Convert YAML to Go struct 转换为 Go struct。
type Server struct {Name string `yaml:"name"`Maxconns int `yaml:"maxconns"`Queuecap int `yaml:"queuecap"`Queuetimeout int `yaml:"queuetimeout"`Loginfo struct {Loglevel string `yaml:"loglevel"`Logsize string `yaml:"logsize"`Lognum int `yaml:"lognum"`Logpath string `yaml:"logpath"`} `yaml:"loginfo"`
}
第二步,利用第三方开源库 go-yaml 来完成对 YAML 文件的解析。
package mainimport("io/ioutil""fmt""os" yaml "gopkg.in/yaml.v3"
)func main() {file, err := os.Open("server.yaml")if err != nil {fmt.Printf("error:%v\n", err)return}defer file.Close()data, err := ioutil.ReadAll(file)if err != nil {fmt.Printf("error:%v\n", err)return}v := Server{}err = yaml.Unmarshal(data, &v)if err != nil {fmt.Printf("error:%v\n", err)return}fmt.Printf("%+v\n", v)
}
运行输出:
{Name:UserProfileServer Maxconns:1000 Queuecap:10000 Queuetimeout:300 Loginfo:{Loglevel:ERROR Logsize:10M Lognum:10 Logpath:/usr/local/app/log}}
完整示例
%YAML 1.2
---
receipt: Oz-Ware Purchase Invoice
date: 2012-08-06
customer:given: Dorothyfamily: Galeitems:- part_no: A4786descrip: Water Bucket (Filled)price: 1.47quantity: 4- part_no: E1628descrip: High Heeled "Ruby" Slipperssize: 8price: 133.7quantity: 1bill-to: &id001street: | 123 Tornado AlleySuite 16city: East Centervillestate: KSship-to: *id001 specialDelivery: >Follow the Yellow BrickRoad to the Emerald City.Pay no attention to theman behind the curtain.
...
注意在 YAML 中,字符串不一定要用双引号标示。另外,在缩进中空白字符的数目并不是非常重要,只要相同层次结构的元素左侧对齐就可以了(不过不能使用 TAB 字符)。
%YAML 1.2 表示版本。
这个文件的顶层由七个键值组成:其中一个键值"items",是两个元素构成的数组(或称清单),这数组中的两个元素同时也是包含了四个键值的散列表。
文件中重复的部分用这个方法处理:使用锚点(&)和引用(*)标签将"bill-to"散列表的内容复制到"ship-to"散列表。也可以在文件中加入选择性的空行,以增加可读性。
在一个文件中,可同时包含多个文件,并用---
分隔。选择性的符号...
可以用来表示文件结尾(在流通信中,这非常有用,可以在不关闭流的情况下,发送结束信号)。
5.TOML
GitHub 联合创始人 Tom Preston-Werner 觉得 YAML 不够简洁优雅,如缩进要严格对齐,因此和其他几位开发者一起捣鼓了一个 TOML(Tom’s Obvious Minimal Language)。TOML 旨在成为一个语义显著且易于阅读的极简配置文件格式,能够无歧义地转化为哈希表,且能够简单地解析成形形色色语言中的数据结构,用于取代 YAML 和 JSON。
5.1 TOML 语法
TOML 的基本语法规则如下:
- TOML 是大小写敏感的
- TOML 文件必须是合法的 UTF-8 编码的 Unicode 文档
- 空白的意思是 Tab(0x09)或空格(0x20)
- 换行的意思是 LF(0x0A)或 CRLF(0x0D0A)
- 井号将此行剩下的部分标记为注释
5.2 TOML 数据结构
5.2.1 键值对
TOML 文档最基本的构成区块是键/值对。
- 键名在等号的左边而值在右边。
- 键名和键值周围的空白会被忽略。
- 键、等号和值必须在同一行(不过有些值可以跨多行)。
key = "value"
值必须是这些类型:字符串,整数,浮点数,布尔值,日期时刻,数组或行内表。不指定值是有误的。
键名可以是裸露的,引号引起来的,或点分隔的。
裸键只能包含 ASCII 字母,ASCII 数字,下划线和短横线(A-Za-z0-9_-)。
key = "value"
bare_key = "value"
bare-key = "value"
1234 = "value"
引号键遵循与基础字符串或字面量字符串相同的规则并允许你使用更为广泛的键名。除非必要,使用裸键为最佳实践。
"127.0.0.1" = "value"
"character encoding" = "value"
"ʎǝʞ" = "value"
'key2' = "value"
'quoted "value"' = "value"
点分隔键是一系列通过点相连的裸键或引号键。这允许了你将相近属性放在一起:
"名称" = "橙子"
"物理属性"."颜色" = "橙色"
"物理属性"."形状" = "圆形"
site."google.com" = true
这在 JSON 那是以下结构:
{"名称": "橙子","物理属性": {"颜色": "橙色","形状": "圆形"},"site": {"google.com": true}
}
点分隔符周围的空白会被忽略,不过,最佳实践是不要使用任何不必要的空白。
多次定义同一个键是不行的。
# 不要这样做
name = "Tom"
name = "Pradyun"
5.2.2 字符串
共有四种方式来表示字符串:基础式,多行基础式,字面量式,和多行字面量式。所有字符串都只能包含有效的 UTF-8 字符。
基础字符串
任何 Unicode 字符都可以使用,除了那些必须转义的:引号,反斜杠,以及控制字符(U+0000 至 U+001F,U+007F)。
str = "我是一个字符串。\"你可以把我引起来\"。姓名\tJos\u00E9\n位置\t旧金山。"
为了方便,一些流行的字符有其简便转义写法。
\b - backspace (U+0008)
\t - tab (U+0009)
\n - linefeed (U+000A)
\f - form feed (U+000C)
\r - carriage return (U+000D)
\" - quote (U+0022)
\\ - backslash (U+005C)
\uXXXX - unicode (U+XXXX)
\UXXXXXXXX - unicode (U+XXXXXXXX)
任何 Unicode 字符都可以用 \uXXXX 或 \UXXXXXXXX 的形式来转义。转义码必须是有效的 Unicode 标量值。
所有上面未列出的其它转义序列都是保留的,如果被用了,TOML 应当生成一个错误。
有时你需要表示一小篇文本(例如译文)或者想要对非常长的字符串进行折行。TOML 对此进行了简化。
多行基础字符串
多行基础字符串由三个引号包裹,允许折行。紧随开头引号的那个换行会被去除。其它空白和换行符会被原样保留。
str1 = """
玫瑰是红色的
紫罗兰是蓝色的"""
TOML 解析器可以相对灵活地解析成对所在平台有效的换行字符。
# 在 Unix 系统,上面的多行字符串可能等同于:
str2 = "玫瑰是红色的\n紫罗兰是蓝色的"# 在 Windows 系统,它可能等价于:
str3 = "玫瑰是红色的\r\n紫罗兰是蓝色的"
想书写长字符串却不想引入无关空白,可以用“行末反斜杠”。当一行的最后一个非空白字符是 \ 时,它会连同它后面的所有空白(包括换行)一起被去除,直到下一个非空白字符或结束引号为止。所有对基础字符串有效的转义序列,对多行基础字符串也同样适用。
# 下列字符串的每一个字节都完全相同:
str1 = "那只 敏捷的 棕 狐狸 跳 过了 那只 懒 狗。"str2 = """
那只 敏捷的 棕 \狐狸 跳 过了 \那只 懒 狗。"""str3 = """\那只 敏捷的 棕 \狐狸 跳 过了 \那只 懒 狗。\"""
任何 Unicode 字符都可以使用,除了那些必须被转义的:反斜杠和控制字符(U+0000 至 U+001F,U+007F)。引号不需要转义,除非它们的存在会造成一个比预期提前的结束标记。
如果你常常要指定 Windows 路径或正则表达式,那么必须转义反斜杠就马上成为啰嗦而易错的了。为了帮助搞定这点,TOML 支持字面量字符串,它完全不允许转义。
字面量字符串
字面量字符串由单引号包裹。类似于基础字符串,他们只能表现为单行:
# 所见即所得。
winpath = 'C:\Users\nodejs\templates'
winpath2 = '\\ServerX\admin$\system32\'
quoted = '汤姆·"达布斯"·普雷斯顿—维尔纳'
regex = '<\i\c*\s*>'
由于没有转义,无法在由单引号包裹的字面量字符串中写入单引号。万幸,TOML 支持一种多行版本的字面量字符串来解决这个问题。
多行字面量字符串
多行字面量字符串两侧各有三个单引号来包裹,允许换行。类似于字面量字符串,无论任何转义都不存在。
紧随开始标记的那个换行会被剔除。
开始结束标记之间的所有其它内容会原样对待。
regex2 = '''I [dw]on't need \d{2} apples'''
lines = '''
原始字符串中的
第一个换行被剔除了。所有其它空白都保留了。
'''
除 tab 以外的所有控制字符都不允许出现在字面量字符串中。因此,对于二进制数据,建议你使用 Base64 或其它合适的 ASCII 或 UTF-8 编码。对那些编码的处理方式,将交由应用程序自己来确定。
5.2.3 整数
整数是纯数字。正数可以有加号前缀。负数的前缀是减号。
int1 = +99
int2 = 42
int3 = 0
int4 = -17
对于大数,你可以在数字之间用下划线来增强可读性。每个下划线两侧必须至少有一个数字。
int5 = 1_000
int6 = 5_349_221
int7 = 1_2_3_4_5 # 无误但不鼓励
前导零是不允许的。整数值 -0 与 +0 是有效的,并等同于无前缀的零。
非负整数值也可以用十六进制、八进制或二进制来表示。在这些格式中,+ 不被允许,而(前缀后的)前导零是允许的。十六进制值大小写不敏感。数字间的下划线是允许的(但不能存在于前缀和值之间)。
# 带有 `0x` 前缀的十六进制
hex1 = 0xDEADBEEF
hex2 = 0xdeadbeef
hex3 = 0xdead_beef# 带有 `0o` 前缀的八进制
oct1 = 0o01234567
oct2 = 0o755 # 对于表示 Unix 文件权限很有用# 带有 `0b` 前缀的二进制
bin1 = 0b11010110
取值范围要求为 64 比特(signed long)(−9,223,372,036,854,775,808 至 9,223,372,036,854,775,807)。
5.2.4 浮点数
浮点数应当被实现为 IEEE 754 binary64 值。
一个浮点数由一个整数部分(遵从与十进制整数值相同的规则)后跟上一个小数部分和/或一个指数部分组成。
如果小数部分和指数部分兼有,那小数部分必须在指数部分前面。
# 小数
flt1 = +1.0
flt2 = 3.1415
flt3 = -0.01# 指数
flt4 = 5e+22
flt5 = 1e6
flt6 = -2E-2# 都有
flt7 = 6.626e-34
小数部分是一个小数点后跟一个或多个数字。
一个指数部分是一个 E(大小写均可)后跟一个整数部分(遵从与十进制整数值相同的规则)。
与整数相似,你可以使用下划线来增强可读性。每个下划线必须被至少一个数字围绕。
flt8 = 224_617.445_991_228
浮点数值 -0.0 与 +0.0 是有效的,并且应当遵从 IEEE 754。
特殊浮点值也能够表示。 它们是小写的。
# 无穷
sf1 = inf # 正无穷
sf2 = +inf # 正无穷
sf3 = -inf # 负无穷# 非数
sf4 = nan # 实际上对应信号非数码还是静默非数码,取决于实现
sf5 = +nan # 等同于 `nan`
sf6 = -nan # 有效,实际码取决于实现
5.2.5 布尔值
布尔值就是你所惯用的那样。要小写。
bool1 = true
bool2 = false
5.2.6 坐标日期时刻
要明确无误地表示世上的一个特定时间,你可以使用指定了时区偏移量的 RFC 3339 格式的日期时刻。
odt1 = 1979-05-27T07:32:00Z
odt2 = 1979-05-27T00:32:00-07:00
odt3 = 1979-05-27T00:32:00.999999-07:00
出于可读性的目的,你可以用空格替代日期和时刻中间的 T(RFC 3339 的第 5.6 节中允许了这样做)。
odt4 = 1979-05-27 07:32:00Z
小数秒的精度取决于实现,但至少应当能够精确到毫秒。如果它的值超出了实现所支持的精度,那多余的部分必须被舍弃,而不能四舍五入。
如果你省略了 RFC 3339 日期时刻中的时区偏移量,这表示该日期时刻的使用并不涉及时区偏移。在没有其它信息的情况下,并不知道它究竟该被转化成世上的哪一刻。
如果仍被要求转化,那结果将取决于实现。
ldt1 = 1979-05-27T07:32:00
ldt2 = 1979-05-27T00:32:00.999999
如果你只写了 RFC 3339 日期时刻中的日期部分,那它表示一整天,同时也不涉及时区偏移。
ld1 = 1979-05-27
如果你只写了 RFC 3339 日期时刻中的时刻部分,它将只表示一天之中的那个时刻,而与任何特定的日期无关、亦不涉及时区偏移。
lt1 = 07:32:00
lt2 = 00:32:00.999999
5.2.7 数组
数组是内含值的方括号。空白会被忽略。子元素由逗号分隔。子元素的数据类型必须一致(不同写法的字符串应当被认为是相同的类型,不同元素类型的数组也同是数组类型)。
arr1 = [ 1, 2, 3 ]
arr2 = [ "red", "yellow", "green" ]
arr3 = [ [ 1, 2 ], [3, 4, 5] ]
arr4 = [ "所有(写法的)", '字符串', """都是一样的""", '''类型''']
arr5 = [ [ 1, 2 ], ["a", "b", "c"] ]arr6 = [ 1, 2.0 ] # 有误
数组也可以跨多行。数组的最后一个值后面可以有终逗号(也称为尾逗号)。值和结束括号前可以存在任意数量的换行和注释。
arr7 = [1, 2, 3
]arr8 = [1,2, # 这是可以的
]
5.2.8 表
表(也被称为哈希表或字典)是键值对的集合。它们在方括号里,并作为单独的行出现。看得出它们不同于数组,因为数组只有值。
在它下方,直至下一个表或文件结束,都是这个表的键值对。表不保证保持键值对的指定顺序。
[table-1]
key1 = "some string"
key2 = 123[table-2]
key1 = "another string"
key2 = 456
表名的规则与键名相同(见前文键名定义)。
[dog."tater.man"]
type.name = "pug"
这在 JSON 那儿,是以下结构:
{ "dog": { "tater.man": { "type": { "name": "pug" } } } }
键名周围的空格会被忽略,然而最佳实践还是不要有任何多余的空白。
[a.b.c] # 这是最佳实践
[ d.e.f ] # 等同于 [d.e.f]
[ g . h . i ] # 等同于 [g.h.i]
[ j . "ʞ" . 'l' ] # 等同于 [j."ʞ".'l']
你不必层层完整地写出你不想写的所有途径的父表。TOML 知道该怎么办。
# [x] 你
# [x.y] 不
# [x.y.z] 需要这些
[x.y.z.w] # 来让这生效
空表是允许的,只要里面没有键值对就行了。
类似于键名,你不能重复定义任何表。这样做是错误的。
# 不要这样做
[a]
b = 1[a]
c = 2# 也不要这样做
[a]
b = 1[a.b]
c = 2
5.2.9 行内表
行内表提供了一种更为紧凑的语法来表示表,即在一行内表示一个表。行内表由花括号包裹,在括号中,可以出现零或多个逗号分隔的键值对。键值对采取与标准表中键值对相同的形式。什么类型的值都可以,包括行内表。
行内表出现在同一行内。不允许花括号中出现换行,除非它们存在于正确的值当中。即便如此,也强烈不建议把一个行内表搞成纵跨多行的样子。如果你发现自己真的需要,那意味着你应该使用标准表。
name = { first = "汤姆", last = "普雷斯顿—维尔纳" }
point = { x = 1, y = 2 }
animal = { type.name = "哈巴狗" }# 上述行内表等同于下面的标准表定义[name]
first = "汤姆"
last = "普雷斯顿—维尔纳"[point]
x = 1
y = 2[animal]
type.name = "哈巴狗"
5.2.10 表数组
最后还剩下一个没法表示的是表数组。这可以通过双方括号来表示。各个具有相同方括号名的表将会成为该数组内的一员。这些表的出现顺序就是它们的插入顺序。一个没有任何键值对的双方括号表将为视为一个空表。
[[products]]
name = "Hammer"
sku = 738594937[[products]][[products]]
name = "Nail"
sku = 284758393
color = "gray"
这在 JSON 那儿,是以下结构。
{"products": [{ "name": "Hammer", "sku": 738594937 },{ },{ "name": "Nail", "sku": 284758393, "color": "gray" }]
}
你还可以创建一个嵌套表数组。只要在子表上使用相同的双方括号语法语法。每个双方括号子表将隶属于上方最近定义的表元素。
[[fruit]]name = "apple"[fruit.physical]color = "red"shape = "round"[[fruit.variety]]name = "red delicious"[[fruit.variety]]name = "granny smith"[[fruit]]name = "banana"[[fruit.variety]]name = "plantain"
上述 TOML 对应下面的 JSON。
{"fruit": [{"name": "apple","physical": {"color": "red","shape": "round"},"variety": [{ "name": "red delicious" },{ "name": "granny smith" }]},{"name": "banana","variety": [{ "name": "plantain" }]}]
}
若试图向一个静态定义的数组追加内容,即便数组尚且为空或类型兼容,也必须在解析时报错。
# 无效的 TOML 文档
fruit = [][[fruit]] # 不允许
若试图用已经确定为数组的名称定义表,必须在解析时报错。# 无效的 TOML 文档
[[fruit]]name = "apple"[[fruit.variety]]name = "red delicious"# 这个表与之前的表冲突了[fruit.variety]name = "granny smith"
你也可以适当使用行内表:
points = [ { x = 1, y = 2, z = 3 },{ x = 7, y = 8, z = 9 },{ x = 2, y = 4, z = 8 } ]
5.3 TOML 实例
以 TOML 表示一个简单的服务配置。
name = "UserProfileServer"
maxconns = 1000
queuecap = 10000
queuetimeout =300[loginfo]
loglevel = "ERROR"
logsize = "10M"
lognum = 10
logpath = "/usr/local/app/log"
5.4 TOML 解析
以 Go 为例,解析上面的 TOML 配置文件。
第一步,通过 TOML-to-Go 快速将 TOML 转换为 Go struct。
type Server struct {Name string `toml:"name"`Maxconns int `toml:"maxconns"`Queuecap int `toml:"queuecap"`Queuetimeout int `toml:"queuetimeout"`Loginfo struct {Loglevel string `toml:"loglevel"`Logsize string `toml:"logsize"`Lognum int `toml:"lognum"`Logpath string `toml:"logpath"`} `toml:"loginfo"`
}
第二步,通过第三方库 BurntSushi/toml 为例完成解析,当然你也可以选择其他自己喜欢的第三方开源库。
package mainimport("fmt""github.com/BurntSushi/toml"
)type Server struct {Name string `toml:"name"`Maxconns int `toml:"maxconns"`Queuecap int `toml:"queuecap"`Queuetimeout int `toml:"queuetimeout"`Loginfo struct {Loglevel string `toml:"loglevel"`Logsize string `toml:"logsize"`Lognum int `toml:"lognum"`Logpath string `toml:"logpath"`} `toml:"loginfo"`
}func main() {v := Server{}if _, err := toml.DecodeFile("server.toml", &v); err != nil {fmt.Printf("parse toml failed, err=%v\n", err)} else {fmt.Printf("%+v\n", v)}
}
运行输出:
{Name:UserProfileServer Maxconns:1000 Queuecap:10000 Queuetimeout:300 Loginfo:{Loglevel:ERROR Logsize:10M Lognum:10 Logpath:/usr/local/app/log}}
6.配置文件格式的选择
面对常见配置文件格式,使用时该如何选择呢?这里给几个选择的原则:
(1)支持嵌套结构。仅仅支持 KV 结构的键值对表达能力有点弱;
(2)支持注释。不支持注释的 JSON 是给机器读的,不是给人读的;
(3)支持不同的数据类型,而不仅仅是 string。这一点,键值对和 XML 表现的非常逊色;
(4)最好支持 include 其他配置文件,方便配置模块化。复杂的配置也是无奈之举,但如果支持 include 语法,可以方便的把配置文件模块化。
通过以上几个对配置文件的要求,发现键值对不支持层级关系,JSON 不支持注释,可读性较差,虽然 XML 支持注释和层级结构,且可读性较好,但是因为起始标签一定要有个与之对应的结束标签,文件内容较大,解析时占用较多内存,传输时占用较多带宽。所以这里推荐使用 YAML 和 TOML,很多语言都有其 library 实现,跨语言不成问题。
不同系统、框架和组件可能使用自家自研的配置文件格式,因为其不具有普适性和通用性,这里就不做过多的介绍。比如 Tencent 开源 RPC 框架 TarsGo 采用了类似于 XML 的配置格式,像下面这样:
<tars><application>enableset=n<server>node=tars.tarsnode.ServerObj@tcp -h 10.120.129.226 -p 19386 -t 60000app=TestAppserver=HelloServerlocalip=10.120.129.226</server><client>locator=tars.tarsregistry.QueryObj@tcp -h 10.120.129.226 -p 17890sync-invoke-timeout=3000</client></application>
</tars>
参考文献
W3CSchool.JSON 教程
在线XML、JSON数据互转
Golang: Convert JSON to Struct
Go Package json
W3CSchool.XML 教程
Go Package xml
XML to Go struct Online Tool
The Official YAML Web Site
Convert YAML to JSON
go-yaml - Github
Convert YAML to Go struct
YAML 详解与实战-CSDN
YAML - 维基百科,自由的百科全书
Brief YAML reference — Camel 0.1.2 documentation
TOML 官网
TOML 翻译
配置文件格式详解之终极无惑相关推荐
- C++ cin 详解之终极无惑
代码编译运行环境:VS2017+Win64+Debug. 文章目录 1.简介 2.常用输入方法 2.1 cin>> 的用法 2.2 cin.get() 的用法 2.2.1 cin.get( ...
- C printf() 详解之终极无惑
FBI WARNING 鄙人首个开源电子书 <Go 编码建议>已经上线啦,欢迎各位大佬斧正指导,协同共建 编译环境:Linux 环境以 g++ 4.4.6 编译成 64 位程序 文章目录 ...
- C语言printf() 详解之终极无惑
关注.星标公众号,直达精彩内容 来源:CSDN - 恋猫大鲤鱼 编译环境:Linux 环境以 g++ 4.4.6 编译成 64 位程序 1.printf() 简介 printf() 是 C 语言标准库 ...
- printf()详解之终极无惑
1.printf()简介 printf()是C语言标准库函数,用于将格式化后的字符串输出到标准输出.标准输出,即标准输出文件,对应终端的屏幕.printf()申明于头文件stdio.h. 函数原型: ...
- C++ 数组与指针详解之终极无惑
代码编译运行环境:VS2017+Debug+Win32 文章目录 1.数组 1.1数组名的意义 1.2数组的初始化 2.指针 2.1指针的定义 2.2定义指针的形式 2.3指针的初始化 2.4指针可以 ...
- printf()详解之终极无惑-转载
https://blog.csdn.net/k346k346/article/details/52252626 http://www.cplusplus.com/reference/cstdio/pr ...
- C++ 数据类型转换详解之终极无惑
程序开发环境:VS2017+Win32+Debug 文章目录 1. 隐式数据类型转换 2. 显示数据类型转换 3.C++ 新式类型转换 3.1 const_cast 3.2 static_cast 3 ...
- Go Context 详解之终极无惑
文章目录 1.什么是 Context 2.为什么要有 Context 3.context 包源码一览 3.1 Context 3.2 CancelFunc 3.3 canceler 3.4 Conte ...
- 高通thermal-engine配置文件格式详解
本文基于高通msm8939/8994平台文档及源码. 说明文档: vendor/qcom/proprietary/thermal-engine/readme.txt: 源码文件: venr ...
最新文章
- 山东计算机类好的民办大学,山东四大坑人学校-山东坑人的民办大学(野鸡大学)...
- javascript扩展插件alook_使用 Kotlin 编写你的第一个 Firefox WebExtension 扩展
- 【Python基础】使用Matplotlib可视化数据的5个强大技巧
- IntelliJ IDEA快捷键(Shortcut)官方文档地址
- Amr and Pins
- 使用“即时消息服务框架”(iMSF)实现分布式事务的三阶段提交协议(电商创建订单的示例)...
- .htaccess防盗链方法
- 2015-04-11一些知识点
- 【ElasticSearch】Es 源码之 UsageService 源码解读
- D3 scalePow
- 【LINUX系列】之字符串搜索命令
- MVC+WebApi+Restful
- [C语言 - 13] 运算符
- foobar2000 正式登陆 iOS/Android
- idea中加入git版本控制
- windows开机启动方法
- 随机生成姓名代码java
- 婚宴座位图html5,婚宴座位安排图 婚宴主桌安排示意图
- raster包—projectRaster函数
- 5g的八大关键指标_5G的七大关键性能指标5G对生活的改变新5G时代的来临身边巨变...
热门文章
- Lessonnbsp;13nbsp;Anbsp;newnbsp;dressnbsp;一件新连衣…
- 小白请教三菱plc串口通讯
- java 链表 深拷贝_单链表深拷贝的实现
- window docker 安装 宝塔bt
- 前端进阶之路——域名(domain)
- 第一章 Oracle介绍
- 数据结构实验大作业(将之前预测ACM获奖的模型搬到Vue和django上)
- C语言慈善募捐程序(在全院10000学生中,征集慈善募捐,当总数达到10万元时就结束,统计此时捐款的人数,以及平均每人捐款的数目。)
- A - Cthulhu
- ecshop 属性自动组合_SpringBoot单元测试:MockMvc的自动配置