Leggi n righe casuali da un file potenzialmente enorme


16

Questa sfida riguarda la lettura di righe casuali da un file potenzialmente enorme senza leggere l'intero file in memoria.

Ingresso

Un numero intero ne il nome di un file di testo.

Produzione

n righe del file di testo scelte in modo uniforme a caso senza sostituzione.

Si può presumere che nsia compreso nell'intervallo 1 rispetto al numero di righe nel file.

Prestare attenzione quando si campionano nnumeri a caso dall'intervallo in cui la risposta che si ottiene è uniforme. rand()%nin C non è uniforme per esempio. Ogni risultato deve essere ugualmente probabile.

Regole e restrizioni

Ogni riga del file di testo avrà lo stesso numero di caratteri e non sarà più di 80.

Il tuo codice non deve leggere nessuno dei contenuti del file di testo tranne:

  • Quelle linee che emette.
  • La prima riga per capire quanti caratteri per riga ci sono nel file di testo.

Possiamo supporre che ogni carattere nel file di testo richieda esattamente un byte.

Si presume che i separatori di linea siano lunghi 1 byte. Le soluzioni possono utilizzare separatori di linea lunga da 2 byte solo se specificano questa esigenza. Puoi anche supporre che l'ultima riga sia terminata da un separatore di riga.

La tua risposta dovrebbe essere un programma completo ma puoi specificare l'input in qualsiasi modo sia conveniente.

Lingue e biblioteche

Puoi usare qualsiasi lingua o biblioteca che ti piace.

Appunti

C'era una preoccupazione sul calcolo del numero di righe nel file. Come sottolinea nimi nei commenti, puoi dedurlo dalle dimensioni del file e dal numero di caratteri per riga.

Motivazione

Nella chat alcune persone hanno chiesto se questa è davvero una domanda "Do X senza Y". Interpreto questo per chiedere se le restrizioni sono insolitamente artificiali.

Il compito di campionare casualmente le linee da file di grandi dimensioni non è raro ed è in effetti uno che a volte devo fare. Un modo per farlo è in bash:

shuf -n <num-lines>

Questo è tuttavia molto lento per file di grandi dimensioni in quanto viene letto nell'intero file.


Perché il downvote?

3
Questo è banale in linguaggi come il C che hanno fseek, e impossibile in altri. Inoltre, cosa succede se nè maggiore del numero di righe nel file?
Mego,

4
@Mego: riguardo al tuo punto b): puoi calcolare il numero di righe dividendo la dimensione del file per la lunghezza di una riga.
nimi,

8
Do X senza Y è un avvertimento che inizia con "Questo non è sempre male". Il problema principale sono le restrizioni artificiali come "non usare +" che offre vantaggi alle lingue che hanno un sum(). Non leggere un file in memoria è una restrizione chiara e coerente che non è affatto arbitraria. Può essere testato con un file più grande della memoria, che non può essere aggirato dalle differenze di lingua. Capita anche di avere applicazioni nel mondo reale (anche se non è necessario per un golf ...).
trichoplax,

1
Sembra che questo sia in realtà un codice golf a complessità limitata in cui l'utilizzo della memoria è limitato nonostante i file potenzialmente enormi. Non si tratta di non avere determinate cose nel codice, ma una limitazione su come il codice può agire.
xnor

Risposte:


5

Dyalog APL , 63 byte

⎕NREAD¨t 82l∘,¨lׯ1+⎕?(⎕NSIZE t)÷l←10⍳⍨⎕NREAD 83 80,⍨t←⍞⎕NTIE 0

Richiede il nome del file, quindi il numero di righe casuali desiderate.

Spiegazione

Richiedi l'immissione di testo (nome file)
⎕NTIE 0Lega il file usando il successivo numero di pareggio disponibile (-1 su un sistema pulito)
t←Memorizza il numero di t
83 80,⍨fascicolo selezionato come Append [83,80] producendo [-1,83,80]
⎕NREADLeggi i primi 80 byte del file -1 come numeri interi a 8 bit (codice di conversione 83)
10⍳⍨Trova l'indice del primo numero 10 (LF)
l←Memorizza la lunghezza della riga come l
(⎕NSIZE t)÷Dividi la dimensione del file -1 con la lunghezza della riga
Richiedi input numerico (numero desiderato di righe )
?X selezioni casuali (senza sostituzione) per i primi Y numeri naturali
¯1+Aggiungi -1 per ottenere i numeri di riga 0-origine *
Moltiplica per la lunghezza della riga per ottenere i byte iniziali
t 82l∘,¨Prependi [-1,82, LineLength] per ogni byte iniziale (crea elenco di argomenti per ⎕NREAD)
⎕NREAD¨ Leggi ogni riga come carattere a 8 bit (codice di conversione 82)

Esempio pratico

Il file /tmp/records.txt contiene:

Hello
Think
12345
Klaus
Nilad

Rendere il programma RandLines contenga il codice sopra riportato alla lettera inserendo quanto segue nella sessione APL:

∇RandLines
⎕NREAD¨t 82l∘,¨lׯ1+⎕?(⎕NSIZE t)÷l←10⍳⍨⎕NREAD 83 80,⍨t←⍞⎕NTIE 0
∇

Nella sessione APL digitare RandLinese premere Invio.

Il sistema sposta il cursore sulla riga successiva, che è un prompt di lunghezza 0 per i dati dei caratteri; entra /tmp/records.txt.

Il sistema ora emette ⎕:e attende input numerici; entra 4.

Il sistema emette quattro linee casuali.

Vita reale

In realtà, potresti voler dare il nome del file e contare come argomenti e ricevere il risultato come una tabella. Questo può essere fatto inserendo:

RandLs←{↑⎕NREAD¨t 82l∘,¨lׯ1+⍺?(⎕NSIZE t)÷l←10⍳⍨⎕NREAD 83 80,⍨t←⍵⎕NTIE 0}

Ora fai MyLines contenere tre linee casuali con:

MyLines←3 RandLs'/tmp/records.txt'

Che ne dici di restituire solo una singola linea casuale se il conteggio non è specificato:

RandL←{⍺←1 ⋄ ↑⎕NREAD¨t 82l∘,¨lׯ1+⍺?(⎕NSIZE t)÷l←10⍳⍨⎕NREAD 83 80,⍨t←⍵⎕NTIE 0}

Ora puoi fare entrambe le cose:

MyLines←2 RandL'/tmp/records.txt'

e (notare l'assenza dell'argomento sinistro):

MyLine←RandL'/tmp/records.txt'

Rendere il codice leggibile

I golfari APL golfizzati sono una cattiva idea. Ecco come scriverei in un sistema di produzione:

RandL←{ ⍝ Read X random lines from file Y without reading entire file
    ⍺←1 ⍝ default count
    tie←⍵⎕NTIE 0 ⍝ tie file
    length←10⍳⍨⎕NREAD 83 80,⍨tie ⍝ find first NL
    size←⎕NSIZE tie ⍝ total file length
    starts←lengthׯ1+⍺?size÷length ⍝ beginning of each line
    ↑⎕NREAD¨tie 82length∘,¨starts ⍝ read each line as character and convert list to table
}

* Potrei salvare un byte eseguendo in modalità origine 0, che è standard su alcuni sistemi APL: rimuovere ¯1+e inserire 1+prima 10.


Ahh .. APL :) C'è un modo per testare questo codice in Linux?

@Lembik Certo, questo codice è multipiattaforma. Scarica da dyalog.com
Adám,

Dato che non leggo APL, potresti spiegare il codice? Le parti difficili sono linee di campionamento senza sostituzione e saltare direttamente nel posto giusto nel file per leggere le linee.

@Lembik Quella parte è facile. ⎕L'argomento NREAD è TieNumber ConversionCode BytesToRead [StartByte]. Legge solo i byte richiesti. Il resto sta solo cercando di capire cosa leggere.
Adám,

@Lembik Sono curioso di sapere perché la mia risposta non ha vinto la taglia.
Adám,

7

Rubino, 104 94 92 90 byte

Il nome del file e il numero di righe vengono passati alla riga di comando. Ad esempio, se il programma è shuffle.rbe il nome del file è a.txt, eseguire ruby shuffle.rb a.txt 3per tre righe casuali.

-4 byte dalla scoperta della opensintassi in Ruby anzichéFile.new

f=open$*[0]
puts [*0..f.size/n=f.gets.size+1].sample($*[1].to_i).map{|e|f.seek n*e;f.gets}

Inoltre, ecco una soluzione di funzione anonima a 85 byte che accetta una stringa e un numero come argomenti.

->f,l{f=open f;puts [*0..f.size/n=f.gets.size+1].sample(l).map{|e|f.seek n*e;f.gets}}

Sotto 100 byte! Forse Ruby è la migliore lingua per giocare a golf dopo tutto. Il "campione" evita le ripetizioni?

@Lembik ruby-doc.org/core-2.2.0/Array.html#method-i-sample Evita le ripetizioni. Non dirmi ... avrei dovuto avere ripetizioni?
Value Ink

No, sei perfetto :)

Puoi salvare qualche byte leggendo da stdin? ruby shuffle.rb 3 < a.txtti dà uno stdin ricercabile. IDK Ruby, però.
Peter Cordes,

1
@PeterCordes Questo ha senso, ma come detto, il punto di errore è che Ruby non è in grado di leggere la dimensione del file di stdin, quindi non ha funzionato.
Value Ink

5

Haskell, 240 224 236 byte

import Test.QuickCheck
import System.IO
g=hGetLine
main=do;f<-getLine;n<-readLn;h<-openFile f ReadMode;l<-(\x->1+sum[1|_<-x])<$>g h;s<-hFileSize h;generate(shuffle[0..div s l-1])>>=mapM(\p->hSeek h(toEnum 0)(l*p)>>g h>>=putStrLn).take n

Legge il nome del file n dallo stdin.

Come funziona:

main=do
  f<-getLine                   -- read file name from stdin
  n<-readLn                    -- read n from stdin
  h<-openFile f ReadMode       -- open the file
  l<-(\x->1+sum[1|_<-x])<$>g h -- read first line and bind l to it's length +1
                               -- sum[1|_<-x] is a custom length function
                               -- because of type restrictions, otherwise I'd have
                               -- to use "toInteger.length"
  s<-hFileSize h               -- get file size
  generate(shuffle[0..div s l-1])>>=
                               -- shuffle all possible line numbers 
  mapM (\->p  ...  ).take n    -- for each of the first n shuffled line numbers 
     hSeek h(toEnum 0).(l*p)>> -- jump to that line ("toEnum 0" is short for "AbsoluteSeek")
     g h>>=                    -- read a line from current position
     putStrLn                  -- and print

Ci vuole molto tempo e memoria per eseguire questo programma per file con molte linee, a causa di un'orribile shufflefunzione inefficiente .

Modifica: ho perso la parte "casuale senza sostituzione" (grazie @feersum per averlo notato!).


Haskell rocks :)

1
Come evita di scegliere una linea che era già stata scelta?
feersum

@feersum: oh, ho perso quella parte. Fisso.
nimi,

Vedo che stackoverflow.com/questions/13779630/… è piuttosto dettagliato!

1
Forse dovrebbe esserci una sfida separata sul campionamento senza sostituzione in spazi ristretti.

3

PowerShell v2 +, 209 byte

param($a,$n)
$f=New-Object System.IO.FileStream $a,"Open"
for(;$f.ReadByte()-ne10){$l++}
$t=$f.Length/++$l-1
[byte[]]$z=,0*$l
0..$t|Get-Random -c $n|%{$a=$f.Seek($l*$_,0);$a=$f.Read($z,0,$l-1);-join[char[]]$z}

Accetta input $acome nome file e $ncome numero di righe. Nota che$a deve essere un nome file a percorso completo e si presume che sia la codifica ANSI.

Costruiamo quindi un nuovo FileStreamoggetto e gli diciamo di accedere al file $acon Openprivilegio.

Il forciclo .Read()passa attraverso la prima linea fino a quando non colpiamo un \npersonaggio, aumentando il contatore della lunghezza di ogni personaggio. Abbiamo quindi impostato$t uguale alla dimensione del file (ovvero, quanto è lungo lo stream) diviso per quanti caratteri per riga (più uno in modo che conti il ​​terminatore), meno uno (poiché siamo indicizzati zero). Costruiamo quindi il nostro buffer $zin modo che sia anche lungo la linea.

L'ultima riga crea un array dinamico con l' ..operatore di intervallo. 1 Effettuiamo il piping dell'array Get-Randomcon una -Cquantità di $nper selezionare casualmente i $nnumeri di riga senza ripetizioni. I numeri fortunati vengono convogliati in un loop con |%{...}. Ogni iterazione ci porta .Seekalla posizione particolare, e quindi .Readnel valore di una riga di caratteri, memorizzati $z. Ri-cast $zcome char-array e-join insieme, lasciando la stringa risultante sulla pipeline e l'output è implicito alla fine del programma.

Tecnicamente dovremmo concludere con una $f.Close()chiamata per chiudere il file, ma questo costa byte! : p

Esempio

a.txt:
a0000000000000000000000000000000000000000000000001
a0000000000000000000000000000000000000000000000002
a0000000000000000000000000000000000000000000000003
a0000000000000000000000000000000000000000000000004
a0000000000000000000000000000000000000000000000005
a0000000000000000000000000000000000000000000000006
a0000000000000000000000000000000000000000000000007
a0000000000000000000000000000000000000000000000008
a0000000000000000000000000000000000000000000000009
a0000000000000000000000000000000000000000000000010

PS C:\Tools\Scripts\golfing> .\read-n-random-lines.ps1 "c:\tools\scripts\golfing\a.txt" 5
a0000000000000000000000000000000000000000000000002 
a0000000000000000000000000000000000000000000000001 
a0000000000000000000000000000000000000000000000004 
a0000000000000000000000000000000000000000000000010 
a0000000000000000000000000000000000000000000000006 

1 Tecnicamente, ciò significa che possiamo supportare solo un massimo di 50.000 linee, poiché questa è la gamma più ampia che può essere creata dinamicamente in questo modo. : - / Ma, non possiamo limitarci a ripetere un ciclo di Get-Randomcomandi $n, scartando i duplicati di ogni ciclo, dato che non è deterministico ...


2

Python 3, 146 139 byte

from random import*
i=input
f=open(i())
l=len(f.readline())
[(f.seek(v*l),print(f.read(l)))for v in sample(range(f.seek(0,2)//l),int(i()))]
#print is here^

Input: [nome file] \ n [linee] \ n

Questa soluzione è stata fortemente rubata da @pppery ma è solo python3.5 ed è un programma completo.

Modifica: grazie a @Mego per l'intervallo inline e la compatibilità python3.x. Edit2: chiarimento su dove si trova la stampa perché ho ricevuto due commenti a riguardo. (Il commento ovviamente non fa parte del codice o del conteggio dei byte.)


Grazie! Quale parte è solo Python 3.5?

2
r=range(f.seek(0,2)//l)funzionerà, che si rade di 3 byte e rimuove la necessità di 3.5. Ancora meglio, radere altri 3 byte incorporando la rangechiamata nella samplechiamata. Inoltre, questo non è un programma completo: è necessario stampare effettivamente l'elenco.
Mego

@Lembik: era 3.5 solo perché l'ho usato r=[*range(f.seek(0,2)//l)]perché pensavo di non poter essere sampleun generatore. Ho scoperto che potrei. @Mega: è completo perché stampa ogni riga all'interno della comprensione della listaprint(f.read(l))
Alexander Nigl,

Tuttavia, è necessaria una dichiarazione stampata.

la stampa è all'interno della comprensione dell'elenco.
Alexander Nigl,

2

Lua, 126 122

r=io.read;f=io.open(r())c=2+f:read():len()for i=1,r()do f:seek("set",c*math.random(0,f:seek("end")/c-1))print(f:read())end

Utilizza 2 byte per le interruzioni di riga. Cambia il 2 in 1 per 1. L'ho solo come 2 perché è quello che aveva il mio file di test.

Mi sono trovato sotto la voce PHP, ma ancora al 2 ° posto (attualmente). Maledizione, voce di Ruby!


1
Lua è stato il primo linguaggio di programmazione che ho imparato e, nonostante tutto quello che ho imparato da allora, è ancora il mio preferito. È così versatile per la sua facilità di scrittura.
Blab,

2

Bash (bene, coreutils), 100 byte

n=`head -1 $2|wc -c`;shuf -i0-$[`stat -c%s $2`/$n] -n$1|xargs -i dd if=$2 bs=$n skip={} count=1 2>&-

Spiegazione

Questo evita di leggere l'intero file usando ddper estrarre le parti del file di cui abbiamo bisogno senza leggere interamente il file, purtroppo finisce abbastanza grande con tutte le opzioni che dobbiamo specificare:

ifè il file di input
bsè la dimensione del blocco (qui lo impostiamo su $nquale è la lunghezza della prima riga
skipè impostata sugli interi casuali estratti da shufed equivale al ibsvalore saltando skip* ibsbyte
count il numero di ibssezioni di lunghezza da restituire
status=noneè necessario per eliminare le informazioni normalmente emesse dadd

Otteniamo la lunghezza della linea usando head -1 $2|wc -ce la dimensione del file con stat -c%s $2.

uso

Salva sopra come file.sh ed esegui usando file.sh n filename.

Tempi

time ~/randlines.sh 4 test.txt
9412647
4124435
7401105
1132619

real    0m0.125s
user    0m0.035s
sys     0m0.061s

vs.

time shuf -n4 test.txt
1204350
3496441
3472713
3985479

real    0m1.280s
user    0m0.287s
sys     0m0.272s

Tempi precedenti per un file di 68 MiB generato utilizzando seq 1000000 9999999 > test.txt .

Grazie a @PeterCordes per il suo suggerimento -1!


1
Adoro sempre una risposta bash, ma puoi spiegare come questo non legge l'intero file?

2
@Lembik aggiunta spiegazione!
Dom Hastings,

1
Puoi bs=invece ibs=, poiché anche l'impostazione obsva bene. Suppongo che non puoi sostituirlo if=$2con <$2, poiché questa è ancora xargsla riga di comando. \<$2non funziona neanche (xargs usa exec direttamente, senza shell).
Peter Cordes,

Spero che non sia troppo, ma mi piace questa risposta :) Ho appena provato con un file da 1 GB.

1
re: reindirizzamento di stderr a stdin: si potrebbe anche chiudere stderr con 2>&-, quindi non c'è pericolo che l'output vada da nessuna parte (es. se stdin fosse un descrittore di file in lettura-scrittura). Funziona con GNU dd: ne produce ancora stdoutprima di provare e non riuscire a scrivere stderr.
Peter Cordes,

1

Python 3 - 161 160 149 byte

from random import*;
def f(n,g):f=open(g);l=len(f.readline());r=list(range(f.seek(0,2)/l));shuffle(r);[(f.seek(v*l),print(f.read(l)))for v in r[:k]]

Questo codice definisce una funzione che si chiama like f(10,'input.txt')


1
La sfida richiede un programma completo, quindi temo che una definizione di funzione non sia sufficiente.
nimi,

Per salvare un byte rimuovere lo spazio tra import e *.
mriklojn,

1
@nimi Richiedere un programma completo per questa sfida sembra sovrascrivere arbitrariamente le regole del formato del codice predefinito
pppery

@ppperry: sì, forse, ma è così.
nimi

Per ottenere la lunghezza del file è possibile cercare (0,2) , che rende obsolete import os e os.stat.
Alexander Nigl,

1

C # 259 byte senza duplicati

class Program{static void Main(string[]a){int c=Convert.ToInt32(a[1]);var h=File.ReadLines(a[0]);HashSet<int>n=new HashSet<int>();while(n.Count<c)n.Add(new Random().Next(0,h.Count()));for(;c>0;c--)Console.WriteLine(h.Skip(n.ElementAt(c-1)).Take(1).First());}}

Ungolfed

class Program{static void Main(string[] a)
{
        int c = Convert.ToInt32(a[1]);
        var h = File.ReadLines(a[0]);
        HashSet<int> n = new HashSet<int>();
        while (n.Count < c)
            n.Add(new Random().Next(0, h.Count()));           
        for (; c > 0; c--)
            Console.WriteLine(h.Skip(n.ElementAt(c-1)).Take(1).First());
    }
}

File.ReadLines è Lazy. Questo ha l'ulteriore vantaggio che tutte le linee possono avere lunghezze diverse.

In esecuzione sarebbe:

sample.exe a.txt 10000

C # 206 byte con duplicati

class Program{static void Main(string[]a){var n=new Random();int c=Convert.ToInt32(a[1]);var h=File.ReadLines(a[0]);for(;c>0;c--)Console.WriteLine(h.Skip((int)(n.NextDouble()*h.Count())).Take(1).First());}}

Ungolfed

class Program
{
    static void Main(string[] a)
    {
        Random n = new Random();
        int c = Convert.ToInt32(a[1]);
        var h = File.ReadLines(a[0]);
        for (; c > 0; c--)
            Console.WriteLine(h.Skip((int)(n.NextDouble()*h.Count())).Take(1).First());
    }
}

Non seguo pienamente la tua soluzione. Se tutte le linee hanno lunghezze diverse, l'attività è impossibile. Inoltre, come si esegue il campionamento casuale delle righe senza sostituzione esattamente? Chiedo scusa che il mio C # non sia abbastanza buono.

@Lembik Hai ragione, non ho pensato ai duplicati. E posso contare la quantità di linee ed estrarre le linee in base al numero di lino, motivo per cui le linee possono essere di lunghezza variabile.
Master117,

Ma devi saltare in una posizione nel file solo sapendo che il numero di riga non è vero? Non puoi dire dove si trova a meno che tutte le linee non abbiano la stessa lunghezza.

@Lembik File.ReadLines ("pathToFile") crea un elenco Lazy su tutte le righe del file, File.ReadLines ("pathToFile"). ElementAt (19) restituisce la diciannovesima riga del file. Un po 'come una mappa di tutti i Linestart.
Master117,

Non penso che l'enumerazione pigra salti (o cerchi) il file tristemente. Quindi attualmente non si adatta alle regole.

1

Python (141 byte)

Mantiene ogni riga con uguale probabilità, utilizzare anche con le pipe. Tuttavia, non risponde alla limitazione salta avanti della domanda ...

Utilizzo cat largefile | python randxlines.py 100o python randxlines 100 < largefile(come sottolineato da @petercordes)

import random,sys
N=int(sys.argv[1])
x=['']*N
for c,L in enumerate(sys.stdin):
    t=random.randrange(c+1)
    if(t<N):x[t] = L
print("".join(x))

3
Il punto centrale di questa domanda è che devi cercare nel flusso di input. Probabilmente dovresti dire che questa è la parte delle restrizioni della domanda che stai ignorando (anche se l'uso dell'esempio read-from-a-pipe lo rende abbastanza chiaro). Leggere dallo stdin con python ./randxlines.py 100 < largefileandrebbe bene per una risposta corretta, però: in tal caso stdinsarà ricercabile.
Peter Cordes,
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.