Подтвердить что ты не робот

Как эффективно прочитать первый символ из каждой строки текстового файла?

Я бы хотел прочитать только первый символ из каждой строки текстового файла, игнорируя остальные.

Вот пример файла:

x <- c(
  "Afklgjsdf;bosfu09[45y94hn9igf",
  "Basfgsdbsfgn",
  "Cajvw58723895yubjsdw409t809t80",
  "Djakfl09w50968509",
  "E3434t"
)
writeLines(x, "test.txt")

Я могу решить проблему, прочитав все с помощью readLines и используя substring, чтобы получить первый символ:

lines <- readLines("test.txt")
substring(lines, 1, 1)
## [1] "A" "B" "C" "D" "E"

Это кажется неэффективным. Есть ли способ убедить R читать только первые символы, вместо того, чтобы отбрасывать их?

Я подозреваю, что должно быть какое-то заклинание, используя scan, но я не могу его найти. Альтернативой может быть манипуляция с файлами низкого уровня (возможно, с seek).


Так как производительность важна только для больших файлов, здесь большой тестовый файл для сравнения:

set.seed(2015)
nch <- sample(1:100, 1e4, replace = TRUE)    
x2 <- vapply(
  nch, 
  function(nch)
  {
    paste0(
      sample(letters, nch, replace = TRUE), 
      collapse = ""
    )    
  },
  character(1)
)
writeLines(x2, "bigtest.txt")

Обновление. Кажется, вы не можете избежать сканирования всего файла. Похоже, что лучшие достижения скорости используют более быструю альтернативу readLines (Ричард Скривен stringi::stri_read_lines решение и Josh O ' Brien data.table::fread) или обрабатывать файл как двоичный (решение Martin Morgan readBin).

4b9b3361

Ответ 1

01/04/2015 Отредактировано, чтобы вывести лучшее решение в начало.


Обновление 2 Изменение метода scan() для запуска в открытом соединении вместо открытия и закрытия на каждой итерации позволяет читать по очереди и исключает цикл. Время немного улучшилось.

## scan() on open connection 
conn <- file("bigtest.txt", "rt")
substr(scan(conn, what = "", sep = "\n", quiet = TRUE), 1, 1)
close(conn)

Я также обнаружил функцию stri_read_lines() в пакете stringi, ее файл справки говорит, что это экспериментальный момент, но это очень быстро.

## stringi::stri_read_lines()
library(stringi)
stri_sub(stri_read_lines("bigtest.txt"), 1, 1)

Ниже приведены тайминги для этих двух методов.

## timings
library(microbenchmark)

microbenchmark(
    scan = {
        conn <- file("bigtest.txt", "rt")
        substr(scan(conn, what = "", sep = "\n", quiet = TRUE), 1, 1)
        close(conn)
    },
    stringi = {
        stri_sub(stri_read_lines("bigtest.txt"), 1, 1)
    }
)
# Unit: milliseconds
#    expr      min       lq     mean   median       uq      max neval
#    scan 50.00170 50.10403 50.55055 50.18245 50.56112 54.64646   100
# stringi 13.67069 13.74270 14.20861 13.77733 13.86348 18.31421   100

Оригинальный [медленный] ответ:

Вы можете попробовать read.fwf() (файл с фиксированной шириной), установив ширину в одиночный 1, чтобы захватить первый символ в каждой строке.

read.fwf("test.txt", 1, stringsAsFactors = FALSE)[[1L]]
# [1] "A" "B" "C" "D" "E"

Конечно, не полностью протестирован, но работает для тестового файла и является хорошей функцией для получения подстрок без чтения всего файла.


Обновление 1: read.fwf() не очень эффективно, вызывая scan() и read.table() внутренне. Мы можем пропустить средних людей и попробовать scan() напрямую.

lines <- count.fields("test.txt")   ## length is num of lines in file
skip <- seq_along(lines) - 1        ## set up the 'skip' arg for scan()
read <- function(n) {
    ch <- scan("test.txt", what = "", nlines = 1L, skip = n, quiet=TRUE)
    substr(ch, 1, 1)
}
vapply(skip, read, character(1L))
# [1] "A" "B" "C" "D" "E"

version$platform
# [1] "x86_64-pc-linux-gnu"

Ответ 2

Если вы разрешаете/имеете доступ к инструментам командной строки Unix, вы можете использовать

scan(pipe("cut -c 1 test.txt"), what="", quiet=TRUE) 

Очевидно, что менее портативный, но возможно очень быстрый.

Используя код сравнения @RichieCotton с OP, предложили файл "bigtest.txt":

           expr         min          lq        mean      median          uq
     RC readLines   14.797830   17.083849   19.261917   18.103020   20.007341
      RS read.fwf  125.113935  133.259220  148.122596  138.024203  150.528754
 BB scan pipe cut    6.277267    7.027964    7.686314    7.337207    8.004137
      RC readChar 1163.126377 1219.982117 1324.576432 1278.417578 1368.321464
          RS scan   13.927765   14.752597   16.634288   15.274470   16.992124

Ответ 3

data.table::fread(), похоже, превзошел все предлагаемые решения и имеет большую эффективность в сравнении с Windows и * NIX:

library(data.table)
substring(fread("bigtest.txt", sep="\n", header=FALSE)[[1]], 1, 1)

Ниже приведены тайм-ауты microbenchmark на ящике Linux (на самом деле это ноутбук с двойной загрузкой, загруженный как Ubuntu):

Unit: milliseconds
             expr         min          lq        mean      median          uq        max neval
     RC readLines   15.830318   16.617075   18.294723   17.116666   18.959381   27.54451   100
        JOB fread    5.532777    6.013432    7.225067    6.292191    7.727054   12.79815   100
      RS read.fwf  111.099578  113.803053  118.844635  116.501270  123.987873  141.14975   100
 BB scan pipe cut    6.583634    8.290366    9.925221   10.115399   11.013237   15.63060   100
      RC readChar 1347.017408 1407.878731 1453.580001 1450.693865 1491.764668 1583.92091   100

И вот тайминг с одного ноутбука, загруженного как компьютер Windows (с помощью инструмента командной строки cut, поставляемого Rtools):

Unit: milliseconds
             expr         min          lq       mean      median          uq        max neval   cld
     RC readLines   26.653266   27.493167   33.13860   28.057552   33.208309   61.72567   100  b 
        JOB fread    4.964205    5.343063    6.71591    5.538246    6.027024   13.54647   100 a  
      RS read.fwf  213.951792  217.749833  229.31050  220.793649  237.400166  287.03953   100   c 
 BB scan pipe cut  180.963117  263.469528  278.04720  276.138088  280.227259  387.87889   100    d 
      RC readChar 1505.263964 1572.132785 1646.88564 1622.410703 1688.809031 2149.10773   100     e

Ответ 4

Объясните размер файла, прочитайте его как единый двоичный блок, найдите смещения интересующих символов (не считайте последние "\n", в конце файла!) и принуждаете к окончательная форма

f0 <- function() {
    sz <- file.info("bigtest.txt")$size
    what <- charToRaw("\n")
    x = readBin("bigtest.txt", raw(), sz)
    idx = which(x == what)
    rawToChar(x[c(1L,  idx[-length(idx)] + 1L)], multiple=TRUE)
}

Решение data.table(было, по моему мнению, самым быстрым - нужно включить первую строку как часть данных!)

library(data.table)
f1 <- function()
    substring(fread("bigtest.txt", header=FALSE)[[1]], 1, 1)

и в сравнении

> identical(f0(), f1())
[1] TRUE
> library(microbenchmark)
> microbenchmark(f0(), f1())
Unit: milliseconds
 expr      min       lq     mean    median        uq       max neval
 f0() 5.144873 5.515219 5.571327  5.547899  5.623171  5.897335   100
 f1() 9.153364 9.470571 9.994560 10.162012 10.350990 11.047261   100

Все еще расточительно, так как весь файл читается в памяти, прежде чем в основном отбрасываться.

Ответ 5

Контрольные показатели для каждого ответа под Windows.

library(microbenchmark)
microbenchmark(
  "RC readLines" = {
    lines <- readLines("test.txt")
    substring(lines, 1, 1)
  },
  "RS read.fwf" = read.fwf("test.txt", 1, stringsAsFactors = FALSE)$V1,
  "BB scan pipe cut" = scan(pipe("cut -c 1 test.txt"),what=character()),
  "RC readChar" = {  
    con <- file("test.txt", "r")
    x <- readChar(con, 1)
    while(length(ch <- readChar(con, 1)) > 0)
    {
      if(ch == "\n")
      {
        x <- c(x, readChar(con, 1))
      }
    }
    close(con)
  } 
)

## Unit: microseconds
##              expr        min         lq        mean     median          uq
##      RC readLines    561.598    712.876    830.6969    753.929    884.8865
##       RS read.fwf   5079.010   6429.225   6772.2883   6837.697   7153.3905
##  BB scan pipe cut 308195.548 309941.510 313476.6015 310304.412 310772.0005
##       RC readChar   1238.963   1549.320   1929.4165   1612.952   1740.8300
##         max neval
##    2156.896   100
##    8421.090   100
##  510185.114   100
##   26437.370   100

И на большем наборе данных:

## Unit: milliseconds
##              expr         min          lq       mean      median          uq         max neval
##      RC readLines   52.212563   84.496008   96.48517  103.319789  104.124623  158.086020    20
##       RS read.fwf  391.371514  660.029853  703.51134  766.867222  777.795180  799.670185    20
##  BB scan pipe cut  283.442150  482.062337  516.70913  562.416766  564.680194  567.089973    20
##       RC readChar 2819.343753 4338.041708 4500.98579 4743.174825 4921.148501 5089.594928    20
##           RS scan    2.088749    3.643816    4.16159    4.651449    4.731706    5.375819    20

Ответ 6

Я не считаю его очень информативным для бенчмарковских операций в порядке микро- или миллисекунд. Но я понимаю, что в некоторых случаях этого избежать нельзя. В этих случаях, тем не менее, я считаю необходимым проверить данные разных (увеличение размеров), чтобы получить приблизительную оценку того, насколько хорошо метод масштабируется.

Здесь мой запуск на тестах @MartinMorgan с использованием f0() и f1() на строках 1e4, 1e5 и 1e6 и вот результаты:

1E4

# Unit: milliseconds
#  expr      min       lq     mean   median        uq      max neval
#  f0() 4.226333 7.738857 15.47984 8.398608  8.972871 89.87805   100
#  f1() 8.854873 9.204724 10.48078 9.471424 10.143601 84.33003   100

1e5

# Unit: milliseconds
#  expr      min        lq     mean   median       uq      max neval
#  f0() 71.66205 176.57649 174.9545 184.0191 187.7107 307.0470   100
#  f1() 95.60237  98.82307 104.3605 100.8267 107.9830 205.8728   100

1E6

# Unit: seconds
#  expr      min       lq     mean   median       uq      max neval
#  f0() 1.443471 1.537343 1.561025 1.553624 1.558947 1.729900    10
#  f1() 1.089555 1.092633 1.101437 1.095997 1.102649 1.140505    10

identical(f0(), f1()) вернул TRUE для всех тестов.

Update:

1e7

Я также работал с строками 1e7.

f1() (data.table) выполнялся через 9.7 секунд, где f0() выполнялся в 7.8 секунд в первый раз, а 9.4 и 6.6 - второй раз.

Тем не менее, f1() не привело к заметным изменениям в памяти при чтении всего файла 0.479GB, тогда как f0() привел к шипу 2,4 ГБ.

Другое наблюдение:

set.seed(2015)
x2 <- vapply(
  1:1e5, 
  function(i)
  {
    paste0(
      sample(letters, 100L, replace = TRUE), 
      collapse = "_"
    )    
  },
  character(1)
)
# 10 million rows, with 200 characters each
writeLines(unlist(lapply(1:100, function(x) x2)), "bigtest.txt")

## readBin() results in a 2 billion row vector
system.time(f0()) ## explodes on memory

Поскольку шаг readBin() приводит к вектору длины в 2 миллиарда (~ 1,9 ГБ для чтения файла), а шаг which(x == what) занимает ~ 4.5 + GB (= ~ 6.5 ГБ), в котором я остановил процесс.

fread() занимает ~ 23 секунды в этом случае.

НТН