在 R 中将前 X 行从一个文件复制到另一个文件的最快方法? (跨平台)

fastest way to copy the first X lines from one file to another within R? (cross-platform)

我无法将文件加载到 RAM 中(假设用户可能想要一个包含 100 亿条记录的文件的前 10 亿条数据)

这是我的解决方案,但我认为必须有更快的方法吗?

谢谢

# specified by the user
infile <- "/some/big/file.txt"
outfile <- "/some/smaller/file.txt"
num_lines <- 1000


# my attempt
incon <- file( infile , "r") 
outcon <- file( outfile , "w") 

for ( i in seq( num_lines ) ){

    line <- readLines( incon , 1 )

    writeLines( line , outcon )

}

close( incon )
close( outcon )

尝试使用

line<-read.csv(infile,nrow=1000)
write(line,file=outfile,append=T)

您可以为此使用 ff::read.table.ffdf。它将数据存储在硬盘上,不使用任何 RAM。

library(ff)
infile <- read.table.ffdf(file = "/some/big/file.txt")

基本上你可以像base::read.table一样使用上面的函数,不同的是生成的对象将存储在硬盘上。

您还可以使用 nrow 参数并加载特定行数。如果您想阅读,文档是 here。读取文件后,您可以对所需的特定行进行子集化,如果它们适合 RAM,甚至可以将它们转换为 data.frames

还有一个 write.table.ffdf 函数可以让您编写一个 ffdf 对象(由 read.table.ffdf 产生),这将使该过程更加容易。


作为如何使用 read.table.ffdf(或几乎相同的 read.delim.ffdf)的示例,请参阅以下内容:

#writting a file on my current directory
#note that there is no standard number of columns
sink(file='test.txt')
cat('foo , foo, foo\n')
cat('foo, foo\n')
cat('bar bar , bar\n')
sink()

#read it with read.delim.ffdf or read.table.ffdf
read.delim.ffdf(file='test.txt', sep='\n', header=F)

输出:

ffdf (all open) dim=c(3,1), dimorder=c(1,2) row.names=NULL
ffdf virtual mapping
   PhysicalName VirtualVmode PhysicalVmode  AsIs VirtualIsMatrix PhysicalIsMatrix PhysicalElementNo PhysicalFirstCol PhysicalLastCol PhysicalIsOpen
V1           V1      integer       integer FALSE           FALSE            FALSE                 1                1               1           TRUE
ffdf data
              V1
1 foo , foo, foo
2 foo, foo      
3 bar bar , bar 

如果您使用的是 txt 文件,那么这是一个通用的解决方案,因为每一行都将以 \n 个字符结尾。

我喜欢管道,因为我们可以使用其他工具。方便的是,R 中的(真正优秀的)连接接口支持它:

## scratch file
filename <- "foo.txt"               

## create a file, no header or rownames for simplicity
write.table(1:50, file=filename, col.names=FALSE, row.names=FALSE)   

## sed command:  print from first address to second, here 4 to 7
##               the -n suppresses output unless selected
cmd <- paste0("sed -n -e '4,7p' ", filename)
##print(cmd)                        # to debug if needed

## we use the cmd inside pipe() as if it was file access so
## all other options to read.csv (or read.table) are available too
val <- read.csv(pipe(cmd), header=FALSE, col.names="selectedRows")
print(val, row.names=FALSE)

## clean up
unlink(filename)

如果我们 运行 这样,我们会按预期得到第四到第七行:

edd@max:/tmp$ r piper.R 
 selectedRows
            4
            5
            6
            7
edd@max:/tmp$ 

请注意,我们对 sed 的使用除了假设

之外没有对文件结构做任何假设
  • 标准"ascii"文本文件以文本模式读取
  • 标准 CR/LF 行结尾为 'record separators'

如果您假设二进制文件具有不同的记录分隔符,我们可以建议不同的解决方案。

另请注意, 控制传递给 pipe() 函数的命令。因此,如果您想要行 1000004 到 1000007,用法是完全相同的:您只需给出第一行和最后一行(每个段可以有多个)。而不是 read.csv() 你的 readLines() 也可以同样好地使用。

最后,sed 随处可用,如果没记错的话,它也是 Rtools 的一部分。也可以使用 Perl 或许多其他工具获得基本的过滤功能。

我通常通过读取和写入块(比如 1000 行)来加快此类循环。如果num_lines是1000的倍数,则代码变为:

# specified by the user
infile <- "/some/big/file.txt"
outfile <- "/some/smaller/file.txt"
num_lines <- 1000000


# my attempt
incon <- file( infile, "r") 
outcon <- file( outfile, "w") 

step1 = 1000
nsteps = ceiling(num_lines/step1)

for ( i in 1:nsteps ){
    line <- readLines( incon, step1 )
    writeLines( line, outcon )  
}

close( incon )
close( outcon )

"right" 或对此的最佳答案是使用一种更容易处理文件句柄的语言。例如,虽然 perl 在很多方面都是一种丑陋的语言,但这正是它的闪光点。 Python 也可以以更冗长的方式很好地做到这一点。


但是,您 明确 表示您想要 R 中的东西。首先,我假设这个东西 可能不是 CSV 或其他带分隔符的平面文件。

使用库readr. Within that library, use read_lines(). Something like this (first, get the # of lines in the entire file,using something like what is shown here):

library(readr)

# specified by the user
infile <- "/some/big/file.txt"
outfile <- "/some/smaller/file.txt"
num_lines <- 1000


# readr attempt
# num_lines_tot is found via the method shown in the link above
num_loops <- ceiling(num_lines_tot / num_lines)
incon <- file( infile , "r") 
outcon <- file( outfile , "w") 

for ( i in seq(num_loops) ){

    lines <- read_lines(incon, skip= (i - 1) * num_lines,
                        n_max = num_lines)
    writeLines( lines , outcon )
}

close( incon )
close( outcon )

注意几点:

  1. 没有很好、方便的方法来 在库 readr 中写入 看起来像你想要的那样通用。 (例如,有 write_delim,但您没有指定分隔符。)
  2. "outfile" 之前版本中的所有信息都将丢失。我不确定您是否打算在追加模式 ("a") 中打开 "outfile",但我想这会有帮助。
  3. 我发现在处理像这样的大文件时,我经常想要过滤数据,同时像这样打开它。做简单的复制似乎很奇怪。也许你想做更多?
  4. 如果您有带分隔符的文件,您需要查看 readr 包中的 read_csvread_delim

操作系统是进行大文件操作的最佳级别。这很快,并带有一个基准(这似乎很重要,因为发帖者询问了一种更快的方法):

# create test file in shell 
echo "hello
world" > file.txt
for i in {1..29}; do cat file.txt file.txt > file2.txt && mv file2.txt file.txt; done
wc -l file.txt
# about a billion rows

十亿行这需要几秒钟的时间。把29改成32,大概是一百亿。

然后在 R 中,使用十亿行中的一千万行(一亿行太慢,无法与海报的解决方案相比)

# in R, copy first ten million rows of the billion
system.time(
  system("head -n 10000000 file.txt > out.txt")
)

# posters solution
system.time({
  infile <- "file.txt"
  outfile <- "out.txt"
  num_lines <- 1e7
  incon <- file( infile , "r") 
  outcon <- file( outfile , "w") 

  for ( i in seq( num_lines )) {
    line <- readLines( incon , 1 )
    writeLines( line , outcon )
  }

  close( incon )
  close( outcon )
})

以及在几年前的中档 MacBook pro 上的结果。

Rscript head.R
   user  system elapsed 
  1.349   0.164   1.581 
   user  system elapsed 
620.665   3.614 628.260

有兴趣了解其他解决方案的速度。

C++解决方案

为此编写一些 C++ 代码并不太难:

#include <fstream>
#include <R.h>
#include <Rdefines.h>

extern "C" {

  // [[Rcpp::export]]
  SEXP dump_n_lines(SEXP rin, SEXP rout, SEXP rn) {
    // no checks on types and size
    std::ifstream strin(CHAR(STRING_ELT(rin, 0)));
    std::ofstream strout(CHAR(STRING_ELT(rout, 0)));
    int N = INTEGER(rn)[0];

    int n = 0;
    while (strin && n < N) {
      char c = strin.get();
      if (c == '\n') ++n;
      strout.put(c);
    }

    strin.close();
    strout.close();
    return R_NilValue;
  }
}

保存为yourfile.cpp时,可以

Rcpp::sourceCpp('yourfile.cpp')

您无需从 RStudio 加载任何内容。在控制台中,您必须加载 Rcpp。您可能必须在 Windows 中安装 Rtools。

更高效的 R 代码

通过读取更大的块而不是单行,您的代码也将加速:

dump_n_lines2 <- function(infile, outfile, num_lines, block_size = 1E6) {
  incon <- file( infile , "r") 
  outcon <- file( outfile , "w") 

  remain <- num_lines

  while (remain > 0) {
    size <- min(remain, block_size)
    lines <- readLines(incon , n = size)
    writeLines(lines , outcon)
    # check for eof:
    if (length(lines) < size) break 
    remain <- remain - size
  }
  close( incon )
  close( outcon )
}

基准

lines <- "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Aenean commodo
imperdiet nunc, vel ultricies felis tincidunt sit amet. Aliquam id nulla eu mi
luctus vestibulum ac at leo. Integer ultrices, mi sit amet laoreet dignissim,
orci ligula laoreet diam, id elementum lorem enim in metus. Quisque orci neque,
vulputate ultrices ornare ac, interdum nec nunc. Suspendisse iaculis varius
dapibus. Donec eget placerat est, ac iaculis ipsum. Pellentesque rhoncus
maximus ipsum in hendrerit. Donec finibus posuere libero, vitae semper neque
faucibus at. Proin sagittis lacus ut augue sagittis pulvinar. Nulla fermentum
interdum orci, sed imperdiet nibh. Aliquam tincidunt turpis sit amet elementum
porttitor. Aliquam lectus dui, dapibus ut consectetur id, mollis quis magna.
Donec dapibus ac magna id bibendum."
lines <- rep(lines, 1E6)
writeLines(lines, con = "big.txt")

infile <- "big.txt"
outfile <- "small.txt"
num_lines <- 1E6L


library(microbenchmark)
microbenchmark(
  solution0(infile, outfile, num_lines),
  dump_n_lines2(infile, outfile, num_lines),
  dump_n_lines(infile, outfile, num_lines)
  )

结果(solution0 是 OP 的原始解决方案):

Unit: seconds
                                     expr       min        lq      mean    median        uq       max neval cld
    solution0(infile, outfile, num_lines) 11.523184 12.394079 12.635808 12.600581 12.904857 13.792251   100   c
dump_n_lines2(infile, outfile, num_lines)  6.745558  7.666935  7.926873  7.849393  8.297805  9.178277   100  b 
 dump_n_lines(infile, outfile, num_lines)  1.852281  2.411066  2.776543  2.844098  2.965970  4.081520   100 a 

可以通过一次读取大块数据来加快 c++ 解决方案的速度。但是,这会使代码复杂得多。除非这是我必须经常做的事情,否则我可能会坚持使用纯 R 解决方案。

备注:当你的数据是表格时,你可以使用我的LaF包从你的数据集中读取任意行和列,而不必读取所有数据存入内存。

试试head实用程序。它应该在 R 支持的所有操作系统上都可用(在 Windows 上它假定您安装了 Rtools 并且 Rtools bin 目录在您的路径上)。例如,要将前 100 行从 in.dat 复制到 out.dat :

shell("head -n 100 in.dat > out.dat")