是否有更快的重写来处理丢失的 XML 属性?

Is there a faster rewrite to deal with missing XML attributes?

我正在处理巨大的 XML 文件,例如75GB 或更多,我的意思是任何小的开销都会变成几分钟甚至数小时的处理速度。

我的代码的核心在解析 XML 块时执行以下操作。假设我有一大块 3 行。请注意,我只关心 abc 属性,但可能存在缺少属性的项目元素,例如

xmlvec <- c('<item a="1" c="2" x="very long whatever" />',
            '<item b="3" c="4" x="very long whatever" />',
            '<item a="5" b="6" c="7" x="very long whatever" />')

我定义了一个映射,包括要查找的属性以及将它们重命名为什么,就是这样,我想阅读的那些:

mapping <- c("a", "b", "c")
# this doesn't matter here
#names(mapping) <- c("aa", "bb", "cc") 

如果我执行以下操作,我会得到缺失值 and/or NA 列名称,因为缺失属性会影响行的绑定,请注意自第一个 item 元素以来缺失的 b 列没有:

df <- as.data.frame(do.call(
  rbind,
  lapply(xml_children(read_xml(paste("<xml>", paste(xmlvec, collapse=""), "</xml>"))),
         function(x) {
           xml_attrs(x)[mapping]
         }
  )
), stringsAsFactors = FALSE)
df
     a   NA c
1    1 <NA> 2
2 <NA>    3 4
3    5    6 7

由于属性 b 在这个小块的第一行中丢失,所以我得到一个 NA 列,以后我无法将其与任何列名匹配。由于任何块的第一行都是任意的并且可以有任何缺失的属性,我需要在读取 每个属性 时强制执行模式,这样封闭的数据框就不会被破坏,但这非常昂贵的性能方面:

df <- as.data.frame(do.call(
  rbind,
  lapply(xml_children(read_xml(paste("<xml>", paste(xmlvec, collapse=""), "</xml>"))),
         function(x) {
           y <- xml_attrs(x)[mapping]
           if (any(is.na(names(y)))) {
             y <- y[-which(is.na(names(y)))]
           }
           y[setdiff(mapping, names(y))] <- NA
           y[order(factor(names(y), levels=mapping))]
         }
  )
), stringsAsFactors = FALSE)
df
     a    b c
1    1 <NA> 2
2 <NA>    3 4
3    5    6 7

看到现在强制执行了列架构和顺序,但由于这是在每行的基础上完成的,因此在性能上付出了非常高的代价。有没有更好的方法?

此解决方案适用于提供的示例。如果您的 XML 嵌套元素更加复杂,您可能需要一个替代方案。

基本思路是将xmlvec的每一个向量转化为一个data.frame,然后用data.table::rbindlist()将data.frame绑定在一起。如果您使用 fill = Tuse.names = T

rbindlist 也会负责填充缺失的数据
library(XML)
library(data.table)
library(magrittr)

xmlvec <- c('<item a="1" c="2" x="very long whatever" />',
            '<item b="3" c="4" x="very long whatever" />',
            '<item a="5" b="6" c="7" x="very long whatever" />')

l <- lapply( xmlvec, function(x) {
  XML::xmlToList( x ) %>%
    t() %>%
    as.data.frame()
})

dt <- data.table::rbindlist(l, use.names = T, fill = T)
dt
#       a c                  x    b
# 1:    1 2 very long whatever <NA>
# 2: <NA> 4 very long whatever    3
# 3:    5 7 very long whatever    6

## Now you can subset the columns of interest if you so wish
mapping <- c("a","b","c")
dt[, ..mapping]
#       a    b c
# 1:    1 <NA> 2
# 2: <NA>    3 4
# 3:    5    6 7

如果您更喜欢 tidyverse 方法,我认为 dplyr::bind_rows() 可能会提供与 rbindlist.

类似的功能

要在绑定前删除 select 列,您可以在 lapply

中删除它们
l <- lapply( xmlvec, function(x) {
  XML::xmlToList( x ) %>%
    t() %>%
    as.data.frame() %>%
    dplyr::select( intersect(names(.), mapping ) )
})

dt <- data.table::rbindlist(l, use.names = T, fill = T)
dt
#       a c    b
# 1:    1 2 <NA>
# 2: <NA> 4    3
# 3:    5 7    6

一种完全不同但可能最有效*的方法是使用 Rcpprapidxmlr 编写您自己的 XML 解析器。

这将适用于单个 .xml 文档(根据您所有 判断您正在使用的文档)。

所以我们可以把你的小例子做成一个单独的xml文档

xml <- '<?xml version="1.0" encoding="UTF-8"?>
<items>
<item a="1" c="2" x="very long whatever" />
<item b="3" c="4" x="very long whatever" />
<item a="5" b="6" c="7" x="very long whatever" />
</items>'

writeLines(xml, "~/Desktop/xml_test/xml.xml")

然后我们可以使用rapidxmlr库通过Rcpp写一些C++代码。

我也在使用开发库 gpx 因为我想要 this node_size() 功能。 (我也是这个 gpx 库的作者)。

对于这个您知道向量 abc 的简单示例,我们可以简单地在顶部定义它们并在属性存在时填充它们。

library(rapidxmlr)
library(Rcpp)
# devtools::install_github("dcooley/gpx")
library(gpx)

cppFunction(
  depends = c("rapidxmlr", "gpx")
  , include = c(
    '#include "rapidxml.hpp"'
    , '#include "gpx/utils.hpp"'
    , '#include <fstream>' 
  )
  , code = "
    Rcpp::DataFrame df_from_xml( std::string xml_file ) {

      // parse document
      rapidxml::xml_document<> doc;
      std::ifstream theFile( xml_file );

      std::vector< char > buffer(
        (std::istreambuf_iterator<char>(theFile)),
        std::istreambuf_iterator<char>()
      );
      buffer.push_back('\0');
      doc.parse<0>( &buffer[0] );

      // get root node
      rapidxml::xml_node<> *root_node = doc.first_node(\"items\");

      size_t n = gpx::utils::node_size( root_node, \"item\");

      // initalise vectors to store the results
      Rcpp::StringVector a( n, Rcpp::StringVector::get_na());
      Rcpp::StringVector b( n, Rcpp::StringVector::get_na());
      Rcpp::StringVector c( n, Rcpp::StringVector::get_na());

      R_xlen_t counter = 0;

      for(
        rapidxml::xml_node<> *item_node = root_node->first_node(\"item\");
        item_node;
        item_node = item_node -> next_sibling()
      ) {

        // get attributes of the node
        if( item_node -> first_attribute(\"a\") ) {
          a[ counter ] = item_node -> first_attribute(\"a\") -> value();
        }
        if( item_node -> first_attribute(\"b\") ) {
          b[ counter ] = item_node -> first_attribute(\"b\") -> value();
        }
        if( item_node -> first_attribute(\"c\") ) {
          c[ counter ] = item_node -> first_attribute(\"c\") -> value();
        }

        counter++;
      }

      return Rcpp::DataFrame::create(
        _[\"a\"] = a,
        _[\"b\"] = b,
        _[\"c\"] = c
      );

    }
  "
)

f <- normalizePath("~/Desktop/xml_test/xml.xml")
df_from_xml( f )
#      a    b c
# 1    1 <NA> 2
# 2 <NA>    3 4
# 3    5    6 7

*代码速度方面的效率,而不是学习如何编写 Rcpp / C++ 代码。