2012-08-26 9 views
6

Ho molte attività in file .txt in più sottocartelle. Sto cercando di raccogliere un totale di 10 compiti a caso da queste cartelle, i loro file contenuti e infine una riga di testo all'interno di un file. La riga selezionata deve essere cancellata o contrassegnata in modo che non venga scelta nella successiva esecuzione. Questa potrebbe essere una domanda troppo ampia, ma apprezzerei qualsiasi input o direzione.Linee casuali Python da sottocartelle

Ecco il codice che ho finora:

#!/usr/bin/python 
import random 
with open('C:\\Tasks\\file.txt') as f: 
    lines = random.sample(f.readlines(),10)  
print(lines) 
+3

Vuoi 10 linee casuali da ogni file o 10 linee _IN total_? –

+1

Grazie, 10 righe casuali in totale. – user1582596

+0

Le linee in questi file sono uniche? Ti aspetti linee/file da aggiungere tra le esecuzioni? Questi file contengono decine o milioni di righe? –

risposta

4

Per ottenere una distribuzione casuale corretta in tutti questi file, avresti bisogno di vederle come un unico grande insieme di linee e pick 10 a caso. In altre parole, dovrai leggere tutti questi file almeno una volta per capire almeno di quante linee hai.

Tuttavia non è necessario tenere tutte le linee in memoria. Dovresti farlo in due fasi: indicizza i tuoi file per contare il numero di linee in ciascuno, quindi scegli 10 linee casuali da leggere da questi file.

Prima indicizzazione:

import os 

root_path = r'C:\Tasks\\' 
total_lines = 0 
file_indices = dict() 

# Based on https://stackoverflow.com/q/845058, bufcount function 
def linecount(filename, buf_size=1024*1024): 
    with open(filename) as f: 
     return sum(buf.count('\n') for buf in iter(lambda: f.read(buf_size), '')) 

for dirpath, dirnames, filenames in os.walk(root_path): 
    for filename in filenames: 
     if not filename.endswith('.txt'): 
      continue 
     path = os.path.join(dirpath, filename) 
     file_indices[total_lines] = path 
     total_lines += linecount(path) 

offsets = list(file_indices.keys()) 
offsets.sort() 

Ora abbiamo una mappatura degli offset, indicando i nomi dei file, e un conteggio totale della linea. Ora prendiamo dieci indici casuali, e leggere questi dai file:

import random 
import bisect 

tasks = list(range(total_lines)) 
task_indices = random.sample(tasks, 10) 

for index in task_indices: 
    # find the closest file index 
    file_index = offsets[bisect.bisect(offsets, index) - 1] 
    path = file_indices[file_index] 
    curr_line = file_index 
    with open(path) as f: 
     while curr_line <= index: 
      task = f.readline() 
      curr_line += 1 
    print(task) 
    tasks.remove(index) 

Si noti che è necessario solo l'indicizzazione una volta; è possibile memorizzare il risultato da qualche parte e aggiornarlo solo quando i file vengono aggiornati.

Inoltre, le attività vengono ora "archiviate" nell'elenco tasks; questi sono indici alle linee nei file e rimuovo l'indice da quella variabile quando si stampa l'attività selezionata. La prossima volta che eseguirai le selezioni random.sample(), le attività precedentemente selezionate non saranno più disponibili per il prelievo la prossima volta. Questa struttura dovrà essere aggiornata se i tuoi file cambiano mai, poiché gli indici devono essere ricalcolati. Lo file_indices ti aiuterà con questa attività, ma questo non rientra nell'ambito di questa risposta. :-)

Se è necessario solo un campione di 10-item, utilizzare Blckknght's solution invece, in quanto solo passerà attraverso i file una volta, mentre la mia richiedono 10 aperture extra file. Se sono necessari campioni multipli, questa soluzione richiede solo 10 aperture di file aggiuntive ogni volta che è necessario il campione, non eseguirà nuovamente la scansione di tutti i file. Se hai meno di 10 file, usa ancora la risposta di Blckknght. :-)

+0

Grazie, durante l'indicizzazione, ha seguito l'errore Traceback (ultima chiamata ultima): File "", riga 1, in AttributeError: l'oggetto 'dict_keys' non ha attributo 'sort'. btw, sto provando questo con Python 3.2.3 – user1582596

+0

@ user1582596: Ah, distinzione importante, ho aggiornato il codice per te ora. –

+2

In realtà non è necessario sapere quante linee totali ci sono per scegliere 10 a caso.Puoi scegliere una riga a caso riducendo la probabilità per ogni riga che è quella che conservi: http://www.perlmonks.org/?node_id=1910. Per le N righe, mantieni una lista di N, e per ogni nuova riga, riduci la probabilità che tu la mantenga: http://www.perlmonks.org/?node_id=1910 (mi dispiace per tutto il Perl). –

0

MODIFICA: Su un esame più attento, questa risposta non corrisponde al conto. La rielaborazione mi ha portato all'algoritmo di campionamento del serbatoio, che @Blckknght ha usato nella sua risposta. Quindi ignora questa risposta.

Pochi modi per farlo. Ecco uno ...

  1. Ottenere un elenco di tutti i file Operazione
  2. Selezionare uno a caso
  3. Selezionare una sola linea da quel file in modo casuale
  4. Ripetere finché non avremo il numero desiderato di linee

Il codice ...

import os 
import random 

def file_iterator(top_dir): 
    """Gather all task files""" 
    files = [] 
    for dirpath, dirnames, filenames in os.walk(top_dir): 
     for filename in filenames: 
      if not filename.endswith('.txt'): 
       continue 
      path = os.path.join(dirpath, filename) 
      files.append(path) 
    return files 


def random_lines(files, number=10): 
    """Select a random file, select a random line until we have enough 
    """ 
    selected_tasks = [] 

    while len(selected_tasks) < number: 
     f = random.choice(files) 
     with open(f) as tasks: 
      lines = tasks.readlines() 
      l = random.choice(lines) 
      selected_tasks.append(l) 
    return selected_tasks 


## Usage 
files = file_iterator(r'C:\\Tasks') 
random_tasks = random_lines(files) 
+0

Ciò può portare a scelte duplicate e dubito che la distribuzione del campione sarà uniforme. Come ricordi o rimuovi compiti selezionati in una corsa futura? Dall'OP: * La riga selezionata deve essere cancellata o contrassegnata in modo da non essere selezionata nell'esecuzione successiva. * –

+0

Doh, avrei dovuto leggere più attentamente. Un po 'in ritardo per modificare la mia risposta ora. Ci arriverò domani. Sospetto che una soluzione semplice sia trasformare l'elenco delle linee in un set –

+0

10x, @Martijn Pieters, ottenuto i seguenti errori, Traceback (ultima chiamata ultima): File "C: \ Dropbox \ Python \ testr1.py", linea 31, in file = file_iterator (r'C: \\ Dropbox \\ ans7i \\ ') File "C: \ Dropbox \ Python \ testr1.py", riga 11, in file_iterator path = os.path. join (dirpath, nomefile) UnboundLocalError: variabile locale 'nomefile' a cui si fa riferimento prima dell'assegnazione – user1582596

14

Ecco una soluzione semplice che effettua un solo passaggio per i file per campione. Se sai esattamente quanti oggetti verranno campionati dai file, probabilmente è ottimale.

Prima di tutto è la funzione di esempio. Questo utilizza lo stesso algoritmo a cui @NedBatchelder si è collegato in un commento su una risposta precedente (sebbene il codice Perl mostrato lì abbia selezionato solo una singola riga, piuttosto che diversi). Seleziona i valori da un iterable di linee e richiede solo che le righe attualmente selezionate vengano conservate in memoria in qualsiasi momento (più la successiva riga di candidati). Solleva uno ValueError se l'iterabile ha meno valori della dimensione campionaria richiesta.

import random 

def random_sample(n, items): 
    results = [] 

    for i, v in enumerate(items): 
     r = random.randint(0, i) 
     if r < n: 
      if i < n: 
       results.insert(r, v) # add first n items in random order 
      else: 
       results[r] = v # at a decreasing rate, replace random items 

    if len(results) < n: 
     raise ValueError("Sample larger than population.") 

    return results 

edit: In un'altra domanda, l'utente @DzinX ha notato che l'uso di insert in questo codice rende la brutta prestazione (O(N^2)) se siete il campionamento di un numero molto elevato di valori. La sua versione migliorata che evita tale problema è here. /edit

Ora abbiamo solo bisogno di fare un iterable adatto di articoli per la nostra funzione da cui provare. Ecco come lo farei usando un generatore. Questo codice manterrà un solo file alla volta e non richiede più di una riga alla volta. Il parametro opzionale exclude, se presente, dovrebbe essere un set contenente righe che sono state selezionate in una corsa precedente (e quindi non dovrebbe essere restituito).

import os 

def lines_generator(base_folder, exclude = None): 
    for dirpath, dirs, files in os.walk(base_folder): 
     for filename in files: 
      if filename.endswith(".txt"): 
       fullPath = os.path.join(dirpath, filename) 
       with open(fullPath) as f: 
        for line in f: 
         cleanLine = line.strip() 
         if exclude is None or cleanLine not in exclude: 
          yield cleanLine 

Ora, abbiamo solo bisogno di una funzione wrapper per legare questi due pezzi insieme (e gestire una serie di linee visto). Può restituire un singolo campione di dimensioni n o un elenco di campioni count, sfruttando il fatto che anche una porzione di un campione casuale è un campione casuale.

_seen = set() 

def get_sample(n, count = None): 
    base_folder = r"C:\Tasks" 
    if count is None: 
     sample = random_sample(n, lines_generator(base_folder, _seen)) 
     _seen.update(sample) 
     return sample 
    else: 
     sample = random_sample(count * n, lines_generator(base_folder, _seen)) 
     _seen.update(sample) 
     return [sample[i * n:(i + 1) * n] for i in range(count)] 

Ecco come può essere utilizzato:

def main(): 
    s1 = get_sample(10) 
    print("Sample1:", *s1, sep="\n") 

    s2, s3 = get_sample(10,2) # get two samples with only one read of the files 
    print("\nSample2:", *s2, sep="\n") 
    print("\nSample3:", *s3, sep="\n") 

    s4 = get_sample(5000) # this will probably raise a ValueError! 
+0

potresti scrivere: '(lettera per parola in frase per lettera in parola se buona (lettera))' invece di 'chain.from_iterable ((per lettera in parola se buona (lettera)) per parola nella frase)' – jfs

+0

Hmm, hai ragione. Penso di aver iniziato a usare 'chain.from_iter' mentre stavo cercando qualcosa di diverso e non è necessario nella versione con cui ho finito e pubblicato. Un'espressione generatore lineare è più chiara, quindi proverò invece (penso che mi salverà anche una riga, dal momento che non avrò bisogno di spogliare le linee separatamente). – Blckknght

+1

Si potrebbe anche scrivere espliciti per-loops e 'yield line' in' task_pipeline() '. Dovrebbe produrre la versione più leggibile. Inoltre è naturale usare 'con open (nomefile) come file:' in questo caso (vuoi questo se l'albero contiene un gran numero di file txt per evitare l'errore "Troppi file aperti") – jfs