2010-11-07 11 views
25

Qual è il modo migliore per leggere un file molto grande (come un file di testo con 100.000 nomi uno su ogni riga) in un elenco (pigramente - caricarlo se necessario) in clojure?Leggere un file di testo molto grande in un elenco in clojure

Fondamentalmente ho bisogno di fare tutti i tipi di ricerche di stringhe su questi elementi (lo faccio con grep e reg ex negli script di shell ora).

Ho provato ad aggiungere '(all'inizio e) alla fine ma a quanto pare questo metodo (il caricamento di una statica?/List costante, ha un limite di dimensione per qualche motivo.

risposta

19

È necessario utilizzare line-seq. Un esempio da clojuredocs:

;; Count lines of a file (loses head): 
user=> (with-open [rdr (clojure.java.io/reader "/etc/passwd")] 
     (count (line-seq rdr))) 

Ma con un elenco pigro di stringhe, non si può fare quelle operazioni in modo efficiente, che richiedono l'intero elenco di essere presenti, come l'ordinamento Se è possibile implementare le operazioni come filter o map allora si può consumare. la lista pigramente, altrimenti sarebbe meglio usare un embed database ded

Si noti inoltre che non si dovrebbe tenere in testa l'elenco, altrimenti l'intero elenco verrà caricato in memoria.

Inoltre, se è necessario eseguire più di un'operazione, è necessario leggere il file più e più volte. Essere avvertiti, la pigrizia può rendere le cose difficili a volte.

+0

Grazie mille, ma cosa succede se volevo mantenere tutta la lista in memoria (non essere pigro), quale sarebbe il modo migliore allora? Come hai detto per alcune operazioni ho bisogno di andare oltre la lista più e più volte (supponiamo di avere abbastanza memoria per mantenere l'intera lista). – Ali

+3

In tal caso, mantieni semplicemente un riferimento al capo della lista pigra. Sarà caricato pigramente la prima volta e poi rimarrà caricato. Qualcosa come: '(nomi def (con-apri [rdr (clojure.java.io/reader"/path/to/names/file ")] (line-seq rdr)))' –

+6

Beh, non credo così. Poiché hai circondato "line-seq" con "with-open", il flusso sottostante verrà chiuso automaticamente al suo ritorno. Quindi non rimane nulla dietro i tuoi "nomi" var. Quindi in pratica dovresti 1: '(def rdr (clojure.java.io/reader"/path/to/names/file "))' then 2: '(def names (line-seq rdr))' then 3 : '(. rdr close)'. Infine, ora puoi giocare con i tuoi "nomi" come: '(conteggi nomi)' –

27

Ci sono vari modi per farlo, a seconda esattamente di ciò che si desidera.

Se si dispone di un function che si desidera applicare a ciascuna riga in un file, è possibile utilizzare codice simile alla risposta di Abhinav:

(with-open [rdr ...] 
    (doall (map function (line-seq rdr)))) 

Questo ha il vantaggio che il file viene aperto, processato, e chiuso il più rapidamente possibile, ma impone l'intero file da consumare contemporaneamente.

Se si vuole ritardare l'elaborazione del file che si potrebbe essere tentati di tornare alle linee, ma questo non funziona:

(map function ; broken!!! 
    (with-open [rdr ...] 
     (line-seq rdr))) 

perché il file è chiuso quando with-open rendimenti, che è prima di elaborate pigramente il file.

Un modo per aggirare questo è quello di tirare l'intero file in memoria con slurp:

(map function (slurp filename)) 

che ha un ovvio svantaggio - l'utilizzo della memoria - ma garantisce che non lasciare il file aperto.

Un'alternativa è quella di lasciare il file aperto fino ad arrivare alla fine della lettura, mentre la generazione di una sequenza pigro:

(ns ... 
    (:use clojure.test)) 

(defn stream-consumer [stream] 
    (println "read" (count stream) "lines")) 

(defn broken-open [file] 
    (with-open [rdr (clojure.java.io/reader file)] 
    (line-seq rdr))) 

(defn lazy-open [file] 
    (defn helper [rdr] 
    (lazy-seq 
     (if-let [line (.readLine rdr)] 
     (cons line (helper rdr)) 
     (do (.close rdr) (println "closed") nil)))) 
    (lazy-seq 
    (do (println "opening") 
     (helper (clojure.java.io/reader file))))) 

(deftest test-open 
    (try 
    (stream-consumer (broken-open "/etc/passwd")) 
    (catch RuntimeException e 
     (println "caught " e))) 
    (let [stream (lazy-open "/etc/passwd")] 
    (println "have stream") 
    (stream-consumer stream))) 

(run-tests) 

che stampa:

caught #<RuntimeException java.lang.RuntimeException: java.io.IOException: Stream closed> 
have stream 
opening 
closed 
read 29 lines 

dimostrando che il wasn di file è stato aperto fino a quando non è stato necessario.

Questo ultimo approccio ha il vantaggio di poter elaborare il flusso di dati "altrove" senza tenere tutto in memoria, ma ha anche uno svantaggio importante: il file non viene chiuso fino alla fine del flusso. Se non stai attento, puoi aprire molti file in parallelo o persino dimenticarti di chiuderli (non leggendo completamente lo stream).

La scelta migliore dipende dalle circostanze: è un compromesso tra valutazione lenta e risorse di sistema limitate.

PS: lazy-open è definito da qualche parte nelle librerie? Sono arrivato a questa domanda cercando di trovare una tale funzione e ho finito per scrivere il mio, come sopra.

18

soluzione di Andrew ha funzionato bene per me, ma annidati defn s non sono così idiomatica, e non c'è bisogno di fare lazy-seq due volte: qui è una versione aggiornata senza le stampe in più e con letfn:

(defn lazy-file-lines [file] 
    (letfn [(helper [rdr] 
        (lazy-seq 
        (if-let [line (.readLine rdr)] 
         (cons line (helper rdr)) 
         (do (.close rdr) nil))))] 
     (helper (clojure.java.io/reader file)))) 

(count (lazy-file-lines "/tmp/massive-file.txt")) 
;=> <a large integer> 
+0

Sarebbe meglio con 'loop' e' recur'. –

+0

@NeloMitranim 'loop' /' recur' non è pigro. – JohnJ

+0

Hmm scusa, mia cattiva. Non l'ho capito. –

1

see my answer here

(ns user 
    (:require [clojure.core.async :as async :refer :all 
:exclude [map into reduce merge partition partition-by take]])) 

(defn read-dir [dir] 
    (let [directory (clojure.java.io/file dir) 
     files (filter #(.isFile %) (file-seq directory)) 
     ch (chan)] 
    (go 
     (doseq [file files] 
     (with-open [rdr (clojure.java.io/reader file)] 
      (doseq [line (line-seq rdr)] 
      (>! ch line)))) 
     (close! ch)) 
    ch)) 

così:

(def aa "D:\\Users\\input") 
(let [ch (read-dir aa)] 
    (loop [] 
    (when-let [line (<!! ch)] 
     (println line) 
     (recur))))