返回首页

教程导航

返回首页

🏷️ 标签(1)

🗂️ 分类(1)

第 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

这实际上会执行文件内容,只能用于程序自己生成且用户不可篡改的数据。加载来自网络或用户上传的文件时,应使用受限制的格式解析器。

安全的文件处理

原子写入

直接覆盖配置时,如果进程在写入中途崩溃,文件可能损坏。更安全的流程是:

  1. 写入同目录临时文件。

  2. 刷新并关闭临时文件。

  3. 用重命名替换目标文件。

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 的返回值。

  • 不要对大文件一次性读入内存。

  • 日志中包含路径、操作与系统错误码,但避免泄露敏感内容。

上一章:错误处理 | 下一章:标准库精讲