Разбор данных с помощью Clojure, интервальная проблема - PullRequest
10 голосов
/ 29 марта 2010

Я пишу небольшой синтаксический анализатор в 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)))

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

Есть какие-нибудь намеки?

Спасибо!

Ответы [ 3 ]

6 голосов
/ 29 марта 2010

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

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

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

  3. Если после каждого заголовка есть хотя бы одна строка процесса, (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)) ...).

  4. Наконец, преобразуйте выходные данные шага 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"))
4 голосов
/ 30 марта 2010

Вы делаете (> (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'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))))
1 голос
/ 29 марта 2010

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

(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" состоит в том, чтобы сгруппировать заголовки с соответствующими подробными данными, вам понадобится еще немного кода для этого, но если это просто попытка "объединить" и вернуть либо заголовок или подробные строки, в зависимости от того, что это, то это должно быть правильно.

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...