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秒内运行并生成正确的结果。我认为这对于非常少量的数据来说是一个很长的时间。我做错了什么?
答案 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)))