Search code examples
rdplyrtidyversepurrrxml2

How to extract xml_attr and xml_text on different levels with xml2 and purrr?


I want to extract information from an XML file and transform it to a data frame.

The information is stored in nested nodes as XML text as well as XML attributes:

An example structure:

<xmlnode node-id = "Text about xmlnode">
    <xmlsubnode subnode-id = "123">
        <xmlsubsubnode>
            I want to extract this text
        </xmlsubsubnode>
        <xmlsubsubnode>
            I want to extract this text
        </xmlsubsubnode>    
        <xmlsubsubnode>
            I want to extract this text
        </xmlsubsubnode>    
        <xmlsubsubnode>
            I want to extract this text
        </xmlsubsubnode>    
    </xmlsubnode>
    <xmlsubnode subnode-id = "456">
        <xmlsubsubnode>
            I want to extract this text
        </xmlsubsubnode>
        <xmlsubsubnode>
            I want to extract this text
        </xmlsubsubnode>    
        <xmlsubsubnode>
            I want to extract this text
        </xmlsubsubnode>    
        <xmlsubsubnode>
            I want to extract this text
        </xmlsubsubnode>    
    </xmlsubnode>
</xmlnode>
<xmlnode node-id = "Text about xmlnode">
    <xmlsubnode subnode-id = "123">
        <xmlsubsubnode>
            I want to extract this text
        </xmlsubsubnode>
        <xmlsubsubnode>
            I want to extract this text
        </xmlsubsubnode>    
        <xmlsubsubnode>
            I want to extract this text
        </xmlsubsubnode>    
        <xmlsubsubnode>
            I want to extract this text
        </xmlsubsubnode>    
    </xmlsubnode>
    <xmlsubnode subnode-id = "456">
        <xmlsubsubnode>
            I want to extract this text
        </xmlsubsubnode>
        <xmlsubsubnode>
            I want to extract this text
        </xmlsubsubnode>    
        <xmlsubsubnode>
            I want to extract this text
        </xmlsubsubnode>    
        <xmlsubsubnode>
            I want to extract this text
        </xmlsubsubnode>    
    </xmlsubnode>
</xmlnode>

I want to get these information:

* node-id (attribute)
* subnode-id (attribute)
* text in `xmlsubnodenode` (text)

I need a longform dataframe like this:

node-id subnode-id  text
Text about xmlnode 1    123 I want to extract this text
Text about xmlnode 1    123 I want to extract this text
Text about xmlnode 1    123 I want to extract this text
Text about xmlnode 1    123 I want to extract this text
Text about xmlnode 1    456 I want to extract this text
Text about xmlnode 1    456 I want to extract this text
Text about xmlnode 1    456 I want to extract this text
Text about xmlnode 1    456 I want to extract this text
Text about xmlnode 2    123 I want to extract this text
Text about xmlnode 2    123 I want to extract this text
Text about xmlnode 2    123 I want to extract this text
Text about xmlnode 2    123 I want to extract this text
Text about xmlnode 2    456 I want to extract this text
Text about xmlnode 2    456 I want to extract this text
Text about xmlnode 2    456 I want to extract this text
Text about xmlnode 2    456 I want to extract this text

I tried to follow Jenny Bryans approach "How to tame XML with nested data frames and purrr", but it only works on the first level.

xml <- xml2::read_xml("input/example.xml")
rows <- 
  xml %>%
  xml_find_all("//xmlnode")
rows_df <- data_frame(row = seq_along(rows), nodeset = rows)
rows_df %>%
  mutate(node_id = nodeset %>% map(~ xml_attr(., "node-id"))) %>%
  select(row, node_id) %>%
  unnest()

Do you have ideas in order to get these information with purrr?


Solution

  • An approach without the need to unwind / adding rows to another dataframe: Create a dataframe containing a row for each subsubnode and use purrr together with xml2 to select and extract values of the xmlsubnode parent and xmlnode ancestor.

    Working sample:

    library(dplyr)
    library(xml2)
    library(purrr)
    library(tidyr)
    xml <- xml2::read_xml("input/example.xml")
    rows <- xml %>% xml_find_all("//xmlsubsubnode")
    rows_df <- data_frame(node = rows) %>%
      mutate(node_id = node %>% map(~ xml_find_first(., "ancestor::xmlnode")) %>% map(~ xml_attr(., "node-id"))) %>%
      mutate(subnode_id = node %>% map(~ xml_parent(.)) %>% map(~ xml_attr(., "subnode-id"))) %>%
      mutate(text = node %>% map(~ xml_text(.))) %>%
      select(-node)