将大而复杂的XML文件解析为data.frame

时间:2017-05-30 09:04:16

标签: r xml performance xml-parsing xml2

所以,我有大量的XML文件和很多报告。我在下面创建了数据示例,以大致显示xml的大小及其结构:

define measure Query_NewSales[isKit] = SUMX(Query_NewSales, IF(NOT (
  ISBLANK (
    LOOKUPVALUE(Query_Kits[KitName],Query_Kits[KitName],Query_NewSales[Item])
  )), 1, 0))
measure Query_NewSales[AvgCPI] = SUMX(Query_NewSales, IF(Query_NewSales[isKit],
    -- lookup to DAX_KitsPricedMissingRemoved
   ,-- lookup to DAXTable_NewPurchasesGroupedByItem
))
EVALUATE ADDCOLUMNS(Query_NewSales, "IsKit", Query_NewSales["IsKit"], ...)     

我想将此数据转换为data.frame,但XML的结构并不简单。以前使用XML我创建了循环,每个报告将其子节点转换为data.frame,但是这里(在此数据中)子节点数大于30(并未将所有这些都放在示例中),并且结构不同(列表节点在XML中甚至可以出现2级深度)。

所以我几乎没有问题:

1)我确信循环报告不是处理此问题的最佳方法。我该如何处理这个问题?

2)我可以以某种方式提取一个报告的所有数据两行data.frame(递归可能)吗?

3)或者我可以为XML的每个列表对象自动创建单独的data.frames吗?

非常感谢任何帮助。

更新

结果示例如下所示:

x <- "<Report><Agreements><AgreementList /></Agreements><CIP><RecordList><Record><Date>2017-05-26T00:00:00</Date><Grade>2</Grade><ReasonsList><Reason><Code>R</Code><Description>local</Description></Reason></ReasonsList><Score>xxx</Score></Record><Record><Date>2017-04-30T00:00:00</Date><Grade>2</Grade><ReasonsList><Reason><Code>R</Code><Description/></Reason></ReasonsList><Score>xyx</Score></Record></RecordList></CIP><Individual><Contact><Email/></Contact><General><FirstName>MM</FirstName></General></Individual><Inquiries><InquiryList><Inquiry><DateOfInquiry>2017-03-19</DateOfInquiry><Reason>cc</Reason></Inquiry><Inquiry><DateOfInquiry>2016-10-14</DateOfInquiry><Reason>er</Reason></Inquiry></InquiryList><Summary><NumberOfInquiries>2</NumberOfInquiries></Summary></Inquiries></Report>"

x <- paste(rep(x, 1.5e+5), collapse = "")
x <- paste0("<R>", x, "</R>")
require(XML)
p <- xmlParse(x)
p <- xmlRoot(p)
p[[1]]

,但正如我之前提到的,子列表也可以在单独的表中。

2 个答案:

答案 0 :(得分:7)

L=xmlToList(x)
str(data.frame(t(unlist(L)), stringsAsFactors=FALSE))
# 'data.frame': 1 obs. of  15 variables:
#  $ CIP.RecordList.Record.Date                          : chr "2017-05-26T00:00:00"
#  $ CIP.RecordList.Record.Grade                         : chr "2"
#  $ CIP.RecordList.Record.ReasonsList.Reason.Code       : chr "R"
#  $ CIP.RecordList.Record.ReasonsList.Reason.Description: chr "local"
#  $ CIP.RecordList.Record.Score                         : chr "xxx"
#  $ CIP.RecordList.Record.Date.1                        : chr "2017-04-30T00:00:00"
#  $ CIP.RecordList.Record.Grade.1                       : chr "2"
#  $ CIP.RecordList.Record.ReasonsList.Reason.Code.1     : chr "R"
#  $ CIP.RecordList.Record.Score.1                       : chr "xyx"
#  $ Individual.General.FirstName                        : chr "MM"
#  $ Inquiries.InquiryList.Inquiry.DateOfInquiry         : chr "2017-03-19"
#  $ Inquiries.InquiryList.Inquiry.Reason                : chr "cc"
#  $ Inquiries.InquiryList.Inquiry.DateOfInquiry.1       : chr "2016-10-14"
#  $ Inquiries.InquiryList.Inquiry.Reason.1              : chr "er"
#  $ Inquiries.Summary.NumberOfInquiries                 : chr "2"

如果要将具有合适表示形式的字符串转换为数字,假设df是上面的数据框:

data.frame(t(lapply(df, function(x) 
               ifelse(is.na(y<-suppressWarnings(as.numeric(x))), x, y))))

不会转换没有数字表示的字符串。

更新

动机

A)在一些评论中,OP增加了对执行速度的进一步请求,这对于一次性任务(例如数据导入)通常不是问题。上面的解决方案基于递归,正如问题中明确要求的那样。当然,遍历节点会增加很多开销。

B)这里最近的一个答案提出了一种基于外部工具集合的复杂方法。当然可能有不同的很好的实用程序来管理XML文件,但是恕我直言,许多XPATH工作可以在R本身中舒适有效地完成。

C)OP想知道是否有可能为XML&#34;的每个列表对象创建单独的data.frames。

D)我注意到在问号标签中,OP(似乎)需要更新的xml2包。

我直接使用XPATH来解决上面的问题。

XPATH方法

下面我在一个单独的数据框中提取Record节点。也可以对其他(子)节点使用相同的方法。

library(xml2)
xx=read_xml(x)                                                                              
xx=(xml_find_all(xx, "//Record"))
system.time(
    xx <- xml_find_all(xx, ".//descendant::*[not(*)]"))
#  user  system elapsed 
# 38.00    0.36   38.35 
system.time(xx <- xml_text(xx))
#  user  system elapsed 
# 68.39    0.05   68.53 
head(data.frame(t(matrix(xx, 5))))
#                    X1 X2 X3    X4  X5
# 1 2017-05-26T00:00:00  2  R local xxx
# 2 2017-04-30T00:00:00  2  R       xyx
# 3 2017-05-26T00:00:00  2  R local xxx
# 4 2017-04-30T00:00:00  2  R       xyx
# 5 2017-05-26T00:00:00  2  R local xxx
# 6 2017-04-30T00:00:00  2  R       xyx

(您可能希望在名称数据框列中添加更多代码)

时间是指我的普通笔记本电脑。

<强>说明

解决方案的核心在于XPATH .//descendant::*[not(*)].//descendant::提取当前上下文的所有后代(Record节点);添加[not(*)]可进一步平整布局。这允许线性化树结构,使其更适合于数据科学建模。

*的灵活性在计算方面付出了代价。然而,计算负担并不依赖于R,这是一种解释性语言,但却是高效的外部C库libxml2的代价。结果应该等于或优于其他公用事业和图书馆。

答案 1 :(得分:2)

因为你提到,我想转换这些数据,考虑XSLT,这是专门用于将复杂XML重构为各种最终用途结构的转换语言。在您的情况下,展平任何以XML格式保存节点的文本,然后可以使用xmlToDataFrame()轻松导入。虽然下面使用xsltproc和.NET Xsl类,但支持XSLT 1.0的任何external processor或语言模块(例如,Python,Java,C#,VB,PHP)都可以工作:

XSLT (另存为.xsl文件)

<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
    <xsl:output method="xml" indent="yes"/>
    <xsl:strip-space elements="*"/>

    <xsl:template match="/R">  
        <xsl:copy> 
            <xsl:apply-templates select="Report"/>
        </xsl:copy>
    </xsl:template>

    <xsl:template match="Report">
        <xsl:copy> 
            <xsl:apply-templates select="descendant::*[string-length(text())>0]"/>
        </xsl:copy>
    </xsl:template>

   <xsl:template match="*">  
      <xsl:element name="{concat(local-name(), position())}">
      <xsl:value-of select="." />
      </xsl:element>
   </xsl:template>

</xsl:stylesheet>

XML 输出(带有编号后缀以避免重复列错误)

<?xml version="1.0"?>
<R>
  <Report>
    <Date1>2017-05-26T00:00:00</Date1>
    <Grade2>2</Grade2>
    <Code3>R</Code3>
    <Description4>local</Description4>
    <Score5>xxx</Score5>
    <Date6>2017-04-30T00:00:00</Date6>
    <Grade7>2</Grade7>
    <Code8>R</Code8>
    <Score9>xyx</Score9>
    <FirstName10>MM</FirstName10>
    <DateOfInquiry11>2017-03-19</DateOfInquiry11>
    <Reason12>cc</Reason12>
    <DateOfInquiry13>2016-10-14</DateOfInquiry13>
    <Reason14>er</Reason14>
    <NumberOfInquiries15>2</NumberOfInquiries15>
  </Report>
</R>

R Mac / Linux脚本(调用xsltproc,unix计算机上的可用软件包)

library(XML)

setwd("/path/to/working/folder")

# COMMAND LINE CALL (INSTALL xsltproc IN TERMINAL)
system(paste("cd", getwd(), " && xsltproc -o Output.xml XSLTScript.xsl Input.xml"))

# PARSE AND LOAD TO DF
doc <- xmlParse('Output.xml')
df <- xmlToDataFrame(nodes = getNodeSet(doc, "//Report"))

str(df)
# 'data.frame': 6 obs. of  15 variables:
#  $ Date1              : chr  "2017-05-26T00:00:00" "2017-05-26T00:00:00" "2017-05-26T00:00:00" "2017-05-26T00:00:00" ...
#  $ Grade2             : chr  "2" "2" "2" "2" ...
#  $ Code3              : chr  "R" "R" "R" "R" ...
#  $ Description4       : chr  "local" "local" "local" "local" ...
#  $ Score5             : chr  "xxx" "xxx" "xxx" "xxx" ...
#  $ Date6              : chr  "2017-04-30T00:00:00" "2017-04-30T00:00:00" "2017-04-30T00:00:00" "2017-04-30T00:00:00" ...
#  $ Grade7             : chr  "2" "2" "2" "2" ...
#  $ Code8              : chr  "R" "R" "R" "R" ...
#  $ Score9             : chr  "xyx" "xyx" "xyx" "xyx" ...
#  $ FirstName10        : chr  "MM" "MM" "MM" "MM" ...
#  $ DateOfInquiry11    : chr  "2017-03-19" "2017-03-19" "2017-03-19" "2017-03-19" ...
#  $ Reason12           : chr  "cc" "cc" "cc" "cc" ...
#  $ DateOfInquiry13    : chr  "2016-10-14" "2016-10-14" "2016-10-14" "2016-10-14" ...
#  $ Reason14           : chr  "er" "er" "er" "er" ...
#  $ NumberOfInquiries15: chr  "2" "2" "2" "2" ...

R Windows (使用Powershell xsl脚本调用.NET Xsl类,请参阅here

library(XML)

# COMMAND LINE CALL (NO INSTALLS NEEDED)
system(paste0('Powershell.exe -File',
              ' "C:\\Path\\To\\PowerShell\\Script.ps1"',
              ' "C:\\Path\\To\\Input.xml"',
              ' "C:\\Path\\To\\XSLT\\Script.xsl"', 
              ' "C:\\Path\\To\\Output.xml"'))

# PARSE AND LOAD TO DF
doc <- xmlParse('Output.xml')
df <- xmlToDataFrame(nodes = getNodeSet(doc, "//Report"))