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