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 

Мне кажется, что это возможно сделать короче, элегантнее, не сохраняя в памяти все содержимое файла. Есть?

Ответ 1

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

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:)). И вы делаете только один проход через файл и никогда не храните сразу несколько строк.

РЕДАКТИРОВАТЬ. Вы не получите настоящего краткого решения с этим алгоритмом, но вы можете превратить его в однострочный, если хотите:

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

Ответ 2

В класс Ruby Array встроен селектор случайных записей: sample().

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

Ответ 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) недостатки:

  • Чем длиннее строка, тем выше вероятность того, что она будет выбрана.

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

Ответ 4

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

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

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

Ответ 5

Один вкладыш:

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

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

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

Ответ 6

Здесь более короткая версия Mark exellent ответ, не такая короткая, как Dave, хотя

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

Ответ 7

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