Python / R: generar el dataframe a partir de XML cuando no todos los nodos contienen todas las variables?

Considere el siguiente ejemplo XML

 library(xml2) myxml <- read_xml('    John   tennis   golf   python     Robert   R    ') 

Aquí me gustaría obtener un dataframe (R o Pandas) de este XML que contiene el name y el hobby las columnas.

Sin embargo, como puede ver, hay un problema de alineación porque falta hobby en el segundo nodo y John tiene dos pasatiempos.

en R, sé cómo extraer valores específicos de uno en uno, por ejemplo, utilizando xml2 siguiente manera:

 myxml%>% xml_find_all("//name") %>% xml_text() myxml%>% xml_find_all("//hobby") %>% xml_text() 

¿Pero cómo puedo alinear estos datos correctamente en un dataframe? Es decir, ¿cómo puedo obtener un dataframe de la siguiente manera (observe cómo me uno con las dos aficiones de John)?

 # A tibble: 2 × 3 name hobby skill    1 John tennis|golf python 2 Robert  R 

En R, preferiría una solución usando xml2 y dplyr . En Python, quiero terminar con un dataframe de Pandas. Además, en mi xml hay muchas más variables que quiero analizar. Me gustaría una solución que le permita al usuario analizar variables adicionales sin ensuciar demasiado con el código.

¡Gracias!

EDIT: gracias a todos por estas grandes soluciones. Todos fueron muy agradables, con muchos detalles y fue difícil elegir el mejor. ¡Gracias de nuevo!

pandas

 import pandas as pd from collections import defaultdict import xml.etree.ElementTree as ET xml_txt = """   John   tennis   golf   python     Robert   R   """ etree = ET.fromstring(xml_txt) def obs2series(o): d = defaultdict(list) [d[c.tag].append(c.text.strip()) for c in o.getchildren()]; return pd.Series(d).str.join('|') pd.DataFrame([obs2series(o) for o in etree.findall('obs')]) hobby name skill 0 tennis|golf John python 1 NaN Robert R 

Cómo funciona

  • construir un árbol de elementos de la cadena. De lo contrario, haga algo como et = ET.parse('my_data.xml')
  • etree.findall('obs') devuelve una lista de elementos dentro de la estructura xml que son tags 'obs'
  • Paso cada uno de estos a un pd.Series constructor obs2series
  • Dentro de obs2series , obs2series todos los nodos secundarios en un elemento 'obs' .
  • defaultdict toma como valor predeterminado una list lo que significa que puedo agregar un valor incluso si la clave no se ha visto antes.
  • Acabo con un diccionario de listas. Le paso esto a pd.Series para obtener una serie de listas.
  • Usando pd.Series.str.join('|') lo convierto a una serie de cadenas que quería.
  • Mi lista de comprensión al principio que se repitió sobre las observaciones ahora es una lista de series y está lista para pasar al constructor pd.DataFrame .

Una solución general de R que no requiere codificar las variables.
Usando xml2 y purrr de tidyverse:

 library(xml2) library(purrr) myxml %>% xml_find_all('obs') %>% # Enter each obs and return a df map_df(~{ # Scan names node_names <- .x %>% xml_children() %>% xml_name() %>% unique() # Remember ob ob <- .x # Enter each node map(node_names, ~{ # Find similar nodes node <- xml_find_all(ob, .x) %>% xml_text(trim = TRUE) %>% paste0(collapse = '|') %>% 'names<-'(.x) # ^ we need to name the element to # overwrite it with its 'sibilings' }) %>% # Return an 'ob' vector flatten() }) #> # A tibble: 2 × 3 #> name hobby skill #>    #> 1 John tennis|golf python #> 2 Robert  R 

Que hace:

  1. ‘Ingresa’ cada obs , encuentra y almacena los nombres de los nodos en esa observación.
  2. Para cada nodo, busque todos los nodos similares en el obs , contraiga y almacene en una lista.
  3. Aplana la lista, sobrescribiendo elementos con el mismo nombre.
  4. rbind (implícito en map_df() ) cada lista ‘aplanada’ en el data.frame resultante.

Datos:

 myxml <- read_xml('    John   tennis   golf   python     Robert   R    ') 

XML

Cree una función que pueda manejar nodos faltantes o múltiples, y luego aplique eso a los nodos obs . xmlGetAttr la columna id para que pueda ver cómo usar xmlGetAttr también (use "." Para el nodo obs y el encabezado "." En otros nodos para que sea relativo al nodo actual en el conjunto).

 xpath2 <-function(x, ...){ y <- xpathSApply(x, ...) ifelse(length(y) == 0, NA, paste(trimws(y), collapse=", ")) } obs <- getNodeSet(doc, "//obs") data.frame( id = sapply(obs, xpath2, ".", xmlGetAttr, "ID"), name = sapply(obs, xpath2, ".//name", xmlValue), hobbies = sapply(obs, xpath2, ".//hobby", xmlValue), skill = sapply(obs, xpath2, ".//skill", xmlValue)) id name hobbies skill 1 a John tennis, golf python 2 b Robert  R 

xml2

No uso xml2 muy a menudo, pero tal vez obtenga los nodos obs y luego aplique xml_find_all si hay tags duplicadas en lugar de usar xml_find_first .

 obs <- xml_find_all(myxml, "//obs") lapply(obs, xml_find_all, ".//hobby") data_frame( name = xml_find_first(obs, ".//name") %>% xml_text(trim=TRUE), hobbies = sapply(obs, function(x) paste(xml_text( xml_find_all(x, ".//hobby"), trim=TRUE), collapse=", " ) ), skill = xml_find_first(obs, ".//skill") %>% xml_text(trim=TRUE) ) # A tibble: 2 x 3 name hobbies skill    1 John tennis, golf python 2 Robert R 

medline17n0853.xml ambos métodos utilizando el archivo medline17n0853.xml en el NCBI ftp . Este es un archivo de 280 MB con 30,000 nodos PubmedArticle, y el paquete XML tomó 102 segundos para analizar las ID de pubmed, revistas y combinar múltiples tipos de publicaciones. El código xml2 se ejecutó durante 30 minutos y luego lo eliminé, por lo que puede que no sea la mejor solución.

En R, probablemente usaría

 library(XML) lst <- xmlToList(xmlParse(myxml)[['/data']]) (df <- data.frame(t(sapply(lst, function(x) { c(x['name'], hobby=paste0(x[which(names(x)=='hobby')], collapse="|")) }))) ) # name hobby # 1 John tennis | golf # 2 Robert 

y tal vez haga un poco de pulido usando df[df==""] <- NA y trimws() para eliminar los espacios en blanco.


O:

 library(xml2) library(dplyr) `%

%` <- function (x, y) if (length(x)==0) y else x (df <- data_frame( names = myxml %>% xml_find_all(“/data/obs/name”) %>% xml_text(trim=TRUE), hobbies = myxml %>% xml_find_all(“/data/obs”) %>% lapply(function(x) xml_text(xml_find_all(x, “hobby”), T) %

% NA_character_) )) # # A tibble: 2 × 2 # names hobbies # # 1 John # 2 Robert