《Programming in Lua 3》读书笔记(十八)
日期:2014.7.28
PartⅢ The Standard Libraries
22 The I/O Library
Lua的I/O库提供了两种不同的文件处理模式。简单模式以I/O操作是基于当前的输入文件和输出文件的;而完全模式则提供了完整的文件处理方式,这种方式采用了面向对象的思想,将所有操作都处理为各种方法。
简单模式对一般上的I/O操作来说是非常方便的,而有些时候例如同时处理多个文件的情况则需要完全模式了。
22.1 The Simple I/O Model
简单模式对两个current file处理其所有的操作。标准库将当前输入文件(current input file)初始化为标准输入(stdin)处理,而将输出文件(current output file)初始化为标准输出(stdout)处理。因此当我们实现如io.read()功能的时候,我们实际上是通过标准输入(standard input)来读取数据的。
我们可以使用io.input 和 io.output函数来改变这些输入输出文件。io.input(filename)打开了指定名字的文件,然后设定了之后的输入文件都是这个文件,知道再通过io.input改变来改变;io.output则可以针对输出工作做出类似的处理。而针对错误处理,两个函数都可以抛出错误,但是要立即处理这些错误则需要使用完全模式。
首先介绍write函数(作者的意识是write比read更简单),该函数接受任意数量的字符串型参数然后将这些字符串型变量写进当前的输出文件中,该函数遵循通用的转换规则进行数值转字符串处理;而如果想完整的控制这种转换,则需要使用string.format函数:
e.g. --Simple Mode io.write("sin(3) = ",math.sin(3),"\n") --sin(3) = 0.14112000805987 io.write(string.format("sin(3) = %.4f\n",math.sin(3))) --sin(3) = 0.1411
要注意避免写:io.wirte(a..b..c)这样的格式,lua实现io.write(a,b,c)操作使用的资源会更少。
作为一个约定:在debugging过程中应该使用print函数,而只有在需要严格做输出格式控制的时候使用write函数:
e.g. print("hello","Lua");print("Hi") --hello Lua --Hi io.write("hello","Lua");io.write("HI","\n") --helloLuaHI
write函数在输出的时候不会额外的添加字符(如tab和下一行)。不仅如此,write函数允许改变输出函数,而print总是使用标准输出(standard output)。最后,print会自动对参数执行tostring操作,这对于debugging来说是挺方便的,但是这可能会在你稍不注意的时候隐藏了一些bug。
io.read函数从输入文件中读取字符串,该函数的参数控制该读取什么:
"*a" 读取整个文件
"*l" 读取下一行(没有 newline字符)
"*L" 读取下一行(有 newline)
"*n" 读取一个数字
num 读取字符串直到遇到数值字符
io.read("*a") 读取整个输入文件,当到达输入文件末尾或者输入文件为空的时候,返回一个空字符串;
因为lua处理长字符串是高效的,因此如果想控制输出过滤可以首先使用read函数将整个文件执行写入操作成为一个字符串,然后通过string的标准库函数gsub做下一步处理,最后使用write函数进行输出:
e.g. t = io.read("*a") t = string.gsub(t, …) --do something io.write(t)
io.read("*l")/io.read("*L")当到达了文件的末尾,函数返回nil;
io.read("*n") 从当前的输入文件中读取一个数字。这是read函数返回一个数值的唯一情况,*n 会跳过空格并且接受所有的数值格式:-3,+5.2等。当该函数从当前的输入文件中找不到数值的时候,将会返回nil。
read函数可以接受多个控制参数,对于每个参数,函数会返回相应的结果,如果一行文件有三个数,我们需要读取这些数据然后得到最大值:
6.0 -3.23 15e12 4.3 234 100001 … e.g. while true do local n1,n2,n3 = io.read("*n","*n","*n) if not n1 then break end print(math.max(n1,n2,n3)) end
除去这些基本的read模式,也可以传递一个数值n调用read函数,这种情况下read会试图从输入文件中读取n个字符,如果不能从文件中读取任何字符那么将会返回nil,否则会返回最多由n个字符组成的字符串。以下例子介绍了一个高效的从stdin复制文件到stdcout的方法:
e.g. while true do local block = io.read(2^13) if not block then break end io.write(block) end
io.read(0)具有特殊用途:用来测试是否达到了文件结尾,在还能读取的时候将会返回一个空的字符串否则将会返回nil。
22.2 The Complete I/O Model
这种模式是为了在I/O操作时能提供更多的控制方法。这种模式的核心概念是——文件句柄,相当于C中的stream(FILE*):it represents an open file with a current position(代表着当前打开的文件及当前读取的位置).
为了打开一个文件,使用io.open函数,参数是文件名和另一个用来控制模式的字符串参数:'r'代表打开文件用来读取,'w'代表打开文件用来写入(会擦除打开的文件中在该位置之前的文件内容),'a’代表打开文件用来在末尾追加内容,可选指令'b'用来打开二进制文件,如:'rb',打开二进制文件用来读取。open函数返回一个新的句柄,在遇到错误的时候,函数返回nil+错误信息和一个错误代码:
e.g. print(io.open("non-existent-file","r")) -- nil no-existent-file:No such file or directory 2 print(io.open("/etc/passwd","w")) --nil /etc/passwd:Permission denied 13
错误代码表示的意思依系统而定。
检查错误的小技巧:
e.g. local f = assert(io.open(filename,mode))
打开了一个文件之后,可以使用方法read/write对文件执行读取操作或者写入操作。这些方法类似于read/write函数,只不过现在是以方法进行调用,使用冒号操作符(:),例如:打开一个文件,然后读取其所有内容:
e.g. local f = assert(io.open(filename,"r")) --打开文件用来读取内容 local t = f:read("*a") --调用read方法,执行读取全部文件内容的操作 f:close()
Lua的标准I/O库提供了三个预定义的C stream句柄:io.stdin,io.stdout,io.stderr.可以使用如下方式给error stream发送错误信息:
e.g. io.stderr:write(message)
我们可以混合使用标准模式(简单模式)和完全模式进行I/O处理。可以通过不带参数调用io.input()得到当前的输入文件句柄,可以通过带参数调用io.input(filename)来设置当前的输入文件句柄(io.output也可以执行类似的操作),例如:想临时改变当前的输入文件,可以执行如下操作:
e.g. local temp = io.input() --得到当前的输入文件 io.input("new") --设置新的输入文件 io.input():close() --关闭当前的输入文件 io.input(temo) --还原之前的输入文件
也可以通过io.lines来读取文件。用io.lines来读取文件的时候,该函数的第一个参数可以是一个文件名字也可以是一个文件句柄。当是文件名字的时候,io.lines将会以读取模式打开该文件并且在达到该文件末尾的时候关闭该文件;而当是文件句柄的时候,以该句柄进行文件读取,但是不会在读取之后关闭该文件。当不带参数调用该函数的时候,io.lines会从当前的输入文件中读取内容。
从Lua5.2开始,io.lines也接受同io.read一样的操作指令作为第二个参数。下面的例子复制一个文件到当前的输出文件中:
for block in io.lines(filename,2^13) do io.write(block) end
A small performance trick
通常在lua中,一次全部读取一个文件会比一行一行的读要快,但是当读取一个非常大的文件的时候,就需要改变这个策略了。此时最佳的做法是每次读取控制在一个合理范围内的块(作者建议是 8kB),to avoid the problem of breaking lines in the middle(),可以在read函数的参数上做点小技巧:
e.g. local lines,rest = f:read(size,"*l") The variable rest will get the rest of any line broken by the chunk,we then concatenate the chunk and this rest of line.this way,the resulting chunk will always break at line boundaries.
Binary files
标准模式下函数io.input/io.output函数是操作文本格式的文件的。在UNIX中,文本文件和二进制文件是没有区别的,但是在别的系统中,尤其是window系统中,二进制文件必须以特殊的方式打开。为了处理这些二进制文件,必须使用io.open,使用参数'b'.
Lua处理二进制文件类似于处理普通的文本文件,lua中的字符串可以存入任何字节,而标准库中几乎所有的函数都可以处理二进制字节。也可以使用二进制数据进行模式匹配,当然此时需要使用字符类:%z.
可以使用'*a",读取整个二进制文件,也可以使用n读取n个字节。以下例子实现的功能是:将windows格式的文本文件转换成UNIX格式。没有使用标准的I/O库(stdin-stdout),因为这些文件是以二进制文本形式打开的;而是假定将输入文件和输出文件的名字作为函数参数使用:
e.g. local inp = assert(io.open(arg[1],"rb")) local out = assert(io.open(arg[2],"wb")) local data = inp:read("*a") data = string.gsub(data,"\r\n","\n") out:write(data) assert(out:close())
使用如下方式调用:
lua prog.lua file.dos file.unix
下面这个例子将从二进制文件中读取到的数据打印出来:
e.g. local f = assert(io.open(arg[1],"rb")) local data = f:read("*a") local validchars = "[%g%s"] local pattern = "(" .. string.rep(validchars,6) .. "+)\0" for w in string.gmatch(data,pattern) do print(w) end
22.3 Other Operations on Files
tmpfile函数返回一个临时文件句柄,以read/write方式打开。被打开的文件会在程序结束的时候自动移除。
flush函数将所有还未写入文件的信息执行写入操作。可以以函数形式使用:io.flush(),将会flush当前的输出文件;也可以以方法形式使用:f:flush(),将特定的文件执行flush操作。
setvbuf方法:设置stream(流)的buff模式。第一个参数是字符串:“no”表示没有缓冲;“full”表示只有当buff缓存已满或者明确指出flush文件,stream流才会执行操作;“line”表示知道新的一行被输出了或者有从别的特殊文件中有输入操作才会将输出信息进行缓存。而对于后两个可选参数,setvbuf接受一个可选参数作为设定缓存大小。
在大多数系统中,标准error流是没有缓存的,而标准output流是以“line”模式进行缓存,因此使用标准输出流执行了某个操作,需要执行flush操作之后才会看到输出消息。
seek方法可以设置和获得当前输入输出文件的操作位置。