Convert XML to dataframe

Viewed 88

I'm aware that this has been an issue many times. However, I don't succeed in converting my xml properly.

This is a data extract of my data. There are several thousand more cases.

 <?xml version="1.0" encoding="UTF-8" ?> 
  <delivery xmlns="http://www.ech.ch/xmlns/eCH-0099/2" xmlns:ns5="http://www.ech.ch/xmlns/eCH-0058/4" xmlns:ns2="http://www.ech.ch/xmlns/eCH-0044/4" xmlns:ns1="http://www.ech.ch/xmlns/eCH-0011/8" xmlns:ns3="http://www.ech.ch/xmlns/eCH-0007/5" xmlns:ns0="http://www.ech.ch/xmlns/eCH-0008/3" xmlns:ns4="http://www.ech.ch/xmlns/eCH-0010/5" version="2.1">
  <deliveryHeader>
  <ns5:senderId>5224</ns5:senderId> 
  <ns5:declarationLocalReference>TSKWE</ns5:declarationLocalReference> 
  <ns5:recipientId>834734</ns5:recipientId> 
  <ns5:messageId>kd-3465-3456-df</ns5:messageId> 
  <ns5:ourBusinessReferenceId>kdsdf-9834</ns5:ourBusinessReferenceId> 
  <ns5:messageType>99</ns5:messageType> 
  <ns5:sendingApplication>
  <ns5:manufacturer>Test AG</ns5:manufacturer> 
  <ns5:product>Test</ns5:product> 
  <ns5:productVersion>6.2478.3285</ns5:productVersion> 
  </ns5:sendingApplication>
  <ns5:messageDate>2021-04-14T09:42:23.194+02:00</ns5:messageDate> 
  <ns5:eventDate>2021-03-31</ns5:eventDate> 
  <ns5:action>1</ns5:action> 
  <ns5:testDeliveryFlag>false</ns5:testDeliveryFlag> 
  <ns5:responseExpected>true</ns5:responseExpected> 
  <ns5:businessCaseClosed>false</ns5:businessCaseClosed> 
  </deliveryHeader>
  <reportedPerson>
  <baseData>
  <ns1:person>
  <ns1:personIdentification>
  <ns2:vn>258698554895</ns2:vn> 
  <ns2:localPersonId>
  <ns2:personIdCategory>NA.8934</ns2:personIdCategory> 
  <ns2:personId>578789</ns2:personId> 
  </ns2:localPersonId>
  <ns2:otherPersonId>
  <ns2:personIdCategory>KI.4785</ns2:personIdCategory> 
  <ns2:personId>5787893</ns2:personId> 
  </ns2:otherPersonId>
  <ns2:otherPersonId>
  <ns2:personIdCategory>KI.4755</ns2:personIdCategory> 
  <ns2:personId>57878987</ns2:personId> 
  </ns2:otherPersonId>
  <ns2:officialName>Muster</ns2:officialName> 
  <ns2:firstName>Karl</ns2:firstName> 
  <ns2:originalName>Muster</ns2:originalName> 
  <ns2:sex>1</ns2:sex> 
  <ns2:dateOfBirth>
  <ns2:yearMonthDay>1901-01-01</ns2:yearMonthDay> 
  </ns2:dateOfBirth>
  </ns1:personIdentification>
  <ns1:nameData>
  <ns1:officialName>Muster</ns1:officialName> 
  <ns1:firstName>Karl</ns1:firstName> 
  <ns1:originalName>Muster</ns1:originalName> 
  <ns1:callName>Karl</ns1:callName> 
  </ns1:nameData>
  <ns1:birthData>
  <ns1:dateOfBirth>
  <ns2:yearMonthDay>1901-01-01</ns2:yearMonthDay> 
  </ns1:dateOfBirth>
  <ns1:placeOfBirth>
  <ns1:swissTown>
  <ns3:municipalityId>657888</ns3:municipalityId> 
  <ns3:municipalityName>Test</ns3:municipalityName> 
  <ns3:cantonAbbreviation>FP</ns3:cantonAbbreviation> 
  <ns3:historyMunicipalityId>45778</ns3:historyMunicipalityId> 
  </ns1:swissTown>
  </ns1:placeOfBirth>
  <ns1:sex>1</ns1:sex> 
  </ns1:birthData>
  <ns1:religionData>
  <ns1:religion>22</ns1:religion> 
  <ns1:religionValidFrom>1901-01-01</ns1:religionValidFrom> 
  </ns1:religionData>
  <ns1:maritalData>
  <ns1:maritalStatus>9</ns1:maritalStatus> 
  <ns1:dateOfMaritalStatus>1901-01-01</ns1:dateOfMaritalStatus> 
  </ns1:maritalData>
  <ns1:nationalityData>
  <ns1:nationalityStatus>27</ns1:nationalityStatus> 
  <ns1:countryInfo>
  <ns1:country>
  <ns0:countryId>978754</ns0:countryId> 
  <ns0:countryIdISO2>ZZ</ns0:countryIdISO2> 
  <ns0:countryNameShort>ZUZI</ns0:countryNameShort> 
  </ns1:country>
  </ns1:countryInfo>
  </ns1:nationalityData>
  <ns1:languageOfCorrespondance>TE</ns1:languageOfCorrespondance> 
  <ns1:placeOfOrigin>
  <ns1:originName>Lachmanan</ns1:originName> 
  <ns1:canton>ZI</ns1:canton> 
  <ns1:placeOfOriginId>45345</ns1:placeOfOriginId> 
  </ns1:placeOfOrigin>
  </ns1:person>
  <ns1:hasMainResidence>
  <ns1:mainResidence>
  <ns1:reportingMunicipality>
  <ns3:municipalityId>567567</ns3:municipalityId> 
  <ns3:municipalityName>Tschicki</ns3:municipalityName> 
  <ns3:cantonAbbreviation>ZI</ns3:cantonAbbreviation> 
  <ns3:historyMunicipalityId>246768</ns3:historyMunicipalityId> 
  </ns1:reportingMunicipality>
  <ns1:arrivalDate>1901-01-01</ns1:arrivalDate> 
  <ns1:comesFrom>
  <ns1:swissTown>
  <ns3:municipalityId></ns3:municipalityId> 
  <ns3:municipalityName></ns3:municipalityName> 
  <ns3:cantonAbbreviation></ns3:cantonAbbreviation> 
  <ns3:historyMunicipalityId></ns3:historyMunicipalityId> 
  </ns1:swissTown>
  </ns1:comesFrom>
  <ns1:dwellingAddress>
  <ns1:EGID>457575454545</ns1:EGID> 
  <ns1:EWID>7</ns1:EWID> 
  <ns1:householdID>54678967864/4</ns1:householdID> 
  <ns1:address>
  <ns4:street>Isadorastr.</ns4:street> 
  <ns4:houseNumber>6</ns4:houseNumber> 
  <ns4:town>Tschidi</ns4:town> 
  <ns4:swissZipCode>899663</ns4:swissZipCode> 
  <ns4:swissZipCodeAddOn>00</ns4:swissZipCodeAddOn> 
  <ns4:country>ZI</ns4:country> 
  </ns1:address>
  <ns1:typeOfHousehold>787</ns1:typeOfHousehold> 
  <ns1:movingDate>1908-04-17</ns1:movingDate> 
  </ns1:dwellingAddress>
  </ns1:mainResidence>
  </ns1:hasMainResidence>
  </baseData>
  </reportedPerson>
 </delivery>

I would like to have a dataframe where each person is mapped in one row. The key element is "reportedPerson".

I manage to get each person in a row, but the values are all in one column without any separator.

doc <- xmlParse('/path/to/Quelle/Response.xml')
nsDefs <- xmlNamespaceDefinitions(doc)
ns <- structure(sapply(nsDefs, function(x) x$uri), names = names(nsDefs))
nsDefs[[1]][["id"]] <- "x"
names(ns)[1] <- "x"

xpathSApply(doc, "//x:reportedPerson", namespaces=ns)

reportedPerson <- getNodeSet(doc, "//x:reportedPerson", namespaces=ns, fun=xmlToList)
df <- data.frame(do.call(rbind, reportedPerson))
head(df)

Do you have any ideas on how to get the multitude of variables into individual columns?

1 Answers

Here is a solution using the xml2 package. The strategy is to find the reported persons nodes and the parse out all of the subnodes. There are few duplicated node names and I attempted to reduce the number of conflicts, see the comments for more details. You may have to use the node's paths as the dataframe' column names.

library(dplyr)
library(xml2)

#identify namespace
ns<-xml_ns(doc)

#strip  namespace for ease of handling
#xml_ns_strip(doc)

#find the reportedPerson Nodes
reportedPersons <- xml_find_all(doc, ".//d1:reportedPerson")

dfs<- lapply(reportedPersons, function(reportedPerson) {
  
  #find all the children nodes
  allchildren <- reportedPerson %>% xml_find_all( './/*') 
  
  #count the number of childern in each node
  branches <-reportedPerson %>% xml_find_all( './/*') %>% xml_length(only_elements = TRUE)
  
  #filter to keep the lowest level - nodes with values - no children
  leafs <- allchildren[which(branches==0)]
  
  #get valuse and names of the leafs
  values <- leafs %>% xml_text()
  names <- leafs %>% xml_name() #xml_name(ns=ns)
  #get names of the parents - still results in a few duplicates
  parentNames <-  sapply(leafs, function(leaf){xml_parent(leaf) %>% xml_name()})
 
  #put into a dataframe 
  df <- as.data.frame(t(values))
  names(df) = paste(parentNames, names, sep="_")
  #there are still a few duplicates could use full path names to avoid dulicates
  # names(df) = xml_path(leafs)
  
  df
})

answer <- bind_rows(dfs)
Related