2012-06-13 2 views
10

Я видел некоторые действительно красивые примеры Ruby, и я пытаюсь изменить свое мышление, чтобы иметь возможность производить их, а не просто восхищаться ими. Вот лучшее, что я мог придумать для выбора случайной строки из файла:Ruby: Какой элегантный способ выбрать случайную строку из текстового файла?

def pick_random_line 
    random_line = nil 
    File.open("data.txt") do |file| 
    file_lines = file.readlines() 
    random_line = file_lines[Random.rand(0...file_lines.size())] 
    end 

    random_line                                        
end 

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

+2

Является ли это более «как я делаю этот _in Ruby_» или более «как это сделать в вопросе меньше чем O (N) space_»? Если последний, исследовать [выборки коллектора] (http://gregable.com/2007/10/reservoir-sampling.html). – zwol

+0

моей тривиальной реализацией было бы искать случайную позицию в файле, а затем искать вперед в новой строке. –

+0

@SamSaffron Это не даст вам равномерно случайную строку, если все строки не будут иметь одинаковую длину. – zwol

ответ

13

Вы можете сделать это, не сохраняя ничего, кроме текущего кандидата для случайной строки.

def pick_random_line 
    chosen_line = nil 
    File.foreach("data.txt").each_with_index do |line, number| 
    chosen_line = line if rand < 1.0/(number+1) 
    end 
    return chosen_line 
end 

Таким образом, первая линия выбрана с вероятностью 1/1 = 1; вторая линия выбирается с вероятностью 1/2, поэтому половина времени она удерживает первую и половину времени, когда она переключается на вторую.

Затем третья линия выбирается с вероятностью 1/3 - так что 1/3 времени она выбирает ее, а остальные 2/3 времени она держит в зависимости от того, какой из первых двух он выбрал. Поскольку каждый из них имел 50% -ный шанс быть выбранным по линии 2, каждый из них заканчивается с вероятностью 1/3 от выбора по строке 3.

И так далее. На линии N каждая строка из 1-N имеет даже 1/N шанс быть выбранным, и она проходит весь путь через файл (пока файл не настолько огромен, что 1/(количество строк в файле) меньше, чем epsilon :)). И вы делаете только один проход через файл и никогда не храните больше двух строк одновременно.

EDIT Вы не собираетесь, чтобы получить реальное краткое решение с помощью этого алгоритма, но вы можете превратить его в один-лайнер, если вы хотите:

def pick_random_line 
    File.foreach("data.txt").each_with_index.reduce(nil) { |picked,pair| 
    rand < 1.0/(1+pair[1]) ? pair[0] : picked } 
end 
+0

Трудный выбор, но ваш был единственным ответом, который обратился к части памяти вопроса. Интересно, есть ли способ уменьшить объемность этого подобным образом до ответа Дейва. Благодаря! – Tres

+0

('.to_f' является излишним). Разве это не будет возвращаться ни разу? – steenslag

+0

@steenslag: true, с учетом '1.0',' .to_f' можно обойтись. Но нет, он никогда не вернется, потому что «rand» всегда меньше 1, поэтому он всегда выбирает первую линию. –

2

Это не намного лучше, чем то, что вы придумали, но, по крайней мере, это короче:

def pick_random_line 
    lines = File.readlines("data.txt") 
    lines[rand(lines.length)] 
end 

Одна вещь, которую вы можете сделать, чтобы сделать ваш код более Rubyish является опуская скобки. Используйте readlines и size вместо readlines() и size().

+0

Спасибо за ваш комментарий. Да, мне было интересно, когда я написал свой ответ. Я пытаюсь выяснить, есть ли способ сохранить краткий синтаксис, закрывая файл должным образом. – Mischa

+0

Вы нашли это, это выглядит :-) –

35

Существует уже случайная запись селектор, встроенный в класс Ruby Array: sample().

def pick_random_line 
    File.readlines("data.txt").sample 
end 
+0

Замечательно, уменьшил метод до одной строки! (Должно быть известно о 'sample' сам, хотя) – Mischa

+5

Предостережение: вы пострадали бы, если файл был большой. –

+2

Обратите внимание, что для тех удержаний, которые все еще используют Ruby 1.8, 'sample' назывался' choice'. –

-1

Stat файла, выбрать случайное число между нулем и размер файла, стремиться к этому байта в файле. Сканируйте до следующей новой строки, затем прочитайте и верните следующую строку (если вы не находитесь в конце файла).

+1

Это плохая идея, потому что первая строка будет * никогда не выбрана, а линии после длинной линии имеют больше шансов быть выбраны, поэтому это не случайность. – Mischa

+0

Хорошая точка. Думаю, вы могли бы просто искать сканирование назад, а затем вперед, чтобы прочитать строку на месте, где вы попали. Если файл не является огромным, хотя требования к производительности глупы; его не стоит усилий. – Bill

0

Один лайнер:

def pick_random_line(file) 
    `head -$((${RANDOM} % `wc -l < #{file}` + 1)) #{file} | tail -1` 
end 

Если возразить, что это не рубин, пойти найти разговор в этом году под названием Euruko рубин в отличие от Banana.

PS: Игнорировать неправильную подсветку синтаксиса SO.

+0

Ну разве что, это не сработает с Ruby на Windows ... – Azolo

+0

True.Я полагаю, что Марти считает, что использовать Ruby вне Unix неестественно. –

+0

Надеюсь, это не слишком неестественно, если это так, я надеюсь, что, поскольку Windows находится в списке поддержки платформы Ruby (http://bugs.ruby-lang.org/projects/ruby-trunk/wiki/SupportedPlatforms) мы будем работать сообществом, чтобы это исправить. Но это не проблема для stackoverflow, поэтому я оставлю это на этом. – Azolo

0

Вот сокращенный вариант EXELLENT ответа Марка, не столь коротким, как Дэйв хотя

def pick_random_line number=1, chosen_line="" 
    File.foreach("data.txt") {|line| chosen_line = line if rand < 1.0/number+=1} 
    chosen_line 
end 
+0

Это показывает, что короче не всегда лучше. Изменение локальных переменных на аргументы просто для сохранения строк ??? – Mischa

3

Эта функция делает именно то, что вам нужно.

Это не один лайнер. Но он работает с текстовыми файлами любого размера (кроме нулевого размера, может быть :).

def random_line(filename) 
    blocksize, line = 1024, "" 
    File.open(filename) do |file| 
    initial_position = rand(File.size(filename)-1)+1 # random pointer position. Not a line number! 
    pos = Array.new(2).fill(initial_position) # array [prev_position, current_position] 
    # Find beginning of current line 
    begin 
     pos.push([pos[1]-blocksize, 0].max).shift # calc new position 
     file.pos = pos[1] # move pointer backward within file 
     offset = (n = file.read(pos[0] - pos[1]).rindex(/\n/)) ? n+1 : nil 
    end until pos[1] == 0 || offset 
    file.pos = pos[1] + offset.to_i 
    # Collect line text till the end 
    begin 
     data = file.read(blocksize) 
     line.concat((p = data.index(/\n/)) ? data[0,p.to_i] : data) 
    end until file.eof? or p 
    end 
    line 
end 

Попробуйте:

filename = "huge_text_file.txt" 
100.times { puts random_line(filename).force_encoding("UTF-8") } 

Незначительные (IMHO) Недостатки:

  1. длиннее линия, тем выше вероятность, что будет определена.

  2. не учитывает разделитель строк «\ r» (зависит от конкретного окна). Используйте файлы с окончанием строки в стиле Unix!

+0

На сегодняшний день лучший вариант для больших файлов, в противном случае время чтения будет сильно отличаться. –

Смежные вопросы