本章介绍将数据保存到永久存储设备中的概念,并展示如何使用诸如文件和数据库等不同种类的永久存储设备。
持久化
目前我们见到的大部分程序在某种程度上都是临时的,因为它们只运行一小段时间,产生一些输出,当程序结束时,数据也随之消失。如果再次运行程序,它将以全新的状态开始。
另一类程序是持久的:它们长时间运行(或者一直运行);它们至少将部分数据保存到永久设备(例如硬盘)中;如果关机后再次重启,它们会从上次结束的地方继续运行。
持久程序的例子是各种操作系统,当计算机启动时它们就会一直运行;网络服务程序也是一个实例,它们持续运行,等待来自于互联网的各种请求。
程序维护数据的一个最简单的方法是读写文本文件。我们已经见过程序读取文本文件;本章我们会看到程序写入文本文件。
另外一个选择是将程序状态保存到数据库中,本章我也会演示如何使用一个简单的数据库。
读和写
文本文件是储存在诸如硬盘、闪存这类永久介质中的一系列字符。要写入一个文件,必须以模式 "w" 作为第二个参数来打开它:
julia> fout = open("output.txt", "w")
IOStream(<file output.txt>)
如果文件已经存在,以写入模式打开文件会清除所有原始数据来形成一个空文档,因此要特别小心!如果文件不存在,则会新建一个。open 返回一个文件对象,write 函数将数据写入文件。
julia> line1 = "This here's the wattle,\n";
julia> write(fout, line1)
24
返回值是写入文件的字符数。文件对象持续跟踪当前位置,因此当你再次调用 write 时,它在文件的末尾添加新数据。
julia> line2 = "the emblem of our land.\n";
julia> write(fout, line2)
24
当写入结束后,你应该将文件关闭。
julia> close(fout)
如果你没有关闭文件,它只有在程序结束时才会自动关闭。
格式
写入的参数必须是一个字符串,因此如果我们想在文件中放入其他值,必须将它们转换成字符串。最简单的方法是使用 string 或字符串插入:
julia> fout = open("output.txt", "w");
julia> write(fout, string(150))
3
另一个选择是使用 print(ln) 函数家族。
julia> camels = 42;
julia> println(fout, "I have spotted $camels camels.")
一个更强大的选项是使用 @printf 宏,它使用C语言风格来格式化字符串,详细可参考相关文档。
文件名和路径
文件被组织到目录(也叫做“文件夹”)内。每个运行着的程序都有一个“当前目录”,它是大部分操作的默认目录。例如,当你打开一个文件用于读取时,Julia在当前目录中查找它。
函数 pwd 返回当前目录的名字:
julia> cwd = pwd()
"C:\\Users\\UNAME\\AppData\\Local\\Programs\\Julia\\Julia-1.6.0"
pwd 代表“打印当前工作目录”(print working directory)。本示例的结果展示了Windows平台下Julia的默认当前目录是其安装目录,这里 UNAME 代表你登录Windows的用户名。像 "C:\\Users\\UNAME\\AppData\\Local\\Programs\\Julia\\Julia-1.6.0" 这样标识一个文件或目录的字符串叫做路径。
一个简单的文件名如 memo.txt 也被认为是一个路径,但由于是相对于当前目录而言的,因此它是一个相对路径。如果当前目录是 C:\\Users\\UNAME\\AppData\\Local\\Programs\\Julia\\Julia-1.6.0,文件名 memo.txt 将会指向 C:\\Users\\UNAME\\AppData\\Local\\Programs\\Julia\\Julia-1.6.0\\memo.txt。
以盘符(Windows系统)或 /(Linux系统)开始的路径不依赖于当前目录;它叫做一个绝对路径。要返回一个文件的绝对路径,你可以使用 abspath:
julia> abspath("memo.txt")
"C:\\Users\\UNAME\\AppData\\Local\\Programs\\Julia\\Julia-1.6.0\\memo.txt"
Julia也提供了其他作用于文件名和路径的函数。例如,ispath 检验是否存在一个文件或目录:
julia> ispath("memo.txt")
true
如果存在,isdir 检验它是否为目录:
julia> isdir("memo.txt")
false
julia> isdir("../Julia-1.6.0")
true
相似地,isfile 检验它是否为文件。
readdir 返回一个由给定目录中的文件(和其他目录)组成的数组:
julia> readdir(cwd)
3-element Array{String,1}:
"memo.txt"
"music"
"photos"
为了展示这些函数,下面的例子“遍历”一个目录,打印所有文件的名字,然后对所有目录递归地调用自己。
function walk(dirname)
for name in readdir(dirname)
path = joinpath(dirname, name)
if isfile(path)
println(path)
else
walk(path)
end
end
end
joinpath 接受一个目录和一个文件名,并将它们连接成一个完整的路径。
Julia提供了一个名为 walkdir 的函数(见帮助文档),它与该函数相似但功能更多。作为练习,阅读帮助文档并使用它打印给定目录和它的子目录中的文件。
捕捉异常
当你试图读写文件时,很多地方可能发生错误。如果试图打开一个不存在的文件,你会得到一个系统错误 SystemError:
julia> fin = open("bad_file")
ERROR: SystemError: opening file "bad_file": 没有那个文件或目录
如果你没有访问一个文件的权限:
julia> fout = open("/etc/passwd", "w")
ERROR: SystemError: opening file "/etc/passwd": 权限不够
为了避免这些错误,你可以使用诸如 ispath 和 isfile 这类函数,但它会花费大量时间和代码来检查所有的可能。
更好的办法是在问题出现的时候才去处理,而这正是 try 语句做的事情。它的语法与 if 语句相似:
try
fin = open("bad_file.txt")
catch exc
println("Something went wrong: $exc")
end
Julia开始执行 try 分支。如果一切正常,它会跳过 catch 分支并继续运行。如果发生异常,它会跳出 try 分支转而运行 catch 分支。
使用 try 语句处理异常叫做捕获一个异常。在这个例子中,异常分支只是打印一条出错信息,这显然没有什么用处。一般来说,捕获到一条异常就给了你一个修补问题的机会:或者尝试重新运行,或者至少可以优雅地结束程序。
在执行状态改变或者使用如文件一类资源的代码中,通常在代码结束时要进行清理工作(例如关闭文件)。异常可能会使这项工作变得复杂,因为它们有可能使一组代码在正常结束前退出。finally 关键字提供了不管给定代码块如何退出都会执行一些代码的方法:
f = open("output.txt")
try
line = readline(f)
println(line)
finally
close(f)
end
函数 close 总会被执行。
带分隔符的文件
如果需要读写一个矩阵,可使用带分割符的文件,下面给出一些例子:
julia> using DelimitedFiles
julia> x = [1;2;3;4];
julia> y = [5;6;7;8];
julia> open("delim_file.txt","w") do io
writedlm(io, [x y])
end
julia> readdlm("delim_file.txt",'\t', Int, '\n')
4×2 Matrix{Int64}:
1 5
2 6
3 7
4 8
julia> rm("delim_file.txt")
readdlm中第一个参数为要读入的文件;第二个参数为矩阵的分隔符,如果省略则表示分隔符是一个或多个空格;第三个参数是矩阵元素的类型,如果省略,则当所有数据都是数字时,结果将是一个数字数组,如果某些元素不能被解析为数字,则返回由数字和字符串组成的异构数组;第四个参数标识一行结束的分隔符,如果省略则表示为\n。
下面给出一个省略第二到第四各参数的例子:
julia> x = [1;2;3;4];
julia> y = ["a";"b";"c";"d"];
julia> open("delim_file.txt","w") do io
writedlm(io, [x y])
end
julia> readdlm("delim_file.txt")
4×2 Matrix{Any}:
1 "a"
2 "b"
3 "c"
4 "d"
julia> rm("delim_file.txt")
上面两个例子同时给出了使用writedlm向文件中写一个矩阵的例子。
TOML
TOML的目标是采用明显的语义使其成为一种易于阅读的最小配置文件格式,其文件后缀为.toml。TOML被设计成无歧义地映射到哈希表。TOML很容易被解析为各种语言中的数据结构。Julia内置了对该数据结构的支持。下面将对该文件的编写规范进行介绍。
一般规定
- TOML是区分大小写的
- TOML文件必须是有效的UTF-8编码的Unicode文档
- 空白的含义是制表符(0x09)或空格(0x20)
- 新一行的含义是换行(0x0A)或回车换行(0x0D 0x0A)
注释
一行中井号以后的部分被标记为注释,但井号在字符串中的情况除外。
# This is a full-line comment
key = "value" # This is a comment at the end of a line
another = "# This is not a comment"
注释中不允许出现制表符以外的控制字符(U+0000到U+0008, U+000A到U+001F, U+007F)。
键/值对
TOML文档的主要构建块是键/值对。键在等号的左侧,值在右侧。键名和值周围的空白被忽略。键、等号和值必须在同一行上(尽管有些值可以在多行上分开)。
key = "value"
值必须是以下类型中的一种:
- String
- Integer
- Float
- Boolean
- Offset Date-Time
- Local Date-Time
- Local Date
- Local Time
- Array
- Inline Table
值必须被给出。
key = # INVALID
每个键/值对都要新起一行(或EOF)。(例外情况参见内联表)
first = "Tom" last = "Preston-Werner" # INVALID
键
键可以不使用引号,也可以使用引号,或点号。不带引号的键只能是ASCII字母、ASCII数字、下划线和破折号(A-Za-z0-9_-)。注意,不使用引号的键允许只由ASCII数字组成,例如1234,但总是被解释为字符串。
key = "value"
bare_key = "value"
bare-key = "value"
1234 = "value"
带引号的键与基本字符串或字面字符串遵循完全相同的规则,并允许你使用更广泛的键名集。除非绝对必要,最好使用不带引号的键。
"127.0.0.1" = "value"
"character encoding" = "value"
"???" = "value"
'key2' = "value"
'quoted "value"' = "value"
不带引号的键必须是非空的,带引号的键可以是空的(但不鼓励这样使用)。
= "no key name" # INVALID
"" = "blank" # VALID but discouraged
'' = 'blank' # VALID but discouraged
点号键是用点连接的不带引号键或带引号键的序列。这样可以将相似的属性分组在一起:
name = "Orange"
physical.color = "orange"
physical.shape = "round"
site."google.com" = true
有关点号键定义的表的详细信息,请参阅下面的表部分。点分隔部分周围的空白将被忽略。但是,最好不要使用任何多余的空白。
fruit.name = "banana" # this is best practice
fruit. color = "yellow" # same as fruit.color
fruit . flavor = "banana" # same as fruit.flavor
缩进被视为空白并忽略。多次定义一个键是无效的。
# DO NOT DO THIS
name = "Tom"
name = "Pradyun"
注意,不带引号的键和带引号的键是等价的:
# THIS WILL NOT WORK
spelling = "favorite"
"spelling" = "favourite"
只要键还没有被直接定义,你就仍然可以对它和其中的名称进行写入。
# This makes the key "fruit" into a table.
fruit.apple.smooth = true
# So then you can add to the table "fruit" like so:
fruit.orange = 2
# THE FOLLOWING IS INVALID
# This defines the value of fruit.apple to be an integer.
fruit.apple = 1
# But then this treats fruit.apple like it's a table.
# You can't turn an integer into a table.
fruit.apple.smooth = true
不建议无序地定义点号键。
# VALID BUT DISCOURAGED
apple.type = "fruit"
orange.type = "fruit"
apple.skin = "thin"
orange.skin = "thick"
apple.color = "red"
orange.color = "orange"
# RECOMMENDED
apple.type = "fruit"
apple.skin = "thin"
apple.color = "red"
orange.type = "fruit"
orange.skin = "thick"
orange.color = "orange"
由于不带引号的键只能由ASCII整数组成,因此可以编写看起来像浮点数但实际上是由2部分组成的点号键。除非你有很好的理由(你可能没有),否则不要这样做。
3.14159 = "pi"
字符串
有四种表达字符串的方法:基本、多行基本、字面和多行字面。所有字符串只能包含有效的UTF-8字符。
基本字符串由引号(")包围。可以使用任何Unicode字符,但必须转义的字符除外:引号、反斜杠和制表符以外的控制字符(U+0000到U+0008、U+000A到U+001F、U+007F)。
str = "I'm a string. \"You can quote me\". Name\tJos\u00E9\nLocation\tSF."
为方便起见,一些常用字符具有紧凑的转义序列。
\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 = """
Roses are red
Violets are blue"""
TOML解析器可以自由地将换行符规范化为对其平台有意义的内容。
# On a Unix system, the above multi-line string will most likely be the same as:
str2 = "Roses are red\nViolets are blue"
# On a Windows system, it will most likely be equivalent to:
str3 = "Roses are red\r\nViolets are blue"
如果要写长字符串而不引入额外的空格,请使用“行结束反斜杠”。当一行中最后一个非空白字符是未转义的\时,它将直到下一个非空白字符或结束分隔符的所有空白字符(包括换行符)一起裁剪掉。对基本字符串有效的所有转义序列对多行基本字符串也有效。
# The following strings are byte-for-byte equivalent:
str1 = "The quick brown fox jumps over the lazy dog."
str2 = """
The quick brown \
fox jumps over \
the lazy dog."""
str3 = """\
The quick brown \
fox jumps over \
the lazy dog.\
"""
可以使用任何Unicode字符,但必须转义的字符除外:反斜杠和制表符、换行符和回车符以外的控制字符(U+0000到U+0008、U+000B、U+000C、U+000E到U+001F、U+007F)。
你可以在多行基本字符串的任何位置写入一个引号或两个相邻的引号。它们也可以直接写在分隔符内。
str4 = """Here are two quotation marks: "". Simple enough."""
# str5 = """Here are three quotation marks: """.""" # INVALID
str5 = """Here are three quotation marks: ""\"."""
str6 = """Here are fifteen quotation marks: ""\"""\"""\"""\"""\"."""
# "This," she said, "is just a pointless statement."
str7 = """"This," she said, "is just a pointless statement.""""
如果你经常使用Windows路径或正则表达式的说明符,那么必须每次都转义反斜杠会很乏味且容易出错。为了方便,TOML支持完全不允许转义的字面值字符串。
字面值字符串由单引号包围。像基本字符串一样,它们必须在同一行中:
# What you see is what you get.
winpath = 'C:\Users\nodejs\templates'
winpath2 = '\\ServerX\admin$\system32\'
quoted = 'Tom "Dubs" Preston-Werner'
regex = '<\i\c*\s*>'
因为没有转义,所以没有办法在由单引号括起来的字面值字符串中写单引号。幸运的是,TOML提供的字面值字符串多行版本解决了这个问题。
多行字面值字符串在每边由三个单引号包围,并允许换行。就像字面值字符串一样,没有转义。紧跟在开始分隔符后面的换行符将被裁剪。分隔符之间的所有其他内容都按原样解释,无需修改。
regex2 = '''I [dw]on't need \d{2} apples'''
lines = '''
The first newline is
trimmed in raw strings.
All other whitespace
is preserved.
'''
你可以在多行字面值字符串的任何地方写入1或2个单引号,但三个或更多单引号序列是不允许的。
quot15 = '''Here are fifteen quotation marks: """""""""""""""'''
# apos15 = '''Here are fifteen apostrophes: '''''''''''''''''' # INVALID
apos15 = "Here are fifteen apostrophes: '''''''''''''''"
# 'That,' she said, 'is still pointless.'
str = ''''That,' she said, 'is still pointless.''''
字面值字符串中不允许使用制表符以外的控制字符。因此,对于二进制数据,建议使用Base64或其他合适的ASCII或UTF-8编码。该编码的处理将是特定于应用程序的。
整数
正数可以用加号作为前缀。负数前面有一个减号。
int1 = +99
int2 = 42
int3 = 0
int4 = -17
对于较大的数字,可以在数字之间使用下划线以增强可读性。每个下划线两边必须至少有一位数字包围。
int5 = 1_000
int6 = 5_349_221
int7 = 53_49_221 # Indian number system grouping
int8 = 1_2_3_4_5 # VALID but discouraged
前导零是不允许的。整数值-0和+0是有效的并与不带前缀的零相同。非负整数值也可以用十六进制、八进制或二进制表示。在这些格式中,前导+是不允许的,前导零是允许的(在前缀之后)。十六进制值不区分大小写。数字之间允许有下划线(但前缀和值之间不允许有下划线)。
# hexadecimal with prefix `0x`
hex1 = 0xDEADBEEF
hex2 = 0xdeadbeef
hex3 = 0xdead_beef
# octal with prefix `0o`
oct1 = 0o01234567
oct2 = 0o755 # useful for Unix file permissions
# binary with prefix `0b`
bin1 = 0b11010110
任意的64位带符号整数(从?2?3到2?3 ? 1)都可以被接受并无损地处理。如果一个整数不能无损地表示,则会抛出一个错误。
浮点数
浮点数被表示为IEEE 754 64位二进制值。浮点数由整数部分(遵循与十进制整数值相同的规则)和小数部分和(或)指数部分组成。如果同时存在小数部分和指数部分,小数部分必须在指数部分之前。
# fractional
flt1 = +1.0
flt2 = 3.1415
flt3 = -0.01
# exponent
flt4 = 5e+22
flt5 = 1e06
flt6 = -2E-2
# both
flt7 = 6.626e-34
小数部分是指小数点后面跟着一个或多个的数字。指数部分是一个E(大写或小写)后跟一个整数部分(它遵循与十进制整数值相同的规则,但可以包含前导零)。如果使用小数点,在每边至少有一个数字包围它。
# INVALID FLOATS
invalid_float_1 = .7
invalid_float_2 = 7.
invalid_float_3 = 3.e+20
与整数类似,你可以使用下划线来增强可读性。每个下划线必须至少被一位数字包围。
flt8 = 224_617.445_991_228
浮点值-0.0和+0.0是有效的,并根据IEEE 754进行映射。还可以表示特殊的浮点值。它们总是小写的。
# infinity
sf1 = inf # positive infinity
sf2 = +inf # positive infinity
sf3 = -inf # negative infinity
# not a number
sf4 = nan # actual sNaN/qNaN encoding is implementation-specific
sf5 = +nan # same as `nan`
sf6 = -nan # valid, actual encoding is implementation-specific
布尔值
布尔值就是你习惯使用的符号并总是小写。
bool1 = true
bool2 = false
带偏移量的日期时间
要明确地表示一个特定的时间点,可以使用带有偏移量的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
数组
数组是方括号,里面有值。空格将被忽略。元素之间用逗号分隔。数组可以包含键/值对中允许的相同数据类型的值。不同类型的值也可以混合使用。
integers = [ 1, 2, 3 ]
colors = [ "red", "yellow", "green" ]
nested_arrays_of_ints = [ [ 1, 2 ], [3, 4, 5] ]
nested_mixed_array = [ [ 1, 2 ], ["a", "b", "c"] ]
string_array = [ "all", 'strings', """are the same""", '''type''' ]
# Mixed-type arrays are allowed
numbers = [ 0.1, 0.2, 0.5, 1, 2, 5 ]
contributors = [
"Foo Bar <foo@example.com>",
{ name = "Baz Qux", email = "bazqux@example.com", url = "https://example.com/bazqux" }
]
数组可以跨多行。允许在数组的最后一个值后面使用结束逗号(也称为尾随逗号)。任意数量的换行符和注释可以出现在值、逗号和右括号之后。数组值和逗号之间的缩进被视为空白并被忽略。
integers2 = [
1, 2, 3
]
integers3 = [
1,
2, # this is ok
]
表
表(也称为哈希表或字典)是键/值对的集合。它们由一行中使用方括号包围的头文件定义。你可以很容易地区分头文件和数组,因为数组永远都是值。
[table]
在下一个头文件或EOF之前,都是该表的键/值。表中的键/值对不能保证以任何特定的顺序排列。
[table-1]
key1 = "some string"
key2 = 123
[table-2]
key1 = "another string"
key2 = 456
表的命名规则与键的命名规则相同(参见上面的键的定义)。
[dog."tater.man"]
type.name = "pug"
键周围的空格将被忽略。但是,最佳实践是不要使用任何多余的空格。
[a.b.c] # this is best practice
[ d.e.f ] # same as [d.e.f]
[ g . h . i ] # same as [g.h.i]
[ j . "?" . 'l' ] # same as [j."?".'l']
缩进被视为空白并被忽略。如果你不想逐一指定所有的上级表,则不需要指定。TOML知道如何为你做到这一点。
# [x] you
# [x.y] don't
# [x.y.z] need these
[x.y.z.w] # for this to work
[x] # defining a super-table afterward is ok
没有键/值对的空表是允许的。与键一样,你不能对表进行多次定义。这样做是无效的。
# DO NOT DO THIS
[fruit]
apple = "red"
[fruit]
orange = "orange"
# DO NOT DO THIS EITHER
[fruit]
apple = "red"
[fruit.apple]
texture = "smooth"
不鼓励无序定义表。
# VALID BUT DISCOURAGED
[fruit.apple]
[animal]
[fruit.orange]
# RECOMMENDED
[fruit.apple]
[fruit.orange]
[animal]
最上一级的表,也称为根表,是从文档的开头开始,在第一个表头(或EOF)之前结束。与其他表不同,它是匿名的,不能被重新定位。
# Top-level table begins.
name = "Fido"
breed = "pug"
# Top-level table ends.
[owner]
name = "Regina Dogman"
member_since = 1999-08-04
点号键为最后一个键之前的每个键创建并定义一个表,前提是这些表以前没有创建过。
fruit.apple.color = "red"
# Defines a table named fruit
# Defines a table named fruit.apple
fruit.apple.taste.sweet = true
# Defines a table named fruit.apple.taste
# fruit and fruit.apple were already created
由于表不能重复定义,所以不允许使用[table]头重新定义这样的表。同样,不允许使用点号键重新定义已经以[table]形式定义的表。然而,[table]形式可以用来在点号键定义的表中定义子表。
[fruit]
apple.color = "red"
apple.taste.sweet = true
# [fruit.apple] # INVALID
# [fruit.apple.taste] # INVALID
[fruit.apple.texture] # you can add sub-tables
smooth = true
内联表
内联表为表示表提供了更紧凑的语法。它们对于分组数据特别有用,否则这些数据很快就会变得冗长。内联表完全在大括号{和}中定义。在大括号内,可能出现零个或多个以逗号分隔的键/值对。键/值对的形式与标准表中的键/值对相同。允许所有值类型,包括内联表。
内联表应该出现在单行上。在内联表的最后一个键/值对之后不允许使用终止逗号(也称为尾随逗号)。在大括号之间不允许换行,除非换行在值内有效。尽管如此,还是强烈建议不要将内联表拆分为多行。如果你发现的确需要这样做,这意味着你应该使用标准表了。
name = { first = "Tom", last = "Preston-Werner" }
point = { x = 1, y = 2 }
animal = { type.name = "pug" }
上面的内联表与下面的标准表定义相同:
[name]
first = "Tom"
last = "Preston-Werner"
[point]
x = 1
y = 2
[animal]
type.name = "pug"
内联表是完全自包含的,并在其中定义所有键和子表。不能再在大括号之外添加键和子表。
[product]
type = { name = "Nail" }
# type.edible = false # INVALID
类似地,内联表也不能用于向已经定义的表添加键或子表。
[product]
type.name = "Nail"
# type = { edible = false } # INVALID
表数组
还没有介绍的最后一种语法允许写表数组。这些可以通过使用双括号中带有名称的头来表示。该头文件的第一个实例定义数组及其第一个表元素,每个后续实例在该数组中创建并定义一个新的表元素。表按出现的顺序插入到数组中。
[[products]]
name = "Hammer"
sku = 738594937
[[products]] # empty table within the array
[[products]]
name = "Nail"
sku = 284758393
color = "gray"
任何对表数组的引用都指向该数组中最近定义的表元素。这允许你在最近的表中定义子表,甚至是子表数组。
[[fruits]]
name = "apple"
[fruits.physical] # subtable
color = "red"
shape = "round"
[[fruits.varieties]] # nested array of tables
name = "red delicious"
[[fruits.varieties]]
name = "granny smith"
[[fruits]]
name = "banana"
[[fruits.varieties]]
name = "plantain"
如果表或表数组的父元素是数组元素,则必须在定义子元素之前已经定义了该元素。试图颠倒这种顺序会在解析时产生错误。
# INVALID TOML DOC
[fruit.physical] # subtable, but to which parent element should it belong?
color = "red"
shape = "round"
[[fruit]] # parser must throw an error upon discovering that "fruit" is
# an array rather than a table
name = "apple"
试图添加到静态定义的数组中,即使该数组是空的,也必须在解析时产生错误。
# INVALID TOML DOC
fruits = []
[[fruits]] # Not allowed
试图定义一个与已经建立的数组同名的普通表,在解析时必然产生错误。试图将普通表重定义为数组同样会产生解析时错误。
# INVALID TOML DOC
[[fruits]]
name = "apple"
[[fruits.varieties]]
name = "red delicious"
# INVALID: This table conflicts with the previous array of tables
[fruits.varieties]
name = "granny smith"
[fruits.physical]
color = "red"
shape = "round"
# INVALID: This array of tables conflicts with the previous table
[[fruits.physical]]
color = "green"
你也可以在适当的地方使用内联表:
points = [ { x = 1, y = 2, z = 3 },
{ x = 7, y = 8, z = 9 },
{ x = 2, y = 4, z = 8 } ]
在Julia中使用TOML.parsefile解析输入文件,TOML.print将数据打印成TOML格式。
julia> using TOML
julia> data = Dict(
"names" => ["Julia", "Julio"],
"age" => [10, 20]
);
julia> fname = tempname();
julia> open(fname, "w") do io
TOML.print(io, data)
end
julia> TOML.parsefile(fname)
Dict{String, Any} with 2 entries:
"names" => ["Julia", "Julio"]
"age" => [10, 20]
数据库
数据库是“按照数据结构来组织、存储和管理数据的仓库(文件)”。Julia为多种数据库提供了接口,方便我们使用和开发。本节以最简单的SQLite数据库为例来介绍在Julia环境下数据库的简单用法。在使用SQLite数据库前,我们要先在电脑中安装它,一般Linux系统都自带了该库,Windows系统需要自己安装,由于安装过程比较简单,请读者参照官方网站自行安装。有了数据库后,我们还要在Julia中安装相应的模块:
(@v1.6) pkg> add DataFrames, SQLite
接下来我们就可以使用相关函数对数据库进行操作了:
julia> using DataFrames, SQLite
julia> db = SQLite.DB("test.db")
SQLite.DB("test.db")
SQLite.DB 函数用于创建或打开一个sqlite数据库文件,当文件存在时就打开它,否则就创建一个新的数据库文件。变量 db 存储函数返回的数据库对象,我们可以使用该变量对数据库进行各种操作:
julia> DBInterface.execute(db, "create table test (id integer primary key, value text)");
julia> DBInterface.execute(db, "insert into test (id, value) values (101,'eenie')");
julia> DBInterface.execute(db, "insert into test (id, value) values (102,'meenie')");
julia> DBInterface.execute(db, "insert into test (id, value) values (103,'miny')");
julia> DBInterface.execute(db, "insert into test (id, value) values (104,'mo')");
julia> DBInterface.execute(db, "select * from test") |> DataFrame
4×2 DataFrame
│ Row │ id │ value │
│ │ Int64 │ String │
├─────┼───────┼────────┤
│ 1 │ 101 │ eenie │
│ 2 │ 102 │ meenie │
│ 3 │ 103 │ miny │
│ 4 │ 104 │ mo │
这里使用 DBInterface.execute 函数对表格进行了创建、增加条目和查询等操作。由于函数的第二个参数涉及到的SQL语句超出了本书的范围,故不进行深入介绍。
序列化
可以想象,我们向数据库中存储字符、数字还是比较容易的,但如果要存储图片或者音、视频,该如果操作?这里我们就要用到序列化的方法。
函数 serialize 可以将几乎任何类型的对象转换成一个适合于数据库存储的字节数组(一个读写缓存),而 deserialize 可以将字节数组转换回对象:
julia> using Serialization
julia> io = IOBuffer();
julia> t = [1, 2, 3];
julia> serialize(io, t)
24
julia> print(take!(io))
UInt8[0x37, 0x4a, 0x4c, 0x09, 0x04, 0x00, 0x00, 0x00, 0x15, 0x00, 0x08, 0xe2, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x03, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00]
这种结构不适合人们阅读,但对Julia来说却更易于解读。deserialize 重新构建对象:
julia> io = IOBuffer();
julia> t1 = [1, 2, 3];
julia> serialize(io, t1)
24
julia> s = take!(io);
julia> t2 = deserialize(IOBuffer(s));
julia> print(t2)
[1, 2, 3]
上例,serialize 和 deserialize 对代表内存中读写流的一个读写缓存对象进行了写入和读取操作。函数 take! 取出读写缓存中的内容——字节数组并将读写缓存重置为它的初始状态。
尽管新对象与老对象的值相同,但它们不是(一般来说)同一个对象:
julia> t1 == t2
true
julia> t1 === t2
false
换句话说,序列化然后再解序列化的效果与复制对象相同。
你可以使用这种方法在数据库中存储非字节对象。
实际上,在数据库中存储非字节对象是一个很普遍的现象,Julia将这种功能封装成一个名为 JLD2 的库包。
JLD2是使用HDF5格式保存和提取Julia数据结构的一种文件格式,其作用类似于Matlab中的 .mat 文件。操作JLD2文件最简单的方法是使用宏 @save 和 @load。宏 @save 向文件中写入变量:
julia> using JLD2
julia> hello = "world";
julia> arr = [1, 2, 3];
julia> @save "file.jld2" hello arr
@save 创建一个新文件 file.jld2 并将变量 hello 和 arr 写入到该文件中。 宏 @load 可以从JLD2文件中将这些变量提取出来:
@load "file.jld2" hello arr
这条语句将存储在文件中的变量 hello 和 arr 的内容提取出来并赋给当前作用域内的同名变量。
命令对象
大多数操作系统提供了一个命令行接口,也被称为壳(Windows系统提供了壳和Dos系统下的命令行接口)。壳通常提供用于浏览文件系统和启动应用程序的命令。例如,在Unix系统中可以使用 cd 来改变目录,ls 显示目录中的内容, firefox(只是举例)启动一个网页浏览器。
在壳中可以启动的任何程序都可以使用命令对象在Julia中启动(本书以Dos命令窗口为例):
julia> a = `cmd /c echo hello`
`cmd /c echo hello`
在Windows系统中, echo 不是一个可执行程序,它只是可执行程序 cmd 中的一项功能,因此在Windows中调用 echo 命令实际上要调用 cmd 命令中的 echo 功能。
`‘所包围的内容是一条系统命令,函数 run 用来执行命令:
julia> run(a);
hello
hello 是echo命令的输出结果,被发送到 STDOUT。run 函数本身返回一个进程对象,如果外部命令执行失败则抛出一个 ErrorException 异常。
如果你想读入外部命令的输出结果,可以使用 read 函数:
julia> b = read(a, String)
"hello\r\n"
举一个例子,大多数Unix系统都提供了一个 md5sum 或 md5 命令用来读取一个文件中的内容并计算出一个“校验和”。Windows默认不具有这个程序,需要从网站下载,本书也提供了该文件。将可执行文件 md5.exe 放入到 %SystemRoot%\system32\ 就可以。这条命令为检验两个文件是否有相同的内容提供了一个高效的方法。内容不同而取得相同校验和的概率非常小。
你可以在Julia中使用命令对象执行 md5 并得到结果:
julia> filename = "emma.txt";
julia> a = `md5 $filename`
`md5 emma.txt`
julia> res = read(a, String)
"670533E523E86AC303F2CF6980A0AF7E emma.txt\r\n"
模块
假设一个名为 "wc.jl" 的文件中有如下代码:
function linecount(filename)
count = 0
for line in eachline(filename)
count += 1
end
count
end
print(linecount("wc.jl"))
如果你运行这个程序,它读入自己并打印文件的行数,结果是9。你也可以在REPL使用 include 函数:
julia> include("wc.jl")
9
Julia引入模块来创建一个单独的变量工作空间,也即一个新的全局作用域。
模块以关键字 module 开始,end 结束。这样可以避免你自己的顶层定义和别人代码中的名字发生冲突。import 允许控制来自于其他模块的哪些名字是可见的;export 指明你使用的名字中哪些是公有的,即在模块外不需要使用模块名作为前缀就可以使用的。
module LineCount
export linecount
function linecount(filename)
count = 0
for line in eachline(filename)
count += 1
end
count
end
end
模块 LineCount 对象提供了 linecount:
julia> using LineCount
ERROR: ArgumentError: Package LineCount not found in current path:
从报错信息可以看到,模块 LineCount 没有在当前搜索路径上。using 使用 LOAD_PATH 内保存的路径进行搜索,因此我们只需将 LineCount 模块所在的目录添加到 LOAD_PATH 内即可:
push!(LOAD_PATH, "path to LineCount" )
再次使用模块:
julia> using LineCount
julia> linecount("LineCount.jl")
11
同样,新增路径只在当前场景内有效,下次启动Julia后如果还要用到该路径,你需要再次手动添加。你也可以将这条语句加到 startup.jl 文件中实现自动化。
本文暂时没有评论,来添加一个吧(●'◡'●)