什么是工作目录
每个 R 会话都恰好“位于”一个文件夹中:工作目录。每当你使用相对路径(不从文件系统根目录开始的路径)时,R 都会基于这个文件夹来解析它。read.csv("data.csv") 的意思不是“在我电脑上的某个地方找到 data.csv”,而是“打开工作目录中名为 data.csv 的文件”。
这一个事实就能解释 R 中最常见的初学者错误:
read.csv("data.csv")
# Error in file(file, "rt") : cannot open the connection
# In addition: Warning message: cannot open file 'data.csv': No such file or directory
文件确实存在,只是不在 R 当前查找的文件夹里。你的 read.csv 调用没有任何问题;问题在于会话的工作目录和文件所在的位置不一致。下面这两个函数能把这个谜团变成十秒钟就能搞定的诊断。
getwd() 和 setwd()
getwd()(get working directory,获取工作目录)告诉你 R 此刻在哪里查找:
运行后你会看到一个绝对路径,也就是这个会话所在的文件夹。setwd()(set working directory,设置工作目录)用来修改它:
setwd("C:/Users/rosa/projects/sales-analysis") # Windows: use / not \
setwd("/Users/rosa/projects/sales-analysis") # macOS / Linux
getwd() # confirm it took
有两点操作上的注意事项。在 Windows 上,路径要用 /(或者双写的 \\):在 R 字符串中,单个 \ 是转义字符,所以 "C:\Users" 是语法错误。另外,如果文件夹不存在,setwd() 会立即报错,这是你一整天里能遇到的最友好的失败:它不会让你在不知不觉中指向错误的位置。
因此,排查任何“cannot open file”错误的固定步骤是:先用 getwd() 看 R 在哪里查找,再和文件的实际位置对比。
list.files():看看 R 能看到什么
工具箱里的第三个工具会列出工作目录中的内容,也就是相对路径能够访问到的全部名字:
如果你要读取的文件出现在输出中,read.csv("thatfile.csv") 就能找到它。如果没有出现,再怎么重新输入 read.csv 那一行也没用,需要修正的是工作目录。list.files("data") 可以查看子文件夹,list.files(pattern = "\\.csv$") 则只筛选出 CSV 文件。当文件名看起来没问题却找不到时,list.files() 还能揭露常见的罪魁祸首:文本编辑器偷偷加上的 .txt 扩展名,或者在区分大小写的系统上 Data.csv 和 data.csv 的差别。
为什么写死的 setwd() 会出错
下面是带有明确立场的部分。你会看到一些脚本以这样一行开头:
setwd("C:/Users/me/Desktop/stuff/project3/final_FINAL")
这行代码只能在一台电脑上运行:作者自己的那台。在你的笔记本上、同事的 Mac 上、院系的服务器上,甚至在你自己整理过文件夹之后的电脑上,它都会在第 1 行就报错。写死绝对路径,等于把某一个人的文件夹结构固化进本应比它更长久的代码里。
解决办法是把责任反过来:不要让脚本去找文件夹,而是在正确的文件夹中启动 R,并在脚本中使用相对路径。具体来说:
- 使用 RStudio 项目。打开一个
.Rproj文件会自动把工作目录设置为项目文件夹,在任何电脑上都一样。这样,脚本中的read.csv("data/sales.csv")对每个拿到这个项目的人都能正常运行。 - 从终端运行也能达到同样的效果:
Rscript analysis.R会把你启动它时所在的文件夹作为工作目录(参见运行 R 脚本)。 - 想更进一步的话,可以用 here 包:
here::here("data", "sales.csv")从项目根目录构建路径,不管工作目录跑到了哪里。不过,项目加相对路径已经能满足大多数需求,不用它也可以。
setwd() 本身并不邪恶,在交互式探索时用它完全没问题。真正有问题的,是在共享脚本开头提交一行绝对的、依赖特定电脑的 setwd()。
工作空间:ls() 和 rm()
除了“我在哪个文件夹”之外,会话状态的第二部分是工作空间(即全局环境):你在本次会话中创建的所有对象。ls() 列出它们,rm() 删除它们:
最后一个 ls() 打印出 character(0),即空的工作空间。rm(list = ls()) 这句“咒语”乍看有些奇怪,拆开来看就明白了:ls() 返回所有对象名组成的字符向量,rm(list = ...) 删除这个向量中的每一个名字。它相当于 RStudio 环境(Environment)面板中扫帚图标的代码版。
要清楚它不会做什么:用 library() 加载的包仍然保持加载,设置过的选项依然有效,工作目录也不会变。它只清除对象,别的什么都不做,所以当你想要一个真正干净的环境时,它不能代替重启 R。
清空控制台,以及 .RData 陷阱
清空控制台(即命令和输出的滚动记录)只是外观上的操作,和清空工作空间是两回事。在 RStudio 中按 Ctrl+L;在代码中,可以用有点古怪的 cat("\014") 发送换页符,大多数控制台会把它理解为“清屏”。两者都不会影响你的对象。
最后是那个陷阱。退出 R 时,它会询问“save workspace image?”(是否保存工作空间映像),也就是把所有对象写入一个隐藏的 .RData 文件,下次你在该文件夹中启动 R 时,它会悄悄重新加载。请选择不保存,并永久关闭这个功能(RStudio:Tools → Global Options → 取消勾选“Restore .RData”,并把“Save workspace”设为 Never)。恢复的工作空间意味着你的会话一开始就被几天前的对象污染了,而创建这些对象的代码你可能早已修改或删除。你的脚本看起来能运行,因为它依赖着一个文件中已经没有任何代码创建的过期 model_v2,直到它在一台干净的电脑上运行时轰然崩溃。
专业的习惯恰恰相反:从干净的环境开始,重新运行脚本。如果你的分析是一个能在全新会话中从头运行到尾的脚本,它天生就是可复现的;工作空间只是可以随时丢弃的产物,从来不是需要珍藏的状态。经常重启 R(RStudio:Session → Restart R),正是为了证明你的脚本依然能独立运行。
本节要点
- 相对路径基于工作目录解析;
getwd()查看它,setwd()修改它,list.files()显示能访问到的文件。 - “cannot open file”几乎总是意味着工作目录和文件所在的文件夹不一致,用
getwd()+list.files()来诊断。 - 永远不要在共享脚本中提交绝对路径的
setwd(),而是使用 RStudio 项目(或在脚本所在的文件夹中运行它)和相对路径。 ls()列出工作空间中的对象;rm(x)删除一个;rm(list = ls())全部清除,但包和选项会保留。- Ctrl+L(或
cat("\014"))只清空控制台中的文字。 - 不要自动保存或恢复
.RData:从干净的环境开始,重新运行脚本,这才能让你的工作可复现。
下一节:读取真实数据,现在 R 已经在正确的文件夹中查找了,来看看 read.csv() 及其同类函数。
常见问题
R 中如何设置工作目录?
调用 setwd("path/to/folder") 修改工作目录,调用 getwd() 查看当前工作目录。在 RStudio 中也可以通过菜单:Session → Set Working Directory。但对于需要重复运行或分享的代码,优先使用 RStudio 项目(它会自动设置工作目录),而不是写死一行 setwd():绝对路径换到任何其他电脑上都会出错。
R 中如何清空环境?
rm(list = ls()) 会删除全局环境中的所有对象:ls() 列出所有对象名,rm() 删除它们。只删除一个对象则用 rm(x)。注意它只清除对象:已加载的包仍然保持加载,控制台中的文字也不受影响(清屏在 RStudio 中用 Ctrl+L)。
文件明明存在,为什么 read.csv 提示“cannot open file”?
因为 R 会基于工作目录解析相对路径,而你的工作目录指向的不是存放该文件的文件夹。运行 getwd() 查看 R 在哪里查找,运行 list.files() 查看它在那里能看到什么。解决办法是在正确的文件夹中打开项目(或使用 setwd()),而不是把绝对路径粘贴到脚本里。