2010-03-29 12 views
10

Sto scrivendo un piccolo parser in clojure a scopo di apprendimento. fondamentalmente è un parser di file TSV che deve essere inserito in un database, ma ho aggiunto una complicazione. La complicazione stessa è che nello stesso file ci sono più intervalli. Il somigli a quanto segue:Analisi dei dati con Clojure, problema con intervallo

###andreadipersio 2010-03-19 16:10:00###                     
USER  COMM    PID PPID %CPU %MEM  TIME 
root  launchd    1  0 0.0 0.0 2:46.97 
root  DirectoryService 11  1 0.0 0.2 0:34.59 
root  notifyd    12  1 0.0 0.0 0:20.83 
root  diskarbitrationd 13  1 0.0 0.0 0:02.84` 
.... 

###andreadipersio 2010-03-19 16:20:00###                     
USER  COMM    PID PPID %CPU %MEM  TIME 
root  launchd    1  0 0.0 0.0 2:46.97 
root  DirectoryService 11  1 0.0 0.2 0:34.59 
root  notifyd    12  1 0.0 0.0 0:20.83 
root  diskarbitrationd 13  1 0.0 0.0 0:02.84 

ho finito con questo codice:

(defn is-header? 
    "Return true if a line is header" 
    [line] 
    (> (count (re-find #"^\#{3}" line)) 0)) 

(defn extract-fields 
    "Return regex matches" 
    [line pattern] 
    (rest (re-find pattern line))) 

(defn process-lines 
    [lines] 
    (map process-line lines)) 

(defn process-line 
    [line] 
    (if (is-header? line) 
    (extract-fields line header-pattern)) 
    (extract-fields line data-pattern)) 

La mia idea è che in 'processo-line' intervallo deve essere fusa con i dati così ho qualcosa di simile this:

('andreadipersio', '2010-03-19', '16:10:00', 'root', 'launchd', 1, 0, 0.0, 0.0, '2:46.97') 

per ogni riga fino all'intervallo successivo, ma non riesco a capire come farlo accadere.

Ho provato con qualcosa di simile:

(def process-line 
    [line] 
    (if is-header? line) 
    (def header-data (extract-fields line header-pattern))) 
    (cons header-data (extract-fields line data-pattern))) 

Ma questo non funziona come eccettuato.

Eventuali suggerimenti?

Grazie!

+2

BTW, non usare 'def' eccetto che al primo livello a meno che tu non sappia davvero cosa stai facendo! E certamente non lo uso mai per la memorizzazione mutevole. Usa invece un Ref o Atom. –

+0

Grazie, questo è un suggerimento prezioso! –

+0

Spero che tu stia bene con questa domanda diventando la base per un esercizio su rubylearning.org (per il corso Clojure 101)? Trovo che sia un bel problema su cui lavorare. –

risposta

4

Stai facendo (> (count (re-find #"^\#{3}" line)) 0), ma puoi semplicemente fare (re-find #"^\#{3}" line) e utilizzare il risultato come booleano. re-find restituisce nil se la partita fallisce.

Se si sta iterando sugli elementi di una raccolta e si desidera saltare alcuni elementi o combinare due o più elementi nell'originale in un elemento nel risultato, quindi il 99% delle volte si desidera reduce. Questo di solito finisce per essere molto semplice.

;; These two libs are called "io" and "string" in bleeding-edge clojure-contrib 
;; and some of the function names are different. 
(require '(clojure.contrib [str-utils :as s] 
          [duck-streams :as io])) ; SO's syntax-highlighter still sucks 

(defn clean [line] 
    (s/re-gsub #"^###|###\s*$" "" line)) 

(defn interval? [line] 
    (re-find #"^#{3}" line)) 

(defn skip? [line] 
    (or (empty? line) 
     (re-find #"^USER" line))) 

(defn parse-line [line] 
    (s/re-split #"\s+" (clean line))) 

(defn parse [file] 
    (first 
    (reduce 
    (fn [[data interval] line] 
     (cond 
     (interval? line) [data (parse-line line)] 
     (skip? line)  [data interval] 
     :else   [(conj data (concat interval (parse-line line))) interval])) 
    [[] nil] 
    (io/read-lines file)))) 
+0

Questo è molto bello. Al momento è la soluzione migliore a cui riesco a pensare e anche la più corta. Grazie mille. –

+2

Questo può o non può avere alcun rilievo nell'esempio in questione, ma non sono d'accordo con la dichiarazione sull'adeguatezza di 'ridurre' per compiti di questo tipo. In Clojure 'reduce' è sempre rigoroso in quanto materializzerà sempre l'intero risultato in memoria prima che qualsiasi parte di esso diventi disponibile per l'elaborazione (perché' riduci 'di Clojure è una piega a sinistra). Questo è in contrasto con un approccio in cui le trasformazioni pigre sono sovrapposte l'una sull'altra (con la sequenza di input nella parte inferiore della pila), dove i risultati possono essere prodotti in blocchi. –

+0

Inoltre, trovo che scrivere una funzione complessa da usare con 'reduce' renda il codice meno leggibile e modificabile rispetto ad un approccio a tubo, anche se potrebbe essere una questione di gusti e dipenderà certamente dai particolari su ogni dato Astuccio. E per non dare l'impressione sbagliata, non sto dicendo "riduci" in alcun modo, è solo che, nel caso particolare di trasformare sequenze in altre sequenze, trovo che sia lo strumento di ultima istanza, piuttosto che la prima scelta. –

1

Non sono completamente sicuro in base alla descrizione, ma forse stai solo facendo un passo avanti nella sintassi. E 'questo quello che vuoi fare?

(def process-line [line] 
    (if (is-header? line) ; extra parens here over your version 
    (extract-fields line header-pattern) ; returning this result 
    (extract-fields line data-pattern))) ; implicit "else" 

Se l'intento dei tuoi "cons" è quello di raggruppare le intestazioni con i dati di dettaglio associati, avrete bisogno di un po 'di codice per realizzare questo, ma se è solo un tentativo di "coalescenza" e il ritorno sia un'intestazione o una riga di dettaglio a seconda di quale è, quindi questo dovrebbe essere corretto.

+0

Grazie per la tua risposta, risolvendo il problema di sintassi sul modulo if purificato l'output, ma ho ancora bisogno di trovare il modo corretto di unire entrambe le sequenze (è il primo caso che hai descritto). ps Ci scusiamo per la mia descrizione, sono un principiante con clojure e programmazione funzionale in generale, quindi potrei aver usato termini sbagliati. –

6

Un possibile approccio:

  1. Split l'input in linee con line-seq. (Se si desidera verificare questo su una stringa, è possibile ottenere un line-seq su di esso facendo (line-seq (java.io.BufferedReader. (java.io.StringReader. test-string))).)

  2. di ripartizione in sub-sequenze ognuna delle quali contiene una singola riga di intestazione o qualche numero di "linee di processo "con (clojure.contrib.seq/partition-by is-header? your-seq-of-lines).

  3. Supponendo c'è almeno una linea di processo dopo ogni intestazione, (partition 2 *2) (dove *2 è la sequenza ottenuta al punto 2) restituirà una sequenza di una forma simile al seguente: (((header-1) (process-line-1 process-line-2)) ((header-2) (process-line-3 process-line-4))). Se l'input potrebbe contenere alcune linee di intestazione non seguite da alcuna riga di dati, il precedente potrebbe apparire come (((header-1a header-1b) (process-line-1 process-line-2)) ...).

  4. Infine, trasformare l'uscita di fase 3 (*3) con la seguente funzione:


(defn extract-fields-add-headers 
    [[headers process-lines]] 
    (let [header-fields (extract-fields (last headers) header-pattern)] 
    (map #(concat header-fields (extract-fields % data-pattern)) 
     process-lines))) 

(Per spiegare il bit (last headers): l'unico caso in cui ci arriveremo multipla le intestazioni qui sono quando alcune di esse non hanno linee dati proprie, quella effettivamente collegata alle linee dati è l'ultima.)


Con questi segni di esempio:

(def data-pattern #"(\w+)\s+(\w+)\s+(\d+)\s+(\d+)\s+([0-9.]+)\s+([0-9.]+)\s+([0-9:.]+)") 
(def header-pattern #"###(\w+)\s+([0-9-]+)\s+([0-9:]+)###") 
;; we'll need to throw out the "USER COMM ..." lines, 
;; empty lines and the "..." line which I haven't bothered 
;; to remove from your sample input 
(def discard-pattern #"^USER\s+COMM|^$|^\.\.\.") 

tutta la 'pipe' potrebbe essere simile a questo:

;; just a reminder, normally you'd put this in an ns form: 
(use '[clojure.contrib.seq :only (partition-by)]) 

(->> (line-seq (java.io.BufferedReader. (java.io.StringReader. test-data))) 
    (remove #(re-find discard-pattern %)) ; throw out "USER COMM ..." 
    (partition-by is-header?) 
    (partition 2) 
    ;; mapcat performs a map, then concatenates results 
    (mapcat extract-fields-add-headers)) 

(. Con il line-seq presumibilmente prendendo l'input da una fonte diversa nel programma finale)

Con l'input di esempio, il precedente produce output come questo (interruzioni di riga aggiunte per maggiore chiarezza):

(("andreadipersio" "2010-03-19" "16:10:00" "root" "launchd" "1" "0" "0.0" "0.0" "2:46.97") 
("andreadipersio" "2010-03-19" "16:10:00" "root" "DirectoryService" "11" "1" "0.0" "0.2" "0:34.59") 
("andreadipersio" "2010-03-19" "16:10:00" "root" "notifyd" "12" "1" "0.0" "0.0" "0:20.83") 
("andreadipersio" "2010-03-19" "16:10:00" "root" "diskarbitrationd" "13" "1" "0.0" "0.0" "0:02.84") 
("andreadipersio" "2010-03-19" "16:20:00" "root" "launchd" "1" "0" "0.0" "0.0" "2:46.97") 
("andreadipersio" "2010-03-19" "16:20:00" "root" "DirectoryService" "11" "1" "0.0" "0.2" "0:34.59") 
("andreadipersio" "2010-03-19" "16:20:00" "root" "notifyd" "12" "1" "0.0" "0.0" "0:20.83") 
("andreadipersio" "2010-03-19" "16:20:00" "root" "diskarbitrationd" "13" "1" "0.0" "0.0" "0:02.84")) 
+0

Grazie mille. Funziona come un fascino e ho imparato due funzioni utili: mapcat e partizione. Grazie ancora. –

+0

Prego! Tieni presente che ho apportato un'altra modifica per gestire correttamente il caso in cui alcune intestazioni potrebbero non avere linee dati che le seguono. –

+0

Sì, l'ho notato! Grazie. –

Problemi correlati