将大而复杂的 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"))
所以,我有一个包含大量报告的大型 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"))