我正在寻找一种快速的方法,从制表符分隔的文本中读取单列,作为内存中的字符向量。
我使用特定于我的字段的文件格式,大致类似于压缩的tsv文件。从这样的文件中读取行的子集是快速和容易的,但由于内存限制,直接读取read.table()
,data.table::fread()
或readr::read_tsv()
的数据是不可行的(并且我需要的行是未知的)。
所以,我最终在内存中得到一个字符向量,每行都有一个元素,但是制表符仍然在那里。我对如何从这篇文章中快速提取特定的一列有点困惑。在下面的示例中,提取第三列的最快方法是什么?文本中没有任何"意外",例如注释或引用的名称,但在我的实际情况中,列没有固定的宽度。到目前为止,我发现最快的方法是使用readr::read_tsv()
函数。
library(readr)
set.seed(0)
# About 88Mb of memory
n_examples <- 1e6
text <- paste(
as.character(as.hexmode(sample(n_examples))),
as.character(as.hexmode(sample(n_examples))),
as.character(as.hexmode(sample(n_examples))),
as.character(as.hexmode(sample(n_examples))),
sep = "t"
)
fun_read.table <- function(x, i) {
read.table(
text = x, sep = "t",
colClasses = c("character", "character", "character", "character")
)[[i]]
}
fun_read_tsv <- function(x, i) {
read_tsv(file = I(x), col_select = all_of(i),
col_types = "cccc", col_names = LETTERS[1:4])[[1]]
}
bm <- bench::mark(
fun_read.table(text, 3),
fun_read_tsv(text, 3),
min_iterations = 5
)
#> Warning: Some expressions had a GC in every iteration; so filtering is disabled.
print(bm)
#> # A tibble: 2 x 13
#> expression min median `itr/sec` mem_alloc `gc/sec` n_itr
#> <bch:expr> <bch:tm> <bch:tm> <dbl> <bch:byt> <dbl> <int>
#> 1 fun_read.table(text, 3) 1.34s 1.57s 0.619 93.2MB 1.11 5
#> 2 fun_read_tsv(text, 3) 879.36ms 903.72ms 1.10 35.1MB 0.219 5
#> # ... with 6 more variables: n_gc <dbl>, total_time <bch:tm>, result <list>,
#> # memory <list>, time <list>, gc <list>
下面是我尝试过的一些替代方案,但都不如read_tsv()
快。data.table::fread()
方法非常慢,因为它首先将输入文本写入临时文件。我还没有设法找出一个基于正则表达式的方法来捕获第三列,所以我不知道这是否会更快。
library(data.table)
#> Warning: package 'data.table' was built under R version 4.1.1
fun_tstrsplit <- function(x, i) {
tstrsplit(x, "t", keep = i)[[1]]
}
fun_fread <- function(x, i) {
fread(
text = x, sep = "t",
colClasses = c("character", "character", "character", "character"),
select = i
)[[1]]
}
fun_scan <- function(x, i) {
ncols <- lengths(regmatches(x[[1]], gregexpr("t", x[[1]]))) + 1
scan(
text = x, sep = "t", what = "", quiet = TRUE
)[seq_along(x) %% ncols == i]
}
由reprex包(v2.0.1)在2018-10-13上创建
使用Rcpp编写的定制函数在这里对我来说工作得最快(比read_tsv
快两倍多),并且使用了read_tsv
的大约四分之一的内存分配,尽管它涉及一些复制并且可能可以优化。
我也包含了一个使用sub
的版本,但这比read_tsv
慢,尽管它不需要太多内存。
Rcpp::cppFunction("
std::vector<std::string> fun_rcpp(CharacterVector a, int col) {
if(col < 1) Rcpp::stop("col must be a positive integer");
std::vector<std::string> b = Rcpp::as<std::vector<std::string>>(a);
std::vector<std::string> result(a.size());
for(uint32_t i = 0; i < a.size() ; i++)
{
int n_tabs = 0;
std::string entry = "";
for(uint16_t j = 0; j < b[i].size(); j++)
{
if(n_tabs == (col - 1) & b[i][j] != '\t') entry.push_back(b[i][j]);
if((b[i][j]) == '\t') n_tabs++;
if(n_tabs == col) break;
}
result[i] = entry;
}
return result;
}
")
fun_sub <- function(x, i)
{
s <- paste0("^", paste0(rep(".*?t", i - 1), collapse = ""), "(.*?)t.*$")
sub(s, "\1", x)
}
这两个函数都给出了预期的输出:
identical(fun_read_tsv(text, 3), fun_rcpp(text, 3))
#> [1] TRUE
identical(fun_read_tsv(text, 3), fun_sub(text, 3))
#> [1] TRUE
和基准测试显示在这里进行比较:
bench::mark(
fun_read.table(text, 3),
fun_read_tsv(text, 3),
fun_sub(text, 3),
fun_rcpp(text, 3),
min_iterations = 5
)
#> # A tibble: 4 x 13
#> expression min median `itr/sec` mem_alloc `gc/sec` n_itr n_gc
#> <bch:expr> <bch:tm> <bch:tm> <dbl> <bch:byt> <dbl> <int> <dbl>
#> 1 fun_read.table(text, 3) 1.35s 1.35s 0.738 93.23MB 5.17 1 7
#> 2 fun_read_tsv(text, 3) 788.86ms 792.35ms 1.26 36.04MB 0.314 4 1
#> 3 fun_sub(text, 3) 1.27s 1.29s 0.777 7.63MB 0.194 4 1
#> 4 fun_rcpp(text, 3) 379.02ms 381.17ms 2.62 7.63MB 0.655 4 1
#> # ... with 5 more variables: total_time <bch:tm>, result <list>, memory <list>,
# time <list>, gc <list>
请注意,Rcpp函数的行为与预期基本一致,如果指定的列数小于1或使用错误的变量类型来选择列,则会发出相应的错误。但是,如果选择的列数大于当前的列数,它将返回一个空字符串向量,而不是抛出错误。如果您想要不同的行为,例如错误或NA