Статьи

Scala Basic XML обработка

Введение

Практически все знают, что такое XML: это структурированный, машиночитаемый текстовый формат для представления информации, который можно легко проверить на «грамматичность» тегов, атрибутов и их взаимосвязь (например, с использованием DTD). Это отличается от HTML, который может иметь элементы, которые не закрываются (например, <p> foo <p> bar, а не <p> foo </ p> <p> bar </ p>) и все еще обрабатываются. XML всегда предназначался для форматирования машин, но он превратился в представление данных, которое многие люди (к сожалению, для них) в конечном итоге редактировали вручную.

Однако даже в машиночитаемом формате у него есть проблемы, такие как то, что он гораздо более многословный, чем на самом деле требуется, что немаловажно, когда вам нужно перенести много данных с машины на машину — в следующем посте я расскажу JSON и Avro, которые можно рассматривать как эволюцию того, для чего предназначен XML, и которые работают намного лучше для многих приложений, имеющих значение в контексте «больших данных». Несмотря на это, существует множество унаследованных данных, которые были созданы в виде XML, и есть много сообществ (например, сообщество цифровых гуманитарных наук), которые по-прежнему обожают XML, поэтому люди, выполняющие любое разумное количество работ по анализу текста, скорее всего, в конечном итоге будут нуждаться в работать с XML-данными.

Существует множество учебных пособий по XML и Scala — просто выполните поиск в Интернете по «Scala XML», и вы их получите. Как и в других сообщениях в блоге, эта цель нацелена на то, чтобы быть очень явными, чтобы новички могли видеть примеры со всеми шагами в них, и я буду использовать это для настройки обработки JSON.

Простой пример XML

Для начала давайте рассмотрим очень простой пример создания и обработки небольшого количества XML.

Первое, что нужно знать о XML в Scala, — это то, что Scala может обрабатывать литералы XML. То есть вам не нужно помещать кавычки вокруг строк XML — вместо этого вы можете просто написать их напрямую, и Scala автоматически интерпретирует их как элементы XML (типа scala.xml.Element).

1
2
scala> val foo = <foo><bar type="greet">hi</bar><bar type="count">1</bar><bar type="color">yellow</bar></foo>
foo: scala.xml.Elem = <foo><bar type="greet">hi</bar><bar type="count">1</bar><bar type="color">yellow</bar></foo>

Теперь давайте немного поработаем над этим. Вы можете получить весь текст, используя текстовый метод.

1
2
scala> foo.text
res0: String = hi1yellow

Итак, это объединило весь текст. Чтобы распечатать их с пробелами между ними, давайте сначала получим все узлы столбцов, а затем получим их тексты и используем mkString для этой последовательности. Чтобы получить узлы панели, мы можем использовать селектор \.

1
2
scala> foo \ "bar"
res1: scala.xml.NodeSeq = NodeSeq(<bar type="greet">hi</bar>, <bar type="count">1</bar>, <bar type="color">yellow</bar>)

Это возвращает нам последовательность узлов баров, которые находятся непосредственно под узлом foo. Обратите внимание, что оператор \ (селектор) — это просто зеркальное отображение селектора / , используемого в XPath .

Конечно, теперь, когда у нас есть такая последовательность, мы можем отобразить ее, чтобы получить то, что мы хотим. Поскольку метод text возвращает текст под узлом, мы можем сделать следующее.

1
2
scala> (foo \ "bar").map(_.text).mkString(" ")
res2: String = hi 1 yellow

Чтобы получить значение атрибута type на каждом узле, мы можем использовать селектор \ , за которым следует «@type».

1
2
3
4
5
scala> (foo \ "bar").map(_ \ "@type")
res3: scala.collection.immutable.Seq[scala.xml.NodeSeq] = List(greet, count, color)
 
(foo \ "bar").map(barNode => (barNode \ "@type", barNode.text))
res4: scala.collection.immutable.Seq[(scala.xml.NodeSeq, String)] = List((greet,hi), (count,1), (color,yellow))

Обратите внимание, что \ selector может получать только дочерние элементы узла, из которого вы выбираете. Чтобы копать сколь угодно глубоко, чтобы вытащить все узлы заданного типа, независимо от того, где они находятся, используйте селектор \\ . Рассмотрим следующий (причудливый) фрагмент XML с узлами ‘z’ на разных уровнях встраивания.

01
02
03
04
05
06
07
08
09
10
<a>
  <z x="1"/>
  <b>
    <z x="2"/>
    <c>
      <z x="3"/>
    </c>
    <z x="4"/>
  </b>
</a>

Давайте сначала поместим это в REPL.

1
2
scala> val baz = <a><z x="1"/><b><z x="2"/><c><z x="3"/></c><z x="4"/></b></a>
baz: scala.xml.Elem = <a><z x="1"></z><b><z x="2"></z><c><z x="3"></z></c><z x="4"></z></b></a>

Если мы хотим получить все узлы ‘z’ , мы делаем следующее.

1
2
scala> baz \\ "z"
res5: scala.xml.NodeSeq = NodeSeq(<z x="1"></z>, <z x="2"></z>, <z x="3"></z>, <z x="4"></z>)

И мы, конечно, можем легко найти значения атрибутов x на каждом из z.

1
2
scala> (baz \\ "z").map(_ \ "@x")
res6: scala.collection.immutable.Seq[scala.xml.NodeSeq] = List(1, 2, 3, 4)

Во всем вышеперечисленном мы использовали XML-литералы, то есть выражения, введенные непосредственно в Scala, который интерпретирует их как типы XML. Однако обычно нам нужно обрабатывать XML, сохраненный в файле или в строке, поэтому у объекта scala.xml.XML есть несколько методов для создания объектов scala.xml.Elem из других источников. Например, следующее позволяет нам создавать XML из строки.

1
2
3
4
5
scala> val fooString = """<foo><bar type="greet">hi</bar><bar type="count">1</bar><bar type="color">yellow</bar></foo>"""
fooString: java.lang.String = <foo><bar type="greet">hi</bar><bar type="count">1</bar><bar type="color">yellow</bar></foo>
 
scala> val fooElemFromString = scala.xml.XML.loadString(fooString)
fooElemFromString: scala.xml.Elem = <foo><bar type="greet">hi</bar><bar type="count">1</bar><bar type="color">yellow</bar></foo>

Этот элемент такой же, как тот, который создан с использованием литерала XML, как показано в следующем тесте.

1
2
scala> foo == fooElemFromString
res7: Boolean = true

См. Объект XML Scala для других способов создания элементов XML, например, из InputStreams, Files и т. Д.

Более богатый пример XML

В качестве более интересного примера обработки XML- кода я создал следующую короткую XML-строку, описывающую исполнителя, альбомы и песни, которую вы можете увидеть в github gist music.xml .

https://gist.github.com/2597611

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

Вы должны сохранить это в файле /tmp/music.xml . После этого вы можете запустить следующий код, который просто распечатывает каждого исполнителя, альбом и песню с отступом для каждого уровня.

01
02
03
04
05
06
07
08
09
10
11
12
val musicElem = scala.xml.XML.loadFile("/tmp/music.xml")
 
(musicElem \ "artist").foreach { artist =>
  println((artist \ "@name").text + "\n")
  val albums = (artist \ "album").foreach { album =>
    println(" " + (album \ "@title").text + "\n")
    val songs = (album \ "song").foreach { song =>
      println(" " + (song \ "@title").text)
    }
  println
  }
}

Преобразование объектов в и из XML

Одним из вариантов использования XML является предоставление машиночитаемого формата сериализации для объектов, которые все еще могут быть легко прочитаны, а иногда и отредактированы людьми. Процесс перетаскивания объектов из памяти в формат диска, такой как XML, называется маршаллингом. Мы начали с некоторого XML, поэтому мы определим некоторые классы и «демаршализируем» XML в объекты этих классов. Поместите следующее в REPL. (Совет: вы можете использовать « : paste » для ввода многострочных операторов, подобных приведенным ниже. Они будут работать без вставки, но это необходимо использовать в некоторых контекстах, например, если вы определили Artist перед песней.)

01
02
03
04
05
06
07
08
09
10
11
12
13
case class Song(val title: String, val length: String) {
  lazy val time = {
    val Array(minutes, seconds) = length.split(":")
    minutes.toInt*60 + seconds.toInt
  }
}
 
case class Album(val title: String, val songs: Seq[Song], val description: String) {
  lazy val time = songs.map(_.time).sum
  lazy val length = (time / 60)+":"+(time % 60)
}
 
case class Artist(val name: String, val albums: Seq[Album])

Довольно просто и понятно. Обратите внимание на использование ленивых значений для определения таких вещей, как время (продолжительность в секундах) песни. Причина этого заключается в том, что если мы создаем объект Song, но никогда не запрашиваем его время, то код, необходимый для его вычисления из строки, такой как «4:38? никогда не бегать; однако, если бы мы оставили lazy off, то он будет вычислен при создании объекта Song. Кроме того, мы не хотим использовать def здесь (то есть, сделать время методом), потому что его значение фиксировано на основе строки длины; использование метода будет означать пересчет времени каждый раз, когда его запрашивают для конкретного объекта.

Учитывая приведенные выше классы, мы можем создавать и использовать объекты из них вручную.

1
2
3
4
5
scala> val foobar = Song("Foo Bar", "3:29")
foobar: Song = Song(Foo Bar,3:29)
 
scala> foobar.time
res0: Int = 209

Использование родного Scala XML API

Конечно, мы больше заинтересованы в создании объектов Artist, Album и Song из информации, указанной в файлах, таких как пример музыки. Хотя я не показываю вывод REPL здесь, вы должны ввести в него все команды ниже, чтобы увидеть, что происходит.

Для начала убедитесь, что вы загрузили файл.

1
val musicElem = scala.xml.XML.loadFile("/tmp/music.xml")

Теперь мы можем работать с файлом, чтобы выбирать различные элементы или создавать объекты классов, определенных выше. Начнем только с песен. Мы можем игнорировать всех исполнителей и альбомы и копаться прямо с оператором \\ .

1
2
3
4
5
6
val songs = (musicElem \\ "song").map { song =>
  Song((song \ "@title").text, (song \ "@length").text)
}
 
scala> songs.map(_.time).sum
res1: Int = 11311

И мы можем пройти весь путь и создать объекты Artist, Album и Song, которые напрямую отражают данные, хранящиеся в файле XML.

01
02
03
04
05
06
07
08
09
10
11
12
val artists = (musicElem \ "artist").map { artist =>
  val name = (artist \ "@name").text
  val albums = (artist \ "album").map { album =>
    val title = (album \ "@title").text
    val description = (album \ "description").text
    val songList = (album \ "song").map { song =>
      Song((song \ "@title").text, (song \ "@length").text)
    }
    Album(title, songList, description)
  }
  Artist(name, albums)
}

Имея последовательность исполнителей в руках, мы можем делать такие вещи, как показ длины каждого альбома.

1
2
3
4
val albumLengths = artists.flatMap { artist =>
  artist.albums.map(album => (artist.name, album.title, album.length))
}
albumLengths.foreach(println)

Что дает следующий вывод.

1
2
3
4
(Radiohead,The King of Limbs,37:34)
(Radiohead,OK Computer,53:21)
(Portished,Dummy,48:46)
(Portished,Third,48:50)

Выделение объектов в XML

В дополнение к построению объектов из спецификаций XML (также называемых десериализацией и отменой маршалинга), часто необходимо маршалировать объекты, сконструированные в коде, в XML (или другие форматы). Использование XML-литералов на самом деле весьма удобно в этом отношении. Чтобы увидеть это, давайте начнем с первой песни первого альбома первого альбома ( Bloom , Radiohead).

1
2
scala> val bloom = artists(0).albums(0).songs(0)
bloom: Song = Song(Bloom,5:15)

Из этого можно построить элемент следующим образом.

1
2
scala> val bloomXml = <song title={bloom.title} length={bloom.length}/>
bloomXml: scala.xml.Elem = <song length="5:15" title="Bloom"></song>

Здесь следует отметить, что используется литерал XML, но когда мы хотим использовать значения из переменных, мы можем выйти из режима литерала с помощью фигурных скобок. Итак, {bloom.title} становится «Блум» и так далее. Напротив, это можно сделать через строку следующим образом.

1
2
3
4
5
scala> val bloomXmlString = "<song title=\""+bloom.title+"\" length=\""+bloom.length+"\"/>"
bloomXmlString: java.lang.String = <song title="Bloom" length="5:15"/>
 
scala> val bloomXmlFromString = scala.xml.XML.loadString(bloomXmlString)
bloomXmlFromString: scala.xml.Elem = <song length="5:15" title="Bloom"></song>

Таким образом, использование литералов немного более читабельно (хотя в Scala это затрудняет использование «<» в качестве оператора для многих случаев использования, что является одной из причин, по которым многие литералы XML считают не очень хорошая идея).

Мы можем создать целый XML для всех исполнителей и альбомов одним махом. Обратите внимание, что в экранированных скобках XML-литерала могут быть литералы XML, что позволяет работать следующим. Примечание : вам нужно использовать режим : paste в REPL, чтобы это работало.

01
02
03
04
05
06
07
08
09
10
11
12
13
val marshalled =
  <music>
  { artists.map { artist =>
    <artist name={artist.name}>
    { artist.albums.map { album =>
      <album title={album.title}>
      { album.songs.map(song => <song title={song.title} length={song.length}/>) }
      <description>{album.description}</description>
      </album>
    }}
    </artist>
  }}
</music>

Обратите внимание, что в этом случае синтаксис for-yield может быть немного более читабельным, поскольку он не требует дополнительных фигурных скобок.

01
02
03
04
05
06
07
08
09
10
11
12
13
val marshalledYield =
<music>
  { for (artist <- artists) yield
    <artist name={artist.name}>
    { for (album <- artist.albums) yield
      <album title={album.title}>
      { for (song <- album.songs) yield <song title={song.title} length={song.length}/> }
        <description>{album.description}</description>
      </album>
    }
    </artist>
  }
</music>

Конечно, вместо этого можно добавить метод toXml для каждого из классов Song, Album и Artist так, чтобы на верхнем уровне у вас было что-то вроде следующего.

1
val marshalledWithToXml = <music> { artists.map(_.toXml) } </music>

Это довольно распространенная стратегия. Тем не менее, обратите внимание, что проблема этого решения заключается в том, что оно создает очень тесную связь между логикой программы (например, тем, что могут делать песни, альбомы и исполнители) с другой, ортогональной логикой, такой как их сериализация. Чтобы увидеть способ устранения таких разных потребностей, ознакомьтесь с превосходным учебным пособием Дэна Розена по типам классов .

Вывод

Стандартный Scala XML API поставляется в комплекте с Scala, и он довольно удобен для некоторой базовой обработки XML. Однако это вызвало некоторую «полемику», так как многие считали, что основной язык не имеет бизнеса, обеспечивающего специализированную обработку для такого формата, как XML. Кроме того, есть некоторые проблемы эффективности. Anti-XML — это библиотека, которая стремится лучше обрабатывать XML (особенно в том, что касается более масштабируемой и более гибкой возможности программного редактирования XML). Насколько я понимаю, в будущем Anti-XML может стать своего рода официальной библиотекой обработки XML, а текущая стандартная библиотека XML постепенно сокращается. Тем не менее, многие из способов взаимодействия с XML-документом, показанными выше, похожи, поэтому знакомство со стандартным Scala XML API предоставляет основные концепции, которые вам понадобятся для других таких библиотек.

Справка: базовая обработка XML с помощью Scala от нашего партнера по JCG Джейсона Болдриджа в блоге Bcomposes .