读取固定宽度文件的更快方法

Posted

技术标签:

【中文标题】读取固定宽度文件的更快方法【英文标题】:Faster way to read fixed-width files 【发布时间】:2014-09-03 03:37:47 【问题描述】:

我使用许多需要读取到 R 中的固定宽度文件(即没有分隔字符)。因此,通常有一个列宽的定义来将字符串解析为变量。我可以使用read.fwf 毫无问题地读取数据。但是,对于大文件,这可能需要 很长 时间。对于最近的数据集,这需要 800 秒才能读取具有约 500,000 行和 143 个变量的数据集。

seer9 <- read.fwf("~/data/rawdata.txt", 
  widths = cols,
  header = FALSE,
  buffersize = 250000,
  colClasses = "character",
  stringsAsFactors = FALSE))

R 中data.table 包中的fread 非常适合解决大多数数据读取问题,但它不解析固定宽度的文件。但是,我可以将每一行读取为单个字符串(约 500,000 行,1 列)。这需要 3-5 秒。 (我喜欢 data.table。)

seer9 <- fread("~/data/rawdata.txt", colClasses = "character",
               sep = "\n", header = FALSE, verbose = TRUE)

关于如何解析文本文件有很多关于 SO 的好帖子。请参阅 JHoward 的建议 here,以创建起始列和结束列的矩阵,以及 substr 以解析数据。请参阅 GSee 的建议 here 以使用 strsplit。我无法弄清楚如何使用这些数据进行操作。 (另外,Michael Smith 对涉及 sed 的 data.table 邮件列表提出了一些建议,这些建议超出了我对 implement. 的能力范围)现在,使用 freadsubstr() 我可以在大约 25-30 内完成整个事情秒。请注意,最后强制转换为 data.table 需要花费大量时间(5 秒?)。

end_col <- cumsum(cols)
start_col <- end_col - cols + 1
start_end <- cbind(start_col, end_col) # matrix of start and end positions
text <- lapply(seer9, function(x) 
        apply(start_end, 1, function(y) substr(x, y[1], y[2])) 
        )
dt <- data.table(text$V1)
setnames(dt, old = 1:ncol(dt), new = seervars)

我想知道这是否可以进一步改进?我知道我不是唯一一个必须读取固定宽度文件的人,所以如果这可以更快,它将使加载更大的文件(数百万行)更容易忍受。我尝试使用parallelmclapplydata.table 而不是lapply,但这些并没有改变任何东西。 (可能是由于我在 R 方面缺乏经验。)我想可以编写一个 Rcpp 函数来非常快地做到这一点,但这超出了我的技能范围。另外,我可能没有正确使用 lapply 和 apply。

我的 data.table 实现(使用magrittr 链接)需要相同的时间:

text <- seer9[ , apply(start_end, 1, function(y) substr(V1, y[1], y[2]))] %>% 
  data.table(.)

任何人都可以提出建议以提高此速度吗?或者这已经是最好的了?

这是在 R 中创建类似 data.table 的代码(而不是链接到实际数据)。它应该有 331 个字符和 500,000 行。有空格可以模拟数据中的缺失字段,但这是NOT空格分隔的数据。 (我正在阅读原始 SEER 数据,以防有人感兴趣。)还包括列宽(cols)和变量名(seervars),以防这对其他人有帮助。这些是 SEER 数据的实际列和变量定义。

seer9 <-
  data.table(rep((paste0(paste0(letters, 1000:1054, " ", collapse = ""), " ")),
                 500000))

cols = c(8,10,1,2,1,1,1,3,4,3,2,2,4,4,1,4,1,4,1,1,1,1,3,2,2,1,2,2,13,2,4,1,1,1,1,3,3,3,2,3,3,3,3,3,3,3,2,2,2,2,1,1,1,1,1,6,6,6,2,1,1,2,1,1,1,1,1,2,2,1,1,2,1,1,1,1,1,1,1,1,1,1,1,1,1,1,7,5,4,10,3,3,2,2,2,3,1,1,1,1,2,2,1,1,2,1,9,5,5,1,1,1,2,2,1,1,1,1,1,1,1,1,2,3,3,3,3,3,3,1,4,1,4,1,1,3,3,3,3,2,2,2,2)
seervars <- c("CASENUM", "REG", "MAR_STAT", "RACE", "ORIGIN", "NHIA", "SEX", "AGE_DX", "YR_BRTH", "PLC_BRTH", "SEQ_NUM", "DATE_mo", "DATE_yr", "SITEO2V", "LATERAL", "HISTO2V", "BEHO2V", "HISTO3V", "BEHO3V", "GRADE", "DX_CONF", "REPT_SRC", "EOD10_SZ", "EOD10_EX", "EOD10_PE", "EOD10_ND", "EOD10_PN", "EOD10_NE", "EOD13", "EOD2", "EOD4", "EODCODE", "TUMOR_1V", "TUMOR_2V", "TUMOR_3V", "CS_SIZE", "CS_EXT", "CS_NODE", "CS_METS", "CS_SSF1", "CS_SSF2", "CS_SSF3", "CS_SSF4", "CS_SSF5", "CS_SSF6", "CS_SSF25", "D_AJCC_T", "D_AJCC_N", "D_AJCC_M", "D_AJCC_S", "D_SSG77", "D_SSG00", "D_AJCC_F", "D_SSG77F", "D_SSG00F", "CSV_ORG", "CSV_DER", "CSV_CUR", "SURGPRIM", "SCOPE", "SURGOTH", "SURGNODE", "RECONST", "NO_SURG", "RADIATN", "RAD_BRN", "RAD_SURG", "SS_SURG", "SRPRIM02", "SCOPE02", "SRGOTH02", "REC_NO", "O_SITAGE", "O_SEQCON", "O_SEQLAT", "O_SURCON", "O_SITTYP", "H_BENIGN", "O_RPTSRC", "O_DFSITE", "O_LEUKDX", "O_SITBEH", "O_EODDT", "O_SITEOD", "O_SITMOR", "TYPEFUP", "AGE_REC", "SITERWHO", "ICDOTO9V", "ICDOT10V", "ICCC3WHO", "ICCC3XWHO", "BEHANAL", "HISTREC", "BRAINREC", "CS0204SCHEMA", "RAC_RECA", "RAC_RECY", "NHIAREC", "HST_STGA", "AJCC_STG", "AJ_3SEER", "SSG77", "SSG2000", "NUMPRIMS", "FIRSTPRM", "STCOUNTY", "ICD_5DIG", "CODKM", "STAT_REC", "IHS", "HIST_SSG_2000", "AYA_RECODE", "LYMPHOMA_RECODE", "DTH_CLASS", "O_DTH_CLASS", "EXTEVAL", "NODEEVAL", "METSEVAL", "INTPRIM", "ERSTATUS", "PRSTATUS", "CSSCHEMA", "CS_SSF8", "CS_SSF10", "CS_SSF11", "CS_SSF13", "CS_SSF15", "CS_SSF16", "VASINV", "SRV_TIME_MON", "SRV_TIME_MON_FLAG", "SRV_TIME_MON_PA", "SRV_TIME_MON_FLAG_PA", "INSREC_PUB", "DAJCC7T", "DAJCC7N", "DAJCC7M", "DAJCC7STG", "ADJTM_6VALUE", "ADJNM_6VALUE", "ADJM_6VALUE", "ADJAJCCSTG")

更新: LaF 在不到 7 秒的时间内从原始 .txt 文件中读取了整个文件。也许有更快的方法,但我怀疑任何事情都可以做得更好。令人惊叹的包装。

2015 年 7 月 27 日更新 只是想对此提供一个小更新。我使用了新的 readr 包,我可以在 5 秒内使用 readr::read_fwf 读取整个文件。

seer9_readr <- read_fwf("path_to_data/COLRECT.TXT",
  col_positions = fwf_widths(cols))

此外,更新后的 stringi::stri_sub 函数的速度至少是 base::substr() 的两倍。因此,在上面的代码中,使用 fread 读取文件(大约 4 秒),然后使用 apply 来解析每一行,使用 stringi::stri_sub 提取 143 个变量大约需要 8 秒,而使用 base::substr 需要 19 秒。所以, fread 加上 stri_sub 仍然只有大约 12 秒的运行时间。还不错。

seer9 <-  fread("path_to_data/COLRECT.TXT",     
  colClasses = "character", 
  sep = "\n", 
  header = FALSE)
text <- seer9[ , apply(start_end, 1, function(y) substr(V1, y[1], y[2]))] %>% 
  data.table(.)

2015 年 12 月 10 日更新:

另请参阅@MichaelChirico 的answer below,他添加了一些出色的基准测试和iotools 包。

【问题讨论】:

并行读取文件不会有帮助。瓶颈是文件 IO。 (当然,当数据分布在多台机器/硬盘上时除外。) @JanvanderLaan,他能够使用fread() 在 5 秒内将所有数据读入 ram。并行解析 500k 字符串是我认为的问题。 @bdemarest 是的,你是对的。对于使用freadsubstr的代码,子串的解析确实是瓶颈,可以并行完成。 【参考方案1】:

现在(在此和关于有效读取固定宽度文件的other major question 之间)提供了相当多的读取此类文件的选项,我认为一些基准测试是合适的。

我将使用以下大型 (400 MB) 文件进行比较。它只是一堆随机定义的字段和宽度的随机字符:

set.seed(21394)
wwidth = 400L
rrows = 1000000
    
#creating the contents at random
contents = write.table(
  replicate(
    rrows,
    paste0(sample(letters, wwidth, replace = TRUE), collapse = "")
  ),
  file = "testfwf.txt",
  quote = FALSE, row.names = FALSE, col.names = FALSE
)
    
#defining the fields & writing a dictionary
n_fields = 40L
endpoints = unique(
  c(1L, sort(sample(wwidth, n_fields - 1L)), wwidth + 1L)
)
cols = list(
  beg = endpoints[-(n_fields + 1L)], 
  end = endpoints[-1L] - 1L
)
    
dict = data.frame(
  column = paste0("V", seq_len(length(endpoints)) - 1L)),
  start = endpoints[-length(endpoints)] - 1,
  length = diff(endpoints)
)
    
write.csv(dict, file = "testdic.csv", quote = FALSE, row.names = FALSE)

我将比较这两个线程之间提到的五种方法(如果作者愿意,我会添加一些其他方法):基本版本(read.fwf),将in2csv 的结果管道传递给fread(@ AnandaMahto 的建议),Hadley 的新 readrread_fwf),使用 LaF/ffbase(@jwijffls 的建议),以及问题作者(@MarkDanese)建议的改进(精简)版本结合freadstri_sub 来自 stringi

这是基准测试代码:

library(data.table)
library(stringi)
library(readr)
library(LaF)
library(ffbase)
library(microbenchmark)
    
microbenchmark(
  times = 5L,
  utils = read.fwf("testfwf.txt", diff(endpoints), header = FALSE),
  in2csv = fread(cmd = sprintf(
    "in2csv -f fixed -s %s %s",
    "testdic.csv", "testfwf.txt"
  )),
  readr = read_fwf("testfwf.txt", fwf_widths(diff(endpoints))),
  LaF = 
    my.data.laf = laf_open_fwf(
      'testfwf.txt', 
      column_widths = diff(endpoints),
      column_types = rep("character", length(endpoints) - 1L)
    )
    my.data = laf_to_ffdf(my.data.laf, nrows = rrows)
    as.data.frame(my.data)
  ,
  fread = 
    DT = fread("testfwf.txt", header = FALSE, sep = "\n")
    DT[ , lapply(seq_len(length(cols$beg)), function(ii) 
      stri_sub(V1, cols$beg[ii], cols$end[ii])
    )]
  
)

还有输出:

# Unit: seconds
#    expr       min        lq      mean    median        uq       max neval cld
#   utils 423.76786 465.39212 499.00109 501.87568 543.12382 560.84598     5   c
#  in2csv  67.74065  68.56549  69.60069  70.11774  70.18746  71.39210     5 a  
#   readr  10.57945  11.32205  15.70224  14.89057  19.54617  22.17298     5 a  
#     LaF 207.56267 236.39389 239.45985 237.96155 238.28316 277.09798     5  b 
#   fread  14.42617  15.44693  26.09877  15.76016  20.45481  64.40581     5 a  

所以看起来readrfread + stri_sub 是最快的;内置的read.fwf 是明显的失败者。

请注意,readr 的真正优势在于您可以预先指定列类型;使用 fread 之后,您必须输入 convert。

编辑:添加一些替代品

根据@AnandaMahto 的建议,我提供了更多选项,包括一个似乎是新赢家的选项!为了节省时间,我在新比较中排除了上面最慢的选项。这是新代码:

library(iotools)
    
microbenchmark(
  times = 5L,
  readr = read_fwf("testfwf.txt", fwf_widths(diff(endpoints))),
  fread = 
    DT = fread("testfwf.txt", header = FALSE, sep = "\n")
    DT[ , lapply(seq_len(length(cols$beg)), function(ii) 
      stri_sub(V1, cols$beg[ii], cols$end[ii])
    )]
  ,
  iotools = input.file(
    "testfwf.txt", formatter = dstrfw, 
    col_types = rep("character", length(endpoints) - 1L), 
    widths = diff(endpoints)
  ),
  awk = fread(header = FALSE, cmd = sprintf(
    "awk -v FIELDWIDTHS='%s' -v OFS=', ' '$1=$1 \"\"; print' < testfwf.txt",
    paste(diff(endpoints), collapse = " ")
  ))
)

还有新的输出:

# Unit: seconds
#     expr       min        lq      mean    median        uq       max neval cld
#    readr  7.892527  8.016857 10.293371  9.527409  9.807145 16.222916     5  a 
#    fread  9.652377  9.696135  9.796438  9.712686  9.807830 10.113160     5  a 
#  iotools  5.900362  7.591847  7.438049  7.799729  7.845727  8.052579     5  a 
#      awk 14.440489 14.457329 14.637879 14.472836 14.666587 15.152156     5   b

所以看起来iotools 既非常快又非常一致。

【讨论】:

基准测试很有用。在另一个问题的 cmets 中,我建议尝试“iotools”包。您能否将其包含在基准测试以及“awk”解决方案中?我猜“awk”方法会比“in2csv”快,但比“fread”/“readr”慢,并且根据我对“iotools”的经验,如果它比到目前为止可用的选项。未经测试,但方法应该类似于:library(iotools); input.file("testfwf.txt", formatter = dstrfw, col_types = rep("character", length(col_ends)-1), widths = diff(col_ends))。 (+1) 哦,对于“sqldf”的错误(我不会费心测试速度比较),这可能是因为我们需要指定header = FALSE 的等效项。暂时没有时间去探索...... 感谢你们俩。这是很好的信息。我将对原问题进行编辑,引导读者往下看。 虽然input.file中没有设置输入文件编码的选项,但让我很困扰。【参考方案2】:

我不确定您使用的是什么操作系统,但这在 Linux 中对我来说非常简单:

第 1 步:为awk 创建一个命令以将文件转换为 csv

如果您也打算在其他软件中使用数据,可以将其存储到实际的 csv 文件中。

myCommand <- paste(
  "awk -v FIELDWIDTHS='", 
  paste(cols, collapse = " "), 
  "' -v OFS=',' '$1=$1 \"\"; print' < ~/rawdata.txt", 
  collapse = " ")

第 2 步:在您刚刚创建的命令上直接使用 fread

seer9 <- fread(myCommand)

我没有计时,因为我使用的系统显然比你和 Jan 慢:-)

【讨论】:

非常感谢。我希望有人可能会提出这样的建议。我试过了,它返回了一个错误。 Error in fread(myCommand) : ' ends field 14 on line 26 when detecting types: 428135680000001527 . . . 我无法粘贴整个 331 字符字符串。不确定是什么问题。这是 OSX(小牛队)。我现在可能应该强迫所有人都烧焦。 我尝试强制所有字符。但问题是 freed 仅检测 15 列,而不是 143 列。这是我的命令的编辑版本,删除了许多 col 值以适应此评论:"awk -v FIELDWIDTHS=' 8 10 1 2 1 1 1 3 4 3 2 2 4 4 1 4 1 4 1 1 1 1 3 2 2 1 2 2 13 2 4 1 1 ' -v OFS=',' '$1=$1 \"\"; print' &lt; ~/file.TXT"【参考方案3】:

您可以使用LaF 包,它是为处理大型固定宽度文件(也太大而无法放入内存)而编写的。要使用它,您首先需要使用laf_open_fwf 打开文件。然后,您可以像使用普通数据框一样对生成的对象进行索引,以读取您需要的数据。在下面的示例中,我读取了整个文件,但您也可以读取特定的列和/或行:

library(LaF)
laf <- laf_open_fwf("foo.dat", column_widths = cols, 
  column_types=rep("character", length(cols)),
  column_names = seervars)
seer9 <- laf[,]

使用 5000 行(而不是 500,000 行)的示例使用 read.fwf 需要 28 秒,使用 LaF 需要 1.6 秒。

加法您的示例使用 50,000 行(而不是您的 500,000 行)在我的机器上使用 read.fwf 需要 258 秒,使用 LaF 需要 7 秒。

【讨论】:

我不知道这个包。哇。 6 秒。优秀。大约与 CSV 文件的 fread 一样快,这非常令人印象深刻。将对此进行更多研究,因为我们有一些大型数据集。谢谢。【参考方案4】:

我昨天为这种事情写了一个解析器,但它是用于头文件的一种非常特殊的输入,所以我将向您展示如何格式化列宽以便能够使用它。

将平面文件转换为 csv

先下载the tool in question。

如果您使用的是 OS X Mavericks(我在其中编译它),则可以从 bin 目录下载二进制文件,或者转到 src 并使用 clang++ csv_iterator.cpp parse.cpp main.cpp -o flatfileparser 进行编译。

平面文件解析器需要两个文件,一个 CSV 头文件,其中每五个元素指定一个可变宽度(同样,这是由于我非常具体的应用程序),您可以使用以下方法生成:

cols = c(8,10,1,2,1,1,1,3,4,3,2,2,4,4,1,4,1,4,1,1,1,1,3,2,2,1,2,2,13,2,4,1,1,1,1,3,3,3,2,3,3,3,3,3,3,3,2,2,2,2,1,1,1,1,1,6,6,6,2,1,1,2,1,1,1,1,1,2,2,1,1,2,1,1,1,1,1,1,1,1,1,1,1,1,1,1,7,5,4,10,3,3,2,2,2,3,1,1,1,1,2,2,1,1,2,1,9,5,5,1,1,1,2,2,1,1,1,1,1,1,1,1,2,3,3,3,3,3,3,1,4,1,4,1,1,3,3,3,3,2,2,2,2)
writeLines(sapply(c(-1, cols), function(x) paste0(',,,,', x)), '~/tmp/header.csv')

并将生成的~/tmp/header.csv 复制到与您的flatfileparser 相同的目录中。将平面文件也移动到同一目录,您可以在平面文件上运行它:

./flatfileparser header.csv yourflatfile

这将产生yourflatfile.csv。使用管道手动添加上面的标题(来自 Bash 的&gt;&gt;)。

快速读取 CSV 文件

通过将文件名传递给fastread::read_csv 来使用Hadley 的实验性fastread package,这会产生data.frame。我不相信他支持fwf 文件,虽然它正在开发中。

【讨论】:

我似乎无法让它工作。我不是命令行人员,所以可能只是我做错了什么。 mark-mbp-osx:bin mark$ flatfileparser header.csv COLRECT.TXT 在小牛队给我-bash: flatfileparser: command not found。这是目录列表:mark-mbp-osx:bin mark$ ls COLRECT.TXT flatfileparser header.csv 试试chmod +x flatfileparser; ./flatfileparser header.csv COLRECT.TXT 它似乎有效,即使它给出了错误:mark-mbp-osx:bin mark$ chmod +x flatfileparserchmod +x flatfileparser; ./flatfileparser header.csv COLRECT.TXTchmod: flatfileparserchmod: No such file or directorychmod: +x: No such file or directorymark-mbp-osx:bin mark$ 我认为您将字符串“chmod +x flatfileparser”粘贴了两次。尝试两个单独的命令:首先是chmod +x flatfileparser,然后是./flatfileparser header.csv COLRECT.TXT 我的错,我将它粘贴到 SO 中两次。我最终得到了 144 列而不是 143 列。它似乎工作正常,所以谢谢。我不确定我是否可以定期或在我们的 Windows 服务器上使用它。如果可以从 R 中轻松访问,那就太好了。我只是不是真正的程序员

以上是关于读取固定宽度文件的更快方法的主要内容,如果未能解决你的问题,请参考以下文章

在 spark java 中读取具有固定宽度和分隔符的文本文件

读取固定宽度文件中相同列的倍数

使用 pyspark 中 json 文件中的模式读取固定宽度文件

在 R 中跨多行的固定宽度文件中读取观察结果

读取固定宽度格式,其中宽度是从列标题中推断出来的

R:如何读取固定宽度的数据文件,其中数据连接成两组,堆叠在一个文件的顶部