Разбор большого и сложного XML-файла в 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 непростая. Ранее, работая с XML, я создал цикл, который для каждого отчета преобразует свои подузлы в data.frame, но здесь (в этих данных) количество подузлов больше 30 (не все они помещены в пример), а структура различается (узлы списка могут встречаться даже на двух уровнях в XML).

Итак, у меня есть несколько вопросов:

1) Я уверен, что цикл отчетов - не лучший способ справиться с этим. Как мне подойти к этой проблеме?

2) Могу ли я как-то извлечь все данные из одного отчета из двух в одну строку data.frame (возможно, рекурсивно)?

3) Или я могу автоматически создавать отдельные data.frames для каждого объекта списка XML?

Любая помощь приветствуется.

Обновлять:

Пример результатов может выглядеть так:

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

, но, как я упоминал ранее, подсписки также могут быть в отдельных таблицах.


person minem    schedule 30.05.2017    source источник
comment
Пожалуйста, попробуйте задать один вопрос для каждого сообщения, несколько вопросов часто означают, что ваш вопрос слишком общий. В частности, проблема утечки памяти должна иметь отдельный вопрос с очень конкретным примером кода, который показывает утечку памяти.   -  person Paul Hiemstra    schedule 30.05.2017
comment
@PaulHiemstra Согласен, удалил последний вопрос. Остальные вопросы, на мой взгляд, один: как лучше всего выполнить мою задачу?   -  person minem    schedule 30.05.2017
comment
xmlToDataFrame(p) поможет вам в этом   -  person Andrew Gustar    schedule 30.05.2017
comment
@AndrewGustar xmlToDataFrame(p) не работает в этом случае (и во многих других), когда xml имеет сложную структуру. Он объединяет все подузлы   -  person minem    schedule 30.05.2017
comment
Да, хотя я подумал, что будет проще разделить их после преобразования в df, чем напрямую из XML.   -  person Andrew Gustar    schedule 30.05.2017
comment
@AndrewGustar В моем опубликованном примере, возможно, кажется, что это могло бы быть проще, но в реальных данных структура более сложная, и некоторые подузлы не имеют значений или содержат списки. Используя xmlToDataFrame, я получу неструктурированные символьные строки, которые будет нелегко разделить (если даже возможно)   -  person minem    schedule 30.05.2017
comment
Противный. Обычно я просто отношусь к ним как к текстовым файлам и тщательно их просматриваю! Надеюсь, у кого-то здесь будет больше изящества.   -  person Andrew Gustar    schedule 30.05.2017
comment
Не могли бы вы привести пример нужной выходной строки, соответствующей одному отчету.   -  person 9Heads    schedule 02.06.2017
comment
@ 9Heads обновлено.   -  person minem    schedule 02.06.2017


Ответы (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 добавил еще один запрос на скорость выполнения, который обычно не является проблемой для одноразовых задач, таких как импорт данных. Приведенное выше решение основано на рекурсии, как явно требуется в вопросе. Конечно, обход узлов вверх и вниз добавляет много накладных расходов.

Б) Один из недавних ответов здесь предлагает сложный метод, основанный на наборе внешних инструментов. Конечно, могут быть разные полезные утилиты для управления файлами XML, но IMHO большая часть работы с XPATH может быть удобно и эффективно выполнена в самом R.

C) OP задается вопросом, возможно ли «создать отдельные data.frames для каждого объекта списка XML».

D) Я заметил, что в тегах вопросов OP (кажется) требует более нового пакета xml2.

Я обращаюсь к вышеперечисленным вопросам, используя XPATH прямо из R.

Подход 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 < / а>. Результаты должны быть такими же или лучше, чем у других утилит и библиотек.

person antonio    schedule 02.06.2017
comment
Вы пробовали запустить это на моих пробных данных? Он содержит 150000 отчетов, и мне потребовалось примерно 18 минут, чтобы запустить xmlToList. Поскольку мои настоящие данные более сложные и большие, мне нечего думать о сроках их преобразования. Я думаю, что должен быть другой способ выполнить мою задачу. - person minem; 02.06.2017
comment
@ MārtiņšMiglinieks: Пожалуйста, укажите в своем вопросе, какое решение вы ищете, а что не. Если вы напишете: структура XML непростая. Ранее, работая с XML, я создал цикл, и я уверен, что цикл отчетов - не лучший способ, это явно означает, что вам нужно рабочее решение на основе пакета XML без циклов for. И это такое решение. Также обратите внимание, что ответы могут быть основаны только на фактических данных, которые вы опубликовали. - person antonio; 02.06.2017
comment
Возможно, вы не заметили, но в своем вопросе я создал образцы данных, которые примерно показывают, насколько велики мои данные. (line: x <- paste(rep(x, 1.5e+5), collapse = "") Я не ищу ответа, основанного на конкретных пакетах, но его следует несколько обобщить, и скорость должна быть одной из основных проблем. - person minem; 03.06.2017
comment
@ MārtiņšMiglinieks: Я заметил: код основан на вашем примере x <- "<Report> .... 20 минут на импорт огромного набора данных XML вполне приемлемы для большинства сценариев, особенно когда импорт данных выполняется только один раз. Если у вас есть определенные временные ограничения, пожалуйста, добавьте их в свой вопрос, указав, каков приемлемый временной интервал, чтобы мы могли проверить, достижим ли он практически. - person antonio; 03.06.2017

Поскольку вы упомянули: Я хотел бы преобразовать эти данные, рассмотрите возможность использования XSLT, язык преобразования специального назначения, предназначенный для преобразования сложного XML в различные структуры конечного использования. И в вашем случае сглаживание любых текстовых узлов в XML, которые затем можно легко импортировать с помощью xmlToDataFrame(). Хотя ниже используется класс xsltproc и .NET Xsl, любой внешний процессор или языковой модуль (например, 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, см. здесь)

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"))
person Parfait    schedule 04.06.2017
comment
Извините, это отличается от того, что было задано в примере результатов. OP также требует отсутствующих значений (см. Поле электронной почты). Каковы улучшения в скорости выполнения по сравнению со всем подходом R? - person antonio; 05.06.2017