Чтение последних n строк файла в Ruby?

Мне нужно прочитать последние 25 строк из файла (для отображения самых последних записей журнала). Есть ли в Ruby возможность начинать с конца файла и читать его задом наперед?


person Josh Moore    schedule 16.04.2009    source источник


Ответы (8)


Если в системе *nix с tail, вы можете читерить так:

last_25_lines = `tail -n 25 whatever.txt`
person rfunduk    schedule 16.04.2009
comment
Я думаю, что библиотеки было бы более достаточно для кросс-платформенных возможностей, но вы поняли идею. - person John T; 16.04.2009
comment
Вероятно, это правда. Я не запускаю код Ruby ни на чем, кроме систем на основе *nix, и я думаю, вам будет трудно найти один из них без «хвоста»... Кроме того, иногда у вас нет такой роскоши, как установка библиотеки . Просто хотел показать «один лайнер» :) - person rfunduk; 16.04.2009
comment
До сих пор я не развертывал среду, в которой нет хвоста. Этот ответ должен быть принят, на мой взгляд. - person Adam B; 20.03.2015

Достаточно ли велик файл, чтобы не читать его целиком? Если нет, вы можете просто сделать

IO.readlines("file.log")[-25..-1]

Если он слишком большой, вам может понадобиться использовать IO#seek для чтения с конца файла и продолжайте поиск в начале, пока не увидите 25 строк.

person Brian Campbell    schedule 16.04.2009
comment
Если вы не хотите возиться с реверсом, вы можете вместо этого использовать [-25..-1]. - person sris; 16.04.2009
comment
Бити. Не нужно ничего предполагать о системных командах, доступных таким образом. Спасибо. :) - person Allain Lalonde; 17.07.2013
comment
@sris проблема с [-25..-1], если в файле меньше 25 строк, то результат нулевой, я бы рекомендовал использовать IO.readlines("file.log").last(25), который возвращает пустой массив в этом случае. - person Rohit Banga; 24.01.2016

Для Ruby существует библиотека под названием File::Tail. Это может дать вам последние N строк файла, как и утилита tail UNIX.

Я предполагаю, что в версии tail для UNIX есть некоторая оптимизация поиска с такими тестами (проверено на текстовом файле чуть более 11 МБ):

[john@awesome]$du -sh 11M.txt
11M     11M.txt
[john@awesome]$time tail -n 25 11M.txt
/sbin/ypbind
/sbin/arptables
/sbin/arptables-save
/sbin/change_console
/sbin/mount.vmhgfs
/misc
/csait
/csait/course
/.autofsck
/~
/usb
/cdrom
/homebk
/staff
/staff/faculty
/staff/faculty/darlinr
/staff/csadm
/staff/csadm/service_monitor.sh
/staff/csadm/.bash_history
/staff/csadm/mysql5
/staff/csadm/mysql5/MySQL-server-community-5.0.45-0.rhel5.i386.rpm
/staff/csadm/glibc-common-2.3.4-2.39.i386.rpm
/staff/csadm/glibc-2.3.4-2.39.i386.rpm
/staff/csadm/csunixdb.tgz
/staff/csadm/glibc-headers-2.3.4-2.39.i386.rpm

real    0m0.012s
user    0m0.000s
sys     0m0.010s

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

Изменить:

для любопытства Пакса:

[john@awesome]$time cat 11M.txt | tail -n 25
/sbin/ypbind
/sbin/arptables
/sbin/arptables-save
/sbin/change_console
/sbin/mount.vmhgfs
/misc
/csait
/csait/course
/.autofsck
/~
/usb
/cdrom
/homebk
/staff
/staff/faculty
/staff/faculty/darlinr
/staff/csadm
/staff/csadm/service_monitor.sh
/staff/csadm/.bash_history
/staff/csadm/mysql5
/staff/csadm/mysql5/MySQL-server-community-5.0.45-0.rhel5.i386.rpm
/staff/csadm/glibc-common-2.3.4-2.39.i386.rpm
/staff/csadm/glibc-2.3.4-2.39.i386.rpm
/staff/csadm/csunixdb.tgz
/staff/csadm/glibc-headers-2.3.4-2.39.i386.rpm

real    0m0.350s
user    0m0.000s
sys     0m0.130s

все еще меньше секунды, но если файловых операций много, это имеет большое значение.

person John T    schedule 16.04.2009
comment
Что значит cat 11M.txt | хвост -n 25 дать вам? Это заставит tail обрабатывать весь поток. - person paxdiablo; 16.04.2009
comment
Или просто cat 11M.txt ›/dev/null в этом отношении — это даст вам время для обработки потока, которое вполне может составлять порядка 1/100 секунды. - person paxdiablo; 16.04.2009
comment
Мой Медвежонок, @JohnT: 29 раз медленнее 100 секунд, это -2800 секунд. Правильная фраза составляет примерно 1/29 скорости. Но я понимаю вашу точку зрения - очевидно, что tail использует метод поиска, когда у него есть файл, а не поток. Можно было бы надеяться, что Руби такой же умный. - person paxdiablo; 16.04.2009
comment
Да, я это и имел в виду... завтра экзамены, я слишком устал, чтобы сосредоточиться =( - person John T; 16.04.2009

Улучшенная версия превосходного решения на основе поиска от manveru. Этот возвращает ровно n строк.

class File

  def tail(n)
    buffer = 1024
    idx = [size - buffer, 0].min
    chunks = []
    lines = 0

    begin
      seek(idx)
      chunk = read(buffer)
      lines += chunk.count("\n")
      chunks.unshift chunk
      idx -= buffer
    end while lines < ( n + 1 ) && pos != 0

    tail_of_file = chunks.join('')
    ary = tail_of_file.split(/\n/)
    lines_to_return = ary[ ary.size - n, ary.size - 1 ]

  end
end
person Donald Scott Wilde    schedule 18.04.2012
comment
Этот код работает на Mac, но не работает на Linux с сообщением об ошибке tail': undefined local variable or method size'. Есть идеи, как это исправить? - person earlyadopter; 05.06.2014
comment
Нет проверки привязки, что означает, что вы можете прочитать в конечном итоге поиск отрицательного idx. Кроме того, это не очень хорошо оптимизировано для файла с очень длинными строками (т. е. для сохранения фрагментов в заданный буфер). Опубликована версия, которая заботится об обоих. - person Shai; 29.01.2015

Я только что написал быструю реализацию с #seek:

class File
  def tail(n)
    buffer = 1024
    idx = (size - buffer).abs
    chunks = []
    lines = 0

    begin
      seek(idx)
      chunk = read(buffer)
      lines += chunk.count("\n")
      chunks.unshift chunk
      idx -= buffer
    end while lines < n && pos != 0

    chunks.join.lines.reverse_each.take(n).reverse.join
  end
end

File.open('rpn-calculator.rb') do |f|
  p f.tail(10)
end
person manveru    schedule 17.09.2010
comment
На самом деле, хотя ваш код на основе поиска близок, это не совсем правильно, потому что он не удаляет часть фрагмента, которая находится перед первым \n. Смотрите новый ответ ниже. :) - person Donald Scott Wilde; 19.04.2012
comment
:11:in tail': undefined method count' для nil:NilClass (NoMethodError) - person Istvan; 18.02.2013
comment
В этом коде есть ошибка, которую легко исправить. Четвертая строка, которая устанавливает idx, должна быть idx = size › buffer ? (размер - буфер) : 0 - это решит проблему, которую видит @Istvan, хотя на самом деле эту ошибку следует обрабатывать лучше, по крайней мере, с чем-то вроде разрыва, если только фрагмент не идет сразу после чтения (буфера) - person David Ljung Madison Stellar; 15.01.2021
comment
На самом деле, должно быть просто «idx=0, если idx‹0» прямо перед seek(), а затем «break if idx‹=0» прямо перед «idx -= буфер». Это также обрабатывает n›1. - person David Ljung Madison Stellar; 15.01.2021

Вот версия tail, которая не хранит никаких буферов в памяти, пока вы идете, а вместо этого использует «указатели». Также выполняет проверку привязки, поэтому вы не ищете отрицательное смещение (если, например, у вас есть больше для чтения, но меньше, чем осталось размера вашего фрагмента).

def tail(path, n)
  file = File.open(path, "r")
  buffer_s = 512
  line_count = 0
  file.seek(0, IO::SEEK_END)

  offset = file.pos # we start at the end

  while line_count <= n && offset > 0
    to_read = if (offset - buffer_s) < 0
                offset
              else
                buffer_s
              end

    file.seek(offset-to_read)
    data = file.read(to_read)

    data.reverse.each_char do |c|
      if line_count > n
        offset += 1
        break
      end
      offset -= 1
      if c == "\n"
        line_count += 1
      end
    end
  end

  file.seek(offset)
  data = file.read
end

тестовые примеры на https://gist.github.com/shaiguitar/6d926587e98fc8a5e301

person Shai    schedule 29.01.2015
comment
Это не закрывает файл. - person Barry Kelly; 30.10.2016

Я не могу поручиться за Ruby, но большинство этих языков следуют идиоме файлового ввода-вывода C. Это означает, что нет другого способа сделать то, о чем вы просите, кроме поиска. Обычно для этого используется один из двух подходов.

  • Начиная с начала файла и просматривая его весь, запоминая последние 25 строк. Затем, когда вы нажмете конец файла, распечатайте их.
  • Аналогичный подход, но с попыткой сначала найти наиболее вероятное местоположение. Это означает поиск (например) конца файла минус 4000 символов, а затем выполнение именно того, что вы делали в первом подходе, с оговоркой, что, если вы не получили 25 строк, вам нужно выполнить резервное копирование и повторить попытку (например, до конца файла минус 5000 символов).

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

И при беглом взгляде на онлайн-документацию по Ruby кажется, что она действительно следует идиоме C. Вы бы использовали "ios.seek(25*-132,IO::SEEK_END)", если бы следовали моему совету, а затем читайте дальше.

person paxdiablo    schedule 16.04.2009
comment
Все мои терминалы и буферы emacs по-прежнему имеют ширину 80 столбцов; это позволяет мне разместить несколько изображений рядом на моем мониторе, что очень удобно. - person Brian Campbell; 16.04.2009
comment
Я почти уверен, что IO#seek будет оптимальным решением с точки зрения производительности. - person Mike Woodhouse; 16.04.2009

Как насчет:

file = []
File.open("file.txt").each_line do |line|
  file << line
end

file.reverse.each_with_index do |line, index|
  puts line if index < 25
end

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

person nitecoder    schedule 16.04.2009