Разбор данных с помощью Clojure, проблема интервала

Я пишу небольшой парсер в clojure для обучения. в основном это парсер TSV файлов, который нужно поместить в базу данных, но я добавил осложнение. Сама усложнение заключается в том, что в том же файле имеется больше интервалов. Файл выглядит следующим образом:

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

Я закончил с этим кодом:

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

Моя идея заключается в том, что в интервале "технологическая линия" необходимо объединить данные, поэтому у меня есть что-то вроде этого:

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

для каждой строки до следующего интервала, но я не могу понять, как это сделать.

Я пробовал что-то вроде этого:

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

Но это не работает как исключено.

Любые подсказки?

Спасибо!

Ответ 1

Вы делаете (> (count (re-find #"^\#{3}" line)) 0), но можете просто сделать (re-find #"^\#{3}" line) и использовать результат как логическое. re-find возвращает nil, если совпадение не выполняется.

Если вы повторяете элементы в коллекции и хотите пропустить некоторые элементы или объединить два или более элемента в оригинале в один элемент в результате, тогда 99% времени вы хотите reduce. Обычно это становится очень простым.

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

Ответ 2

Возможный подход:

  • Разделите вход в строки с помощью line-seq. (Если вы хотите проверить это на строке, вы можете получить line-seq на нем, выполнив (line-seq (java.io.BufferedReader. (java.io.StringReader. test-string))).)

  • Разделите его на подпоследовательности, каждая из которых содержит либо одну строку заголовка, либо некоторое количество "технологических линий" с (clojure.contrib.seq/partition-by is-header? your-seq-of-lines).

  • Предполагая, что по крайней мере одна технологическая строка после каждого заголовка (partition 2 *2) (где *2 - это последовательность, полученная на шаге 2 выше), вернет последовательность формы, напоминающую следующее: (((header-1) (process-line-1 process-line-2)) ((header-2) (process-line-3 process-line-4))). Если на входе могут содержаться некоторые строки заголовка, за которыми не следует никаких строк данных, то приведенное выше может выглядеть как (((header-1a header-1b) (process-line-1 process-line-2)) ...).

  • Наконец, преобразуйте вывод шага 3 (*3) со следующей функцией:


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

(Чтобы объяснить бит (last headers): единственный случай, когда мы получим несколько заголовков здесь, - это когда некоторые из них не имеют собственных линий данных, последний из которых привязан к линиям данных, является последним.)


С этими примерами шаблонов:

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

весь "труба" может выглядеть так:

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

line-seq предположительно принимающий вход от другого источника в вашей последней программе.)

При вводе примера вышесказанное создает выходные данные следующим образом (для ясности добавляются разрывы строк):

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

Ответ 3

Я не совсем уверен, основываясь на вашем описании, но, возможно, вы просто ускользаете от синтаксиса. Это то, что вы хотите сделать?

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

Если цель вашего "cons" состоит в объединении заголовков со связанными подробными данными, для этого вам понадобится еще один код, но если это просто попытка "объединения" и возврата либо заголовка или подробно, в зависимости от того, что это такое, тогда это должно быть правильно.