在Clojure中解析这种数据格式的优雅方法是什么?

时间:2013-12-18 09:58:39

标签: parsing clojure

A legacy application I work with有一种称为SGS的时髦数据格式。我已经考虑并开始使用一些蛮力解决方案,包括手动生成的有限状态机和自定义递归下降解析器,但我正在努力构建一个应用程序,其中(非库)源代码的数量是足以表达需要做的事情。

所以我一直在寻找基于Clojure的解析器。我摆弄了

这些都没有足够的文件/支持在网上让我开始运行。所以我正在寻找有这些工具经验的人(或者一个很好的选择)来帮助我。

这是数据语言:


  • 数据由具有标签(从第1列开始)和1个或多个字段的行表示,由一个或多个空格分隔。

  • 字段由一个或多个子字段组成,以逗号分隔。逗号可以跟随空格以便易读,但这些并不重要。

  • 标签是由[ - $ 0-9A-Z _ *%]中的字符组成的标识符,不一定是唯一的。

  • 子字段是标识符或带引号的字符串或缺少(nil)。带引号的字符串由 2 前导单引号和 2 尾随单引号分隔。带引号的字符串不包含单引号,因此无需担心引用嵌套或转义。

  • 空格点空间开始进行其余的评论。一行末尾的空格点是一个空的剩余行注释。一行开头的点空间使整行成为评论。仅由点组成的行也是行注释。

  • 可以在两条或更多条线上继续行。分号作为行上的最后一个非空白,非注释字符表示该行在下一行继续,就好像分号和换行符都不存在一样。

  • 分号和点(带或不带空格)在注释或引用的字符串中没有特殊意义。

示例

. Comment
.
LAB1  F1S1                       . Minimal data row, with line comment
LAB1  F1S1,F1S2,F1S3  F2S1  F3S1 . 2nd row with same label
LAB2  , , , F1S4     ''Field #2 (only 1 subfield)''  F3S1,,F3S3
LAB99 F1S1,                      . Field 1 has 2 subfields, 2nd is nil
LAB3  F1S1,F1S2, ;
      F1S3       ;
      F2S1                       . Row continued over 3 lines. 

手工解析我的例子,我想得到这样的结果:

[
 ("LAB1" ["F1S1"])
 ("LAB1" ["F1S1" "F1S2" "F1S3"] ["F2S1"] ["F3S1"])
 ("LAB2" [nil nil nil "F1S4"] ["Field #2 (only 1 subfield"] ["F3S1" nil "F3S3"])
 ("LAB99" ["F1S1" nil])
 ("LAB3" ["F1S1" "F1S2" "F1S3"] ["F2S1"])
]

更新

@edwood建议展示我自己的实现,以供人们作为起点。我没有犹豫要做到这一点,以避免在特定方向偏向于人,但由于缺乏回应,这可能“总比没有好。”

然后,这是我自己的InstaParse解决方案,它可以分类:

     SGS = (<COMMENT_LINE> / DATA_LINES) *
     COMMENT_LINE = #' *\\.(?: [^\\n]*)?\\n' 
     DATA_LINES = LABEL FIELDS SEPARATOR? (LINE_COMMENT | '\\n')
     LABEL = IDENTIFIER
     FIELDS = '' | (SEPARATOR FIELD)+
     SEPARATOR = CONTINUATION #' +' | #' +' (CONTINUATION #' *')?
     CONTINUATION = #'; *\\n'
     LINE_COMMENT = #' .[^\\n]*\\n'  
     FIELD = SUBFIELD (',' SEPARATOR? SUBFIELD)*
     SUBFIELD = IDENTIFIER | QUOTED_STRING | ''
     IDENTIFIER = #'[-$0-9A-Z_*%]+'
     QUOTED_STRING = #'\\'\\'[^\\']*\\'\\''

在调试时,它设法处理249行,然后遇到我需要调试的错误。但是一旦我解决了这个问题,它可能会在我的整个431行数据上工作,并且在大约2分钟后结束了窒息,

  

CompilerException java.lang.OutOfMemoryError:Java堆空间,   编译:(sgs2.clj:40:13)

我将易于使用regexp处理的内容移动到regexp,这似乎有助于提高性能。例如,注释行现在很容易解析,因为它们直接匹配单个正则表达式。


如果我将输入数据剪切为228行,则解析器会在16秒内运行并生成正确的结果。我认为这对于非常少量的数据来说是一个很长的时间。我做错了什么?

2 个答案:

答案 0 :(得分:2)

这是我最终得到的instaparse解析器:

"<SGS> = (<COMMENT_ROW> | ROW)+
<NL> = '\\n'
<qq> = \"''\"
space = <#'\\s*'>
COMMENT_ROW = COMMENT NL?
LABEL = 'LAB' #'\\d+'
EMPTY_F = <space>
FFIELD = 'F' #'[0-9A-Z]+'
QFIELD = (<qq> (!qq #'.')+ <qq>)
<F> = FFIELD / QFIELD / EMPTY_F
F_SEP = ((space? | ',')* ';' NL space?) / (<space?> ',' <space?>) / <space>
<NEXT_FIELDS> = F <space?> (<F_SEP> NEXT_FIELDS)? <space?>
FIELDS = F <space?> (<F_SEP> NEXT_FIELDS)? <space?>
COMMENT = '.' #'.*'
ROW = LABEL <space?> FIELDS <space?> <COMMENT?> <NL?>"

我希望有人可以改进或简化。 解析示例输入的结果:

sgs.core> (sgs example-input)
([:ROW [:LABEL "LAB" "1"] [:FIELDS [:FFIELD "F" "1S1"]]] [:ROW [:LABEL "LAB" "1"] [:FIELDS [:FFIELD "F" "1S1"] [:FFIELD "F" "1S2"] [:FFIELD "F" "1S3"] [:FFIELD "F" "2S1"] [:FFIELD "F" "3S1"]]] [:ROW [:LABEL "LAB" "2"] [:FIELDS [:EMPTY_F] [:EMPTY_F] [:EMPTY_F] [:FFIELD "F" "1S4"] [:QFIELD "F" "i" "e" "l" "d" " " "#" "2" " " "(" "o" "n" "l" "y" " " "1" " " "s" "u" "b" "f" "i" "e" "l" "d" ")"] [:FFIELD "F" "3S1"] [:EMPTY_F] [:FFIELD "F" "3S3"]]] [:ROW [:LABEL "LAB" "99"] [:FIELDS [:FFIELD "F" "1S1"] [:EMPTY_F]]] [:ROW [:LABEL "LAB" "3"] [:FIELDS [:FFIELD "F" "1S1"] [:FFIELD "F" "1S2"] [:FFIELD "F" "1S3"] [:FFIELD "F" "2S1"]]])

我的机器需要大约50毫秒。 我添加了几个函数来清理结果。

sgs.core> (pprint (parse-and-transform sgs example-input))
[("LAB1" ["F1S1"])
 ("LAB1" ["F1S1" "F1S2" "F1S3"] ["F2S1"] ["F3S1"])
 ("LAB2"
  [nil nil nil "F1S4"]
  ["Field #2 (only 1 subfield)"]
  ["F3S1" nil "F3S3"])
 ("LAB99" ["F1S1" nil])
 ("LAB3" ["F1S1" "F1S2" "F1S3"] ["F2S1"])]

此处的完整源代码:https://gist.github.com/edbond/8052305

在演出时,您可以阅读https://github.com/Engelberg/instaparse/blob/master/docs/Performance.md

我会尝试将大输入分区为小块。

答案 1 :(得分:1)

这是使用Parse-EZ获得所需内容的一种方法。请注意,我关闭了默认设置 Parse-EZ的空白/评论功能(带-tr-off)。输入功能是列表底部的“sgs”。您可以调用解析器: (解析sgs your-input-string)

(ns sgs-parser
  (:use [protoflex.parse]))

(defn line-comments [] (multi* #(regex #"(\r?\n)?\..*\r?\n")))
(defn wsp [] (regex #"[ \t]*(\. .*)?"))
(defn trim [parse-fn] (wsp) (let [r (parse-fn)]  (wsp) r))

(defn label [] (regex #"[-$0-9A-Z_*%]+"))
(defn quoted-str [] (between #(string "''") #(regex #"[^']*") #(string "''")))

(defn sub-field [] (trim #(any label quoted-str)))

(defn- eol? [] (starts-with-re? #"\r?\n"))
(defn field [] 
  (when (not (or (eol?) (at-end?)))
    (when (starts-with? ";") (skip-over "\n"))
    (loop [sfs []]
      (let [sf (opt sub-field)]
        (if (opt #(trim comma))
          (recur (conj sfs sf))
          (conj sfs sf))))))

(defn record [] 
  (line-comments) 
  (let [ret (into [(trim label)] (multi+ field))]
    (any #(regex #"\r?\n") at-end?)
    ret))

(defn sgs [] (with-trim-off (wsp) (multi* record)))