Поиск xml в Clojure

У меня есть следующий пример xml:

<data>
  <products>
    <product>
      <section>Red Section</section>
      <images>
        <image>img.jpg</image>
        <image>img2.jpg</image>
      </images>
    </product>
    <product>
      <section>Blue Section</section>
      <images>
        <image>img.jpg</image>
        <image>img3.jpg</image>
      </images>
    </product>
    <product>
      <section>Green Section</section>
      <images>
        <image>img.jpg</image>
        <image>img2.jpg</image>
      </images>
    </product>
  </products>
</data>

Я знаю, как разбирать его в Clojure

(require '[clojure.xml :as xml])
(def x (xml/parse 'location/of/that/xml'))

Это возвращает вложенную карту, описывающую xml

{:tag :data,
 :attrs nil,
 :content [
     {:tag :products,
      :attrs nil,
      :content [
          {:tag :product,
           :attrs nil,
           :content [] ..

Эта структура, конечно, может быть пройдена со стандартными функциями Clojure, но она может оказаться действительно многословной, особенно если сравнивать ее, например, с запросом XPath. Есть ли помощник для прохождения и поиска такой структуры? Как я могу, например,

  • получить список всех <product>
  • получить только продукт, тэг <images> содержит <image> с текстом "img2.jpg"
  • получить продукт, чей section - "Красный раздел"

Спасибо

Ответ 1

Вы можете использовать библиотеку, например clj-xpath

Ответ 2

Используя Молнии из data.zip, это решение для вашего второго варианта использования:

(ns core
  (:use clojure.data.zip.xml)
  (:require [clojure.zip :as zip]
            [clojure.xml :as xml]))

(def data (zip/xml-zip (xml/parse PATH)))
(def products (xml-> data :products :product))

(for [product products :let [image (xml-> product :images :image)]
                       :when (some (text= "img2.jpg") image)]
  {:section (xml1-> product :section text)
   :images (map text image)})
=> ({:section "Red Section", :images ("img.jpg" "img2.jpg")}
    {:section "Green Section", :images ("img.jpg" "img2.jpg")})

Ответ 3

Здесь альтернативная версия, использующая data.zip, для всех трех случаев. Я обнаружил, что xml-> и xml1-> имеет довольно мощную навигационную систему с подзапросами в векторах.

;; [org.clojure/data.zip "0.1.1"]

(ns example.core
  (:require
   [clojure.zip :as zip]
   [clojure.xml :as xml]
   [clojure.data.zip.xml :refer [text xml-> xml1->]]))

(def data (zip/xml-zip (xml/parse "/tmp/products.xml")))

(let [all-products (xml-> data :products :product)
      red-section (xml1-> data :products :product [:section "Red Section"])
      img2 (xml-> data :products :product [:images [:image "img2.jpg"]])]
  {:all-products (map (fn [product] (xml1-> product :section text)) all-products)
   :red-section (xml1-> red-section :section text)
   :img2 (map (fn [product] (xml1-> product :section text)) img2)})

=> {:all-products ("Red Section" "Blue Section" "Green Section"),
    :red-section "Red Section",
    :img2 ("Red Section" "Green Section")}

Ответ 4

Библиотека Tupelo может легко решить такие проблемы, используя структуру данных дерева tupelo.forest. Пожалуйста, см. Этот вопрос для получения дополнительной информации. API docs можно найти здесь.

Здесь мы загружаем ваши xml-данные и сначала конвертируем их в призыв, а затем собственную структуру дерева, используемую tupelo.forest. Libs и data def:

(ns tst.tupelo.forest-examples
  (:use tupelo.forest tupelo.test )
  (:require
    [clojure.data.xml :as dx]
    [clojure.java.io :as io]
    [clojure.set :as cs]
    [net.cgrand.enlive-html :as en-html]
    [schema.core :as s]
    [tupelo.core :as t]
    [tupelo.string :as ts]))
(t/refer-tupelo)

(def xml-str-prod "<data>
                    <products>
                      <product>
                        <section>Red Section</section>
                        <images>
                          <image>img.jpg</image>
                          <image>img2.jpg</image>
                        </images>
                      </product>
                      <product>
                        <section>Blue Section</section>
                        <images>
                          <image>img.jpg</image>
                          <image>img3.jpg</image>
                        </images>
                      </product>
                      <product>
                        <section>Green Section</section>
                        <images>
                          <image>img.jpg</image>
                          <image>img2.jpg</image>
                        </images>
                      </product>
                    </products>
                  </data> " )

и код инициализации:

(dotest
  (with-forest (new-forest)
    (let [enlive-tree          (->> xml-str-prod
                                 java.io.StringReader.
                                 en-html/html-resource
                                 first)
          root-hid             (add-tree-enlive enlive-tree)
          tree-1               (hid->hiccup root-hid)

Скрытый суффикс означает "Hex ID", который является уникальным шестнадцатеричным значением, которое действует как указатель на node/leaf в дереве. На этом этапе мы только что загрузили данные в структуру данных леса, создав дерево-1, которое выглядит так:

[:data
 [:tupelo.forest/raw "\n                    "]
 [:products
  [:tupelo.forest/raw "\n                      "]
  [:product
   [:tupelo.forest/raw "\n                        "]
   [:section "Red Section"]
   [:tupelo.forest/raw "\n                        "]
   [:images
    [:tupelo.forest/raw "\n                          "]
    [:image "img.jpg"]
    [:tupelo.forest/raw "\n                          "]
    [:image "img2.jpg"]
    [:tupelo.forest/raw "\n                        "]]
   [:tupelo.forest/raw "\n                      "]]
  [:tupelo.forest/raw "\n                      "]
  [:product
   [:tupelo.forest/raw "\n                        "]
   [:section "Blue Section"]
   [:tupelo.forest/raw "\n                        "]
   [:images
    [:tupelo.forest/raw "\n                          "]
    [:image "img.jpg"]
    [:tupelo.forest/raw "\n                          "]
    [:image "img3.jpg"]
    [:tupelo.forest/raw "\n                        "]]
   [:tupelo.forest/raw "\n                      "]]
  [:tupelo.forest/raw "\n                      "]
  [:product
   [:tupelo.forest/raw "\n                        "]
   [:section "Green Section"]
   [:tupelo.forest/raw "\n                        "]
   [:images
    [:tupelo.forest/raw "\n                          "]
    [:image "img.jpg"]
    [:tupelo.forest/raw "\n                          "]
    [:image "img2.jpg"]
    [:tupelo.forest/raw "\n                        "]]
   [:tupelo.forest/raw "\n                      "]]
  [:tupelo.forest/raw "\n                    "]]
 [:tupelo.forest/raw "\n                   "]]

Затем мы удалим любые пустые строки с помощью этого кода:

blank-leaf-hid?      (fn [hid] (and (leaf-hid? hid) ; ensure it is a leaf node
                                 (let [value (hid->value hid)]
                                      (and (string? value)
                                        (or (zero? (count value)) ; empty string
                                          (ts/whitespace? value)))))) ; all whitespace string

blank-leaf-hids      (keep-if blank-leaf-hid? (all-hids))
>>                   (apply remove-hid blank-leaf-hids)
tree-2               (hid->hiccup root-hid)

чтобы создать гораздо более приятное дерево результатов (формат hiccup)

[:data
 [:products
  [:product
   [:section "Red Section"]
   [:images [:image "img.jpg"] [:image "img2.jpg"]]]
  [:product
   [:section "Blue Section"]
   [:images [:image "img.jpg"] [:image "img3.jpg"]]]
  [:product
   [:section "Green Section"]
   [:images [:image "img.jpg"] [:image "img2.jpg"]]]]]

Следующий код затем вычисляет ответы на три вопроса выше:

product-hids         (find-hids root-hid [:** :product])
product-trees-hiccup (mapv hid->hiccup product-hids)

img2-paths           (find-paths-leaf root-hid [:data :products :product :images :image] "img2.jpg")
img2-prod-paths      (mapv #(drop-last 2 %) img2-paths)
img2-prod-hids       (mapv last img2-prod-paths)
img2-trees-hiccup    (mapv hid->hiccup img2-prod-hids)

red-sect-paths       (find-paths-leaf root-hid [:data :products :product :section] "Red Section")
red-prod-paths       (mapv #(drop-last 1 %) red-sect-paths)
red-prod-hids        (mapv last red-prod-paths)
red-trees-hiccup     (mapv hid->hiccup red-prod-hids)]

с результатами:

 (is= product-trees-hiccup
   [[:product
     [:section "Red Section"]
     [:images
      [:image "img.jpg"]
      [:image "img2.jpg"]]]
    [:product
     [:section "Blue Section"]
     [:images
      [:image "img.jpg"]
      [:image "img3.jpg"]]]
    [:product
     [:section "Green Section"]
     [:images
      [:image "img.jpg"]
      [:image "img2.jpg"]]]] )

(is= img2-trees-hiccup
  [[:product
    [:section "Red Section"]
    [:images
     [:image "img.jpg"]
     [:image "img2.jpg"]]]
   [:product
    [:section "Green Section"]
    [:images
     [:image "img.jpg"]
     [:image "img2.jpg"]]]])

(is= red-trees-hiccup
  [[:product
    [:section "Red Section"]
    [:images
     [:image "img.jpg"]
     [:image "img2.jpg"]]]]))))

Полный пример можно найти в примерах леса unit test.

Ответ 5

во многих случаях макрос первого потока, а также карта clojures и векторная семантика являются адекватным синтаксисом для доступа к xml. Есть много случаев, когда вам нужно что-то более конкретное для xml (например, библиотека xpath), хотя во многих случаях существующий язык почти такой же кратким, что и добавление каких-либо зависимостей.

(pprint (-> (xml/parse "/tmp/xml") 
        :content first :content second :content first :content first))
"Blue Section"