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

Parsing large and complicated XML file to data.frame

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

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]]

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

所以我有几个问题:

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

2) 我能否以某种方式提取一份报告的所有数据,两行 data.frame(也许是递归的)?

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

如有任何帮助,我们将不胜感激。

更新:

结果示例可能如下所示:

Classes ‘tbl_df’, ‘tbl’ and 'data.frame':   1 obs. of  17 variables:
 $ Record.1.Date                : chr "2017-05-26T00:00:00"
 $ Record.1.Grade               : num 2
 $ Record.1.Reason.1.Code       : chr "R"
 $ Record.1.Reason.1.Description: chr "local"
 $ Record.1.Score               : chr "xxx"
 $ Record.2.Date                : chr "2017-05-26T00:00:00"
 $ Record.2.Grade               : num 2
 $ Record.2.Reason.1.Code       : chr "R"
 $ Record.2.Reason.1.Description: chr "NA"
 $ Record.2.Score               : chr "xyx"
 $ Email.1                      : chr "NA"
 $ FirstName                    : chr "MM"
 $ Inquiry.1.DateOfInquiry      : POSIXct, format: "2017-03-19"
 $ Inquiry.1.Reason             : chr "cc"
 $ Inquiry.2.DateOfInquiry      : POSIXct, format: "2016-10-14"
 $ Inquiry.2.Reason             : chr "er"
 $ NumberOfInquiries            : num 2

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

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 想知道是否可以 "create separate data.frames for each list object of XML".

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

我直接使用 R 中的 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。结果应该等于或优于其他实用程序和库。

因为你提到,我想转换此数据,考虑 XSLT, the special-purpose transformation language designed to restructure complex XML to various end-use structures. And in your case flattening any text holding nodes in XML which then can easily be imported with xmlToDataFrame(). While below uses xsltproc and .NET Xsl class, any external processor 或语言模块(例如,Python、Java , C#, VB, PHP) 支持XSLT 1.0的可以工作:

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 class,参见

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"))