Как установить Pandas read_xml на определенный узел?

#python #pandas #xml #dataframe #xpath

Вопрос:

Я отправляю запрос в API и получаю ответ xml, который я хотел бы проанализировать в фрейм данных. Недавно я наткнулся на опцию pd.read_xml, и до сих пор у меня было несколько попыток, но, похоже, я не могу заставить ее работать должным образом.

Мой xml выглядит примерно так:

 <?xml version="1.0" encoding="UTF-8"?>
<html>
 <body>
  <searchretrieveresponse xmlns="http://www.loc.gov/zing/srw/">
   <version>
    1.1
   </version>
   <numberofrecords>
    1
   </numberofrecords>
   <records>
    <record>
     <recordschema>
      oai_dc
     </recordschema>
     <recordpacking>
      xml
     </recordpacking>
     <recorddata>
      <dc xmlns="http://www.openarchives.org/OAI/2.0/oai_dc/" xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:dnb="http://d-nb.de/standards/dnbterms" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
       <dc:title>
        [Erg.-H.]. Myst IV Revelation
       </dc:title>
       <dc:date>
        2004
       </dc:date>
       <dc:identifier xmlns:tel="http://krait.kb.nl/coop/tel/handbook/telterms.html" xsi:type="tel:ISBN">
        978-3-8272-9125-7 kart. : EUR 16.95, EUR 17.50 (AT)
       </dc:identifier>
       <dc:identifier xmlns:tel="http://krait.kb.nl/coop/tel/handbook/telterms.html" xsi:type="tel:ISBN">
        3-8272-9125-9 kart. : EUR 16.95, EUR 17.50 (AT)
       </dc:identifier>
       <dc:identifier xsi:type="dnb:IDN">
        97274004X
       </dc:identifier>
       <dc:format>
        32 S.
       </dc:format>
       <dc:relation>
        http://d-nb.info/973086416
       </dc:relation>
      </dc>
     </recorddata>
     <recordposition>
      1
     </recordposition>
    </record>
   </records>
   <nextrecordposition>
    2
   </nextrecordposition>
   <echoedsearchretrieverequest>
    <version>
     1.1
    </version>
    <query>
     978-3-8272-9125-7
    </query>
    <xquery xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:nil="true">
    </xquery>
    <recordschema>
     oai_dc
    </recordschema>
   </echoedsearchretrieverequest>
  </searchretrieveresponse>
 </body>
</html>
 

Это копия ответа API, вставленная после того, как я красиво напечатал ее в блокноте Jupyter.

Если я просто распечатаю ответ с помощью печати(r1.content) Я получаю следующее:

 b'<?xml version="1.0" encoding="UTF-8"?>n<searchRetrieveResponse xmlns="http://www.loc.gov/zing/srw/"><version>1.1</version><numberOfRecords>1</numberOfRecords><records><record><recordSchema>oai_dc</recordSchema><recordPacking>xml</recordPacking><recordData><dc xmlns:dnb="http://d-nb.de/standards/dnbterms" xmlns="http://www.openarchives.org/OAI/2.0/oai_dc/" xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">n  <dc:title>[Erg.-H.]. Myst IV Revelation</dc:title>n  <dc:date>2004</dc:date>n  <dc:identifier xmlns:tel="http://krait.kb.nl/coop/tel/handbook/telterms.html" xsi:type="tel:ISBN">978-3-8272-9125-7 kart. : EUR 16.95, EUR 17.50 (AT)</dc:identifier>n  <dc:identifier xmlns:tel="http://krait.kb.nl/coop/tel/handbook/telterms.html" xsi:type="tel:ISBN">3-8272-9125-9 kart. : EUR 16.95, EUR 17.50 (AT)</dc:identifier>n  <dc:identifier xsi:type="dnb:IDN">97274004X</dc:identifier>n  <dc:format>32 S.</dc:format>n  <dc:relation>http://d-nb.info/973086416</dc:relation>n</dc></recordData><recordPosition>1</recordPosition></record></records><nextRecordPosition>2</nextRecordPosition><echoedSearchRetrieveRequest><version>1.1</version><query>978-3-8272-9125-7</query><xQuery xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:nil="true"/><recordSchema>oai_dc</recordSchema></echoedSearchRetrieveRequest></searchRetrieveResponse>'
 

У меня был некоторый успех со следующим кодом:

 df = pd.read_xml(r1.content, namespaces={"xmlns":"http://www.openarchives.org/OAI/2.0/oai_dc/", 
                                                               "dc": "http://purl.org/dc/elements/1.1/", 
                                                               "dnb": "http://d-nb.de/standards/dnbterms", 
                                                               "xsi": "http://www.w3.org/2001/XMLSchema-instance"})
 

Однако, похоже, это учитывает только верхний уровень, поскольку я получаю такой фрейм данных, как этот:

     version     numberOfRecords     record  nextRecordPosition  query   xQuery  recordSchema
0   1.1         NaN                 NaN     NaN                 None    NaN     None
1   NaN         15315.0             NaN     NaN                 None    NaN     None
2   NaN         NaN                 NaN     NaN                 None    NaN     None
3   NaN         NaN                 NaN     11.0                None    NaN     None
4   1.1         NaN                 NaN     NaN                 Händel  NaN     oai_dc
 

Поскольку меня интересуют фактические «записи», которые возвращает API, я попробовал следующее:

 df = pd.read_xml(r1.content, xpath='.//records', namespaces={"xmlns":"http://www.openarchives.org/OAI/2.0/oai_dc/", 
                                                               "dc": "http://purl.org/dc/elements/1.1/", 
                                                               "dnb": "http://d-nb.de/standards/dnbterms", 
                                                               "xsi": "http://www.w3.org/2001/XMLSchema-instance"})
 

но я получаю ошибку: ValueError: xpath does not return any nodes. Be sure row level nodes are in xpath. If document uses namespaces denoted with xmlns, be sure to define namespaces and use them in xpath.

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

     dc:title                       dc:date     dc:identifier        
0   [Erg.-H.]. Myst IV Revelation  2004        978-3-8272-9125-7 kart. : EUR 16.95, EUR 17.50 (AT)
 

Насколько я знаю, я добавил все пространства имен, а также попытался настроить xpath для поиска «записи» вместо «записей» или даже «dc:заголовок», но до сих пор я всегда получаю сообщение об ошибке, как только добавляю атрибут xpath. Что я делаю не так? Я подозреваю, что это как-то связано с правильным пространством имен, но не могу понять, что это такое… любая помощь очень ценится!

Комментарии:

1. Пример xml в вашем вопросе не очень хорошо сформирован. Можете ли вы отредактировать вопрос и убедиться, что у вас есть упрощенный, хорошо сформированный образец xml и ожидаемые результаты этого образца? Это значительно облегчит попытку ответить.

2. Здравствуйте и спасибо, теперь я включил в текст объяснение xml-ответа и немного отредактировал его — он действительно должен быть хорошо сформирован, но я скопировал не все, а только начало.

3. Ещё нет. Во-первых, вам все равно нужно добавить образец требуемого вывода. Во-вторых, вы должны запустить свой образец xml через XML-валидатор ( например, этот ) и убедиться, что он не содержит ошибок.

4. Я посмотрю необходимые выходные данные, но я ничего не могу сделать с самим xml, потому что это то, что я получаю от API?

5. «это то, что я получаю от API» — очень маловероятно; проверьте r1 еще раз.

Ответ №1:

Как read_xml() говорится в документации:

Примечание: Анализатор etree поддерживает ограниченное количество выражений XPath. Для более сложных XPath используйте lxml, который требует установки.

К сожалению, мне кажется, что это одна из тех ситуаций, когда требуется «более сложный» xpath… Итак, давайте использовать lxml:

 from lxml import etree
import pandas as pd

rec = """[your xml response above]"""
doc = etree.XML(rec.encode())

#now to deal with those pesky namespaces
ns = {"x":"http://www.loc.gov/zing/srw/", "y":"http://purl.org/dc/elements/1.1/","z":"http://www.w3.org/2001/XMLSchema-instance"}

#we can now look for the data
rows = []
targets = doc.xpath('//x:record',namespaces=ns)
for target in targets:
    title = target.xpath('//y:title',namespaces=ns)
    date = target.xpath('//y:date',namespaces=ns)
    identifier = target.xpath('//y:identifier[@z:type="tel:ISBN"][1]',namespaces=ns)    
    rows.append([title[0].text.strip(),date[0].text.strip(),identifier[0].text.strip()])

#and, finally, create the dataframe
columns = ['dc:title','dc:date','dc:identifier']
pd.DataFrame(rows,columns=columns)
 

Выход:

      dc:title                       dc:date     dc:identifier
0   [Erg.-H.]. Myst IV Revelation   2004    978-3-8272-9125-7 kart. : EUR 16.95, EUR 17.50...
 

Комментарии:

1. Большое вам спасибо за ваш ответ — итак, чтобы уточнить: вы думаете, что невозможно будет использовать pd.read_xml с этой конкретной xml-структурой? Я надеялся, что это будет более простой вариант, чем использовать цикл и извлекать заголовок, дату и т. Д. Отдельно.

2. @ssp24 — Боюсь, что да. На самом деле я довольно долго пытался это сделать, read_xml() но в конце концов сдался. Я видел подобные проблемы с глубоко вложенным html и read_html() . Может быть, кто-нибудь поумнее или более опытный сможет придумать способ. Но, как я заметил, даже документация говорит вам, что иногда вам приходится сдаваться…

3. Большое вам спасибо за попытку! Я только что нашел альтернативу с помощью xmltodict, которая помогает создавать более привлекательный фрейм данных, чем read_xml, но все еще на верхнем уровне узлов. Есть ли способ удалить определенный «уровень» узлов из xml?

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