Переход к узлам с использованием xpath в плоской структуре

StackOverflow https://stackoverflow.com/questions/614370

  •  03-07-2019
  •  | 
  •  

Вопрос

У меня есть XML-файл в плоской структуре. Мы не контролируем формат этого xml-файла, просто имеем дело с ним. Я переименовал поля, потому что они сильно зависят от предметной области и не имеют никакого значения для проблемы.

<attribute name="Title">Book A</attribute>
<attribute name="Code">1</attribute>
<attribute name="Author">
   <value>James Berry</value>
   <value>John Smith</value>
</attribute>
<attribute name="Title">Book B</attribute>
<attribute name="Code">2</attribute>
<attribute name="Title">Book C</attribute>
<attribute name="Code">3</attribute>
<attribute name="Author">
    <value>James Berry</value>
</attribute>

Ключевые моменты, на которые следует обратить внимание: файл не является особенно иерархическим. Книги ограничены вхождением элемента атрибута с именем = 'Заголовок'. Но узел атрибута name = 'Author' является необязательным.

Есть ли простое утверждение xpath, которое я могу использовать, чтобы найти авторов книги 'n'? Легко определить название книги 'n', но значение авторов является необязательным. И вы не можете просто взять следующего автора, потому что в случае с книгой 2 это даст автору для книги 3.

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

Это было полезно?

Решение

Мы хотим, чтобы " атрибут " элемент @name 'Author', следующий за атрибутом " attribute " элемент @name 'Title' со значением 'Book n', без каких-либо других " атрибутов " элемент @name 'Title' между ними (потому что, если они есть, автор является автором какой-то другой книги).

Другими словами, это означает, что нам нужен автор, для которого first предшествующий заголовок (тот, который он " принадлежит ") тот, который мы ищем:

//attribute[@name='Author']
[preceding-sibling::attribute[@name='Title'][1][contains(.,'Book N')]]

N = C = > находит <attribute name="Author"><value>James Berry</value></attribute>

N = B = > ничего не находит

Использование ключей и / или группирующих функций, доступных в XSLT 2.0, упростит эту задачу (и намного быстрее, если файл большой).

(парсер кода SO, кажется, думает, что «//» означает «комментарии», но в XPath это не так! Sigh.)

Другие советы

Ну, я использовал Elementtree для извлечения данных из вышеуказанного XML. Я сохранил этот XML в файле с именем foo.xml

from xml.etree.ElementTree import fromstring

def extract_data():
    """Returns list of dict of book and
    its authors."""

    f = open('foo.xml', 'r+')
    xml = f.read()
    elem = fromstring(xml)
    attribute_list = elem.findall('attribute')
    dic = {}
    lst = []

    for attribute in attribute_list:
        if attribute.attrib['name'] == 'Title':
            key = attribute.text
        if attribute.attrib['name'] == 'Author':
            for v in attribute.findall('value'):
                lst.append(v.text)
            value = lst
            lst = []
            dic[key] = value
    return dic

Когда вы запустите эту функцию, вы получите следующее:

{'Book A': ['James Berry', 'John Smith'], 'Book C': ['James Berry']}

Я надеюсь, что это то, что вы ищете. Если нет, то просто укажите немного больше. :)

Как отметил bambax в своем ответе, решение с использованием ключей XSLT более эффективно , особенно для больших XML-документов:

<xsl:stylesheet version="1.0"
 xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
 <xsl:output omit-xml-declaration="yes"/>
 <!--                                            -->
 <xsl:key name="kAuthByTitle" 
  match="attribute[@name='Author']"
  use="preceding-sibling::attribute[@name='Title'][1]"/>
 <!--                                            -->
    <xsl:template match="/">
      Book C Author:
      <xsl:copy-of select=
         "key('kAuthByTitle', 'Book C')"/>
  <!--                                            -->
         ====================
      Book B Author:
      <xsl:copy-of select=
         "key('kAuthByTitle', 'Book B')"/>
    </xsl:template>
</xsl:stylesheet>

Когда вышеуказанное преобразование применяется к этому XML-документу:

<t>
    <attribute name="Title">Book A</attribute>
    <attribute name="Code">1</attribute>
    <attribute name="Author">
        <value>James Berry</value>
        <value>John Smith</value>
    </attribute>
    <attribute name="Title">Book B</attribute>
    <attribute name="Code">2</attribute>
    <attribute name="Title">Book C</attribute>
    <attribute name="Code">3</attribute>
    <attribute name="Author">
        <value>James Berry</value>
    </attribute>
</t>

произведен правильный вывод:

  Book C Author:
  <attribute name="Author">
    <value>James Berry</value>
</attribute>

     ====================
  Book B Author:

Обратите внимание, что следует максимально избегать использования "//" аббревиатуры XPath , так как это обычно приводит к сканированию всего документа XML при каждой оценке выражения XPath.

Выберите все заголовки и примените шаблон

<xsl:template match="/">
  <xsl:apply-templates select="//attribute[@name='Title']"/>
</xsl:template>

В выходном заголовке шаблона проверьте, существует ли следующий заголовок. Если нет, выведите следующего автора. Если он существует, проверьте, совпадает ли следующий авторский узел следующей книги со следующим авторским узлом текущей книги. Если это так, это означает, что у текущей книги нет автора:

<xsl:template match="*">
   <book>
     <title><xsl:value-of select="."/></title> 
   <author>
   <xsl:if test="not(following::attribute[@name='Title']) or following::attribute[@name='Author'] != following::attribute[@name='Title']/following::attribute[@name='Author']">
   <xsl:value-of select="following::attribute[@name='Author']"/>
   </xsl:if>
   </author>
   </book>
</xsl:template>

Я не уверен, что вы действительно хотите туда пойти: самое простое, что я нашел, это пойти от автора, получить предыдущий заголовок, а затем убедиться, что первый автор или следующий заголовок действительно были заголовком. Гадкий!

/books/attribute[@name="Author"]
  [preceding-sibling::attribute[@name="Title" and string()="Book B"]
                               [following-sibling::attribute[ @name="Author" 
                                                             or @name="Title"
                                                            ]
                                 [1]
                                 [@name="Author"]
                               ]
  ][1]

(я добавил тег books , чтобы обернуть файл).

Я проверил это с помощью libxml2 BTW, используя xml_grep2 , но только на предоставленных вами образцах данных, поэтому дополнительные тесты приветствуются).

Лицензировано под: CC-BY-SA с атрибуция
Не связан с StackOverflow
scroll top