用 R 批量读取文本文件、拼接文件内容

· 2166字 · 5分钟

本文整理自统计之都论坛的一个帖子

笔者想把一些 sql 脚本拼接到一个文件里,此事特点如下:

  1. 读取:从多个不同层级的目录下批量读取所有的.sql 文件。

  2. 拼接:将读取的 sql 脚本内容拼接到一起,同时保留原脚本里的格式(换行、缩进等)。

  3. 导出:将拼接好的内容导出到一个文件中。

关于读取和导出数据,在 R 中有许多函数可以做到:

  1. read.tablewrite.table

一个普通的.sql 文件被 read.table 读取后,在 R 中是一个多行1列的数据框,在导出时需要特别注意设定 quote = FALSE,否则字符都会被加上双引号。

test <-
  read.table(file = '目标目录/scripts.sql', header = FALSE, sep = "\t")
write.table(test,
            '目标目录/文件名称.sql',
            row.names = FALSE, # 去掉行名
            col.names = FALSE, # 去掉列名
            quote = FALSE) 
  1. data.table 包的 freadfwrite

功能同上。但为了保留基本的缩进格式,需在读取数据时设定 strip.white = FALSE,意即不去掉每行首尾空格。

  1. readLineswriteLines

这是专门用于读取和导出文本的函数,在 R 中输入??readline 能搜到不少类似函数。在 readLines 函数读入大量的脚本文件后,会形成一个大的列表,读取的每个文件中的文本会成为列表中的一个元素,直接导出的话,原先脚本中的换行缩进等格式会消失,因此需要在导出之前用 unlist 函数改变列表里的数据结构。

  1. xfun 包的read_allwrite_utf8

方法一:用 R base 的文本读取(readLines) 和导出(writeLines) 🔗

首先需要用 list.files 函数取出目标目录下面的所有文件名称,以及文件的具体路径,倘若目标目录下有多层不同的文件夹,需要加上 recursive = TRUE,递归匹配出所有层级目录下的所有文件,否则读取的文件不全。其中 unlist 函数的作用是将读取且拼接后的大列表(Large list)转换成大字符串(Large character)。

files <- list.files(
  path = '目标目录/',
  pattern = '\\.sql$',
  full.names = TRUE,
  ignore.case = TRUE,
  recursive = TRUE)

text <- unlist(lapply(files, readLines))

writeLines(text, '目标目录/文件名称.sql')

方法二:用 xfun 包的(read_all)和导出(write_utf8) 🔗

此方法比方法一慢一些。需要注意的是 read_all 函数是 xfun 0.29及以后的版本才新增的,若要使用该函数需要升级 xfun 包。

library(xfun)

files <- list.files(
  path = '目标目录/',
  pattern = '\\.sql$',
  full.names = TRUE,
  ignore.case = TRUE,
  recursive = TRUE)
  
text <- xfun::read_all(files)

xfun::write_utf8(text, '目标目录/文件名称.sql')

方法三:用 Linux 的 cat 命令 🔗

笔者的电脑系统是 Windows,但是刚好装了 Git,于是可以打开 git-bash.exe 这个应用程序来使用 Linux 的命令。cat 命令的全名是 concatenate,专门用来连接文件并打印到标准输出设备上。需要注意的是,Linux 系统里面列示下级目录的斜杠符号是/,跟 Windows 的\是正好相反的。

如果只是需要批量读取一个文件夹下面的文件,那么可以如下执行命令来完成,即找出目标目录下所有.sql 脚本且合并拼接到一处,最后导出到目标目录的目标文件中。

cd d:/目标目录/

cat *.sql > 目标目录/文件名称.sql

如果目标目录下有多层级目录,那么需要开启 globstar,递归匹配出所有层级目录下的所有文件。这与在 R 中用 list.files 函数查找文件时需设定 recursive = TRUE 是一样的效果。

# 开启 globstar
shopt -s globstar 
cat **/*.sql > 目标目录/文件名称.sql

方法四:用 R base 的浏览(scan)和连接打印(cat) 🔗

R base 中也有与 Linux 中 cat 命令同名的函数,配合 list.filesscan 函数,也可以完成目标。

# 找到所有符合条件的文件名
files <-
  list.files(
    path = "目标目录/",
    pattern = "*.sql",
    full.names = TRUE,
    recursive = TRUE
  )

# 逐个浏览和拼接
n <- length(files)
res_list <- list()
for (i in 1:n) {
  res_list[[i]] <- scan(file = files[i], what = character(0), sep = "\t")
}

text <- unlist(res_list)

# 打印和导出
cat(
  text,
  file = "目标目录/文件名称.sql",
  fill = TRUE,
  sep = '\n',
  append = TRUE
)

踩的坑:用 data.table 包读取(fread)和导出(fwrite) 🔗

用 data.table 包的 fread 或者 read.table 来读取文本文件时,一次只能读取一个文件。同样需要先用 list.files 或者 dir 函数得到所需读取的所有文件的文件名,然后将读取的内容拼接在一起,最后导出。但是此方法会报出下面的错,至今没有解决。

Error in fread(file = dir[i], header = FALSE, sep = "\t", fill = TRUE,  : 
  单列输入包含了不合法的引用。自我修正只有在列数大于1(ncol>1)时才有效

fread 函数读取的数据是一个数据框,在用 rbind 将数据框按行拼接时,需要设置 fill = TRUE,不然的话会报出不同数据框列数不等的错误。并且还存在一个问题,由于是将普通文本形式的脚本转成数据框来拼接的,复杂的脚本拼接在一起后形成了许多不同的列,最后的结果数据框里面会存在许多缺失,导出时默认列与列之间是逗号分隔,会造成的效果是结果文件中原脚本里面一个普通逗号变成很多个逗号。

library(data.table)

# 仅列出目标文件夹下面所有文件的名称
files_name <-
  list.files(
    path = "目标目录/",
    pattern = "*.sql",
    full.names = TRUE,
    recursive = TRUE
  )

# 计算所需读取文件的个数
n <- length(files_name)

# 读取第一个文件
merge.data <- fread(
  file = files_name[1],
  header = FALSE,
  sep = "\t",
  fill = TRUE,
  # 设置不去掉每行首尾空格
  strip.white = FALSE
)

# 从第二个开始拼接
for (i in 2:n) {
  new.data = fread(
    file = files_name[i],
    header = FALSE,
    sep = "\t",
    fill = TRUE,
    strip.white = FALSE
  )
  merge.data = rbind(merge.data, new.data, fill = TRUE)
}

# 导出
fwrite(merge.data,
       file = "目标目录/文件名称.sql",
       row.names = F,
# 当quote="auto"时,若字段内容中包含分隔符、以\n结尾、单双引号等导出时整行字段会用双引号括起来
       quote = FALSE) 

上面数据拼接的步骤也可以不用拆成两步,可以将数据读入一个大列表中,然后用 R base 中的 do.call 函数来拼接。虽然,但是,踩的坑都是一样的。

n <- length(files_name)
res_list <- list()
for (i in 1:n) {
  res_list[[i]] <- fread(
    file = files_name[i], header = FALSE, sep = "\t", fill = TRUE, strip.white = FALSE)
}

rbind_new <- function(...) {
  rbind(..., fill = TRUE)
}
merge.data <- do.call(what = rbind_new, arg = res_list)
R