解析具有已知结构和重复元素的XML文件

时间:2015-08-26 00:49:22

标签: xml r

我正在尝试从包含大量具有重复名称的元素的XML文件中解析信息。

以下是我尝试解析的文件类型的示例,其中只包含一条记录:

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<array>
    <!--
        Start of the FIRST record.
    -->
    <dict>
        <key>80211D_IE</key>
        <dict>
            <key>IE_KEY_80211D_CHAN_INFO_ARRAY</key>
            <array>
                <dict>
                    <key>IE_KEY_80211D_FIRST_CHANNEL</key>
                    <integer>1</integer>
                    <key>IE_KEY_80211D_MAX_POWER</key>
                    <integer>27</integer>
                    <key>IE_KEY_80211D_NUM_CHANNELS</key>
                    <integer>11</integer>
                </dict>
            </array>
            <key>IE_KEY_80211D_COUNTRY_CODE</key>
            <string>US</string>
        </dict>
        <key>AGE</key>
        <integer>0</integer>
        <key>AP_MODE</key>
        <integer>2</integer>
        <key>BEACON_INT</key>
        <integer>100</integer>
        <key>BSSID</key>
        <string>ac:5d:10:73:c3:11</string>
        <key>CAPABILITIES</key>
        <integer>1073</integer>
        <key>CHANNEL</key>
        <integer>2</integer>
        <key>CHANNEL_FLAGS</key>
        <integer>10</integer>
        <key>IE</key>
        <data>
        AAZPbGl2ZXIBCIKEiwwSlhgkAwECBwZVUyABCxswGAEAAA+sAgIAAA+sBAAP
        rAIBAAAPrAIAAN0aAFDyAQEAAFDyAgIAAFDyBABQ8gIBAABQ8gIqAQAyBDBI
        YGw=
        </data>
        <key>NOISE</key>
        <integer>0</integer>
        <key>RATES</key>
        <array>
            <integer>1</integer>
            <integer>2</integer>
            <integer>5</integer>
            <integer>6</integer>
            <integer>9</integer>
            <integer>11</integer>
            <integer>12</integer>
            <integer>18</integer>
            <integer>24</integer>
            <integer>36</integer>
            <integer>48</integer>
            <integer>54</integer>
        </array>
        <key>RSN_IE</key>
        <dict>
            <key>IE_KEY_RSN_AUTHSELS</key>
            <array>
                <integer>2</integer>
            </array>
            <key>IE_KEY_RSN_MCIPHER</key>
            <integer>2</integer>
            <key>IE_KEY_RSN_UCIPHERS</key>
            <array>
                <integer>4</integer>
                <integer>2</integer>
            </array>
            <key>IE_KEY_RSN_VERSION</key>
            <integer>1</integer>
        </dict>
        <key>RSSI</key>
        <integer>-74</integer>
        <key>SSID</key>
        <data>
        T2xpdmVy
        </data>
        <key>SSID_STR</key>
        <string>Oliver</string>
        <key>WPA_IE</key>
        <dict>
            <key>IE_KEY_WPA_AUTHSELS</key>
            <array>
                <integer>2</integer>
            </array>
            <key>IE_KEY_WPA_MCIPHER</key>
            <integer>2</integer>
            <key>IE_KEY_WPA_UCIPHERS</key>
            <array>
                <integer>4</integer>
                <integer>2</integer>
            </array>
            <key>IE_KEY_WPA_VERSION</key>
            <integer>1</integer>
        </dict>
    </dict>
    <!--
        End of the FIRST record.
        In reality, more records follow.
    -->
</array>
</plist>

我遇到的问题是我想基本上展平每个观察(上例中只有一个观察),以便第一个<array>内的每个元素(即<dict>内的每个<array> }})是数据框中的一行,<dict>中的每个元素都是一列,由相应的<key>命名。

我已经在XML包中试验了函数,主要是xmlToList,但还没有找到解析XML数据的正确方法。

修改

我想要的输出是将每个记录或多或少地压平到数据框或列表中的一行,以便可以通过键轻松访问值。我不一定关心保留任何层次结构,例如每个记录都有<key>80211D_IE</key>后跟一个包含实际信息的dict - <key>80211D_IE</key>不是必需的,因为它不是不包含任何真实信息,但只是对一组项目的不必要的分组。我可以将其存储为列表,例如mydata$record1$X80211D_IE$I.E._KEY_80211D_CHAN_INFO_ARRAY$IE_KEY_80211D_FIRST_CHANNEL,或者存储在mydata[1, 'I.E._KEY_80211D_FIRST_CHANNEL']等数据框中。

我现在面临的最大问题是这种XML结构似乎不适合解析。例如,如果我想将XML子集化为SSID_STR匹配字符串的记录,我不能只使用xmlToList,因为它不知道密钥应该与它们的值相关联。所以我得到一个这样的列表:

> str(xmlToList("path/to/my/file.xml"), max.level=2)
List of 2
 $ array :List of 25
  ..$ dict:List of 36
  ..$ dict:List of 32
  ..$ dict:List of 32
  ..$ dict:List of 38
  ..$ dict:List of 36
  ..$ dict:List of 34
  ..$ dict:List of 34
  ..$ dict:List of 34
  ..$ dict:List of 34
  ..$ dict:List of 34
  ..$ dict:List of 32
  ..$ dict:List of 38
  ..$ dict:List of 38
  ..$ dict:List of 34
  ..$ dict:List of 36
  ..$ dict:List of 34
  ..$ dict:List of 36
  ..$ dict:List of 34
  ..$ dict:List of 36
  ..$ dict:List of 36
  ..$ dict:List of 40
  ..$ dict:List of 42
  ..$ dict:List of 36
  ..$ dict:List of 38
  ..$ dict:List of 38
 $ .attrs: Named chr "1.0"
  ..- attr(*, "names")= chr "version"

并查看其中一个

> str(xmlToList("path/to/my/file.xml")$array[[1]], max.level = 1)
List of 36
 $ key    : chr "80211D_IE"
 $ dict   :List of 4
 $ key    : chr "AGE"
 $ integer: chr "0"
 $ key    : chr "AP_MODE"
 $ integer: chr "2"
 $ key    : chr "BEACON_INT"
 $ integer: chr "100"
 $ key    : chr "BSSID"
 $ string : chr "a:18:a:31:0:83"
 $ key    : chr "CAPABILITIES"
 $ integer: chr "4145"
 $ key    : chr "CHANNEL"
 $ integer: chr "11"
 $ key    : chr "CHANNEL_FLAGS"
 $ integer: chr "10"
 $ key    : chr "HT_CAPS_IE"
 $ dict   :List of 12
 $ key    : chr "HT_IE"
 $ dict   :List of 34
 $ key    : chr "IE"
 $ data   : chr "\n\t\tAAR0ZXN0AQiWlgwSGCQwSAMBCwcGVVMgAQseKgEDMBgBAAAPrAICAAAPrAQA\n\t\tD6wCAQAAD6wCAAAyAmBsRgVzwAEAADMCDAstGowRG///AAAAAAAAAAA"| __truncated__
 $ key    : chr "NOISE"
 $ integer: chr "0"
 $ key    : chr "RATES"
 $ array  :List of 9
 $ key    : chr "RSN_IE"
 $ dict   :List of 8
 $ key    : chr "RSSI"
 $ integer: chr "-86"
 $ key    : chr "SSID"
 $ data   : chr "\n\t\tdGVzdA==\n\t\t"
 $ key    : chr "SSID_STR"
 $ string : chr "test"
 $ key    : chr "WPA_IE"
 $ dict   :List of 8

很容易看到,实际上只有18个项目,但是密钥存储为自己的项目(36个)。

xmlToList函数实际上几乎我想要它做什么 - 而是使用相应键的值命名包含数据的列表元素。

这看起来像是:

List of 18
 $ AGE          : chr "0"
 $ AP_MODE      : chr "2"
 $ BEACON_INT   : chr "100"
 $ BSSID        : chr "a:18:a:31:0:83"
 $ CAPABILITIES : chr "4145"
 $ CHANNEL      : chr "11"
 $ CHANNEL_FLAGS: chr "10"
 $ HT_CAPS_IE   :List of 12
 $ HT_IE        :List of 34
 $ IE           : chr "\n\t\tAAR0ZXN0AQiWlgwSGCQwSAMBCwcGVVMgAQseKgEDMBgBAAAPrAICAAAPrAQA\n\t\tD6wCAQAAD6wCAAAyAmBsRgVzwAEAADMCDAstGowRG///AAAAAAAAAAA"| __truncated__
 $ NOISE        : chr "0"
 $ RATES        :List of 9
 $ RSN_IE       :List of 8
 $ RSSI         : chr "-86"
 $ SSID         : chr "\n\t\tdGVzdA==\n\t\t"
 $ SSID_STR     : chr "test"
 $ WPA_IE       :List of 8
 $ X80211D_IE   :List of 4

在这个假设输出中,使用适当的密钥很容易获得值。此外,很容易继续删除列表(因为不需要分组结构)来生成数据帧。

2 个答案:

答案 0 :(得分:3)

我将OP的XML存储在文件中,但复制了所提供的单个记录!

使用一些额外的附加软件包(我会使用dplyr%>%),这可能会更加流畅,但我拒绝了。我建议使用xml2代替XML。您可以使用XPATH表达式来定位感兴趣的节点。

x <- read_xml("so.xml")
(elements <- xml_find_all(x, ".//dict/dict/array/dict"))
#> {xml_nodeset (2)}
#> [1] <dict>\n                    <key>IE_KEY_80211D_FIRST_CHANNEL</key>\n ...
#> [2] <dict>\n                    <key>IE_KEY_80211D_FIRST_CHANNEL</key>\n ...

## isolate the key nodes ... will become variable names
keys <- lapply(elements, xml_find_all, "key")
keys <- lapply(keys, xml_text)
## I advise checking that keys are uniform across the records here!
(keys <- keys[[1]])
#> [1] "IE_KEY_80211D_FIRST_CHANNEL" "IE_KEY_80211D_MAX_POWER"    
#> [3] "IE_KEY_80211D_NUM_CHANNELS"

## isolate integer data
integers <- lapply(y, xml_find_all, "integer")
integers <- lapply(integers, xml_text)
integers <- lapply(integers, type.convert)
yay <- as.data.frame(do.call(rbind, integers))
names(yay) <- keys
yay
#>   IE_KEY_80211D_FIRST_CHANNEL IE_KEY_80211D_MAX_POWER
#> 1                           1                      27
#> 2                           1                      27
#>   IE_KEY_80211D_NUM_CHANNELS
#> 1                         11
#> 2                         11

答案 1 :(得分:1)

重要编辑问题后的新答案。

我将OP的XML存储在一个文件中但是重复提供了单个记录! 我现在让自己使用%>%。 OP每个记录有16个元素 获得18,因为发布的实际XML不包含任何证据 HT_CAPS_IEHT_IE。鉴于我们现在采取的方式,还有更多 关于列表上的计算而不是XML,这似乎是不可避免的。链接 密钥和数据之间的关系更多地基于邻接而不是结构。

library(magrittr)
library(xml2)

## ugly workaround: xml2 does not seem to ignore insignificant whitespace?
x <- "so.xml" %>%
  scan(what = character(), sep = "\n", strip.white = TRUE) %>%
  paste0(collapse = "") %>% 
  read_xml

## isolate each record
(records <- x %>%
  xml_children() %>%
  xml_children())
#> {xml_nodeset (2)}
#> [1] <dict>\n  <key>80211D_IE</key>\n  <dict>\n    <key>IE_KEY_80211D_CHA ...
#> [2] <dict>\n  <key>80211D_IE</key>\n  <dict>\n    <key>IE_KEY_80211D_CHA ...

## turn each record into a list
records_list <- records %>% lapply(as_list)
str(records_list, max.level = 1)
#> List of 2
#>  $ :List of 32
#>  $ :List of 32

## IRL here's where I check that ...
##  we have key, THINGY, key, THINGY, etc. within each record
##  we have THINGY1, THINGY2, etc. across all records

## store item names from record 1
keys <- records_list[[1]][c(TRUE, FALSE)] %>% unlist

## isolate the data, do obvious simplifications, apply item names
jfun <- function(x) if(is.list(x) && length(x) > 1) x else unlist(x)
z <- records_list %>%
  lapply(`[`, c(FALSE, TRUE)) %>% 
  lapply(`names<-`, keys) %>% 
  lapply(lapply, jfun)

## done!
str(z[[1]], max.level = 1)
#> List of 16
#>  $ 80211D_IE    :List of 4
#>  $ AGE          : chr "0"
#>  $ AP_MODE      : chr "2"
#>  $ BEACON_INT   : chr "100"
#>  $ BSSID        : chr "ac:5d:10:73:c3:11"
#>  $ CAPABILITIES : chr "1073"
#>  $ CHANNEL      : chr "2"
#>  $ CHANNEL_FLAGS: chr "10"
#>  $ IE           : chr "AAZPbGl2ZXIBCIKEiwwSlhgkAwECBwZVUyABCxswGAEAAA+sAgIAAA+sBAAPrAIBAAAPrAIAAN0aAFDyAQEAAFDyAgIAAFDyBABQ8gIBAABQ8gIqAQAyBDBIYGw="
#>  $ NOISE        : chr "0"
#>  $ RATES        :List of 12
#>  $ RSN_IE       :List of 8
#>  $ RSSI         : chr "-74"
#>  $ SSID         : chr "T2xpdmVy"
#>  $ SSID_STR     : chr "Oliver"
#>  $ WPA_IE       :List of 8