第 15 章 文件与输入输出
第 15 章 文件与输入输出
Lua 的 io 库提供文件打开、读取、写入和标准输入输出。它简单但偏底层,路径安全、编码、原子写入和并发控制需要应用自行处理。
io 库基础
标准输出:
io.write("hello", " ", "lua", "\n")
标准输入:
io.write("请输入名称: ")
local name = io.read("l")
print("你好,", name)
io.read 常用格式:
| 格式 | 含义 |
|---|---|
"l" 或 "*l" |
读取一行,不含换行 |
"L" 或 "*L" |
读取一行,包含换行 |
"a" 或 "*a" |
读取剩余全部内容 |
"n" 或 "*n" |
读取一个数字 |
| 数字 | 最多读取指定字节数 |
io.write 不自动添加分隔符或换行,格式化内容应使用 string.format。
读写文本文件
io.open 返回文件句柄;失败时返回 nil、错误消息和系统错误码:
local function readFile(path)
local file, message, code = io.open(path, "r")
if not file then
return nil, {
message = message,
code = code
}
end
local content = file:read("*a")
file:close()
return content
end
写入文件:
local function writeFile(path, content)
local file, message = io.open(path, "w")
if not file then
return nil, message
end
local ok, writeError = file:write(content)
local closeOk, closeError = file:close()
if not ok then
return nil, writeError
end
if not closeOk then
return nil, closeError
end
return true
end
模式 "w" 会截断已有文件。读取时可使用 "r",追加使用 "a",同时读写使用 "r+" 或 "w+"。
逐行读取
local file, message = io.open("app.log", "r")
if not file then
error(message)
end
for line in file:lines() do
if line:find("ERROR", 1, true) then
print(line)
end
end
file:close()
file:lines 每轮返回一行,结束时返回 nil。处理超大文件时不要用 read("*a")。
二进制文件
追加 b 可以明确二进制模式,尤其在 Windows 上:
local file = assert(io.open("image.bin", "rb"))
local header = file:read(8)
file:close()
读取固定字节:
local function copyBinary(sourcePath, targetPath, chunkSize)
local source = assert(io.open(sourcePath, "rb"))
local target = assert(io.open(targetPath, "wb"))
chunkSize = chunkSize or 8192
while true do
local chunk = source:read(chunkSize)
if not chunk then
break
end
assert(target:write(chunk))
end
source:close()
target:close()
end
定位
seek 的起点参数包括 "set"、"cur" 和 "end":
local file = assert(io.open("data.bin", "rb"))
print(file:seek("end")) -- 文件大小
file:seek("set", 0)
print(file:read(4))
file:close()
文本模式下的任意 seek 可能受换行转换影响。需要精确定位时使用二进制模式。
路径与序列化
Lua 标准库没有现代路径 API。package.config 的第一行可以取得目录分隔符:
local directorySeparator = package.config:sub(1, 1)
local path = "data" .. directorySeparator .. "tasks.txt"
拼接路径时不要假设 / 在所有平台都适用。也不要直接把用户输入拼进文件路径:
local function rejectTraversal(path)
if path:find("..", 1, true) then
return nil, "path traversal is not allowed"
end
return path
end
这种字符串检查只是第一层防护。真正安全的路径访问需要宿主程序提供规范化路径,并确认结果仍位于允许目录内。
序列化为 Lua 字面量
%q 可以转义字符串,适合生成本地可信数据文件:
local function saveTasks(path, tasks)
local file = assert(io.open(path, "w"))
file:write("return {\n")
for _, task in ipairs(tasks) do
file:write(string.format(
" { id = %d, title = %q, status = %q },\n",
task.id,
task.title,
task.status
))
end
file:write("}\n")
file:close()
end
读取时可以使用 loadfile:
local function loadTasks(path)
local chunk, message = loadfile(path)
if not chunk then
return nil, message
end
local ok, tasks = pcall(chunk)
if not ok then
return nil, tasks
end
return tasks
end
这实际上会执行文件内容,只能用于程序自己生成且用户不可篡改的数据。加载来自网络或用户上传的文件时,应使用受限制的格式解析器。
安全的文件处理
原子写入
直接覆盖配置时,如果进程在写入中途崩溃,文件可能损坏。更安全的流程是:
-
写入同目录临时文件。
-
刷新并关闭临时文件。
-
用重命名替换目标文件。
local function writeAtomic(path, content)
local temporary = path .. ".tmp"
local file = assert(io.open(temporary, "w"))
assert(file:write(content))
assert(file:flush())
assert(file:close())
local ok, message = os.rename(temporary, path)
if not ok then
os.remove(temporary)
return nil, message
end
return true
end
Windows 对覆盖已存在目标文件的重命名语义可能不同,跨平台程序需要针对目标平台测试或使用宿主提供的原子替换 API。file:flush 也不等同于所有平台上的持久化同步。
文件句柄管理
-
打开后确保所有退出路径都关闭文件。
-
优先使用
assert(io.open(...))或显式处理nil。 -
检查
write和close的返回值。 -
不要对大文件一次性读入内存。
-
日志中包含路径、操作与系统错误码,但避免泄露敏感内容。