2014-01-19 22 views
4

我正在使用instaparse来解析最终用户评估为布尔结果(例如“(AGE> 35)AND(GENDER =”MALE“))”所使用的简单查询语言,然后此查询需要应用于许多数千行数据来决定每行是否满足表达式。如何将instaparse输出转换为可以评估的函数?

我的问题是什么是将instaparse的输出转换为函数的最佳方法,随后将对每一行进行评估?如上面的查询将被转换成类似

FN [年龄,性别(AND(= 35岁)(=性别“男”))

请注意,我是一个Clojure的小白......

回答

7

您可以使用instaparse生成一个分析树,使用常规Clojure函数将其转换为Clojure代码,然后生成一个Clojure函数,然后将其应用于您的记录,从而为查询语言编写一个小编译器。

eval的初始调用会比较昂贵,但其结果函数将相当于手动写入源文件并且不会影响性能。事实上,这是eval罕见的有效用例之一 - 生成一个函数,其代码是以真正动态的方式构建的,然后将被称为很多次。

很明显,当你遵循这种方法时,你需要确保你不会在不知情的情况下允许不受信任的源代码执行任意代码。

为了演示,这里是基于一个非常简单的语法的instaparse分析器,这只是刚刚能够分析您的样本查询:

(def p (insta/parser " 

expr = and-expr | pred 
and-expr = <'('> expr <')'> ws? <'AND'> ws? <'('> expr <')'> 
pred = (atom ws? rel ws? atom) 
rel = '<' | '>' | '=' 
atom = symbol | number | string 
symbol = #'[A-Z]+' 
string = <'\"'> #'[A-Za-z0-9]+' <'\"'> 
number = #'\\d+' 
<ws> = <#'\\s+'> 

")) 

对于样品的查询,这将产生以下分析树:

[:expr 
[:and-expr 
    [:expr 
    [:pred [:atom [:symbol "AGE"]] [:rel ">"] [:atom [:number "35"]]]] 
    [:expr 
    [:pred 
    [:atom [:symbol "GENDER"]] 
    [:rel "="] 
    [:atom [:string "MALE"]]]]]] 

我们现在可以编写一个multimethod将其转换为Clojure表达式,同时收集符号;这里的ctx说法,就是要一个原子拿着符号的集合,到目前为止,遇到:

(defmulti expr-to-sexp (fn [expr ctx] (first expr))) 

(defmethod expr-to-sexp :symbol [[_ name] ctx] 
    (let [name (clojure.string/lower-case name) 
     sym (symbol name)] 
    (swap! ctx conj sym) 
    sym)) 

(defmethod expr-to-sexp :string [[_ s] ctx] 
    s) 

(defmethod expr-to-sexp :number [[_ n] ctx] 
    (Long/parseLong n)) 

(defmethod expr-to-sexp :atom [[_ a] ctx] 
    (expr-to-sexp a ctx)) 

(defmethod expr-to-sexp :rel [[_ name] ctx] 
    (symbol "clojure.core" name)) 

(defmethod expr-to-sexp :pred [[_ left rel right] ctx] 
    (doall (map #(expr-to-sexp % ctx) [rel left right]))) 

(defmethod expr-to-sexp :and-expr [[_ left right] ctx] 
    `(and ~(expr-to-sexp left ctx) ~(expr-to-sexp right ctx))) 

(defmethod expr-to-sexp :expr [[_ child] ctx] 
    (expr-to-sexp child ctx)) 

让我们将这个应用到我们的样本分析树:

(expr-to-sexp (p "(AGE > 35) AND (GENDER = \"MALE\")") (atom #{})) 
;= (clojure.core/and (clojure.core/> age 35) (clojure.core/= gender "MALE")) 

(let [ctx (atom #{})] 
    (expr-to-sexp (p "(AGE > 35) AND (GENDER = \"MALE\")") ctx) 
    @ctx) 
;= #{age gender} 

最后,这里使用上述功能建立一个Clojure的功能:

(defn compile-expr [expr-string] 
    (let [expr (p expr-string) 
     ctx (atom #{}) 
     body (expr-to-sexp expr ctx)] 
    (eval `(fn [{:keys ~(vec @ctx)}] ~body)))) 

您可以使用它像这样:

(def valid? (compile-expr "(AGE > 35) AND (GENDER = \"MALE\")")) 

(valid? {:gender "MALE" :age 36}) 
;= true 

(valid? {:gender "FEMALE" :age 36}) 
;= false 
+0

谢谢,这正是我正在寻找的。我知道eval可能会很贵,但查询表达式在蓝色月亮中会改变一次,并会对数百万条记录进行评估,因此感觉这是一个合理的折衷。 –

1

我不知道我理解你的问题。但是,这是我的猜测:)

您可以使用clojure的“过滤器”功能来重新收集一个集合。我不熟悉instaparse,所以我就嘲笑了一些测试数据(集合):

(def ls [{:age 10, :gender "m"} {:age 15 :gender "fm"}]) 

因此,“LS”是我们的集合。要限制收集到具有greated然后5的“m”和一个年龄值性别值的元素,我们采用以下过滤器:

(filter (fn [a] (if (and (= (:gender a) "m") (> (:age a) 5)) a)) ls) 

结果是:

({:age 10, :gender "m"}) 

或者你可以表达作为匿名函数的过滤器“fn”:

(filter #(if (and (= (:gender %1) "m") (> (:age %1) 5)) %1) ls) 

希望有所帮助。注意!如果你可以发布你正在尝试处理的“结果数据”的样本,它会有很大的帮助:)

相关问题