将XML数据转换为数据框。

39

我正在尝试将一个XML文件转换为R中的数据框,这是一项作业任务。我已经尝试了许多不同的方法,并在互联网上寻找了一些想法,但都没有成功。以下是我目前的代码:

library(XML)
url <- 'http://www.ggobi.org/book/data/olive.xml'
doc <- xmlParse(myUrl)
root <- xmlRoot(doc)

dataFrame <- xmlSApply(xmltop, function(x) xmlSApply(x, xmlValue))
data.frame(t(dataFrame),row.names=NULL)

我得到的输出结果就像一个巨大的数字向量。我试图将数据组织成数据框,但我不知道如何正确地调整我的代码以获得那个结果。

5
如果只是因为Hadley经常推出高质量且实用的R语言包,我会选择使用xml2。这可能会更加顺利,并且有更好的文档支持。请查看自述文件底部以了解细微差别:https://github.com/hadley/xml2 - Jack Wasey
2
很好。根据GitHub页面和我的简短经验,xml2具有更简单的接口。R是由R核心开发人员编写的,并且充满了怪癖,而Hadley通常能够在简洁与强大之间取得平衡,并使一切变得干净整洁。 - Jack Wasey
4个回答

42

虽然xml2可能不像XML包那样冗长,但它没有内存泄漏问题,并且专注于数据提取。我使用的是trimws,它是 R 核心中一个非常新的补充。

library(xml2)

pg <- read_xml("http://www.ggobi.org/book/data/olive.xml")

# get all the <record>s
recs <- xml_find_all(pg, "//record")

# extract and clean all the columns
vals <- trimws(xml_text(recs))

# extract and clean (if needed) the area names
labs <- trimws(xml_attr(recs, "label"))

# mine the column names from the two variable descriptions
# this XPath construct lets us grab either the <categ…> or <real…> tags
# and then grabs the 'name' attribute of them
cols <- xml_attr(xml_find_all(pg, "//data/variables/*[self::categoricalvariable or
                                                      self::realvariable]"), "name")

# this converts each set of <record> columns to a data frame
# after first converting each row to numeric and assigning
# names to each column (making it easier to do the matrix to data frame conv)
dat <- do.call(rbind, lapply(strsplit(vals, "\ +"),
                                 function(x) {
                                   data.frame(rbind(setNames(as.numeric(x),cols)))
                                 }))

# then assign the area name column to the data frame
dat$area_name <- labs

head(dat)
##   region area palmitic palmitoleic stearic oleic linoleic linolenic
## 1      1    1     1075          75     226  7823      672        NA
## 2      1    1     1088          73     224  7709      781        31
## 3      1    1      911          54     246  8113      549        31
## 4      1    1      966          57     240  7952      619        50
## 5      1    1     1051          67     259  7771      672        50
## 6      1    1      911          49     268  7924      678        51
##   arachidic eicosenoic    area_name
## 1        60         29 North-Apulia
## 2        61         29 North-Apulia
## 3        63         29 North-Apulia
## 4        78         35 North-Apulia
## 5        80         46 North-Apulia
## 6        70         44 North-Apulia

更新

现在我可能会这样完成最后一部分:

library(tidyverse)

strsplit(vals, "[[:space:]]+") %>% 
  map_df(~as_data_frame(as.list(setNames(., cols)))) %>% 
  mutate(area_name=labs)

tidyvere 是一个打字错误。而且 as_data_frame 似乎不再是导出的对象了。 - userJT
1
错别字已经修正,但在做出“未导出”这样的陈述之前,您应该检查tibbledplyr两个包。 - hrbrmstr
@hrbrmstr 谢谢,看起来这是一个非常好的解决方案。我对 xml 完全不熟悉,所以你的代码是否也适用于其他 xml 文件?例如,每个 xml 中都有一个 record 吗?recs <- xml_find_all(pg, "//record") - ℕʘʘḆḽḘ
1
嗯,应该可以。请参考https://rud.is/rpubs/xml2power/ ,里面有一个类似的节点定位示例。 - hrbrmstr
@hrbrmstr,我正在尝试将您的解决方案适应一个相关但不同的问题。请参见此处https://dev59.com/mFcP5IYBdhLWcg3wi6gA 。当存在缺失节点时,您知道如何修改代码吗? - ℕʘʘḆḽḘ

12

以上回答都很好!对于将来的读者,如果你面对一个需要用R导入的复杂XML,请考虑使用XSLT(一种特殊目的的声明性编程语言,可以将XML内容转换为各种最终使用需求)。然后只需使用R的xmlToDataFrame()函数从XML包中进行操作。

不幸的是,R在所有操作系统上都没有可用的专用XSLT软件包。列出的SXLT似乎是一个Linux软件包,无法在Windows上使用。请参见未解决的SO问题herehere。我知道@hrbrmstr(上面)维护一个GitHub XSLT项目。尽管如此,几乎所有通用语言都维护XSLT处理器,包括Java、C#、Python、PHP、Perl和VB。

以下是开源Python路由,由于XML文档相当复杂,因此使用了两个XSLT(当然,XSLT大师可以将它们合并成一个,但我尝试了很多次都无法使其工作。 第一个XSLT(使用递归模板
<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
<xsl:output omit-xml-declaration="yes" indent="yes"/>
<xsl:strip-space elements="*"/>

<!-- Identity Transform -->    
<xsl:template match="node()|@*">
    <xsl:copy>
       <xsl:apply-templates select="node()|@*"/>
    </xsl:copy>
</xsl:template>

<xsl:template match="record/text()" name="tokenize">        
    <xsl:param name="text" select="."/>
    <xsl:param name="separator" select="' '"/>
    <xsl:choose>            
        <xsl:when test="not(contains($text, $separator))">                
            <data>
                <xsl:value-of select="normalize-space($text)"/>
            </data>              
        </xsl:when>
        <xsl:otherwise>
            <data>                  
                <xsl:value-of select="normalize-space(substring-before($text, $separator))"/>                  
            </data>                  
            <xsl:call-template name="tokenize">
                <xsl:with-param name="text" select="substring-after($text, $separator)"/>
            </xsl:call-template>                
        </xsl:otherwise>            
    </xsl:choose>        
</xsl:template>     

<xsl:template match="description|variables|categoricalvariable|realvariable">        
</xsl:template> 

第二个 XSLT

<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">

    <!-- Identity Transform -->    
    <xsl:template match="records">
        <xsl:copy>
           <xsl:apply-templates select="node()|@*"/>
        </xsl:copy>
    </xsl:template>

    <xsl:template match="record">
        <record>
            <area_name><xsl:value-of select="@label"/></area_name>
            <area><xsl:value-of select="data[1]"/></area>
            <region><xsl:value-of select="data[2]"/></region>
            <palmitic><xsl:value-of select="data[3]"/></palmitic>
            <palmitoleic><xsl:value-of select="data[4]"/></palmitoleic>
            <stearic><xsl:value-of select="data[5]"/></stearic>
            <oleic><xsl:value-of select="data[6]"/></oleic>
            <linoleic><xsl:value-of select="data[7]"/></linoleic>
            <linolenic><xsl:value-of select="data[8]"/></linolenic>
            <arachidic><xsl:value-of select="data[9]"/></arachidic>
            <eicosenoic><xsl:value-of select="data[10]"/></eicosenoic>
        </record>
   </xsl:template>         

</xsl:stylesheet>

Python(使用lxml模块)

import lxml.etree as ET

cd = os.path.dirname(os.path.abspath(__file__))

# FIRST TRANSFORMATION
dom = ET.parse('http://www.ggobi.org/book/data/olive.xml')
xslt = ET.parse(os.path.join(cd, 'Olive.xsl'))
transform = ET.XSLT(xslt)
newdom = transform(dom)

tree_out = ET.tostring(newdom, encoding='UTF-8', pretty_print=True,  xml_declaration=True)

xmlfile = open(os.path.join(cd, 'Olive_py.xml'),'wb')
xmlfile.write(tree_out)
xmlfile.close()    

# SECOND TRANSFORMATION
dom = ET.parse(os.path.join(cd, 'Olive_py.xml'))
xslt = ET.parse(os.path.join(cd, 'Olive2.xsl'))
transform = ET.XSLT(xslt)
newdom = transform(dom)

tree_out = ET.tostring(newdom, encoding='UTF-8', pretty_print=True,  xml_declaration=True)    

xmlfile = open(os.path.join(cd, 'Olive_py.xml'),'wb')
xmlfile.write(tree_out)
xmlfile.close()

R

library(XML)

# LOADING TRANSFORMED XML INTO R DATA FRAME
doc<-xmlParse("Olive_py.xml")
xmldf <- xmlToDataFrame(nodes = getNodeSet(doc, "//record"))
View(xmldf)

输出

area_name   area    region  palmitic    palmitoleic stearic oleic   linoleic    linolenic   arachidic   eicosenoic
North-Apulia 1      1       1075        75          226     7823        672          na                     60
North-Apulia 1      1       1088        73          224     7709        781          31          61         29
North-Apulia 1      1       911         54          246     8113        549          31          63         29
North-Apulia 1      1       966         57          240     7952        619          50          78         35
North-Apulia 1      1       1051        67          259     7771        672          50          80         46
   ...

需要对第一条记录进行轻微清理,因为在xml文档中"na"后面添加了一个额外的空格,导致arachidiceicosenoic向前移动了


1
现在有xslt包。我自己没有尝试过,但没有迹象表明它不是跨平台的。https://cran.r-project.org/web/packages/xslt/index.html。(似乎与https://github.com/hrbrmstr/xslt无关) - Aurèle
@Aurèle - 终于了!感谢您的留言。我将测试这个xml2扩展。 - Parfait

3
这是我想到的。它与橄榄油csv文件相匹配,该文件也在同一页上提供。他们将X显示为第一列名称,但我在xml中没有看到它,所以我手动添加了它。
最好将其分成几个部分,然后在我们获得所有部分后组装最终数据框。我们还可以使用XPath的[.XML*快捷方式和其他[[便利访问器函数。
library(XML)
url <- "http://www.ggobi.org/book/data/olive.xml"

## parse the xml document and get the top-level XML node
doc <- xmlParse(url)
top <- xmlRoot(doc)

## create the data frame
df <- cbind(
    ## get all the labels for the first column (groups)
    X = unlist(doc["//record//@label"], use.names = FALSE), 
    read.table(
        ## get all the records as a character vector
        text = xmlValue(top[["data"]][["records"]]), 
        ## get the column names from 'variables'
        col.names = xmlSApply(top[["data"]][["variables"]], xmlGetAttr, "name"), 
        ## assign the NA values to 'na' in the records
        na.strings = "na"
    )
)

## result
head(df)
#              X region area palmitic palmitoleic stearic oleic linoleic linolenic arachidic eicosenoic
# 1 North-Apulia      1    1     1075          75     226  7823      672        NA        60         29
# 2 North-Apulia      1    1     1088          73     224  7709      781        31        61         29
# 3 North-Apulia      1    1      911          54     246  8113      549        31        63         29
# 4 North-Apulia      1    1      966          57     240  7952      619        50        78         35
# 5 North-Apulia      1    1     1051          67     259  7771      672        50        80         46
# 6 North-Apulia      1    1      911          49     268  7924      678        51        70         44

## clean up
free(doc); rm(doc, top); gc()

2

对我来说,规范答案是:

doc<-xmlParse("Olive_py.xml")
xmldf <- xmlToDataFrame(nodes = getNodeSet(doc, "//record"))

这在@Parfait的答案中有所体现,但有些隐蔽。

然而,如果某些节点具有相同类型的多个子节点,则会失败。在这种情况下,提取函数将解决问题:

示例数据

<?xml version="1.0" encoding="UTF-8"?>
<testrun duration="25740" footerText="Generated by IntelliJ IDEA on 11/20/19, 9:21 PM" name="All in foo">
    <suite duration="274" locationUrl="java:suite://com.foo.bar.LoadBla" name="LoadBla"
           status="passed">
        <test duration="274" locationUrl="java:test://com.foo.bar.LoadBla/testReadWrite"
              name="LoadBla.testReadWrite" status="passed">
            <output type="stdout">ispsum ..</output>
        </test>
    </suite>
    <suite duration="9298" locationUrl="java:suite://com.foo.bar.TestFooSearch" name="TestFooSearch"
           status="passed">
        <test duration="7207" locationUrl="java:test://com.foo.bar.TestFooSearch/TestFooSearch"
              name="TestFooSearch.TestFooSearch" status="passed">
            <output type="stdout"/>
        </test>
        <test duration="2091" locationUrl="java:test://com.foo.bar.TestFooSearch/testSameSearch"
              name="TestFooSearch.testSameSearch" status="passed"/>
    </suite>
</testrun>

代码

require(XML)
require(tidyr)
require(dplyr)

node2df <- function(node){
    # (Optinonally) read out properties of  some optional child node
    outputNodes = getNodeSet(node, "output")
    stdout = if (length(outputNodes) > 0) xmlValue(outputNodes[[1]]) else NA

    vec_as_df <- function(namedVec, row_name="name", value_name="value"){
        data_frame(name = names(namedVec), value = namedVec) %>% set_names(row_name, value_name)
    }

    # Extract all node properties
    node %>%
        xmlAttrs %>%
        vec_as_df %>%
        pivot_wider(names_from = name, values_from = value) %>%
        mutate(stdout = stdout)
}

testResults = xmlParse(xmlFile) %>%
    getNodeSet("/testrun/suite/test", fun = node2df) %>%
    bind_rows()

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接