This work is licensed under a Creative Commons Attribution 3.0 Unported License (including images & stylesheets). The source is available on Github.

What Version of Clojure Does This Guide Cover?

This guide covers Clojure 1.11 and Leiningen 2.x.

Overview

Try as you might, XML is difficult to avoid. This is particularly true in the Java ecosystem. This guide will show you how to parse XML with the minimum amount of pain using the excellent tools available in Clojure.

Parsing NZB files

For the purpose of the tutorial I have chosen a simple and fairly well known XML file format: NZB. An NZB file is used to describe files to download from NNTP servers. In this tutorial we will take a basic NZB document and turn it into a Clojure map.

Let us start by creating a new project (for details on using Leiningen, see this guide:

$ lein new nzb

Now edit project.clj to contain the following:

(defproject nzb "0.1.0-SNAPSHOT"
  :description ""
  :url ""
  :license {:name "Eclipse Public License"
            :url "https://www.eclipse.org/legal/epl-v10.html"}
  :dependencies [[org.clojure/clojure "1.11.4"]
                 [org.clojure/data.zip "1.0.0"]])

We are including a dependency on clojure.data.zip, which is a "system for filtering trees, and XML trees in particular".

Make a dir called dev-resources at the root of your project, and create a file named example.nzb inside of it. This will be the file we use to test our code (taken from wikipedia). dev-resources is by convention the location to store file resources you use during development / testing.

Put the following XML in the example.nzb file:

<?xml version="1.0" encoding="iso-8859-1" ?>
<!-- <!DOCTYPE nzb PUBLIC "-//newzBin//DTD NZB 1.1//EN" "http://www.newzbin.com/DTD/nzb/nzb-1.1.dtd"> -->
<nzb xmlns="http://www.newzbin.com/DTD/2003/nzb">
 <head>
   <meta type="title">Your File!</meta>
   <meta type="tag">Example</meta>
 </head>
 <file poster="Joe Bloggs &lt;bloggs@nowhere.example&gt;" date="1071674882" subject="Here's your file!  abc-mr2a.r01 (1/2)">
   <groups>
     <group>alt.binaries.newzbin</group>
     <group>alt.binaries.mojo</group>
   </groups>
   <segments>
     <segment bytes="102394" number="1">123456789abcdef@news.newzbin.com</segment>
     <segment bytes="4501" number="2">987654321fedbca@news.newzbin.com</segment>
   </segments>
 </file>
</nzb>

Note The eagle eyed among you will notice that I have commented out the DOCTYPE declaration, as this causes an Exception to be thrown. I will show you how to get around this towards the end of the tutorial.

Let's write a high level test to illustrate more clearly what we are trying to do. Open up the test/nzb/core_test.clj file and make enter the following:

(ns nzb.core-test
  (:use clojure.test
        nzb.core)
  (:require [clojure.java.io :as io]))

(deftest test-nzb->map
  (let [input (io/resource "example.nzb")]
    (is (= {:meta {:title "Your File!"
                   :tag "Example"}
            :files [{:poster "Joe Bloggs <bloggs@nowhere.example>"
                     :date 1071674882
                     :subject "Here's your file!  abc-mr2a.r01 (1/2)"
                     :groups ["alt.binaries.newzbin"
                              "alt.binaries.mojo"]
                     :segments [{:bytes 102394
                                 :number 1
                                 :id "123456789abcdef@news.newzbin.com"}
                                {:bytes 4501
                                 :number 2
                                 :id "987654321fedbca@news.newzbin.com"}]}]}
           (nzb->map input)))))

This should be fairly self-explanatory, I have directly translated the XML into Clojure data structures of maps and vectors. If we were to just use the clojure.xml library to parse the NZB file, we get a tree based representation. For example:

$ lein repl
...
user=> (require '[clojure.java.io :as io] '[clojure.xml :as xml])
nil
user=> (-> "example.nzb" io/resource io/file xml/parse clojure.pprint/pprint)
{:tag :nzb,
 :attrs {:xmlns "http://www.newzbin.com/DTD/2003/nzb"},
 :content
 [{:tag :head,
   :attrs nil,
   :content
   [{:tag :meta, :attrs {:type "title"}, :content ["Your File!"]}
    {:tag :meta, :attrs {:type "tag"}, :content ["Example"]}]}
  {:tag :file,
   :attrs
   {:subject "Here's your file!  abc-mr2a.r01 (1/2)",
    :date "1071674882",
    :poster "Joe Bloggs <bloggs@nowhere.example>"},
   :content
   [{:tag :groups,
     :attrs nil,
     :content
     [{:tag :group, :attrs nil, :content ["alt.binaries.newzbin"]}
      {:tag :group, :attrs nil, :content ["alt.binaries.mojo"]}]}
    {:tag :segments,
     :attrs nil,
     :content
     [{:tag :segment,
       :attrs {:number "1", :bytes "102394"},
       :content ["123456789abcdef@news.newzbin.com"]}
      {:tag :segment,
       :attrs {:number "2", :bytes "4501"},
       :content ["987654321fedbca@news.newzbin.com"]}]}]}]}
nil

That's great, and can sometimes be enough. But I would rather work with the representation I have in the test. To do that, we need a way of traversing this tree and picking out the pieces of information we require. The clojure.zip and clojure.data.zip libraries are perfect for this. The documentation for the data.zip library on github is nice, but it initially left me a little confused as to how to go about using the library (not being familiar with zippers).

A Simple Example

Zippers allow you to easily traverse a data structure. Let's play with it in a REPL and start with the root node of our NZB file:

(require '[clojure.java.io :as io])
(require '[clojure.xml :as xml])
(require '[clojure.zip :as zip])
(require '[clojure.data.zip.xml :as zip-xml])

(def root (-> "example.nzb" io/resource io/file xml/parse zip/xml-zip))

Now we have a zipper for the root element of our document, we can start traversing it for information. The two main functions we will use for this are xml-> and xml1->. The former returns a sequence of items based on the predicates given to it, the latter returning the first matching item. As an example, let's get the meta data from the NZB document root and create a Clojure map:

(into {}
      (for [m (zip-xml/xml-> root :head :meta)]
        [(keyword (zip-xml/attr m :type))
         (zip-xml/text m)]))
;; => {:title "Your File!", :tag "Example"}

A couple of things are happening here. First of all we use xml-> to return a sequence of <meta> tags that live under the <head> tag:

(zip-xml/xml-> root :head :meta)

We use the for list comprehension macro to evaluate each item in the sequence. For each item we find the contents of the :type attribute using the attr function:

(keyword (zip-xml/attr m :type))

This returns us the contents of the attribute as a string, which we turn into a keyword to use as the key in the map. We then use the text function to get the textual contents of the meta tag:

(zip-xml/text m)

We make a tuple of these values, and pass the resulting sequence to into to build the map.

Putting It Together

Using only these functions, we can parse the raw XML into the Clojure data structure from our unit test. If you like, open ./src/nzb/core.clj, and make the changes as you read along.

First let's define our nzb->map function from the test, and pull in the code we have already written for parsing the metadata of the NZB:

(ns nzb.core
  (:require [clojure.xml :as xml]
            [clojure.java.io :as io]
            [clojure.zip :as zip]
            [clojure.data.zip.xml :as zip-xml]))

(defn meta->map
  [root]
  (into {}
        (for [m (zip-xml/xml-> root :head :meta)]
          [(keyword (zip-xml/attr m :type))
           (zip-xml/text m)])))

(defn file->map
  [file]
  ;; TODO
)

(defn nzb->map
  [input]
  (let [root (-> input
                 io/input-stream
                 xml/parse
                 zip/xml-zip)]
    {:meta  (meta->map root)
     :files (mapv file->map (zip-xml/xml-> root :file))}))

The only new thing here is the use of io/input-stream to allow us to use anything as input that the io/input-stream supports. These are currently OutputStream, File, URI, URL, Socket, byte array, and String arguments. See the clojure.java.io docs for details.

Now let's fill in the file->map function:

(defn segment->map
  [seg]
  {:bytes  (Long/valueOf (zip-xml/attr seg :bytes))
   :number (Integer/valueOf (zip-xml/attr seg :number))
   :id     (zip-xml/xml1-> seg zip-xml/text)})

(defn file->map
  [file]
  {:poster   (zip-xml/attr file :poster)
   :date     (Long/valueOf (zip-xml/attr file :date))
   :subject  (zip-xml/attr file :subject)
   :groups   (vec (zip-xml/xml-> file :groups :group zip-xml/text))
   :segments (mapv segment->map
                   (zip-xml/xml-> file :segments :segment))})

Again, nothing new. We simply pick out the pieces of the document we wish to process using a combination of the xml1->, xml->, attr, and text functions. Run the test, and it should pass.

Prevent Parsing the DTD

Interestingly, if we uncomment the DTD declaration in the example.nzb file, our code now explodes with an Exception:

org.xml.sax.SAXParseException: The markup declarations contained or pointed to by the document type declaration must be well-formed

We can fix this by swapping out the SAXParserFactory and setting a feature to not validate the DTD. Here's how:

Update the ns declaration to include some required classes:

(ns nzb.core
  (:require [clojure.xml :as xml]
            [clojure.java.io :as io]
            [clojure.zip :as zip]
            [clojure.data.zip.xml :as zip-xml])
  (:import (javax.xml.parsers SAXParser SAXParserFactory)))

Define a function to switch out the SAXParserFactory:

(defn startparse-sax
  "Don't validate the DTDs, they are usually messed up."
  [s ch]
  (let [factory (SAXParserFactory/newInstance)]
    (.setFeature factory "http://apache.org/xml/features/nonvalidating/load-external-dtd" false)
    (let [^SAXParser parser (.newSAXParser factory)]
      (.parse parser s ch))))

Update our nzb->map definition to use it:

(defn nzb->map
  [input]
  (let [root (-> input
                 io/input-stream
                 (xml/parse startparse-sax)
                 zip/xml-zip)]
    {:meta  (meta->map root)
     :files (mapv file->map (zip-xml/xml-> root :file))}))

Yay, our test passes again.

$ lein test

lein test nzb.core-test

Ran 1 tests containing 1 assertions.
0 failures, 0 errors.

Query Predicates

There are a few other useful functions in the clojure.data.zip.xml ns we haven't yet looked at, namely: text=, attr=, and tag=. These functions allow you to construct query predicates to run against a given node. As an example, let's pull out the first file segment from the example.nzb file using the attr= function:

(zip-xml/xml1-> root
                :file
                :segments
                :segment
                (zip-xml/attr= :number "1")
                zip-xml/text)
"123456789abcdef@news.newzbin.com"

From the root node of the document we reach down into :file, :segments, and :segment in turn, then use the attr= query predicate to match a :segment with a value of "1".

Interestingly enough, the other two query predicates have shortcuts for their use. You have already been using the tag= query predicate every time you use a keyword to locate a tag. To use the text= predicate easily, just use a string. For example, to retrieve the second :segment based on its content of 987654321fedbca@news.newzbin.com:

(zip-xml/xml1-> root
                :file
                :segments
                :segment
                "987654321fedbca@news.newzbin.com")
;; ... the resulting node

Finally, you can combine these query predicates to match multiple things on a given node by using a vector:

(zip-xml/xml1-> root
                :file
                :segments
                :segment
                [(zip-xml/attr= :number "1")
                 (zip-xml/attr= :bytes "102394")]
                zip-xml/text)
"123456789abcdef@news.newzbin.com"

Here we are matching on both the :number attribute being "1", and the :bytes attribute being "102394". Obviously, you can use strings here to match against content too.

Creating New Predicates

OK, now let's suppose we want to use some kind of numerical comparison in our XML (like we might do with XPath). As it stands, we have no way to do that with the built-in functions but we can easily define our own.

Let's start with a general function for comparing attribute values:

(defn attr-fn
  [attrname f test-val & [conv-fn]]
  (fn [loc]
    (let [conv-fn (or conv-fn identity)
          val (conv-fn (zip-xml/attr loc attrname))]
      (f val test-val))))

This function takes an attribute name (attrname), a function for making a comparison (f), a value to test agains (test-val) and optionally a conversion function. Imagine our example.nzb file had 100 segments, and we only wanted to get segments over 75. We could now achieve this using our general function:

(zip-xml/xml-> root
               :file
               :segments
               :segment
               (attr-fn :number > 75 #(Long/valueOf %))
               zip-xml/text)

Let's provide a helper for this to make the syntax clearer:

(defn attr>
  [attrname val]
  (attr-fn attrname > val #(Long/valueOf %)))

(zip-xml/xml-> root
               :file
               :segments
               :segment
               (attr> :number 75)
               zip-xml/text)

We could build a whole suite of helper functions for examining XML nodes, if we are unlucky enough to be required to do so :)

Conclusion

I hope these simple examples have given you an idea of the ease with which you can process XML using Clojure, and how simple it is to extend the tools already provided in interesting directions.

Contributors

Gareth Jones, 2012 (original author) Sean Corfield, 2023 (updated to Clojure 1.11 etc)