2012-06-13 7 views
10

He visto algunos ejemplos realmente hermosos de Ruby y estoy tratando de cambiar mi forma de pensar para poder producirlos en lugar de solo admirarlos. Esto es lo mejor que podía llegar a para recoger una línea aleatoria de un archivo:Ruby: ¿Cuál es una manera elegante de elegir una línea al azar de un archivo de texto?

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 

me siento como que tiene que ser posible hacer esto de una manera más corta, más elegante sin almacenar los contenidos de todo el archivo en la memoria . ¿Esta ahí?

+2

¿Se trata más de una pregunta "¿cómo hago esto _en Ruby_", o más de una pregunta "¿cómo hago esto _en menos de O (N) espacio_"? Si es este último, investigue [muestreo de yacimientos] (http://gregable.com/2007/10/reservoir-sampling.html). – zwol

+0

mi implementación trivial sería buscar una posición aleatoria en el archivo y luego buscar hacia adelante una nueva línea –

+0

@SamSaffron Eso no le dará una línea uniformemente al azar a menos que todas las líneas sean exactamente de la misma longitud. – zwol

Respuesta

13

Puede hacerlo sin almacenar nada excepto el candidato actual para la línea aleatoria.

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 

Así que la primera línea se elige con probabilidad 1/1 = 1; la segunda línea se elige con probabilidad 1/2, por lo que la mitad de las veces se conserva la primera y la mitad del tiempo que cambia a la segunda.

Luego se elige la tercera línea con probabilidad 1/3, por lo que 1/3 de las veces la elige, y la otra 2/3 veces guarda cualquiera de las dos primeras que eligió. Como cada uno de ellos tenía un 50% de probabilidades de ser elegido a partir de la línea 2, cada uno termina con una probabilidad de 1/3 de ser elegido a partir de la línea 3.

Y así sucesivamente. En la línea N, cada línea de 1-N tiene una probabilidad de 1/N de ser elegida, y eso se mantiene hasta el final del archivo (siempre que el archivo no sea tan grande que 1/(número de líneas en el archivo)) es menor que épsilon :)). Y solo realiza una pasada en el archivo y nunca almacena más de dos líneas a la vez.

EDITAR Usted no va a conseguir una solución concisa real con este algoritmo, pero se puede convertir en una sola línea si desea:

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

Difícil elección, pero la tuya fue la única respuesta que se dirigió a la parte de la memoria de la pregunta. Me pregunto si hay una manera de reducir la verbosidad de esto de una manera similar a la respuesta de Dave. ¡Gracias! – Tres

+0

(El '.to_f' es superfluo). ¿No volvería esto a veces? – steenslag

+0

@steenslag: cierto, dado el '1.0', se puede prescindir de' .to_f'. Pero no, nunca volverá a cero, porque 'rand' siempre es menor que 1, por lo que siempre elige la primera línea. –

2

Esto no es mucho mejor que lo que se le ocurrió, pero al menos es más corto:

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

Una cosa que usted puede hacer para que el código sea más Rubyish apoyos se omiten. Use readlines y size en lugar de readlines() y size().

+0

Gracias por su comentario. Sí, me preguntaba sobre eso cuando escribí mi respuesta. Estoy tratando de averiguar si hay una manera de mantener la sintaxis concisa, mientras cierro el archivo correctamente. – Mischa

+0

Lo encontraste, parece ;-) –

35

Ya existe una entrada al azar selector integrado en la clase Ruby Array: sample().

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

¡Maravilloso, redujo el método a una línea! (Debería haber sabido acerca de 'sample' a mí mismo) – Mischa

+5

Advertencia: Sufrirías si el archivo fuera grande. –

+2

Tenga en cuenta que para aquellos holdouts que aún usan Ruby 1.8, 'sample' se llamaba 'choice'. –

-1

Stat el archivo, seleccione un número aleatorio entre cero y el tamaño del archivo, que tratan de byte en el archivo. Escanee hasta la próxima línea nueva, luego lea y devuelva la siguiente línea (suponiendo que no se encuentra al final del archivo).

+1

Esta es una mala idea porque la primera línea * nunca * será recogido y las líneas después de una línea larga tienen una mayor probabilidad de ser recogidas, por lo que no es aleatorio. – Mischa

+0

Buen punto. Supongo que podrías buscar escanear hacia atrás y luego hacia adelante para leer la línea en el lugar donde golpeas. A menos que el archivo sea enorme, o los requisitos de rendimiento son tontos; no vale la pena el esfuerzo. – Bill

0

Un chiste:

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

Si protesta que no es Ruby, ir a buscar a una charla en Euruko de este año titulado Ruby es a diferencia de un plátano.

PD: Ignora el resaltado de sintaxis incorrecto de SO.

+0

Bueno, excepto que no funcionaría con Ruby en Windows ... – Azolo

+0

Verdadero.Lo cual supongo que fue el argumento de Martí de que no es natural usar Ruby fuera de Unix. –

+0

Espero que no sea demasiado antinatural, si es así espero que, dado que Windows esté en la [Lista de plataformas admitidas por Ruby] (http://bugs.ruby-lang.org/projects/ruby-trunk/wiki/SupportedPlatforms) Trabajaríamos como comunidad para arreglarlo. Pero eso no es un problema para stackoverflow, así que lo dejo así. – Azolo

0

Aquí una versión más corta de respuesta excelente de Marcos, no tan corto como Dave aunque

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

Esto se muestra más corto, no siempre es mejor. ¿Cambiar las variables locales a argumentos solo para guardar líneas? – Mischa

3

Esta función hace exactamente lo que necesita.

No es un trazador de líneas. Pero funciona con archivos de texto de cualquier tamaño (excepto el tamaño cero, tal vez :).

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 

Inténtelo:

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

despreciable (francamente) inconvenientes:

  1. la larga sea la línea, mayor será la probabilidad de que va a ser recogido.

  2. no tiene en cuenta el separador de líneas "\ r" (específico de Windows). ¡Usa archivos con terminaciones de líneas estilo Unix!

+0

La mejor opción para archivos grandes, de lo contrario, el tiempo de lectura variará drásticamente. –

Cuestiones relacionadas