2010-03-29 12 views
10

Escribo un pequeño analizador en clojure para fines de aprendizaje. básicamente es un analizador de archivos TSV que debe colocarse en una base de datos, pero agregué una complicación. La complicación en sí es que en el mismo archivo hay más intervalos. El archivo se ve así:Análisis de datos con Clojure, problema de intervalo

###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 

que terminó con este código:

(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)) 

Mi idea es que en el 'proceso de línea' intervalo necesita ser combinado con los datos, así que tengo algo así como esto:

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

por cada fila hasta el siguiente intervalo, pero no sé cómo hacer que esto suceda.

me trataron con algo como esto:

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

Pero esto no funciona como exceptuado.

¿Alguna pista?

Gracias!

+2

Por cierto, no use 'def' excepto en el nivel superior a menos que realmente sepa lo que está haciendo! Y ciertamente nunca lo use para almacenamiento mutable. Use un Ref o Atom en su lugar. –

+0

¡Gracias, esta es una preciosa pista! –

+0

Espero que estés de acuerdo con que esta pregunta se convierta en la base de un ejercicio en rubylearning.org (para el curso Clojure 101). Encuentro que es un problema muy bueno para trabajar. –

Respuesta

4

Está haciendo (> (count (re-find #"^\#{3}" line)) 0), pero puede hacer (re-find #"^\#{3}" line) y usar el resultado como booleano. re-find devuelve nil si la coincidencia falla.

Si está iterando sobre los elementos de una colección y desea omitir algunos elementos o combinar dos o más elementos en el original en un elemento del resultado, entonces el 99% del tiempo desea reduce. Esto generalmente termina siendo muy sencillo.

;; 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

Esto es muy bueno. Por el momento es la mejor solución que puedo pensar y también la más corta. Muchas gracias. –

+2

Esto puede o no tener relación con el ejemplo en cuestión, pero no estoy de acuerdo con la declaración sobre la idoneidad de 'reduce' para tareas de este tipo. En Clojure 'reduce' es siempre estricto, ya que siempre materializará todo el resultado en memoria antes de que cualquier parte de él esté disponible para el procesamiento (porque el' reducir' de Clojure es un pliegue hacia la izquierda). Esto está en contraste con un enfoque donde las transformaciones perezosas se superponen unas sobre otras (con la secuencia de entrada en la parte inferior de la pila), donde los resultados se pueden producir en trozos. –

+0

Además, creo que escribir una función compleja para usar con 'reduce' hace que el código sea menos legible y modificable en comparación con un enfoque similar a un tubo, aunque eso podría ser una cuestión de gusto y sin duda dependerá de los detalles de cualquier caso. Y para no dar la impresión equivocada, no estoy descifrando 'reduce' de ninguna manera, es solo que para el caso particular de transformar secuencias en otras secuencias, creo que es la herramienta de último recurso, en lugar de la primera opción. –

1

No estoy del todo seguro en base a su descripción, pero tal vez simplemente esté cayendo en la sintaxis. ¿Es esto lo que quieres hacer?

(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" 

Si la intención de sus "cons" es agrupar los encabezados con sus datos de detalle asociados, necesitará algo más de código para lograr eso, pero si es sólo un intento de "coalescencia" y devolver bien un encabezado o línea de detalle dependiendo de cuál sea, entonces esto debería ser correcto.

+0

Gracias por su respuesta, solucionar el problema de sintaxis en el formulario if limpió la salida, pero aún necesito encontrar la forma correcta de combinar ambas secuencias (ese es el primer caso que describió). pd Perdón por mi descripción, soy un principiante con clojure y programación funcional en general, así que puede haber usado términos incorrectos. –

6

Un posible enfoque:

  1. Dividir la entrada en líneas con line-seq. (Si desea probar esto en una cadena, se puede obtener una line-seq en él haciendo (line-seq (java.io.BufferedReader. (java.io.StringReader. test-string))).)

  2. de reparto que en sub-secuencias de cada uno de los cuales contiene ya sea una línea de cabecera única o un número de "líneas de proceso "con (clojure.contrib.seq/partition-by is-header? your-seq-of-lines).

  3. Suponiendo que hay al menos una línea de proceso después de cada cabecera, (partition 2 *2) (donde *2 es la secuencia obtenida en la etapa 2 anterior) devolverá una secuencia de una forma similar a lo siguiente: (((header-1) (process-line-1 process-line-2)) ((header-2) (process-line-3 process-line-4))). Si la entrada puede contener algunas líneas de encabezado que no estén seguidas por ninguna línea de datos, entonces lo anterior podría verse como (((header-1a header-1b) (process-line-1 process-line-2)) ...).

  4. Por último, transformar la salida de la etapa 3 (*3) con la siguiente función:


(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))) 

(para explicar el poco (last headers): el único caso en el que vamos a llegar múltiple aquí hay encabezados cuando algunos de ellos no tienen sus propias líneas de datos; el que está conectado a las líneas de datos es el último.)


Con estos ejemplos de patrones:

(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|^$|^\.\.\.") 

toda la 'tubería' podría tener este aspecto:

;; 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 el line-seq presumiblemente teniendo la entrada de una fuente diferente en su programa final)

Con su entrada de ejemplo, lo anterior produce resultados como este (se agregaron saltos de línea para mayor claridad):

(("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

Muchas gracias. Funciona como un amuleto y aprendí dos funciones útiles: mapcat y partición. Gracias de nuevo. –

+0

¡De nada! Tenga en cuenta que he hecho otra edición para que maneje correctamente el caso donde algunos encabezados pueden no tener líneas de datos que los sigan. –

+0

Sí, lo noté Gracias. –

Cuestiones relacionadas