#python #pandas #xml
Вопрос:
У меня есть xml-документ, содержащий несколько временных рядов, полученных из веб-запроса к веб-PI Delft-FEWS. Пример реалистичного ответа этой конкретной веб-службы приведен в документации, но формат сводится к:
<?xml version="1.0" encoding="UTF-8"?>
<TimeSeries xmlns="http://www.wldelft.nl/fews/PI" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.wldelft.nl/fews/PI http://fews.wldelft.nl/schemas/version1.0/pi-schemas/pi_timeseries.xsd" version="1.23" xmlns:fs="http://www.wldelft.nl/fews/fs">
<timeZone>0.0</timeZone>
<series>
<header>
...
<locationId>63306260000</locationId>
<parameterId>T.obs.mean</parameterId>
...
</header>
<event date="2013-01-01" time="00:00:00" value="2" flag="0"/>
<event date="2013-02-01" time="00:00:00" value="1.7" flag="0"/>
<event date="2013-03-01" time="00:00:00" value="2.5" flag="2"/>
...
</series>
<series>
...
</series>
...
</TimeSeries>
Как загрузить этот XML-документ в pandas
фрейм данных для дальнейшей обработки?
Примечание: Я публикую этот вопрос, поскольку он возник для коллеги, и я подумал, что это может принести пользу другим, так как было несколько ошибок. Я скоро опубликую ответ.
Ответ №1:
Начиная с версии 1.3.0, pandas
имеет read_xml()
функцию, которая может быть полезна здесь. Кроме того, при импорте lxml
read_xml()
поддерживается более полная реализация XPath.
Решение:
from lxml import etree
from pandas import read_xml, DataFrame, MultiIndex, to_datetime
with open('time_series.xml') as f:
# load the FEWS Series xml document
tree = etree.parse(f)
# relabel the None namespace to 'fews', so we can refer to it in xpath expressions
nsmap = tree.getroot().nsmap.copy()
nsmap['fews'] = nsmap.pop(None)
# obtain the series in the document
series = tree.xpath('//fews:series', namespaces=nsmap)
# create and fill a new dataframe
df = DataFrame()
for s in series:
# location and parameter names are in the header on the series
loc_id = s.xpath('fews:header/fews:locationId/text()', namespaces=nsmap)[0]
parameter_id = s.xpath('fews:header/fews:parameterId/text()', namespaces=nsmap)[0]
# etree.tostring(s) is the literal xml for just the series,
# which read_xml accepts as source data for a new dataframe
series_df = read_xml(etree.tostring(s), xpath='//fews:series/fews:event', namespaces=nsmap)
# combine the date and time columns into a single datetime and set it as the index for the dataframe
series_df['datetime'] = to_datetime((series_df['date'] ' ' series_df['time']))
series_df.set_index(['datetime'], inplace=True)
# take the value and flag columns and add them to the dataframe being constructed
df[loc_id, parameter_id, 'value'] = series_df['value']
df[loc_id, parameter_id, 'flag'] = series_df['flag']
# turn the columns labeled with tuples into a proper multi-index on the dataframe
df.columns = MultiIndex.from_tuples(df.columns, names=['location', 'parameter', 'type'])
Это разбивает документ на отдельные серии, используется read_xml()
для их разбора на фреймы данных, столбцы которых добавляются в результат в виде столбцов с соответствующими метками. И, наконец, индекс превращается в мультииндекс для легкого доступа.
Несколько примеров о том, как получить доступ к данным во фрейме данных:
# now you can access df as needed, for example for all the values (not the flags):
print(df.xs('value', level='type', axis=1))
# ... or just the values and flags for '63306260000':
print(df.xs('63306260000', level='location', axis=1))
# ... or if you only need the for any of the parameters:
print(df.xs(('63306260000', 'value'), level=('location', 'type'), axis=1))
# ... of course, if you know exactly what you need, you can skip .xs:
print(df[('63306260000', 'T.obs.mean', 'value')])