【问题标题】:XML with varying number of subchildren to data frame/tableXML 具有不同数量的子数据到数据框/表
【发布时间】:2019-10-03 16:45:01
【问题描述】:

我正在尝试将数据从 XML 转换为表格形式。我正在与子元素的元素作斗争。这是一个例子:

library(xml2)
library(data.table)

doc =
"<doc>
    <rec>
        <name> John </name>
        <address>
            <street> 2nd Av </street>
            <number> 1036 </number>
        </address>
        <hobbies>
            <hobby> tennis </hobby>
            <hobby> gardening </hobby>    
        </hobbies>
    </rec>
    <rec>
        <name> Mary </name>
        <address>
            <street>55th St</street>
            <number> 132 </number>
        </address>
        <hobbies>
            <hobby> running </hobby>
        </hobbies>
    </rec>
</doc>
"

# read in
pg <- read_xml(doc)

# make a list of records
recs = xml_find_all(pg, "//rec", xml_ns(pg))

# function to loop over list
extractRecord = function(x) {
    
    txt = xml_text(xml_children(x))
    name = xml_name(xml_children(x))
    names(txt) = name
    
    dt = setDT(as.list(txt))[]
    return(dt)
}

# loop over list of records
lst = lapply(recs, extractRecord)

# bind elements do a data table
dt  = rbindlist(lst, use.names = T, fill = T); dt

>      name        address             hobbies
> 1:  John   2nd Av  1036   tennis  gardening 
> 2:  Mary    55th St 132             running 

这就像一个魅力,除了我想拥有:

  1. 两列用于“地址”,每个子元素各一列,例如 address.street 和 address.number。
  2. 爱好的两列——比如说爱好 1 和爱好 2。需要注意的是,子子节点的数量是允许变化的。

最后,我会得到类似的东西

如果可能的话,我还想坚持使用xml2 包(因为我有很多大文件,而XML 包有已知的内存问题,这对我来说是个问题)。

【问题讨论】:

    标签: r xml xml2


    【解决方案1】:

    考虑XSLT 专用的声明性语言(与SQL 相同的类型),旨在转换XML 文件,例如展平您的原始输入。在 R 中,XSLT 可以与 xml2:xslt 的姐妹包一起运行。由于它是一种工业语言,它可以与其他通用语言(即 Java、Python)、CLI(Bash、PowerShell)或 R 可以在命令行中调用的executables(Saxon、Xalan)一起运行system().

    library(xslt)
    
    xsl <- '<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
      <xsl:output indent="yes"/>
      <xsl:strip-space elements="*"/>
    
      <xsl:template match="@*|node()">
        <xsl:copy>
          <xsl:apply-templates select="@*|node()"/>
        </xsl:copy>
      </xsl:template>
    
      <xsl:template match="text()">
        <xsl:value-of select="normalize-space()"/>
      </xsl:template>
    
      <xsl:template match="/*/*">
        <xsl:copy>
          <xsl:copy-of select="*[not(*)]"/>
          <xsl:apply-templates select="*"/>
        </xsl:copy>
      </xsl:template>
    
      <xsl:template match="/*/*/*">
          <xsl:apply-templates select="*"/>
      </xsl:template>
    
    </xsl:stylesheet>'
    

    见:Online demo

    与以前类似的过程,但有创建 new_pg 的转换步骤:

    # read in
    pg <- read_xml(doc)
    style <- read_xml(xsl, package = "xslt")
    
    # transform original
    new_pg <- xml_xslt(pg, style)
    
    # make a list of records
    recs <- xml_find_all(new_pg, "//rec")
    
    # function to loop over list
    extractRecord <- function(x) {      
      txt <- setNames(xml_text(xml_children(x)), 
                      xml_name(xml_children(x))
             )
    
      dt <- setDT(as.list(txt))[]
      return(dt)
    }
    
    # loop over list of records
    lst <- lapply(recs, extractRecord)
    
    # bind elements do a data table
    dt <- rbindlist(lst, use.names = TRUE, fill = TRUE)
    dt
    #     name   street number     hobby       hobby
    # 1:  John   2nd Av   1036    tennis   gardening 
    # 2:  Mary  55th St    132   running        <NA>
    

    为避免重复列(即,爱好)在 XSLT 的末尾添加此模板(在关闭 &lt;/xsl:stylesheet&gt; 之前),您可以在其中使用管道分隔您预先知道的任何其他列将具有重复列:

      <!-- PIPE DELIMIT ANY REPEAT NAMED COLS IN TEMPLATE MATCH-->
      <xsl:template match="hobby|anothernode|othernode|stillothernode">
        <xsl:variable name="num" select="concat(name(), count(preceding-sibling::*)+1)"/>
        <xsl:element name="{$num}">
          <xsl:value-of select="normalize-space()"/>
        </xsl:element>  
      </xsl:template>
    

    【讨论】:

    • 很好,我不知道xslt。它是否能很好地处理大型(即,有许多记录,在 100 兆字节的球场上)XML 文件?唯一让我失望的是(就像上面@JasonAizkalns 的回答一样)必须枚举每个要提取的字段。我很惊讶我的问题没有现成的解决方案。
    • XSLT 非常适合 100 MB 到中等大小的文件。超过 1 GB,它会遇到内存问题,因为它必须读取整个 XML 文件并进行解析。不清楚每个字段的含义。您设置的相同代码正在此处运行,即 rec 节点的所有子节点。
    • 谢谢,@Parfait。对于每一个领域,我的意思是以下。想象一下,除了姓名、地址和爱好之外,每条记录还有另外 50 种类型的标签(在表格格式中将变成 50 个额外的列或字段),还有子子项。在您的解决方案中,您对带有子子项的字段进行了硬编码以进行解析。有了更多这样的元素,它变得有点痛苦。
    • 请参阅更新 XSLT,其中没有节点是硬编码的,但假定只有三级 XML。但是,挑战是像 hobby 这样的同名列将在没有 # 后缀的情况下重复。您可以在 R 中清理后端或添加另一个 XSLT 模板(请参阅末尾的注释),这将只是硬编码的项目,希望您可以提前知道并且数量有限。
    【解决方案2】:

    因此,如果您愿意使用tidyverse,这是一种方法。首先更改您的函数以提取 individual 记录的所有数据:

    library(tidyverse)
    
    get_elements <- function(rec) {
    
      name = xml_find_all(rec, "name") %>% xml_text
    
      hobbies = xml_find_all(rec, "hobbies")
      hobby_list = hobbies %>% xml_find_all("hobby") %>% xml_text
    
      address = xml_find_all(rec, "address")
      street = address %>% xml_find_all("street") %>% xml_text
      street_num = address %>% xml_find_all("number") %>% xml_text
    
      df = tibble(
        name = str_squish(name),
        street = str_squish(street), 
        street_num = str_squish(street_num),
        hobbies = str_squish(hobby_list)
      )
      return(df)
    }
    

    所以现在,对于任何给定的记录(例如recs[1]recs[2]),我们都会返回一个表格:

    get_elements(recs[1])
    #> # A tibble: 2 x 4
    #>   name  street street_num hobbies  
    #>   <chr> <chr>  <chr>      <chr>    
    #> 1 John  2nd Av 1036       tennis   
    #> 2 John  2nd Av 1036       gardening
    get_elements(recs[2])
    #> # A tibble: 1 x 4
    #>   name  street  street_num hobbies
    #>   <chr> <chr>   <chr>      <chr>  
    #> 1 Mary  55th St 132        running
    

    然后使用您喜欢的方法组合这些表格:

    res_df <- 
    bind_rows(
      get_elements(recs[1]),
      get_elements(recs[2])
    )
    
    # More tidyverse/purrr-like:
    res_df <- 
      recs %>%
      map_df(get_elements)
    
    res_df
    #> # A tibble: 3 x 4
    #>   name  street  street_num hobbies  
    #>   <chr> <chr>   <chr>      <chr>    
    #> 1 John  2nd Av  1036       tennis   
    #> 2 John  2nd Av  1036       gardening
    #> 3 Mary  55th St 132        running
    

    最后,进行一些数据整理以将数据转换为所需的最终格式:

    res_df %>% 
      group_by(name) %>%
      mutate(
        hobby_idx = paste0("hobby", row_number())
      ) %>%
      pivot_wider(
        names_from = hobby_idx,
        values_from = hobbies
      )
    #> # A tibble: 2 x 5
    #> # Groups:   name [2]
    #>   name  street  street_num hobby1  hobby2   
    #>   <chr> <chr>   <chr>      <chr>   <chr>    
    #> 1 John  2nd Av  1036       tennis  gardening
    #> 2 Mary  55th St 132        running <NA>
    

    【讨论】:

    • 我发现这种方法有两个问题。首先,在大数据中搜索每个标签(姓名、爱好、地址等)可能很麻烦。现在我看到我应该在我的 OP 中更明确地说明这一点。其次,更重要的是,如果一条记录没有爱好(正如我在 OP 中所说,数量不同),则整个记录“丢失”,即代码返回一个 0 行的 tibble。
    猜你喜欢
    • 2017-03-17
    • 1970-01-01
    • 1970-01-01
    • 2014-12-16
    • 2015-03-11
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多