如何使用specter

specter是Clojure(Script)的数据处理API,它的作者就是大名鼎鼎的Storm的作者NathanMarz.作者开发这套函数的目的是弥补Clojure自带函数在查询或更改复杂数据结构时的不足。使用specter不仅可以使数据处理变得更简单,处理的效率也比使用Clojure自带函数更好。specter还有一个优点,即不会更改数据结构,例如不会像filter那样会把vector的输入转换为list输出。但specter的语法可能需要一点时间去熟悉,下面就通过例子的方式简单的介绍下specter的用法。

引入specter包

project.clj中加入依赖

[com.rpl/specter "1.1.3"]

选择

使用select选择输入数据的子集:

(require
  [com.rpl.specter :refer :all])

(filter odd? [1 2 3 4 5 6]) ;; (1 3 5)
(select [ALL odd?] [1 2 3 4 5 6]) ;; [1 3 5]

选择奇数位的元素

(select [INDEXED-VALS (comp odd? first) LAST] [:a :b :c :d :e]) ;; [:b :d]

修改

使用transform修改选中的元素:

;; 选中所有map的val,并替换为(count val)的值
(transform [MAP-VALS] count {:a "hello" :b "world"})
;; {:a 5, :b 5}

如果只需要替换为同一值,可使用setval:

(setval [MAP-VALS] "w" {:a "hello" :b "world"})
;; {:a "w", :b "w"}

删除

将值设为NONE表示删除此元素:

(setval [:b] NONE {:a "hello" :b "world"})
;; {:a "hello"}

一个稍复杂的例子

此示例输入是一条常见的hiccup数据,其中包含网站列表,每个网站有名称、网址、成立日期及简介内容。 我们希望将此输入转换为clojure数据结构,即提取名称、网址、成立日期及简介再转换为map队列输出

(def root {:content
           [{:type :element,
             :attrs nil,
             :tag :h3,
             :content
             [{:type :element,
               :attrs
               {:title "Permanent Link to Bcelebrated",
                :href
                "http://www.thedigitalbeyond.com/online-services-list/bcelebrated/",
                :rel "bookmark"},
               :tag :a,
               :content ["Bcelebrated"]}]}
            "\n"
            {:type :element,
             :attrs
             {:href "http://www.bcelebrated.com",
              :name "Link to Bcelebrated"},
             :tag :a,
             :content ["www.bcelebrated.com"]}
            {:type :element,
             :attrs nil,
             :tag :p,
             :content
             [{:type :element,
               :attrs nil,
               :tag :strong,
               :content ["Founded: "]}
              "July, 2009"]}
            {:type :element,
             :attrs nil,
             :tag :p,
             :content
             ["Bcelebrated enables members to create a multi-media website that will become their autobiographical memorial site when the time comes."]}
            {:type :element,
             :attrs nil,
             :tag :h3,
             :content
             [{:type :element,
               :attrs
               {:title "Permanent Link to Boxego",
                :href
                "http://www.thedigitalbeyond.com/online-services-list/boxego/",
                :rel "bookmark"},
               :tag :a,
               :content ["Boxego"]}]}
            "\n"
            {:type :element,
             :attrs
             {:href "http://www.boxego.com",
              :name "Link to Boxego"},
             :tag :a,
             :content ["www.boxego.com"]}
            {:type :element,
             :attrs nil,
             :tag :p,
             :content
             [{:type :element,
               :attrs nil,
               :tag :strong,
               
               :content ["Founded: "]}
              "2013"]}
            {:type :element,
             :attrs nil,
             :tag :p,
             :content
             ["Boxego is a private journal that can be shared privately and socially, now and in the future."]}]})

选取标题,使用pred自定义路径选择器:

(select [:content ALL (pred #(= :h3 (:tag %))) :content ALL :content FIRST] root) 
;; ["Bcelebrated" "Boxego"]

选取网站链接:

(select [:content ALL (pred #(= :a (:tag %))) :attrs :href] root)
;; ["http://www.bcelebrated.com" "http://www.boxego.com"]

选取时间与文章描述:

(select [:content ALL (pred #(= :p (:tag %))) :content LAST] root)
;; ["July, 2009"
;;  "Bcelebrated enables members to create a multi-media website that will become their autobiographical memorial site when the time comes."
;;  "2013"
;;  "Boxego is a private journal that can be shared privately and socially, now and in the future."]

同时取上面三类内容,使用multi-path同时选择多个路径:

(def paths [[(pred #(= :h3 (:tag %))) :content ALL :content FIRST]
            [(pred #(= :a (:tag %))) :attrs :href]
            [(pred #(= :p (:tag %))) :content LAST]])

(select [:content ALL (apply multi-path paths)] root)
;; ["Bcelebrated"
;;  "http://www.bcelebrated.com"
;;  "July, 2009"
;;  "Bcelebrated enables members to create a multi-media website that will become their autobiographical memorial site when the time comes."
;;  "Boxego"
;;  "http://www.boxego.com"
;;  "2013"
;;  "Boxego is a private journal that can be shared privately and socially, now and in the future."]

下面我们来看如何转换成我们期望的map集合的结构。这里要用到transform

(->>
 (select [:content ALL (apply multi-path paths)] root)
 (partition 3)
 (transform [ALL] (fn [[title url description]] {:title title :url url :description description}))) 
;; ({:title "Bcelebrated",
;;   :url "http://www.bcelebrated.com",
;;   :description "July, 2009"}
;;  {:title
;;   "Bcelebrated enables members to create a multi-media website that will become their autobiographical memorial site when the time comes.",
;;   :url "Boxego",
;;   :description "http://www.boxego.com"})

完整示例specter.example.clj

递归

(def MAP-NODES
	 (recursive-path [] p
	   (if-path map?
		 (continue-then-stay MAP-VALS p))))

(setval [MAP-NODES MAP-VALS nil?] NONE m)
Comment