R data.table fread命令:如何使用不规则分隔符读取大文件?

4

我需要处理一个包含120个2GB(525600行x302列)的文件集合。目标是进行一些统计,并将结果放入一个干净的SQLite数据库中。

当我的脚本使用read.table()导入数据时,一切都正常,但速度较慢。因此,我尝试使用data.table包(版本1.9.2)中的fread,但它给了我这个错误:

Error in fread(txt, header = T, select = c("YYY", "MM", "DD",  : 
Not positioned correctly after testing format of header row. ch=' '

我的数据的前2行和7行如下所示:
 YYYY MM DD HH mm             19490             40790
 1991 10  1  1  0      1.046465E+00      1.568405E+00

所以,在开头有一个空格,然后日期栏之间只有一个空格,其他栏目之间有任意数量的空格。
我尝试使用以下命令将空格转换为逗号:
DT <- fread(
            paste("sed 's/\\s\\+/,/g'", txt),
            header=T,
            select=c('HHHH','MM','DD','HH')
)

没有成功:问题仍然存在,并且使用sed命令似乎很慢。

fread似乎不喜欢“任意数量的空格”作为分隔符或在开头为空的列。你有什么想法吗?

这里是一个(可能)最小的可重现示例(40790后面跟着换行符):

txt<-print(" YYYY MM DD HH mm             19490             40790
 1991 10  1  1  0      1.046465E+00      1.568405E+00")

testDT<-fread(txt,
              header=T,
              select=c("YYY","MM","DD","HH")
)

感谢您的帮助!

更新: - 错误不会在data.table 1.8.*版本中发生。使用此版本时,表格被读取为一行,但这并没有更好。

更新2: - 如评论所述,我可以使用sed格式化表格,然后使用fread读取它。我在上面的答案中放置了一个脚本,其中我创建了一个样本数据集,然后比较了一些system.time()。


1
+1 不错的例子。我已经在fread.c文件的顶部添加了一个链接,希望很快就能解决fread todo列表中的问题。在此期间可以使用sed。 - Matt Dowle
1
顺便提一下,通常认为使用空格分隔不如使用字符分隔符(如,;或|)稳健。如果可能的话,请避免使用空格分隔。我假设您不能这样做是因为文件已经提供给您了。 - Matt Dowle
感谢您的回复和在待办事项中提供的链接!对于这些文件,是的:它们是某个晦涩的水文随机模型的一些输出。 - fxi
1
尝试将sed作为单独的步骤运行,以创建一个修改后的清理文件;然后读取它;然后看看是否更快。 - Clayton Stanley
Clayton Stanley,我认为NeronLeVelu提出了相同的方法。我编写了一个小函数,它接受一个sed命令作为参数(或使用默认命令)将格式化数据写入临时文件,然后使用fread()读取它。 - fxi
5个回答

5

刚刚提交到devel,v1.9.5。 fread() 增加了 strip.white 参数,默认为TRUE(与 base::read.table() 相反,因为更可取)。示例数据现在已添加到测试中。

有了这个最近的提交:

require(data.table) # v1.9.5, commit 0e7a835 or more recent
ans <- fread(" YYYY MM DD HH mm             19490             40790\n   1991 10  1  1  0      1.046465E+00      1.568405E+00")
#      V1 V2 V3 V4 V5           V6           V7
# 1: YYYY MM DD HH mm 19490.000000 40790.000000
# 2: 1991 10  1  1  0     1.046465     1.568405
sapply(ans, class)
#          V1          V2          V3          V4          V5          V6          V7 
# "character" "character" "character" "character" "character"   "numeric"   "numeric" 

4
sed 's/^[[:blank:]]*//;s/[[:blank:]]\{1,\}/,/g' 

针对您提出的sed问题

是否有可能将fread的所有结果收集到1个(临时)文件中(添加源引用),并使用sed(或其他工具)来处理此文件,以避免在每次迭代时都要派生工具?


谢谢你的sed命令。我不知道为什么,但是当我在这一行“YYYY MM TEST2”上使用你的命令时,我得到了这个结果:",Y,Y,Y,Y,M,M,T,E,S,T,2,"。在家里我用的是Mac OS 10.9,可能存在一些不兼容性!但是...我也尝试过(homebrew)gnu-sed。同样的事情发生了。你的代码很有意义:我们用空白替换字符串开头的任意数量的空格(这部分单独工作),然后我们用逗号替换任意数量的空格。我还在vim上尝试过:同样的事情发生了...还有在一个CentOS服务器上。我不明白 :)也许我太累了... - fxi
抱歉,我在第二个操作中放了一个 * 而不是 \{1,\},这是我的失误。我已在回复中进行了更正。 - NeronLeVelu
我刚刚又看了一遍你的建议,发现我在代码里弄反了(更新2):先用sed再用fread()... 我不知道怎么按照你的方式做 :/ 你说的“工具的分支”是什么意思?再次感谢你提供的sed命令! - fxi

4

在NeronLeVelu和Clayton Stanlay的回答基础上,我用自定义函数、示例数据和一些system.time()进行了补充。这些测试是在Mac os 10.9和R 3.0.2上进行的。然而,我在Linux机器上进行了相同的测试,发现与预先计算nrows和colClasses的read.table()相比,sed命令执行速度非常慢。fread部分非常快,在两个系统上都只需要5秒就能处理500万行。

library(data.table)


# create path to new temporary file
origData <- tempfile(pattern="origData",fileext=".txt")
# write table with irregular blank spaces separators.
write(paste0(" YYYY MM DD HH mm             19490             40790","\n",
                 paste(rep(" 1991 10  1  1  0      1.046465E+00      1.568405E+00", 5e6), 
                       collapse="\n"),"\n"),
      file=origData
)

# define column classes for read.table() optimization
colClasses <- c(rep('integer',5),rep('numeric',2))

# Function to count rows with command wc for read.table() optimization.
fileRowsCount <- function(file){
    if(file.exists(file)){
            sysCmd <- paste("wc -l", file)
            rowCount <- system(sysCmd, intern=T)
            rowCount <- sub('^\\s', '', rowCount)
        as.numeric(
                       strsplit(rowCount, '\\s')[[1]][1]
                      )
    }
}

# Function to sed data into temp file before importing with sed
sedFread<-function(file, sedCmd=NULL, ...){
    require(data.table)
    if(is.null(sedCmd)){
        #default : sed for convert blank separated table to csv. Thanks NeronLevelu !
        sedCmd <- "'s/^[[:blank:]]*//;s/[[:blank:]]\\{1,\\}/,/g'"
    }
    #sed into temp file
    tmpPath<-tempfile(pattern='tmp',fileext='.txt')
    sysCmd<-paste('sed',sedCmd, file, '>',tmpPath)
    try(system(sysCmd))
    DT<-fread(tmpPath,...)
    try(system(paste('rm',tmpPath)))
    return(DT)
}

Mac OS 的结果:

# First sed into temp file and then fread.
system.time(
DT<-sedFread(origData, header=TRUE)
)
> user  system elapsed
> 23.847   0.628  24.514

# Sed directly in fread command :
system.time(
DT <- fread(paste("sed 's/^[[:blank:]]*//;s/[[:blank:]]\\{1,\\}/,/g'", origData),
            header=T)
)
> user  system elapsed
> 23.606   0.515  24.219


# read.table without nrows and colclasses
system.time(
DF<-read.table(origData, header=TRUE)
)
> user  system elapsed
> 38.053   0.512  38.565

# read.table with nrows an colclasses
system.time(
DF<-read.table(origData, header=TRUE, nrows=fileRowsCount(origData), colClasses=colClasses)
)
> user  system elapsed
> 33.813   0.309  34.125

Linux 结果:

# First sed into temp file and then fread.
system.time(
  DT<-sedFread(origData, header=TRUE)
)
> Read 5000000 rows and 7 (of 7) columns from 0.186 GB file in 00:00:05
> user  system elapsed 
> 47.055   0.724  47.789 

# Sed directly in fread command :
system.time(
DT <- fread(paste("sed 's/^[[:blank:]]*//;s/[[:blank:]]\\{1,\\}/,/g'", origData),
            header=T)
)
> Read 5000000 rows and 7 (of 7) columns from 0.186 GB file in 00:00:05
> user  system elapsed 
> 46.088   0.532  46.623 

# read.table without nrows and colclasses
system.time(
DF<-read.table(origData, header=TRUE)
)
> user  system elapsed 
> 32.478   0.436  32.912 

# read.table with nrows an colclasses
system.time(
DF<-read.table(origData,
               header=TRUE, 
               nrows=fileRowsCount(origData),
               colClasses=colClasses)
 )
> user  system elapsed 
> 21.665   0.524  22.192 

# Control if DT and DF are identical : 
setnames(DT, old=names(DT), new=names(DF))
identical(as.data.frame(DT), DF)                                                              
>[1] TRUE

在这种情况下,我最初使用的方法是最有效的。感谢NeronLeVelu、Matt Dowle和Clayton Stanley!

2
我已找到一种比使用sed更快的方法,可以使用awk。这里有另一个示例:
library(data.table)

# create path to new temporary file
origData <- tempfile(pattern="origData",fileext=".txt")

# write table with irregular blank spaces separators.
write(paste0(" YYYY MM DD HH mm             19490             40790","\n",
            paste(rep(" 1991 10  1  1  0      1.046465E+00      1.568405E+00", 5e6),
            collapse="\n"),"\n"),
            file=origData
  )


# function awkFread : first awk, then fread. Argument : colNums = selection of columns. 
awkFread<-function(file, colNums, ...){
        require(data.table)
        if(is.vector(colNums)){
            tmpPath<-tempfile(pattern='tmp',fileext='.txt')
            colGen<-paste0("$",colNums,"\",\"", collapse=",")
            colGen<-substr(colGen,1,nchar(colGen)-3)
            cmdAwk<-paste("awk '{print",colGen,"}'", file, '>', tmpPath)
            try(system(cmdAwk))
            DT<-fread(tmpPath,...)
            try(system(paste('rm', tmpPath)))
            return(DT)
        }
}

# check read time :
system.time(
            DT3 <- awkFread(origData,c(1:5),header=T)
            )

> user  system elapsed 
> 6.230   0.408   6.644

1
如果峰值内存不是问题,或者您可以将其以可管理的块流式传输,那么以下的gsub()/fread()混合方法应该可以工作。在fread()解析之前,将所有连续的空格字符转换为您选择的单个分隔符(例如"\t"):
fread_blank = function(inputFile, spaceReplace = "\t", n = -1, ...){
  fread(
    input = paste0(
      gsub(pattern = "[[:space:]]+",
           replacement = spaceReplace,
           x = readLines(inputFile, n = n)),
      collapse = "\n"),
    ...)
}

我必须同意其他人的看法,空格分隔的文件并不是理想的选择,但无论喜欢与否,我经常遇到这种文件。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接