首页 > 解决方案 > xml到数据框重复节点attr

问题描述

我正在尝试将一个巨大的 .xml 文件转换为数据框。它看起来像这样(但有数百万这样的 <Cli 集群):

<Cli Cd="11300000029" CoobAss="0.00" CoobRec="0.00">
    <Op Mod="0202" VincME="N">
        <Venc v165="4934.84" v170="4856.16"/>
    </Op>
    <Op Mod="1901" VincME="N">
        <Venc v20="22877.77"/>
    </Op>
</Cli>

<Cli Cd="11400000029" CoobAss="0.00" CoobRec="0.00">
    <Op Mod="0204" VincME="N">
        <Venc v165="5000.10"/>
    </Op>
    <Op Mod="1902" VincME="N">
        <Venc v20="32000.22"/>
    </Op>
</Cli>

每个 <Cli 可以有多个 <Op,每个 <Op 可以有多个 <Venc。

预期结果是:

tibble::tribble(
  ~Cd, ~CoobAss, ~CoobRec, ~Mod, ~VincME, ~v165, ~v170, ~v20,
  "11300000029", "0.00", "0.00", "0202", "N", "4934.84", "4856.16", NA,
  "11300000029", "0.00", "0.00", "1901", "N", NA, NA, "22877.77",
  "11400000029", "0.00", "0.00", "0204", "N", "5000.10", NA, NA,
  "11400000029", "0.00", "0.00", "1902", "N", NA, NA, "32000.22"
)
#> # A tibble: 4 x 8
#>   Cd          CoobAss CoobRec Mod   VincME v165    v170    v20     
#>   <chr>       <chr>   <chr>   <chr> <chr>  <chr>   <chr>   <chr>   
#> 1 11300000029 0.00    0.00    0202  N      4934.84 4856.16 <NA>    
#> 2 11300000029 0.00    0.00    1901  N      <NA>    <NA>    22877.77
#> 3 11400000029 0.00    0.00    0204  N      5000.10 <NA>    <NA>    
#> 4 11400000029 0.00    0.00    1902  N      <NA>    <NA>    32000.22

reprex 包(v0.3.0)于 2020 年 10 月 1 日创建

我可以在不同的数据帧中获得 Cli、Op 和 Venc,但我不知道如何将它们像那样放在一起。

编辑:创建一个大文件

为了进行性能测试,您可以复制数据以增加文件大小。更改n为所需大小:

xml = c('
<Cli Cd="11300000029" CoobAss="0.00" CoobRec="0.00">
    <Op Mod="0202" VincME="N">
        <Venc v165="4934.84" v170="4856.16"/>
    </Op>
    <Op Mod="1901" VincME="N">
        <Venc v20="22877.77"/>
    </Op>
</Cli>

<Cli Cd="11400000029" CoobAss="0.00" CoobRec="0.00">
    <Op Mod="0204" VincME="N">
        <Venc v165="5000.10"/>
    </Op>
    <Op Mod="1902" VincME="N">
        <Venc v20="32000.22"/>
    </Op>
</Cli>
')

n = 3
data = do.call("rbind", replicate(n, xml, simplify = FALSE))

write(data, "xml.xml")

标签: rxml

解决方案


也许只是遍历所有Venc节点并找到每个Venc节点及其父节点(即Op)和祖父节点(即Cli)的属性?这里的关键是,尽管您有许多Clis、Ops 或Vencs,但树结构确保每个孩子只有一个父母/祖父母。对此,我们可以只做一个向后搜索。尝试这个:

library(rvest)
library(xml2)
library(purrr)

map_dfr(
  html_nodes(xml, xpath = "//Venc"), 
  function(x) c(html_attrs(html_node(x, xpath = "ancestor::Cli")), html_attrs(html_node(x, xpath = "parent::Op")), html_attrs(x))
)

这里, xml是一个由 .xml_document返回的对象xml2::read_xml

更新

我使用规范测试了以下代码n = 1,000,000,它创建了一个 424MB 的 XML 文档。在我的笔记本电脑上,完成所有所需的计算大约需要 20 分钟。

library(xml2)
library(rvest)
library(data.table)
library(tibble)

# xml should be replaced with your `xml_document` object
xml_ls <- as_list(html_nodes(xml, xpath = "//Cli"))

unpack_attrs <- function(x) {
  f <- function(i) {
    attrs <- attributes(i)
    if (length(i) > 0L) i <- list(. = `attributes<-`(i, NULL))
    c(i, `[[<-`(attrs, "names", NULL))
  }
  rbindlist(lapply(x, f), fill = TRUE)
}

recur_unpack_attrs <- function(xml_tree) {
  recur_ <- function(dt, out) {
    init <- dt[, unpack_attrs(.)]
    if ("." != names(init)[[1L]]) {
      out[, names(init) := init]
      return(NULL)
    }
    out[, (names(init)[-1L]) := init[, -1L]]
    recur_(init, out)
  }
  start <- unpack_attrs(xml_tree)
  result <- start[, -1L]
  recur_(start, result)
  as_tibble(result)
}

res <- recur_unpack_attrs(xml_ls)

CPU 时间

  user  system elapsed 
971.05  107.61 1150.56 

试试看。让我知道结果。

第二次也是最后一次更新

我重新编写了大部分代码以获得更稳定的性能。我还删除了对rvest. 但是,我不能永远这样做,因为我还有其他优先事项要处理。抱歉,这是我对您问题的最后一次更新。

library(xml2)
library(data.table)
library(tibble)

unpack_attr <- function(xi, i) {
  attrs <- attributes(xi)
  if (length(xi) < 1L || is.null(attrs[["names"]]) ) {
    xi <- list(. = NA)
  } else {
    xi <- list(. = `attributes<-`(xi, NULL))
  }
  c(xi, `[[<-`(attrs, "names", NULL))
}

unpack_attrs <- function(x, ids = NULL) {
  out <- list()
  i <- 1L
  while (i <= length(x)) {
    out[[i]] <- unpack_attr(x[[i]])
    i <- i + 1L
  }
  names(out) <- ids
  rbindlist(out, fill = TRUE, idcol = TRUE)
}

recur_unpack_attrs <- function(xml_tree) {
  out <- unpack_attrs(xml_tree)[, .id := as.character(.I)]
  nms <- names(out)[-1:-2]
  out_nms <- nms
  while (!all(is.na(out[["."]]))) {
    last <- copy(out)
    out <- out[, unpack_attrs(., .id)]
    tmp <- names(out)
    conf <- match(out_nms, tmp, 0L); conf <- conf[conf > 0L]
    if (length(conf) > 0L) tmp[conf] <- paste0(tmp[conf], "_", conf - 2L + length(out_nms))
    out_nms <- c(out_nms, tmp[-1:-2])
    names(out) <- tmp
    out[last, (nms) := mget(paste0("i.", nms)), on = ".id"][, .id := as.character(.I)]
    nms <- names(out)[-1:-2]
  }
  out[rowSums(is.na(out)) < length(out) - 1L, ..out_nms]
}

# replace xml with your xml_document object or "data.xml" the your file path
xml <- read_xml("data.xml")
xml_ls <- as_list(xml_find_all(xml, xpath = "//Cli"))
index <- seq_len(length(xml_ls))
tasks <- split(index, (index - 1L) %/% 50000L)
res <- as_tibble(rbindlist(lapply(tasks, function(task) recur_unpack_attrs(xml_ls[task])), fill = TRUE))

上面的代码在包含 500,000 个重复 (437MB) 以下内容的示例 xml 文档上运行良好:

<Cli Cd="11300000029" CoobAss="0.00" CoobRec="0.00">
    <Op Mod="0202" VincME="N">
        <Venc v165="4934.84" v170="4856.16"/>
    </Op>
    <Op Mod="1901" VincME="N">
        <Venc v20="22877.77"/>
    </Op>
</Cli>

<Cli Cd="11400000029" CoobAss="0.00" CoobRec="0.00">
    <Op Mod="0204" VincME="N">
        <Venc v165="5000.10"/>
    </Op>
    <Op Mod="0201" VincME="N">
        <Venc v165="we000.10"/>
        <Venc v165="we000.10"/>
        <Venc v165="we000.10"/>
    </Op>
    <Op Mod="1902" VincME="N">
        <Venc v20="32000.22"/>
    </Op>
</Cli>
<Cli><Op><Venc v165="400.0"/></Op></Cli>
<Cli></Cli>
<Cli><Venc/></Cli>
<Cli><Venc v165="4343000.10"/><Venc v20="4343000.10"/></Cli>
<Cli>
</Cli>
<Cli><Op><Venc/></Op></Cli>
<Cli Cd="11400000024" CoobAss="0.00" CoobRec="0.00"/>
<Cli Cd="11400000024" CoobAss="0.00" CoobRec="0.00">
    <Op Mod="4757" VincME="N"/>
</Cli>

性能(很明显,从 xml_document 对象到 R 列表的转换是瓶颈,但运行时间仍然可以接受)

> system.time({
+   xml_ls <- as_list(xml_find_all(xml, xpath = "//Cli"))
+ })
   user  system elapsed 
1206.36   28.74 1250.89 
> 
> system.time({
+   index <- seq_len(length(xml_ls))
+   tasks <- split(index, (index - 1L) %/% 50000L)
+   res <- as_tibble(rbindlist(lapply(tasks, function(task) recur_unpack_attrs(xml_ls[task])), fill = TRUE))
+ })
   user  system elapsed 
 161.03   12.68  175.41 

由于您的 xml 结构没有组织好,我必须做出几个关键假设:

  1. 您的所有数据都存储为标签属性而不是文本内容(即没有这样的东西<Cli>blah blah</Cli>

  2. 每个<OP>or<Venc>必须驻留在<Cli>. 但是,可以有没有属性的标签(例如<Cli><Op><Venc v165="4343000.10"/></Op></Cli>)或缺少非<Cli>层的标签(例如<Cli><Venc v165="4343000.10"/></Cli>)。

  3. 没有属性的标签将被丢弃。

  4. 拆包过程从最外层(即<Cli>)开始到最内层(即<Venc>)。每次解压图层时,都会根据在该图层中找到的属性名称创建新列。这意味着如果存在以下情况,那么我们将看到冲突。

<Cli><Venc v160="333"></Cli>
<Cli><Op><Venc v160="434"></Op></Cli>

在上述情况下,v160将在第一个解包时创建一个名为的列,但稍后将在第二个解包时<Cli>尝试创建相同的v160列。第二个将被赋予一个新名称以避免冲突。<Op><Cli>v160

像这样的输出只是表明列名冲突。您必须手动决定如何合并这些列(例如v165v165_8

# A tibble: 6,000,000 x 10
   Cd          CoobAss CoobRec Mod   VincME v165       v20        v165_8   v170    v20_10  
   <chr>       <chr>   <chr>   <chr> <chr>  <chr>      <chr>      <chr>    <chr>   <chr>   
 1 11300000029 0.00    0.00    0202  N      NA         NA         4934.84  4856.16 NA      
 2 11300000029 0.00    0.00    1901  N      NA         NA         NA       NA      22877.77
 3 11400000029 0.00    0.00    0204  N      NA         NA         5000.10  NA      NA      

最后但同样重要的是,R 的指针保护堆栈的默认最大大小为50000. data.table::rbindlist在加入列表之前预先分配指向列表中每个元素的指针,以最大限度地提高速度。然而,这也意味着我们必须在为该函数提供一个大列表(超过 50000 个元素)时进行内存管理。这就是为什么我们需要这一行tasks <- split(index, (index - 1L) %/% 50000L)来将一个大列表绑定成不超过限制的小任务。

如果这次代码再次失败,那么您可能不得不向其他人寻求帮助。对于那个很抱歉。


推荐阅读