Chapter 21: The I/O Library
21.1 The Simple I/O Model
io.write 函数接受任意数量的参数,并将它们写入当前output file。
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.write(string.format("sin (3) = %.4f/n", 3.14159)) -- 四舍五入
--> sin (3) = 3.1416
io.write(a,b,c) 和io.write(a..b..c) 的效果一样,而且避免了连接操作。
不同于print,write 不会自动添加如tabs 或换行符
print("hello", "Lua"); print("Hi")
--> hello Lua
--> Hi
io.write("hello", "Lua"); io.write("Hi", "/n")
--> helloLuaHi
print 总是使用standard output,而wirte 使用current output
print 会对其参数自动应用tostring
print({}) -->table: 003CAB88
print(nil) -->nil
io.read 从curren input file 读strings。传给它的参数控制其读的内容:
“*all” 读整个文件
“*line” 读下一行
“*number” 读一个数
“num” 读一个string,最多读num 字符
io.read("*all") 从当前位置读整个current input file。如果当前位置处于文件的尾部,或文件是空文件,这个调用返回空串。
因为Lua 处理长strings 非常高效,一个实现string过滤器的简单技术是从文件读整个串,处理这个串(典型的是使用gsub 进行全局替换),然后将串写入output。
t = io.read("*all") -- read the whole file
t = string.gsub(t, ...) -- do the job
io.write(t) -- write the file
作为一个完整的例子,以下代码是将文件的内容使用MIME 编码(quoted-printable)。这种编码将非ASCII 字符编码成 =xx,其中xx 是字符的十六进制数码。为保持一致性,字符“=”必须被妥善处理。
将文件用MIME 编码
t = io.read("*all")
t = string.gsub(t, "([/128-/255=])", function (c)
return string.format("=%02X", string.byte(c))
end)
io.write(t)
"([/128-/255=])" 这个模式捕获所有值为128 到255 的字符和“=”字符。
io.read("*line") 返回current input file 的下一行,并且不带换行符。当到达文件尾,io.read("*line") 返回nil。通常,仅当程序自然的需要一行一行处理文件的时侯才使用io.read("*line");否则,我更喜欢使用io.read("*all")一次读取整个文件。
将当前input 加上行号拷贝到当前output
for count = 1, math.huge do
local line = io.read()
if line == nil then break end
io.write(string.format("%6d ", count), line, "/n")
end
但是,对文件进行一行一行的迭代,我们最好使用io.lines 迭代器。
local lines = {}
-- read the lines in table 'lines'
for line in io.lines() do lines[#lines + 1] = line end
-- sort
table.sort(lines)
-- write all the lines
for _, l in ipairs(lines) do io.write(l, "/n") end
io.read("*number") 从curren input file 返回一个数字,这是read 返回数字而不是string 的唯一情形。当一个程序需要从一个文件读许多数,没有作为中间结果的strings 可以改善程序性能。“*number”选项跳过number 前的所有空白,并且接受像这样的数字格式: -3, +5.2, 1000, 和-3.4e-23。如果它在当前文件位置不能找到一个数字(因为错误的格式或到达文件尾部),就返回nil。
假设你有一个文件,每行三个数:
6.0 -3.23 15e12
4.3 234 1000001
...
现在你想打印每行的最大值,你可以只用一行代码读取所有三个数:
while true do
local n1, n2, n3 = io.read("*number", "*number", "*number")
if not n1 then break end
print(math.max(n1, n2, n3))
end
无论如何,你应当总是考虑读取整个文件的方法,然后用gmatch 将它们分开:
local pat = "(%S+)%s+(%S+)%s+(%S+)%s+"
for n1, n2, n3 in string.gmatch(io.read("*all"), pat) do
print(math.max(tonumber(n1), tonumber(n2), tonumber(n3)))
end
"(%S+)%s+(%S+)%s+(%S+)%s+" 表示匹配一个或多个不是空白的字符 + 一或多空白 +不是一或多空白…
read(n) 从input 文件读取n 个字符。如失败返回nil;否则,返回最多n 个字符的串。
下面的例子在lua 中是一种非常高效的做法:
while true do
local block = io.read(2^13) -- buffer size is 8K
if not block then break end
io.write(block)
end
做为一个特殊的例子,io.read(0) 返回空串,if there is more to be read or nil otherwise
21.2 The Complete I/O Model
io.open 读用“r”,写用“w”(写入前擦除原文件),appending 用“a”,还可以加上一个“b”表示打开的是二进制文件。
io.open 如成功则返回文件句柄,失败则返回nil,加上错误消息和错误号。错误号是系统相关的。
print(io.open("non-existent-file", "r"))
--> nil non-existent-file: No such file or directory 2
print(io.open("/etc/passwd", "w"))
--> nil /etc/passwd: Permission denied 13
f = io.open("c://input.txt","r") -- open input file
assert(f)
典型地检查错误用法:
local f = assert(io.open(filename, mode))
从文件逐行读3 个数并输出最大值
-- input.txt
6.0 -3.23 15e12
6.0 -3.23 15e12
4.3 234 1000001
----
local f = assert(io.open("c://input.txt", "r"))
local s = f:read("*all")
local pat = "(%S+)%s+(%S+)%s+(%S+)%/n-"
for n1, n2, n3 in string.gmatch(s, pat) do
print(math.max(tonumber(n1), tonumber(n2), tonumber(n3)))
end
f:close()
I/O 库提供三个预定义的C 流:io.stdin,io.stdout,和io.stderr。所以,你可以直接发送一个消息到错误流:
io.stderr:write(message)
我们可以混合complete model 和simple model。我们不带参数调用io.input() 获得当前文件句柄。设置文件句柄用io.input(handle)。(类似的调用对io.output 也有效)
例如,如果你想临时改变current input file,你可以写类似这样的代码:
Lua 的“文件重定向”
local temp = io.input() -- save current file
io.input("c://input.txt") -- open a new current file
local pat = "(%S+)%s+(%S+)%s+(%S+)%/n-"
for n1, n2, n3 in string.gmatch(io.read("*all"), pat) do
print(math.max(tonumber(n1), tonumber(n2), tonumber(n3)))
end
io.input():close() -- close current file
io.input(temp) -- restore previous current file
A small performance trick
通常,在lua 中一次读整个文件比一行一行读取要快。但是,有时我们必须面对大文件(意思是,数十或数百兆),这种情况下不可能一次读取整个文件。如果你想要在处理这种大文件时获得最大性能,最快的方法是以每次“一大块”(例如 8K 每块)的方式读取大文件。为避免中间断行的问题,只需这样做:
local lines, rest = f:read(BUFSIZE, "*line")
变量rest 将获得当一行中没被读完时剩下的部分大小。我们只需将这两部分连起来组成完整的一行。
输出文件的单词数、行数和字符数(用全局替换函数来计算)
local BUFSIZE = 2^13 -- 8K
--local f = io.input(arg[1]) -- open input file
local f = io.input("c://input.txt") -- open input file
local cc, lc, wc = 0, 0, 0 -- char, line, and word counts
while true do
local lines, rest = f:read(BUFSIZE, "*line")
if not lines then break end
if rest then lines = lines .. rest .. "/n" end
cc = cc + #lines
-- count words in the chunk
local _, t = string.gsub(lines, "%S+", "")
wc = wc + t
-- count newlines in the chunk
_,t = string.gsub(lines, "/n", "/n")
lc = lc + t
end
print(lc, wc, cc)
Binary files
简单模式函数io.input 和io.output 总是默认以文本模式打开文件。在Unix 中,二进制文件和文本文件没差别。但在一些系统,特别是windows,二进制文件必须以特殊flag 打开。为处理这种二进制文件,你必须使用io.open,并在mode string 中加上一个字符“b”
Lua 中处理二进制数据和文本数据是差不多的。一个strings 可以包含任意bytes,并且几乎所有库函数都可以处理任意bytes。你甚至可以对二进制数据进行模式匹配,只要数据中不含一个zero byte。如果你想要匹配这个byte,你可以使用“%z”代替。
通常使用“*all”读整个文件,或是使用参数n,读n 个bytes。作为一个简单例子,下面的程序将一个文本文件从Dos 格式转换成Unix 格式。(就是将“回车 – 换行”转成“换行”)。
将文件从Dos 格式转换成Unix 格式
local inp = assert(io.open("c://input.txt", "rb"))
local out = assert(io.open("c://output.txt", "wb"))
local data = inp:read("*all")
data = string.gsub(data, "/r/n", "/n")
out:write(data)
assert(out:close())
打印二进制文件中找到的所有string
-- 把下面的文本文件的回车0x0D,换行符0x0A 全部用十六进制编缉器用0x00 替换掉
-- input.txt
6.0 -3.23 15e12
6.0 -3.23 15e12
4.3 234 1000001
----
local f = assert(io.open("c://input.txt", "rb"))
local data = f:read("*all")
local validchars = "[%w%p%s]"
local pattern = string.rep(validchars, 6) .. "+%z"
for w in string.gmatch(data, pattern) do
print(w)
end
上面的代码假定一个string 以任意0x00 结尾的六个或更多个有效字符序列,所谓有效是指能被模式pattern 接受的。“%z”匹配byte zero
将文件以“十六进制编缉器”的格式打印
local f = assert(io.open("c://input.txt", "rb"))
local block = 16
while true do
local bytes = f:read(block)
if not bytes then break end
for _, b in pairs{string.byte(bytes, 1, -1)} do
io.write(string.format("%02X ", b))
end
io.write(string.rep(" ", block - string.len(bytes))) -- 前面的输出不足十六个的部分用空格补
io.write(" ", string.gsub(bytes, "%c", "."), "/n") -- 将控制字符(%c control characters)转换成点
end
输出二进制文件中每个byte 的十六进制表示,接着在后面输出这个byte 的文本表示,然后将控制字符(%c control characters)转换成点“.”。注意这个{string.byte(bytes,1,-1)} 的使用。
21.3 Other Operations on Files
tmpfile 函数返回一个临时文件的句柄,以读写的模式打开。当你的程序结束时,这个文件会自动被删除。flush 函数将所有还没有写入文件的内容立既写入文件。像write 函数一样,你可以这样调用io.flush(),flush 到current output file;或是这样调用f:flush(),flush 到一个特定文件。
seek 函数可以同时获得或设置一个文件的当前位置。通常形式为f:seek(whence,offset)。whence 参数是一个string,指定如何去解释偏移。file:seek("set") 将文件定位到头部,并返回0;file:seek("end") 将文件定位到尾部,并返回文件的大小。
获得文件大小,而且不改变文件的当前位置
local f = assert(io.open("c://input.txt", "rb"))
function fsize (file)
local current = file:seek() -- get current position
local size = file:seek("end") -- get file size
file:seek("set", current) -- restore position
return size
end
print(fsize(f))
f:close()
这些函数出错时都返回nil 加上错误消息。