Menu

R 写入文件:write.csv 和 saveRDS

从 R 导出数据:write.csv 搭配 row.names = FALSE(每次都要加),write.table 处理其他分隔符,saveRDS 保存后再读回时类型不会丢失。

本页包含可运行的编辑器。编辑、运行,立即查看输出。

write.csv() 导出数据框

把数据框从 R 导出成其他程序也能打开的文件,只需要一次调用:

write.csv(df, "results.csv", row.names = FALSE)

这一行就是要记住的写法,包括 row.names = FALSE,稍后会解释原因。第一个参数是数据框,第二个是文件名,函数没有返回值:它的全部工作就是创建文件这个副作用。

你不必写入磁盘,就能清楚地看到 write.csv() 会输出什么,因为在需要文件名的地方,它接受任何连接,包括只会直接打印的 stdout():

这就是你的 CSV:一行表头,每行数据一行,文本值用引号括起来,数字不加引号。(加引号是默认的 quote = TRUE;所有主流的 CSV 读取器都能处理,保持默认就好。)写入真实文件也是同样的调用,只要把 stdout() 换成 "results.csv"。

为什么 row.names = FALSE 应该成为默认写法

每个数据框都带有行名,通常就是数字 1、2、3……而 write.csv() 的默认行为是把它们作为额外的第一列写入文件,表头为空。看看默认输出是什么样子,以及这个文件再读回 R 时会发生什么:

导出的文件多了一个没有名字的第一列,里面是 "1"、"2";重新导入时,read.csv() 必须给它起个名字,于是编出了 X。这就是让每个初学者困惑的神秘 X 列:它不是数据,而是 R 的行号泄漏到了你的文件里。多导出导入几次,你就会积攒出 X、X.1、X.2……

行名几乎从不包含真正的信息;即使包含,这些信息也应该放在一个正式的列里。所以要把 row.names = FALSE 变成条件反射。它本来或许就该是默认值;出于向后兼容的原因它不是,所以这个习惯只能靠你自己养成。

文件保存在哪里

write.csv(df, "results.csv") 会写入 R 的工作目录,如果那不是你预期的位置,文件就好像消失了。其实它并没有失败,只是待在 getwd() 打印出的那个文件夹里。这和读取时的陷阱一模一样,只是方向相反,解决它的也是同样的两个工具:

getwd()                          # the folder your file went to
write.csv(df, "C:/Users/ada/results/out.csv", row.names = FALSE)   # or: remove all doubt

完整路径(正斜杠在所有平台上都能用,包括 Windows)能让脚本毫无歧义。工作目录如何设置和管理,请参阅工作目录。

用 write.table() 处理其他分隔符,以及追加的问题

write.csv() 其实是通用函数 write.table() 的一层薄封装,预先设置好了逗号分隔。当你需要其他格式时,就直接调用 write.table(),最常见的是制表符分隔:

制表符分隔,不加引号,这是很多下游工具偏爱的格式。注意,使用 write.table() 时所有设置都要显式指定;它不会继承 write.csv 的逗号或引号默认值。

write.csv() 拒绝做的一件事是追加:传入 append = TRUE 只会得到一条警告,说这个设置被忽略了,因为追加会把表头行写进文件中间。如果你确实需要向现有 CSV 添加行,比如在多次运行之间记录结果,就退回到 write.table(),并自己去掉表头:

write.table(new_rows, "log.csv", sep = ",",
            append = TRUE, col.names = FALSE, row.names = FALSE)

如果你在用 tidyverse,readr::write_csv(df, "out.csv") 就是 read_csv() 的对应函数:输出相同,处理大数据框时更快,而且它根本不会写入行名,又少了一个需要记住的参数。

saveRDS() 和 readRDS():原样保存与读取

CSV 是一种有损格式。它只存储文本,仅此而已:所以 Date 列会变成字符串 "2026-08-07",因子会变成它的标签而丢失水平顺序,属性会消失,导入时每种类型都必须重新推断(通常没问题,但有时会出错,说的就是前导零)。而且 CSV 只能存放一张矩形表格:拟合好的模型、列表、带 dimnames 的矩阵,根本没有 CSV 表示形式。

当文件是给 R 用的,比如把今天清洗好的数据保存起来供明天的会话使用时,就完全跳过文本格式:

saveRDS(df, "clean_data.rds")        # today

df <- readRDS("clean_data.rds")      # tomorrow: identical object, types intact

saveRDS() 精确地序列化一个 R 对象,任何对象都可以。readRDS() 逐字节地把它还原:因子保留水平,日期依然是日期,不需要重新推断任何东西。它适用于任何对象:一个数据框、一个由数据框组成的列表、一个拟合好的回归模型。.rds 文件是压缩的二进制文件,所以通常也比 CSV 更小。唯一的代价是除了 R 没有别的程序能打开它,这正是经验法则的由来:与人和其他工具分享用 CSV,留给自己用 RDS。

save() 和 load(),以及为什么 RDS 通常更好

你还会遇到一对更老的函数。save() 把多个命名对象写入一个 .RData 文件,load() 则把它们恢复到你的工作空间中,并沿用原来的名字:

save(df, model, params, file = "session.RData")
load("session.RData")    # df, model, params silently appear

这种便利恰恰就是问题所在。决定变量名的是 load(),而不是你:它在你看不到的情况下把对象放进你的环境,并悄悄覆盖任何同名的已有对象。六个月后,如果不加载一下,谁也不记得 session.RData 里到底装了什么。

readRDS() 的接口则坦率得多:名字由你来选(df <- readRDS(...)),不会有东西在背后被覆盖,而且一个文件就是一个对象。除非你确实需要把多个对象打包进一个文件,否则优先使用 RDS。(如果真的需要打包,把这些对象放进一个命名列表,再对这个列表调用 saveRDS(),效果相同,也不会有意外。)

本节要点

  • write.csv(df, "out.csv", row.names = FALSE) 就是导出的全部,没错,每次都要加 row.names = FALSE。
  • 重新导入时出现的神秘 X 列,是行名泄漏到了文件里;默认值会写入行名,你不应该这样做。
  • 文件保存在工作目录中:用 getwd() 找回“丢失”的导出文件,用完整路径防止文件“丢失”。
  • write.table() 能处理其他分隔符,还能(配合 col.names = FALSE)真正地追加;write.csv() 则故意不支持追加。
  • saveRDS() / readRDS() 能原样保存和读取任何 R 对象,类型、因子和属性都完好无损。分享用 CSV,自己用 RDS。
  • 优先使用 RDS 而不是 save()/load():名字明确,不会悄悄覆盖。

下一节:一种值得单独讲一页的数据类型,免得它在导入时坑了你:日期。

常见问题

R 中如何把数据框导出为 CSV?

write.csv(df, "output.csv", row.names = FALSE)。row.names = FALSE 这部分很重要:没有它,R 会把内部的行号作为额外的第一列写入文件,任何人再导入这个文件时,都会看到一个莫名其妙、名为 X 的列。

为什么导出的 CSV 读回来时多了一个 X 列?

因为 write.csv 的默认值是 row.names = TRUE:它把行号作为一个没有列名的第一列写了进去,而 read.csv 在读回时给这一列取名为 X。用 row.names = FALSE 导出,这个幽灵列就会消失。

R 中 write.csv 和 saveRDS 有什么区别?

write.csv 生成任何人都能打开的纯文本表格,但它只保存文本:因子水平、Date 类型、属性以及非表格对象都会被扁平化或丢失,导入时还得重新推断类型。saveRDS 逐字节保存一个 R 对象;readRDS 会原样还原它。与他人分享用 CSV,留给自己的 R 会话用 RDS。

write.csv 把文件保存在哪里?

除非你给出完整路径,否则保存在 R 当前的工作目录中。运行 getwd() 查看它在哪里:如果你的文件好像不见了,它就在 getwd() 打印出的那个文件夹里。传入类似 "C:/Users/ada/results/out.csv" 的完整路径就能消除歧义。

Coddy 编程语言插图

和 Coddy 一起学编程

开始