read_csv读取大量CSV文件时因“打开文件过多”崩溃的解决方法
问题:批量读取大量小型CSV文件时read_csv崩溃的解决方法?
问题背景
我有多个文件夹,每个文件夹下有约10000个小型CSV文件,希望快速将它们读入内存并合并为每个文件夹对应一个数据框。readr包的read_csv可直接接受文件路径向量并自动合并,非常便捷,但读取大量文件时会崩溃。
可复现示例
continents <- c("africa", "americas", "asia", "europe", "oceania") filepaths <- vapply( paste0("mini-gapminder-", continents, ".csv"), FUN = readr_example, FUN.VALUE = character(1) ) filepaths_10k <- rep(filepaths, 2000) # 可正常运行 read_csv(filepaths, id = "file") # 运行失败 read_csv(filepaths_10k, id = "file")
报错信息
Error in file(con, "r") : cannot open the connection In addition: Warning message: In file(con, "r") : cannot open file '/usr/lib/rstudio/resources/CITATION': Too many open files Error in file(con, "rb") : cannot open the connection In addition: Warning message: In file(con, "rb") : cannot open file '/home/simon/R/x86_64-pc-linux-gnu-library/3.6/readr/extdata/mini-gapminder-asia.csv': Too many open files
补充说明
- 曾尝试用
lapply+read_csv+rbindlist的组合代码,但运行一整晚都未完成;基准测试显示直接用read_csv的方法速度快得多。 - 后续补充:显式设置
read_csv禁用惰性求值(lazy = FALSE)对速度无影响,也无法解决报错,推测是Ubuntu 20.04的系统特定问题。readr已默认切回立即求值,且后续需要做数据清理,原本也不需要惰性求值。
基准测试结果
microbenchmark(l_apply_rbindlist = lapply(filepaths, read_csv) %>% rbindlist(), l_apply_bindrows = lapply(filepaths, read_csv) %>% bind_rows(), read_csv_map = map_df(filepaths, ~read_csv(.)), readr_default = read_csv(filepaths), readr_eager_expl = read_csv(filepaths, lazy = FALSE), times = 10, check = "equivalent")
输出:
Unit: milliseconds expr min lq mean median uq max neval cld l_apply_rbindlist 214.08594 219.90338 223.36077 222.36070 227.47078 232.48656 10 b l_apply_bindrows 225.47465 232.00539 235.62815 234.78071 239.32159 249.53793 10 b read_csv_map 215.86775 225.37601 229.41726 231.70719 232.17263 240.49416 10 b readr_default 57.66125 59.77418 77.79516 60.41160 69.10050 214.88023 10 a readr_eager_expl 56.21319 57.05472 61.06905 62.67377 63.66434 64.61471 10 a
最佳解决方法
方法1:分批次读取+合并
针对系统文件句柄限制,最稳妥的方式是分批次读取,既保留read_csv批量读取的高效性,又避免打开过多文件。
示例代码:
library(readr) library(data.table) # 定义批次大小,比如每次处理1000个文件 batch_size <- 1000 file_list <- split(filepaths_10k, ceiling(seq_along(filepaths_10k)/batch_size)) # 分批次读取并合并 combined_df <- rbindlist(lapply(file_list, function(batch) { read_csv(batch, id = "file") }))
原理:将10k文件路径拆分成多个小批次,每个批次调用read_csv读取,最后用rbindlist合并所有批次结果。这样每个批次打开的文件数远低于系统限制,同时每个批次内依然利用read_csv的底层高效合并逻辑,速度比单文件循环快很多。
方法2:临时调整系统文件句柄限制(不推荐长期使用)
如果系统允许,可临时提升用户的文件句柄上限:
- 在终端执行:
ulimit -n 20000(临时生效,重启后恢复) - 若要永久修改,需编辑
/etc/security/limits.conf,添加:
然后重启系统或重新登录。soft nofile 20000 hard nofile 20000
但这种方法只是绕过系统限制,并非从代码层面解决问题,且可能因系统环境限制无法修改,因此优先推荐分批次读取。
方法3:使用vroom包替代(推荐)
vroom是readr团队开发的新一代高效文件读取包,处理大量小文件时的资源管理更优,且速度与readr相当甚至更快,默认不会一次性打开所有文件:
library(vroom) combined_df <- vroom(filepaths_10k, id = "file")
vroom采用流式读取机制,不会一次性打开所有文件句柄,能避免"Too many open files"错误,同时保持高效的合并性能,是当前场景下的最优选择之一。
内容的提问来源于stack exchange,提问作者Simon
相关产品推荐
相关产品推荐

