【问题标题】:How to parse an html file with a nested structure?如何解析具有嵌套结构的html文件?
【发布时间】:2016-12-27 03:12:46
【问题描述】:

使用 R 和 XML 包,我一直在尝试从结构类似于以下的 html 文件中提取地址:

<!DOCTYPE html>
  <body>
    <div class='entry'>
      <span class='name'>Marcus Smith</span>
      <span class='town'>New York</span>
      <span class='phone'>123456789</span>
    </div>
    <div class='entry'>
      <span class='name'>Henry Higgins</span>
      <span class='town'>London</span>
    </div>
    <div class='entry'>
      <span class='name'>Paul Miller</span>
      <span class='town'>Boston</span>
      <span class='phone'>987654321</span>
    </div>
  </body>
</html>

我先做以下事情

library(XML)
html <- htmlTreeParse("test.html", useInternalNodes = TRUE)
root <- xmlRoot(html)

现在,我可以用这个得到所有的名字:

xpathSApply(root, "//span[@class='name']", xmlValue)
## [1] "Marcus Smith"  "Henry Higgins" "Paul Miller"

现在的问题是某些元素并不存在于所有地址中。在示例中,这是电话号码:

xpathSApply(root, "//span[@class='phone']", xmlValue)
## [1] "123456789" "987654321"

如果我这样做,我就无法将电话号码分配给正确的人。所以,我尝试先将整个通讯录条目提取如下:

divs <- getNodeSet(root, "//div[@class='entry']")
divs[[1]]
## <div class="entry">
##   <span class="name">Marcus Smith</span>
##   <span class="town">New York</span>
##   <span class="phone">123456789</span>
## </div> 

从输出我想我已经达到了我的目标,我可以得到,例如,对应于第一个条目的名称如下:

xpathSApply(divs[[1]], "//span[@class='name']", xmlValue)
## [1] "Marcus Smith"  "Henry Higgins" "Paul Miller" 

但即使divs[[1]] 的输出只显示了Marcus Smith 的数据,我还是得到了所有三个名字。

这是为什么?我该怎么做才能以这样的方式提取地址数据,我知道nametownphone 的哪些值属于一起?

【问题讨论】:

    标签: html r html-parsing


    【解决方案1】:

    也许 xpath 表达式有问题,“//”总是指向根元素?

    这段代码作用于测试数据:

    one.entry <- function(x) {
        name <- getNodeSet(x, "span[@class='name']")
        phone <- getNodeSet(x, "span[@class='phone']")
        town <- getNodeSet(x, "span[@class='town']")
    
        name <- if(length(name)==1) xmlValue(name[[1]]) else NA
        phone <- if(length(phone)==1) xmlValue(phone[[1]]) else NA
        town <- if(length(town)==1) xmlValue(town[[1]]) else NA
    
        return(data.frame(name=name, phone=phone, town=town, stringsAsFactors=F))
    }
    
    do.call(rbind, lapply(divs, one.entry))
    

    【讨论】:

    • 非常感谢。确实,// 似乎进入了根目录。这也有效:xpathSApply(divs[[1]], "span[@class='name']", xmlValue)。我知道您可以使用 //node/node 搜索节点,但不知道 node 也可以。
    【解决方案2】:

    如果每个条目的项目数量未知,您可以利用 dplyr::bind_rowsdata.table::rbindlistrvest 组合,如下所示:

    require(rvest)
    require(dplyr)
    # Little helper-function to extract all children and set Names
    extract_info <- function(node){
      child <- html_children(node)
      as.list(setNames(child %>% html_text(), child %>% html_attr("class")))
    }
    
    doc <- read_html(txt)
    doc %>% html_nodes(".entry") %>% lapply(extract_info) %>% bind_rows
    

    给你:

               name     town     phone
              (chr)    (chr)     (chr)
    1  Marcus Smith New York 123456789
    2 Henry Higgins   London        NA
    3   Paul Miller   Boston 987654321
    

    或者使用rbindlist(fill=TRUE) 而不是bind_rows,这会导致data.table。或者使用purrr 改用map_df(as.list)

    【讨论】:

    • 感谢rvest,我不知道。对于我目前的小项目,我不会重写所有内容以使用rvest,因为感谢 Karsten 的回答,我只通过更改我的代码的小部分来实现我的目标。不过下次我会试一试的。
    【解决方案3】:

    purrr 使rvest 更有用,因为它可以嵌套节点并将结果列表转换为 data.frame:

    library(rvest)
    library(purrr)
    
    html %>% read_html() %>% 
        # select all entry divs
        html_nodes('div.entry') %>% 
        # for each entry div, select all spans, keeping results in a list element
        map(html_nodes, css = 'span') %>% 
        # for each list element, set the name of the text to the class attribute
        map(~setNames(html_text(.x), html_attr(.x, 'class'))) %>% 
        # convert named vectors to list elements; convert list to a data.frame
        map_df(as.list) %>% 
        # convert character vectors to appropriate types
        dmap(type.convert, as.is = TRUE)
    
    ## # A tibble: 3 x 3
    ##            name     town     phone
    ##           <chr>    <chr>     <int>
    ## 1  Marcus Smith New York 123456789
    ## 2 Henry Higgins   London        NA
    ## 3   Paul Miller   Boston 987654321
    

    当然,您可以将所有 purrr 替换为 base,尽管这需要更多步骤。

    【讨论】:

    • 可能会使用 purrr::set_names()setNames(),因为您已经对 purrr 习语投入了很多。
    • 我从来没有发现它非常必要,因为setNames 已经很容易pipable。最终我想我是出于习惯坚持使用setNames,因为我并不总是加载purrr
    【解决方案4】:

    丑陋的基础 R+rvest 解决方案(但我作弊并使用管道来避免地狱般的嵌套括号或临时分配)来展示 ++gd @alistaire 的解决方案是如何的:

    library(rvest)
    library(magrittr)
    
    read_html("<!DOCTYPE html>
      <body>
        <div class='entry'>
          <span class='name'>Marcus Smith</span>
          <span class='town'>New York</span>
          <span class='phone'>123456789</span>
        </div>
        <div class='entry'>
          <span class='name'>Henry Higgins</span>
          <span class='town'>London</span>
        </div>
        <div class='entry'>
          <span class='name'>Paul Miller</span>
          <span class='town'>Boston</span>
          <span class='phone'>987654321</span>
        </div>
      </body>
    </html>") -> pg
    
    pg %>% 
      html_nodes('div.entry') %>% 
      lapply(html_nodes, css='span') %>% 
      lapply(function(x) { 
        setNames(html_text(x), html_attr(x, 'class')) %>% 
          as.list() %>% 
          as.data.frame(stringsAsFactors=FALSE)
      }) %>% 
      lapply(., unlist) %>% 
      lapply("[", unique(unlist(c(sapply(., names))))) %>% 
      do.call(rbind, .) %>% 
      as.data.frame(stringsAsFactors=FALSE)
    

    【讨论】:

      猜你喜欢
      • 2015-01-18
      • 2019-05-28
      • 2021-12-17
      • 2011-01-28
      • 1970-01-01
      • 2018-04-10
      • 2015-01-15
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多