xml в dataframe повторяющийся узел attr

#r #xml

#r #xml

Вопрос:

Я пытаюсь преобразовать огромный XML-файл в dataframe. Это выглядит так (но с миллионами:

 <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>
  

Каждый

Ожидаемый результат:

 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
  

Создан 2020-10-01 пакетом reprex (версия 0.3.0)

Я могу получить 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")
  

Ответ №1:

Может быть, просто пройти через все Venc узлы и найти атрибуты для каждого Venc узла вместе с его родительским узлом (т.Е. Op ) и дедушкиным узлом (т.Е. Cli )? Ключевым моментом здесь является то, что, хотя у вас много Cli s, Op s или Venc s, древовидная структура обеспечивает одного и только одного родителя / дедушку / бабушку для каждого дочернего элемента. В связи с этим мы можем просто выполнить обратный поиск. Попробуйте это:

 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 , который создает XML-документ размером 424 МБ. На моем ноутбуке потребовалось около 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)
  

Время процессора

   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))
  

Приведенный выше код хорошо работает с образцом XML-документа, состоящего из 500 000 копий (437 МБ) следующего:

 <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> или <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 будет присвоено новое имя, чтобы избежать конфликтов.

Такой вывод просто указывает на конфликты имен столбцов. Вы должны вручную решить, как объединить эти столбцы (например v165 , и v165_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) , чтобы превратить привязку большого списка в небольшие задачи, которые не превышают лимит.

Если на этот раз снова произойдет сбой кода, вам, вероятно, придется обратиться за помощью к другим. Извините за это.

Комментарии:

1. Он действительно работал с reprex. Но это очень медленно, хотя: (Я нажал его вчера на файл размером 400 МБ, и он работал всю ночь и еще не завершен. Есть ли какая-либо альтернатива {purrr} в этом случае, которая не займет вечность?

2. @AlbersonMiranda Как насчет data.table::rbindlist(lapply(html_nodes(xml, xpath = "//Venc"), function(x) as.list(c(html_attrs(html_node(x, xpath = "ancestor::Cli")), html_attrs(html_node(x, xpath = "parent::Op")), html_attrs(x)))), fill = TRUE) . Это должно быть быстрее, чем указано выше. Но я думаю, что узким местом здесь должна быть html_nodes часть. Требуется больше данных для тестирования.

3. прошло 6 часов, и он все еще работает. Это расстраивает.

4. @AlbersonMiranda Мне нужно больше данных для тестирования, если вы хотите дальнейшего повышения скорости. Вы не возражаете против совместного использования большего набора данных, который содержит, например, 20 Cli секунд?

5. У меня следующая ошибка с файлом: Ошибка in [.data.table (out, , := ((names(init)[-1L]), init[, -1L])): Предоставлено 175 элементов для назначения 191 элементу столбца ‘Mod’. Если вы хотите «переработать» RHS, пожалуйста, используйте rep(), чтобы сделать это намерение понятным для читателей вашего кода.