Разбор большого XML в фрейм данных в R

У меня есть большие файлы XML, которые я хочу превратить в фреймы данных для дальнейшей обработки в R и других программах. Все это делается в macOS.

Каждый ежемесячный XML имеет размер около 1 ГБ, содержит 150 тыс. записей и 191 различную переменную. В конце концов, мне может не понадобиться полная 191 переменная, но я хотел бы сохранить их и решить позже.

Доступ к файлам XML можно получить здесь (прокрутите вниз, чтобы просмотреть ежемесячные почтовые индексы, в несжатом виде следует смотреть XML-файлы «dming»)

Я добился определенного прогресса, но обработка больших файлов занимает слишком много времени (см. ниже)

XML выглядит следующим образом:

<ROOT>
 <ROWSET_DUASDIA>
  <ROW_DUASDIA NUM="1">
   <variable1>value</variable1>
   ...
   <variable191>value</variable191>
  </ROW_DUASDIA>
  ...
  <ROW_DUASDIA NUM="150236">
   <variable1>value</variable1>
   ...
   <variable191>value</variable191>
  </ROW_DUASDIA>
 </ROWSET_DUASDIA>
</ROOT>

Я надеюсь, что это достаточно ясно. Я впервые работаю с XML.

Я просмотрел здесь много ответов и фактически сумел получить данные в кадре данных, используя меньшую выборку (используя ежедневный XML вместо ежемесячных) и xml2. Вот что я сделал

library(xml2) 

raw <- read_xml(filename)

# Find all records
dua <- xml_find_all(raw,"//ROW_DUASDIA")

# Create empty dataframe
dualen <- length(dua)
varlen <- length(xml_children(dua[[1]]))
df <- data.frame(matrix(NA,nrow=dualen,ncol=varlen))

# For loop to enter the data for each record in each row
for (j in 1:dualen) {
  df[j, ] <- xml_text(xml_children(dua[[j]]),trim=TRUE)
}

# Name columns
colnames(df) <- c(names(as_list(dua[[1]])))

Я предполагаю, что это довольно рудиментарно, но я также новичок в R.

Во всяком случае, это прекрасно работает с ежедневными данными (4-5 тысяч записей), но, вероятно, это слишком неэффективно для 150 тысяч записей, и на самом деле я ждал пару часов, и это не закончилось. Конечно, мне нужно будет запускать этот код только раз в месяц, но тем не менее я хотел бы его улучшить.

Я попытался превратить элементы для всех записей в список, используя функцию as_list в xml2, чтобы продолжить работу с plyr, но это также заняло слишком много времени.

Заранее спасибо.


person GranDurismo    schedule 27.12.2018    source источник


Ответы (2)


Хотя нет гарантии лучшей производительности для больших файлов XML, пакет XML ("старой школы") поддерживает компактный обработчик фреймов данных xmlToDataFrame для плоских файлов XML, таких как ваш. Любые отсутствующие узлы, доступные в других одноуровневых узлах, приводят к NA для соответствующих полей.

library(XML)

doc <- xmlParse("/path/to/file.xml")
df <- xmlToDataFrame(doc, nodes=getNodeSet(doc, "//ROW_DUASDIA"))

Вы даже можете загружать ежедневные почтовые индексы, разархивировать нужный XML и анализировать его во фрейм данных, если большие ежемесячные XML-файлы создают проблемы с памятью. В качестве примера ниже извлекаются ежедневные данные за декабрь 2018 года в список фреймов данных, которые должны быть привязаны к строке в конце. Process даже добавляет поле DDate. Метод заключен в tryCatch из-за пропущенных дней в последовательности или других проблем с URL или почтовым индексом.

dec_urls <- paste0(1201:1231)
temp_zip <- "/path/to/temp.zip"
xml_folder <- "/path/to/xml/folder"

xml_process <- function(dt) {      
  tryCatch({
    # DOWNLOAD ZIP TO URL
    url <- paste0("ftp://ftp.aduanas.gub.uy/DUA%20Diarios%20XML/2018/dd2018", dt,".zip")
    file <- paste0(xml_folder, "/dding2018", dt, ".xml")

    download.file(url, temp_zip)
    unzip(temp_zip, files=paste0("dding2018", dt, ".xml"), exdir=xml_folder)
    unlink(temp_zip)           # DESTROY TEMP ZIP

    # PARSE XML TO DATA FRAME
    doc <- xmlParse(file)        
    df <- transform(xmlToDataFrame(doc, nodes=getNodeSet(doc, "//ROW_DUASDIA")),
                    DDate = as.Date(paste("2018", dt), format="%Y%m%d", origin="1970-01-01"))
    unlink(file)               # DESTROY TEMP XML

    # RETURN XML DF
    return(df)
  }, error = function(e) NA)      
}

# BUILD LIST OF DATA FRAMES
dec_df_list <- lapply(dec_urls, xml_process)

# FILTER OUT "NAs" CAUGHT IN tryCatch
dec_df_list <- Filter(NROW, dec_df_list)

# ROW BIND TO FINAL SINGLE DATA FRAME
dec_final_df <- do.call(rbind, dec_df_list)
person Parfait    schedule 28.12.2018
comment
Я собирался использовать аналогичный подход, ежедневно обрабатывая XML-файлы, а затем связывая их. Затем я заметил, что за 2001-2017 годы ежедневные файлы были удалены, и остались только ежемесячные (я предполагаю, что они удаляют все ежедневные, когда у них есть каждый месяц за год). Это означает, что мне придется найти способ работать с ежемесячными XML-файлами. Все, что я пробовал с пакетом xml2, выполняется за несколько часов на ежемесячных XML-файлах с 150 000 записей. Прямо сейчас я пробую ваш подход с xmlToDataFrame. Если это не сработает, мне придется как-то подумать о разделении XML. - person GranDurismo; 28.12.2018
comment
Обновление: февраль 2018 г. (130 тыс. записей) заняло 19 минут с использованием xmlToDataFrame. Это очень обнадеживает! Большое спасибо! Я протестирую несколько тяжелых месяцев и отчитаюсь. - person GranDurismo; 28.12.2018
comment
Рад слышать. Да, неприятный аспект для XML-файлов заключается в том, что вам нужно загрузить весь документ в память перед синтаксическим анализом. Закройте все остальные приложения перед запуском в R. Если вы обрабатываете один месяц, попробуйте уничтожить вспомогательные объекты rm(doc, dec_df_list) и освободить ресурсы с помощью gc() или сохранить фрейм данных на диск и перезапустите сеанс R. - person Parfait; 28.12.2018

Вот решение, которое обрабатывает весь документ сразу, а не читает каждую из 150 000 записей в цикле. Это должно обеспечить значительный прирост производительности.

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

library(xml2)
doc<-read_xml('<ROOT>
 <ROWSET_DUASDIA>
              <ROW_DUASDIA NUM="1">
              <variable1>value1</variable1>
              <variable191>value2</variable191>
              </ROW_DUASDIA>
              <ROW_DUASDIA NUM="150236">
              <variable1>value3</variable1>
              <variable2>value_new</variable2>
              <variable191>value4</variable191>
              </ROW_DUASDIA>
              </ROWSET_DUASDIA>
              </ROOT>')

#find all of the nodes/records
nodes<-xml_find_all(doc, ".//ROW_DUASDIA")

#find the record NUM and the number of variables under each record
nodenum<-xml_attr(nodes, "NUM")
nodeslength<-xml_length(nodes)

#find the variable names and values
nodenames<-xml_name(xml_children(nodes))
nodevalues<-trimws(xml_text(xml_children(nodes)))

#create dataframe
df<-data.frame(NUM=rep(nodenum, times=nodeslength), 
       variable=nodenames, values=nodevalues, stringsAsFactors = FALSE)

#dataframe is in a long format.  
#Use the function cast, or spread from the tidyr to convert wide format
#      NUM    variable    values
# 1      1   variable1    value1
# 2      1 variable191    value2
# 3 150236   variable1    value3
# 4 150236   variable2 value_new
# 5 150236 variable191    value4

#Convert to wide format
library(tidyr)
spread(df, variable, values)
person Dave2e    schedule 27.12.2018
comment
Ого, это выглядит намного проще. Однако последний час или около того он провел в части имен узлов. Ошибок нет, просто обработка. - person GranDurismo; 27.12.2018
comment
Мой плохой, я пропустил это. застрял при обработке имен узлов и значений. Я попробую на своем рабочем столе, когда смогу, или мне придется обрабатывать каждый ежедневный файл, а затем объединять их. - person GranDurismo; 28.12.2018
comment
Вам нужно обработать только один из узлов, чтобы получить имена, учитывая, что имена одинаковы для всех узлов. Используйте следующее, чтобы получить имена: nodenames <- xml_name(xml_children(nodes[1])). - person Edward Carney; 28.12.2018
comment
@EdwardCarney Да, я так и сделал, но мне нужно получить значения для всех записей, а это тоже очень медленно обрабатывается. - person GranDurismo; 28.12.2018