Как извлечь значения из комплекса XML в R, не отбрасывая узлы с несуществующим значением? Мой фор-л oop очень медленный - PullRequest
2 голосов
/ 07 апреля 2020

У меня большой, сложный XML -файл, и мне нужно извлечь значения и атрибуты определенных под (под ...) узлов. Но так как не все сноски имеют все нужные значения (некоторые отсутствуют), я не могу легко использовать очень быстрый xml_find_all (пакетный XML2), потому что он, конечно, не будет включать сноски с пропущенными значениями.

Мое решение должен использовать for-l oop, циклически проходящий по всем моим xml -узлам (объектам), и проверять в каждом узле, существует ли мое желаемое значение - если да, извлеките его. Благодаря индексу l oop я знаю, к какому объекту он принадлежит, и записал его в соответствующий data.frame$Feature[i].

. Этот подход работает хорошо, но для моего большого XML -Node он занимает ОЧЕНЬ долго (20 минут) и занимает очень много памяти (~ 1,5 ГБ из-за if-l oop). Мои XML: 100 МБ, около 30 000 «записей / объектов», каждая из которых содержит около 50 функций (~ 2 млн. Строк)

Основная проблема, которую я выяснил: xpathSApply(...xml_path(Obj[i]...) очень медленная, если индексация [ я] из моего l oop довольно высоко (> 5000)

Мои вопросы:

  • У вас есть идея лучше или проще решить моя проблема с очень сложным и сильно неоднородным c, структурированным XML, где не все функции присутствуют во всех объектах (узлах)?
  • Я прочитал , этот интересный подход , но не мог понять, как перевести его в мой очень сложный XML, где мои желаемые значения находятся на разных уровнях Nodeset ...
  • Возможно, есть какое-то вложенное выражение xpathSApply, чтобы обойти for-l oop и избегать использования индекса?
  • Есть ли у вас какие-либо "векторные" подходы (которые в R более быстры) для моей проблемы?

См. Мой MWE-код с еще несколько комментариев ниже.

XML

<?xml version="1.0" encoding="UTF-8"?>
<featureMember>
        <Object>
                <XML_Name>Object 1</XML_Name>
               <XML_Feature1>
                   <XML_Feature1a href="URL1"></XML_Feature1a>
                </XML_Feature1>
                <XML_Feature2>
                   <XML_Feature2a>1</XML_Feature2a>
                   <XML_Feature2a>1x</XML_Feature2a>
                   <XML_Feature2a>1y</XML_Feature2a>
                </XML_Feature2>
                <XML_Feature3>
                   <XML_Feature3a>F3a_1</XML_Feature2a>
                   <XML_Feature3b>F3b_1</XML_Feature2a>
                </XML_Feature3>
                <XML_Feature3>
                   <XML_Feature3a>F3a_2</XML_Feature2a>
                   <XML_Feature3b>F3b_2</XML_Feature2a>
                </XML_Feature3>
                <XML_Feature4>F4_1</XML_Feature4>
                <XML_Feature4>F4_2</XML_Feature4>   
        </Object>       
        <Object >
            <XML_Name>Object 2</XML_Name>
               <XML_Feature1>
                   <XML_Feature1a href="URL2"></XML_Feature1a>
                </XML_Feature1>         
        </Object>       
        <Object >
        <XML_Name>Object 3</XML_Name>
            <XML_Feature1>
               <XML_Feature1>               
               </XML_Feature1>
            </XML_Feature1>
            <XML_Feature2>
                <XML_Feature2a>Value 3</XML_Feature2a>
            </XML_Feature2>
        </Object>
</featureMember>

R * 10 39 *

require(xml2)
require(XML)
test_xml2 <- read_xml("above_file.xml") # using Packet xml2 (for using xml_find_all)
test_XML <- xmlParse("above_file.xml") # Packet XML (for using xpathSApply)

  # XML-Noteset of all Objects I want to process:
Obj <- xml_find_all(test_xml2, "//Object") # --> has 3 nodes, contains all Objects!

  # initialize a destination dataframe and fill with NAs
df <- data.frame('Name'=integer(), 'f2a'=character() , 'f1a'=character(), stringsAsFactors = FALSE)
df[1:length(Obj),] <- NA

# My Initial approach to extract all features by xml_find_all (which is very fast) is not working because not all xml-nodes have all wanted xml-features:
Name <- xml_text(xml_find_all(test_xml2, "//XML_Name")) 
  # --> length(Name)=3, because all 3 Objects have a name!
f1a  <- xml_attr(xml_find_all(test_xml2, "//XML_Feature1/XML_Feature1a"),"href") 
  # --> length(f1a)=2, because XML_Feature1a is missing in Object3! 
f2a  <- xml_text(xml_find_all(test_xml2, "//XML_Feature2/XML_Feature2a")) 
  # --> length(f2a)=2, because XML_Feature2a is missing in Object2!
# Joining these to a final df is not possible, because "Name", "f2a" and "f1a" have of course different lengths, plus correct data matching is not possible!


# Therefore I decided to make instead the following approach.
  # 1.) crawl all features, which are present in all nodes, because its fast (here: "Name"):
df$Name <- xml_text(xml_find_all(test_xml2, "//XML_Name"))

  # 2.) making a for-loop over all Objects/XML-Nodes of interest and check if eacht wanted feature exist.
    # if yes: write to df$FeatureXY[i]
    # if not: make nothing (thus df$FeatureXY[i]stays NA from initialization)
for (i in 1:length(Obj))
{  # 1. Feature:
 tmp  <- xpathSApply(test_XML, paste0(xml_path(Obj[i]),"/XML_Feature1/XML_Feature1a"),  xmlGetAttr, "href")
 if(length(tmp )>0) { df$f1a[i] <- tmp # otherwise it would produce an error-message}
    # 2. Feature:
 tmp  <- xpathSApply(test_XML, paste0(xml_path(Obj[i]),"/XML_Feature2/XML_Feature2a"),  xmlValue)
 if(length(tmp )>0) { df$f2a[i] <- tmp} 
}  

# Result of df as it should be:
# Name      f2a             f1a   f3a            f3b             f4
# Object 1  1 # 1x # 1y     URL1  F3a_1 # F3a_2  F3b_1 # F3b_2   F4_1 # F4_2
# Object 2  NA              URL2  NA             NA              NA 
# Object 3  Value 3         NA    NA             NA              NA

правка 1: расширенный пример XML (несколько элементов feature2a, feature3a / b feature4)

Ответы [ 2 ]

2 голосов
/ 07 апреля 2020

Подобные проблемы могут быть сложными, чтобы справиться с любыми потенциальными изменениями между данными выборки и фактическими данными. Если мы предположим, что существует не более одного узла «Feature1a» и не более одного узла «Feature2a» на «Объект», то это приводит к прямой проблеме.

Сначала найдите все родительские «Объекты» "узлы, затем использующие этот вектор узлов, анализируют каждый на предмет имени, атрибута feature1a и текста feature2a. xml_find_first вернет значение, если узел существует, если нет, то вернет NA. Поскольку функция xml_find_first векторизована, она будет работать с вектором родительских узлов без необходимости использования al oop и с очень значительным улучшением производительности.

library(xml2)
library(dplyr)

#Read file to process
doc<- read_xml("above_file.xml")

#find parent nodes
parents <- xml_find_all(doc, ".//Object")

#Now extract the requested data from each parent
# Notice the use of the . in the xpath. 
# //  finds anywhere in the document (ignoring the current node)
# .// finds anywhere beneath the current node
Names<- xml_find_first(parents, ".//XML_Name") %>% xml_text()
feature1 <- xml_find_first(parents, ".//XML_Feature1a") %>% xml_attr("href")

#fill features with first elements as default
feature2 <- xml_find_first(parents, ".//XML_Feature2a") %>% xml_text()
#find parents with more than 1 feature2
moretwos<-which(xml_find_all(parents, ".//XML_Feature2")  %>% xml_length() >1)
#reparse the parent nodes with more than one child
feature2[moretwos] <-sapply(parents[moretwos], function(node){
        xml_find_all(node, ".//XML_Feature2a") %>% xml_text() %>% paste(collapse = "#")
})


#Make combinded dataframe
answer <-data.frame(Names, feature1, feature2)
answer

Вот аналогичный вопрос, но с неизвестным числом подузлов: Создание фрейма данных из xml с различным количеством элементов

ОБНОВЛЕНИЕ Для вашей исправленной проблемы наличия нескольких подузлов с несколькими дочерними элементами, но здесь нет внуков.

#find parent nodes
parents<-xml_find_all(doc, ".//Object")

dfs<-lapply(parents, function(parent) {
  #Get oject name
  object<-xml_find_first(parent, ".//XML_Name") %>% xml_text()

  #find the number of children under each child
  numchild<-xml_children(parent) %>% xml_length()

  #if number of children is zero get name and value
  name  <- xml_children(parent)[numchild==0] %>% xml_name()
  value <- xml_children(parent)[numchild==0] %>% xml_text()

   #if the number of childern is 1 or more the get the name value of the child
   namec2  <- xml_children(parent)[numchild>=1] %>% xml_children() %>% xml_name()
   valuec2 <- xml_children(parent)[numchild>=1] %>% xml_children() %>% xml_text()

  #make data frame of the values and column headings
  df<-data.frame(object, name=c(name, namec2), value=c(value, valuec2), stringsAsFactors = FALSE)
  print(df)
  df
})

#Make combinded dataframe
answer<-bind_rows(dfs)
answer
library(tidyr) 
pivot_wider(answer, object, names_from = name, values_from= value, values_fn = list(value = toString))

В окончательном ответе потребуется некоторая очистка столбцов, gsub(", ", " # ", ...) и получение URL-адреса. атрибут сверху

1 голос
/ 07 апреля 2020

При 100 МБ рассмотрим XSLT , язык специального назначения, предназначенный для преобразования файлов XML, таких как очень вложенные уровни, в более плоские выходные данные для простого импорта фрейма данных R. R может запустить XSLT с xslt, расширенный пакет до xml2. В противном случае используйте любой исполняемый файл XSLT для обработки преобразования, как показано ниже. И поскольку вы также используете XML, рассмотрите его удобный метод xmlToDataFrame для импорта более плоских XML файлов.

XSLT (сохранить как файл .xsl, специальный . xml file)

<?xml version="1.0" encoding="UTF-8"?>
<xsl:stylesheet xmlns:xsl="http://www.w3.org/1999/XSL/Transform" version="1.0">

  <xsl:output method="xml" indent="yes"/>
  <xsl:strip-space elements="*"/>

  <xsl:template match="/featureMember">
      <root>
        <xsl:apply-templates select="Object"/>
      </root>
  </xsl:template>

  <xsl:template match="Object">
      <data>
        <Name><xsl:apply-templates select="XML_Name"/></Name>
        <f1><xsl:apply-templates select="XML_Feature1/XML_Feature1a/@href"/></f1>
        <f2><xsl:apply-templates select="XML_Feature2"/></f2>
      </data>
  </xsl:template>

</xsl:stylesheet>

XSLT Demo

XML Выход

<?xml version="1.0" encoding="utf-16"?>
<root>
  <data>
    <Name>Object 1</Name>
    <f1>URL1</f1>
    <f2>Value 1</f2>
  </data>
  <data>
    <Name>Object 2</Name>
    <f1>URL2</f1>
    <f2 />
  </data>
  <data>
    <Name>Object 3</Name>
    <f1 />
    <f2>Value 3</f2>
  </data>
</root>

R (без for петель, apply вызовов, if logi c или поиск XPath не требуется)

library(xml2)
library(xslt)
library(XML)

# PARSE XML AND XSLT
doc <- read_xml('/path/toInput.xml')
style <- read_xml('/path/to/Script.xsl', package = "xslt")

# TRANSFORM NESTED INPUT INTO FLATTER OUTPUT
new_xml <- as.character(xslt::xml_xslt(doc, style))

# PARSE FLATTER XML
flat_xml <- XML::xmlParse(new_xml, asText=TRUE)

# BUILD DATA FRAME
final_df <- XML::xmlToDataFrame(flat_xml, XML::getNodes(nodes="//data"))

Чтобы продемонстрировать внешнее решение XSLT, ниже приведены интерфейсы для инструмента командной строки xsltproc, доступного для установки на Unix машины (т. Е. Linux, MacOS):

library(XML)

# COMMAND LINE CALL TO UNIX'S XSLTPROC (ALTERNATIVE TO xslt PACKAGE)
system("xsltproc -o /path/to/input.xml /path/to/script.xsl /path/to/output.xml")

flat_xml <- xmlParse("/path/to/output.xml")

final_df <- xmlToDataFrame(flat_xml, getNodes(nodes="//data"))
Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...