Эффективно преобразовывать XML в фрейм данных

Мне нужно преобразовать некоторый ванильный xml в фрейм данных. XML представляет собой простое представление прямоугольных данных (см. пример ниже). Я могу довольно просто добиться этого в R с помощью xml2 и пары циклов for. Однако я уверен, что есть способ намного лучше/быстрее (муррр?). XML-файлы, с которыми мне в конечном итоге придется работать, очень велики, поэтому предпочтительны более эффективные методы. Буду признателен за любые советы от сообщества.

library(tidyverse)
library(xml2)

demo_xml <- 
"<DEMO>
  <EPISODE>
    <item1>A</item1>
    <item2>1</item2>
  </EPISODE>
  <EPISODE>
    <item1>B</item1>
    <item2>2</item2>
  </EPISODE>
</DEMO>"


dx <- read_xml(demo_xml)

episodes <- xml_find_all(dx, xpath = "//EPISODE")
dx_names <- xml_name(xml_children(episodes[1]))

df <- data.frame()

for(i in seq_along(episodes)) {
  for(j in seq_along(dx_names)) {
    df[i, j] <- xml_text(xml_find_all(episodes[i], xpath = dx_names[j]))
  }
}

names(df) <- dx_names
df
#>   item1 item2
#> 1     A     1
#> 2     B     2

Создано 19 сентября 2019 г. с помощью пакета reprex (v0.3.0)

Заранее спасибо.


person DocEd    schedule 19.09.2019    source источник


Ответы (2)


Это общее решение, которое обрабатывает различное количество различных подузлов для каждого родительского узла. У каждого узла Episode могут быть разные подузлы.
Эта стратегия анализирует дочерние узлы, определяя имя и значения каждого подузла. Затем он преобразует этот список в более длинный фрейм данных стиля, а затем изменяет его форму в желаемом более широком стиле:

library(tidyr)
library(xml2)

demo_xml <- 
  "<DEMO>
  <EPISODE>
    <item1>A</item1>
    <item2>1</item2>
  </EPISODE>
  <EPISODE>
    <item1>B</item1>
    <item2>2</item2>
  </EPISODE>
</DEMO>"

dx <- read_xml(demo_xml)

#find all episodes
episodes <- xml_find_all(dx, xpath = "//EPISODE")
#extract the node names and values from all of the episodes
nodenames<-xml_name(xml_children(episodes))
contents<-trimws(xml_text(xml_children(episodes)))

#Idenitify the number of subnodes under each episodes for labeling
IDlist<-rep(1:length(episodes), sapply(episodes, length))

#make a long dataframe
df<-data.frame(episodes=IDlist, nodenames, contents, stringsAsFactors = FALSE)

#make the dataframe wide, Remove unused blank nodes:
answer <- spread(df[df$contents!="",], nodenames, contents)

#tidyr 1.0.0 version
#answer <- pivot_wider(df, names_from = nodenames, values_from = contents)


# A tibble: 2 x 3
  episodes item1 item2
     <int> <chr> <chr>
1        1 A     1    
2        2 B     2  
person Dave2e    schedule 19.09.2019
comment
Большое спасибо. Это более или менее удар по тому, что я искал! - person DocEd; 20.09.2019

Это может быть вариант без использования цикла for,

episodes <- xml_find_all(dx, xpath = "//EPISODE") %>% xml_attr("item1")
dx_names <- xml_name(xml_children(episodes[1]))

# You can get all values between the tags by xml_text()
values <- xml_children(episodes) %>% xml_text()


as.data.frame(matrix(values,
            ncol=length(dx_names),
            dimnames =list(seq(dx_names),dx_names),byrow=TRUE))

дает,

  item1 item2
1     A     1
2     B     2

Обратите внимание, что вам может потребоваться изменить столбец Item2 на числовой на as.numeric(), поскольку в этом решении он был назначен как коэффициент.

person maydin    schedule 19.09.2019