在 clojure 中使用正则表达式实现一个简单的扫描器/标记器
Posted
技术标签:
【中文标题】在 clojure 中使用正则表达式实现一个简单的扫描器/标记器【英文标题】:Implementing a simple scanner/tokenizer using a regex in clojure 【发布时间】:2019-10-27 11:55:39 【问题描述】:我正在尝试编写一个名为scan-for
的函数,该函数将字符串集合(“令牌”)作为输入,返回一个“令牌生成器”函数,该函数将字符串作为输入,返回一个(最好是惰性的)字符串序列
由输入中包含的“令牌”组成,以贪婪的方式识别,以及它们之间以及开始和结束的非空子字符串,按照它们在输入中出现的顺序。
例如((scan-for ["an" "ban" "banal" "d"]) "ban bananas and banalities")
应该产生:
("ban" " " "ban" "an" "as " "an" "d" " " "banal" "ities")
在我的第一次尝试中,我使用正则表达式来匹配“令牌”(使用re-seq
)并找到中间的子字符串(使用split
),然后交错生成的序列。问题是输入字符串被构造的正则表达式解析了两次,并且结果序列不是惰性的,因为split
。
[在scan-for
的定义中,我使用了tacit/point-free style(避免了lambda 及其糖化的伪装),我觉得它通常优雅且有用(John Backus would probably agree)。在 clojure 中,这需要扩展使用 partial
来处理 uncurried 函数。如果你不喜欢它,你可以添加 lambdas、threading-macros 等]
(defn rpartial
"a 'right' version of clojure.core/partial"
[f & args] #(apply f (concat %& args)))
(defn interleave*
"a 'continuing' version of clojure.core/interleave"
[& seqs]
(lazy-seq
(when-let [seqs (seq (remove empty? seqs))]
(concat
(map first seqs)
(apply interleave* (map rest seqs))))))
(defn make-fn
"makes a function from a symbol and an (optional) arity"
([sym arity]
(let [args (repeatedly arity gensym)]
(eval (list `fn (vec args) (cons sym args)))))
([sym] (make-fn sym 1)))
(def scan-for
(comp
(partial comp
(partial remove empty?)
(partial apply interleave*))
(partial apply juxt)
(juxt
(partial rpartial clojure.string/split)
(partial partial re-seq))
re-pattern
(partial clojure.string/join \|)
(partial map (make-fn 'java.util.regex.Pattern/quote))
(partial sort (comp not neg? compare))))
在我的第二次尝试中,我使用正则表达式来匹配“令牌”和中间的单个符号,然后对这些单个符号进行分组。在这里,我不喜欢在正则表达式匹配之外完成的处理量。
(defn scan-for [tokens]
(comp
(partial remove empty?)
(fn group [s]
(lazy-seq
(if-let [[sf & sr] s]
(if (or (get sf 1)
(some (partial = sf) tokens))
(list* "" sf (group sr))
(let [[gf & gr] (group sr)]
(cons (str sf gf) gr)))
(cons "" nil))))
(->> tokens
(sort (comp not neg? compare))
(map #(java.util.regex.Pattern/quote %))
(clojure.string/join \|)
(#(str % "|(?s)."))
(re-pattern)
(partial re-seq))))
那么有没有办法使用一些合适的正则表达式来解析输入一次并在解析之外进行最少的处理?
(split
的惰性版本也返回正则表达式匹配会很有帮助......如果它存在的话。)
【问题讨论】:
【参考方案1】:这是一个不懒惰的快速和肮脏的版本,但我认为可以通过在几个地方使用lazy-seq
来变成一个懒惰的版本,就像你在你的版本中所做的那样:
(defn scan-for
([tokens text unmatched xs]
(if (empty? text)
(concat xs [unmatched])
(let [matching (filter #(clojure.string/starts-with? text %) tokens)]
(if (empty? matching)
(recur tokens (subs text 1) (str unmatched (subs text 0 1)) xs)
(let [matched (first matching)]
(recur tokens
(subs text (count matched))
""
(concat xs
(when-not (empty? unmatched) [unmatched])
[matched])))))))
([tokens text]
(scan-for tokens text "" [])))
;; (scan-for ["an" "ban" "banal" "d"] "ban bananas and banalities")
;; => ("ban" " " "ban" "an" "as " "an" "d" " " "ban" "alities")
编辑:
这是一个非常有趣的,所以我不得不尝试一下。我发现clojure.string/split
还带有一个可选参数,该参数限制了它将产生的拆分数量。假设达到限制不会扫描输入的其余部分,您可以根据您的原始建议实现它:
(defn create-regex [xs]
(->> xs (interpose "|") (apply str) re-pattern))
(defn split-lazy [s re]
(when-not (empty? s)
(let [[part remaining] (clojure.string/split s re 2)]
(lazy-seq (cons part (split-lazy remaining re))))))
(defn scan-lazy [xs s]
(let [re (create-regex xs)
no-matches (split-lazy s re)
matches (concat (re-seq re s) (repeat nil))]
(remove empty?
(interleave no-matches matches))))
(defn scan-for [xs] (partial scan-lazy xs))
;; ((scan-for ["an" "ban" "banal" "d"]) "ban bananas and banalities")
;; => ("ban" " " "ban" "an" "as " "an" "d" " " "ban" "alities")
在上面的代码中,我使用了一个技巧,其中matches
用nil
s 填充,以便interleave
可以消耗两个集合,否则当其中一个结束时它将停止。
你也可以检查它是否懒惰:
bananas.core> (def bananas ((scan-for ["an" "ban" "banal" "d"]) "ban bananas and banalities"))
#'bananas.core/bananas
bananas.core> (realized? bananas)
false
bananas.core> bananas
("ban" " " "ban" "an" "as " "an" "d" " " "ban" "alities")
bananas.core> (realized? bananas)
true
编辑 2:
如果您通过减少长度对标记进行排序,您会得到您期望的“贪婪”版本:
(defn create-regex [xs]
(->> xs (sort-by count) reverse (interpose "|") (apply str) re-pattern))
;; ((scan-for ["an" "ban" "banal" "d"]) "ban bananas and banalities")
;; => ("ban" " " "ban" "an" "as " "an" "d" " " "banal" "ities")
【讨论】:
感谢您的尝试,但它错过了我的问题的重点。正如您在我的文本中的标题、标签和最后一个问题中看到的那样,我希望大部分处理都使用正则表达式完成(我很想知道是否可以完成这样的“正则表达式魔法”,因为我对该主题的了解是只有基本的)。另一个缺陷是你也提到了缺乏懒惰。使用xs
参数来构建结果意味着必须进行一些相当大的修改以实现惰性(......但这是可行的)。
其他容易解决的问题包括: (a) 令牌不贪婪匹配。 (matched
不应该是matching
的第一项,而是最长的一项。正如您在我的示例中看到的,"banalities"
应该拆分为("banal" "ities")
。) (b) 如果最后没有什么不匹配的,添加一个空字符串。 (c) 函数没有柯里化。
...以及对代码的主要风格修改:由于 scan-for
的 4 参数版本不应该在外部使用,我将只使用 2 个参数并编写 (loop [text text unmatched "" xs []] ... )
ommiting tokens
recur
。
我写了一个改进的版本,我认为它应该符合构建正则表达式、懒惰和创建柯里化/部分函数的标准,请检查编辑
split
的惰性化是一个非常好的改进!但是,此解决方案仍然不满足输入中搜索到的标记仅被识别一次的标准(因为split-lazy
和re-seq
都将识别相同的出现)。【参考方案2】:
我想出了一个看起来可以接受的解决方案。它基于捕获组和*?
量词的使用,*
的不情愿/非贪婪版本。
这里是:
(defn scan-for [tokens]
(comp
(partial remove empty?)
flatten
(partial map rest)
(->> tokens
(sort (comp not neg? compare)) ;alternatively, we can short by decreasing length
(map #(java.util.regex.Pattern/quote %))
(clojure.string/join \|)
(#(str "((?s).*?)(" % "|\\z)"))
(re-pattern)
(partial re-seq))))
并且在默契的风格中:
(def scan-for
(comp
(partial comp
(partial remove empty?)
flatten
(partial map rest))
(partial partial re-seq)
re-pattern
(partial str "((?s).*?)(")
(rpartial str "|\\z)")
(partial clojure.string/join \|)
(partial map (make-fn 'java.util.regex.Pattern/quote))
(partial sort (comp not neg? compare))))
【讨论】:
以上是关于在 clojure 中使用正则表达式实现一个简单的扫描器/标记器的主要内容,如果未能解决你的问题,请参考以下文章