我正在为了学习目的而在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))
我的想法是,'process-line'间隔需要与数据合并,所以我有这样的事情:
('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)))
但这不是例外情况。
任何提示?
谢谢!
答案 0 :(得分:6)
可能的方法:
将输入拆分为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中获得的序列)将返回类似于以下内容的表单序列:{ {1}}。如果输入可能包含一些标题行后面没有任何数据行,那么上面的内容可能看起来像(((header-1) (process-line-1 process-line-2)) ((header-2) (process-line-3 process-line-4)))
。
最后,使用以下函数转换步骤3((((header-1a header-1b) (process-line-1 process-line-2)) ...)
)的输出:
*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
答案 1 :(得分:4)
您正在执行(> (count (re-find #"^\#{3}" line)) 0)
,但您可以执行(re-find #"^\#{3}" line)
并将结果用作布尔值。如果匹配失败,re-find
会返回nil
。
如果您正在迭代集合中的项目,并且想要跳过某些项目或将原始项目中的两个或更多项目合并到结果中的一个项目中,那么99%的时间需要{{1} }。这通常会非常简单。
reduce
答案 2 :(得分:1)
根据您的描述,我不完全确定,但也许您只是在语法上滑落。这是你想要做的吗?
(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
”的意图是将标题与其关联的详细数据组合在一起,则需要更多代码才能完成此操作,但如果只是尝试“合并”并返回根据它的标题或细节线,这应该是正确的。