2009-11-17 17 views
9

Sono vecchio e nuovo alla programmazione, principalmente scrivo un sacco di piccoli script Perl al lavoro. Clojure è uscito proprio quando volevo imparare Lisp, quindi sto cercando di imparare Clojure senza conoscere Java. È dura, ma è stato divertente finora.Newbie che trasforma i file CSV in Clojure

Ho visto diversi esempi di problemi simili ai miei, ma nulla che mappi abbastanza al mio spazio problema. Esiste un modo canonico per estrarre liste di valori per ogni riga di un file CSV in Clojure?

Ecco alcuni codici Perl funzionanti; commenti inclusi per i non Perlers:

# convert_survey_to_cartography.pl 
open INFILE, "< coords.csv";  # Input format "Northing,Easting,Elevation,PointID" 
open OUTFILE, "> coords.txt";  # Output format "PointID X Y Z". 
while (<INFILE>) {     # Read line by line; line bound to $_ as a string. 
    chomp $_;      # Strips out each line's <CR><LF> chars. 
    @fields = split /,/, $_;  # Extract the line's field values into a list. 
    $y = $fields[0];    # y = Northing 
    $x = $fields[1];    # x = Easting 
    $z = $fields[2];    # z = Elevation 
    $p = $fields[3];    # p = PointID 
    print OUTFILE "$p $x $y $z\n" # New file, changed field order, different delimiter. 
} 

ho perplesso un po 'in Clojure e provato a mettere insieme in uno stile imperativo:

; convert-survey-to-cartography.clj 
(use 'clojure.contrib.duck-streams) 
(let 
    [infile "coords.csv" outfile "coords.txt"] 
    (with-open [rdr (reader infile)] 
    (def coord (line-seq rdr)) 
    (...then a miracle occurs...) 
    (write-lines outfile ":x :y :z :p"))) 

non mi aspetto l'ultima riga per funzionare davvero, ma ottiene il punto attraverso. Sto cercando qualcosa sulla falsariga di:

(def values (interleave (:p :y :x :z) (re-split #"," coord))) 

Grazie, Bill

+2

'my ($ x, $ y, $ z, $ p) = diviso /, /;' –

+0

Buon punto - TIMTOWTDI. Grazie. –

risposta

8

Ecco un modo:

(use '(clojure.contrib duck-streams str-utils))     ;;' 
(with-out-writer "coords.txt" 
    (doseq [line (read-lines "coords.csv")] 
    (let [[x y z p] (re-split #"," line)] 
     (println (str-join \space [p x y z]))))) 

with-out-writer lega *out* tale che tutto ciò che si stampa andrà al nome del file o flusso specificato, piuttosto che output standard.

L'utilizzo di def mentre lo si utilizza non è un problema. Un modo migliore è utilizzare let. Uso la destrutturazione per assegnare i 4 campi di ciascuna riga a 4 nomi let -bound; allora puoi fare quello che vuoi con quelli.

Se si sta iterando su qualcosa a scopo di effetti collaterali (ad esempio I/O), in genere si dovrebbe andare per doseq. Se si voleva raccogliere fino ogni linea in un hash-map e fare qualcosa con loro più tardi, si potrebbe utilizzare for:

(with-out-writer "coords.txt" 
    (for [line (read-lines "coords.csv")] 
    (let [fields (re-split #"," line)] 
     (zipmap [:x :y :z :p] fields)))) 
+0

Esattamente quello di cui avevo bisogno! E anche elegantemente fatto! doseq non aveva molto senso fino ad ora, e ora capisco di aver frainteso anche alcune altre cose. Ho provato il tuo codice in ClojureBox e ha funzionato; Sono stato anche in grado di avvolgere in una funzione e anche questo ha funzionato, quindi sembra che mi abbia messo sulla strada giusta. Grazie ancora. –

15

prega di non utilizzare nidificate def di. Non funziona, cosa pensi che faccia. la def è sempre globale! Per i locali si usa invece let. Mentre le funzioni della libreria sono piacevoli da sapere, ecco una versione che orchestra alcune funzionalità della programmazione funzionale in generale e del clojure in particolare.

(import 'java.io.FileWriter 'java.io.FileReader 'java.io.BufferedReader) 

(defn translate-coords

Le docstrings possono essere interrogate nel REPL tramite (doc translate-coords). Funziona ad es. per tutte le funzioni principali. Quindi fornirne una è una buona idea.

"Reads coordinates from infile, translates them with the given 
    translator and writes the result to outfile."

Il traduttore è una funzione (forse anonima) che estrae la traduzione dal foglio caldaia circostante. Quindi possiamo riutilizzare queste funzioni con diverse regole di trasformazione. I suggerimenti del tipo qui evitano la riflessione per i costruttori.

[translator #^String infile #^String outfile]

Aprire i file. with-open si prenderà cura che i file siano chiusi quando il suo corpo è rimasto. Sia tramite normale "drop off the bottom" o sia tramite un'eccezione generata.

(with-open [in (BufferedReader. (FileReader. infile)) 
       out (FileWriter. outfile)]

leghiamo il flusso *out* temporaneamente al file di output. Quindi qualsiasi stampa all'interno della rilegatura verrà stampata sul file.

(binding [*out* out]

I mezzi map: prendere il seq e applicare la funzione specificata per ogni elemento e restituire la seq dei risultati. Lo #() è una notazione breve per una funzione anonima. Prende un argomento, che viene compilato allo %. Il doseq è fondamentalmente un ciclo sull'input. Dal momento che lo facciamo per gli effetti collaterali (ovvero la stampa su un file), doseq è il costrutto corretto. Regola generale: map: lazy => per risultato, doseq: eager => per effetti collaterali.

 (doseq [coords (map #(.split % ",") (line-seq in))]

println prende cura per il \n alla fine della linea. interpose prende il seq e aggiunge il primo argomento (nel nostro caso "") tra i suoi elementi. (apply str [1 2 3]) equivale a (str 1 2 3) ed è utile per costruire chiamate di funzione dinamicamente. Il ->> è una macro relativamente nuova in clojure, che aiuta un po 'con la leggibilità. Significa "prendi il primo argomento e aggiungilo come ultimo elemento alla chiamata di funzione". Il dato ->> è equivalente a: (println (apply str (interpose " " (translator coords)))). (Modifica: Un'altra nota: dal momento che il separatore è \space, potremmo scrivere altrettanto bene (apply println (translator coords)), ma la versione interpose consente di parametrizzare anche il separatore come abbiamo fatto con la funzione di traduttore, mentre la versione breve sarebbe stata cablata \space.)

 (->> (translator coords) 
      (interpose " ") 
      (apply str) 
      println))))) 

(defn survey->cartography-format 
    "Translate coords in survey format to cartography format."

Qui usiamo la destrutturazione (notare il doppio [[]]). Significa che l'argomento della funzione è qualcosa che può essere trasformato in un seq, ad es. un vettore o una lista. Associare il primo elemento a y, il secondo a x e così via.

[[y x z p]] 
    [p x y z]) 

(translate-coords survey->cartography-format "survey_coords.txt" "cartography_coords.txt")

Anche in questo caso meno mosso:

(import 'java.io.FileWriter 'java.io.FileReader 'java.io.BufferedReader) 

(defn translate-coords 
    "Reads coordinates from infile, translates them with the given 
    translator and writes the result to outfile." 
    [translator #^String infile #^String outfile] 
    (with-open [in (BufferedReader. (FileReader. infile)) 
       out (FileWriter. outfile)] 
    (binding [*out* out] 
     (doseq [coords (map #(.split % ",") (line-seq in))] 
     (->> (translator coords) 
      (interpose " ") 
      (apply str) 
      println))))) 

(defn survey->cartography-format 
    "Translate coords in survey format to cartography format." 
    [[y x z p]] 
    [p x y z]) 

(translate-coords survey->cartography-format "survey_coords.txt" "cartography_coords.txt")

Spero che questo aiuti.

Modifica: per la lettura CSV probabilmente vuoi qualcosa come OpenCSV.

+1

Grazie per il tutorial - ci sono MOLTE informazioni utili che ci metterò del tempo per digerire. Ho modellato una mia funzione su quella che hai usato qui e ha funzionato come un fascino. Grazie ancora! –