R语言学习数据科学与R语言

简单认识XPath及在R语言中的简单应用

2017-10-25  本文已影响57人  distiner

这是一篇简单文章,主要目的在于展示XPath的不同使用方法,当然,因为个人的喜好,所以示例当然是通过R语言来实现,顺带也简单的介绍了通过RCurl配合XML或者rvest这几个package来从网页获取简单数据,不涉及复杂数据的获取。本文的主要实例来自于凤凰网的汽车板块

以下是我的简单的初始代码

library(RCurl)
library(XML)
library(tidyverse)
library(stringr)

#定向解析网页
url = 'http://car.auto.ifeng.com/'
urlpage = XML::htmlParse(url)

我在这里用XML::htmlParse(url)的方式来表示对某个特定package的具体函数的引用,这样方便我们以后能清晰的记得某个函数的具体来源,做为新手,这是一个较好的建议,事实上,我在看网上代码的时候经常对某个函数的来源非常疑惑。

我们利用firefox浏览器的firebug插件查看随意的两个我们需要提取的汽车品牌名称,可以发现以下的xpath路径:

看看下面的截图:

get the absolute xpath with FirePath in Firefox.JPG

我们对我们关心的简单的分析一下:

一级品牌名称

html/body/div[7]/div[2]/dl[1]/dt/a[2]
html/body/div[7]/div[2]/dl[2]/dt/a[2]
html/body/div[7]/div[2]/dl[3]/dt/a[2]
html/body/div[7]/div[2]/dl[4]/dt/a[2]

二级品牌名称

html/body/div[7]/div[2]/dl[3]/dd/div/a
html/body/div[7]/div[2]/dl[4]/dd/div[1]/a
html/body/div[7]/div[2]/dl[4]/dd/div[3]/a

三级车型名称

html/body/div[7]/div[2]/dl[1]/dd/ul/li[1]/a
html/body/div[7]/div[2]/dl[1]/dd/ul/li[2]/a
html/body/div[7]/div[2]/dl[3]/dd/ul/li/a
html/body/div[7]/div[2]/dl[4]/dd/ul[1]/li[1]/a
html/body/div[7]/div[2]/dl[4]/dd/ul[2]/li[1]/a

我们首先需要分析上述xpath的绝对路径的规律:

以上的分析结论可以为我们对本次提取任务有一个大概的认知,我们需要在这个基础上进行分析和验证,最终得到我们需要的方法。

除了使用绝对路径之外,我们还可以使用相对路径以及谓词等来实现提取的过程。

在这里,我们不推荐使用相对路径,对于较小的html文件,我们可以使用相对路径,因为这不会导致计算量的增加,但是在解析大型网页的时候,使用绝对路径是比较安全和便捷的方法,这样并不会增加计算量,从而导致解析的时间大大缩短。至于谓词以及继承关系等等其它的xpath方式,我们接下来尽量一一实现一次。

我们使用以下的语句来实现提取汽车一级、二级以及三级品牌的过程:

# 利用XML package的xpathSApply函数来解决直接读取凤凰网汽车板块的所有汽车品牌名称
# 第一个参数是已经解析的网页,第二参数是xpath的绝对路径,第三个参数是指定需要获取的节点的具体部分,xmlvalue指取该节点的参数
# 节点参数见下表
# 注意a[2]这个写法,如果不加入[2]的话,会导致后面处理的时候有其它问题出现,可以试着不加[2]看看
MainBrand = XML::xpathSApply(urlpage, '//body/div[7]/div/dl/dt/a[2]', fun = xmlValue)
SubBrand = XML::xpathSApply(urlpage, '//body/div[7]/div/dl/dd/div/a', fun = xmlValue)
ModelBrand = XML::xpathSApply(urlpage, '//body/div[7]/div/dl/dd/ul/li/a', fun = xmlValue)

简单的浏览下节点参数对照(对应fun = xmlvlue)

xml节点参数的取值.JPG

以上虽然实现了提取的过程,但是很明显,这种结果不是我们需要的,我们无法将各级品牌以及车型对应起来。那么唯一能做的就是用函数来实现提取的过程。稍后,我们会编写自己的代码来实现这个过程。现在让我们仔细来回顾下前面的提取过程。

让我们仔细分析下MainBrand的提取过程:

  1. 我们利用FirePath提取的绝对路径是类似于html/body/div[7]/div[2]/dl[1]/dt/a[2]这样的,但是我们的提取并不是这样的过程,而是类似于//body/div[7]/div/dl/dt/a[2]这样的结构,我们来仔细解读下:
TotalWebPage.JPG

接下来再看看我们的webpage的html分析的总体结果:

TotalHtmlPageParse.JPG
我们总共发现了8个div节点,那么div[7]是不是就是我们需要的第7个div节点呢?我们点击这个我们猜测中的 正确 的div节点前面的+,展开它,然后把鼠标放上去看看?看起来这个<div class="w1000">节点包含了我们需要的数据呀 We are so wise!!! 接下来的其它分析也是如此的顺利成章了。现在让我们来一一回答上面的几个问题:

既然说到了这里,那么我们就干脆先放下我们的终极目标--获取汽车品牌及车型,我们先好好对这个div[7]唠嗑唠嗑

我们首先想到的:div[7]难道就因为它是body的第7个子节点并且我们的数据在里面,so,我们就只能用这一种写法?

白眼送给你

那么我们能够用哪些方法来表述这div[7]呢?

让我们再看看XPath相关的两个介绍图

节点继承关系图

以及

节点继承关系说明表

好了,截至到目前,我们没有对该页面有任何实质性的进展,那么,在了解了如何使用XPath之后,我们分别用RCurl+XML以及RVEST这两种方式来分别实现一次对我们关心的数据的解析吧。

以下的实际代码中XPath并不是上述的方法,大家可以自行比较优劣

首先来看RCurl+XML的方法:

整体的解析规则:

我们可以通过以下代码段知道有多少个字母(本例总共应该有22个字母打头的)

NumAlph = length(XML::xpathSApply(urlpage, '//body/div[@class="w1000"]/div[@class="lt-list"]'))

我们也可以通过下面的代码块获取详细的22个打头字母

XML::xpathSApply(urlpage, '//body/div[@class="w1000"]/div[@class="lt-list"]/div/a', fun = xmlValue)

关于每个打头字母下分别对应有多少个主品牌,我们的示例代码如下:

此处的div[@class="w1000"]/div[position()=2]必须从position()=2开始,从2开始,到23结束

NumMainBrand = length(XML::xpathSApply(urlpage, '//body/div[@class="w1000"]/div[position()=2]//dl'))

接下来的代码试着获取了字母A对应的主品牌的名称:

XML::xpathSApply(urlpage, '//body/div[@class="w1000"]/div[position()=2]//dl//a[@class="brand"]', fun = xmlValue)

每个主品牌对应多少个子品牌:

下列语句解析了第一个字母对应的其中一个主品牌的子品牌的个数
本例为字母为"A"开头的(div[position()=2])(总共有5个主品牌)主品牌,第4个主品牌(dl[position()=4])的子品牌数目

NumSubBrand = length(XML::xpathSApply(urlpage, '//body/div[@class="w1000"]/div[position()=2]//dl[position()=4]//div[@class="md-tit"]'))

下面的示例的解释:字母为"A"开头的(div[position()=2])(总共有5个主品牌)主品牌,第4个主品牌(dl[position()=4])的子品牌名称

XML::xpathSApply(urlpage, '//body/div[@class="w1000"]/div[position()=2]//dl[position()=4]//div[@class="md-tit"]/a', fun = xmlValue)

接下里我们需要分析每一个子品牌对应的车型的具体数量

下列语句表示为字母为"A"开头的(div[position()=2])(总共有5个主品牌)主品牌,第4个主品牌(dl[position()=4])的第一个子品牌(ul[position()=1])的具体车型数量

NumModelBrand = length(XML::xpathSApply(urlpage, '//body/div[@class="w1000"]/div[position()=2]//dl[position()=4]//ul[position()=1]/li'))

相应的,下列语句表示为字母为"A"开头的(div[position()=2])(总共有5个主品牌)主品牌,第4个主品牌(dl[position()=4])的第一个子品牌(ul[position()=1])的具体车型

XML::xpathSApply(urlpage, '//body/div[@class="w1000"]/div[position()=2]//dl[position()=4]//ul[position()=1]/li/a', fun = xmlValue)

第一次的代码如下:

###=======================================================
library(XML)
library(tidyverse)
library(stringr)

#定向解析网页
url = 'http://car.auto.ifeng.com/'
urlpage = XML::htmlParse(url)
Brand.list = list()
SubBrand.list = list()
ModelBrand.list = list()
NumAlph = length(XML::xpathSApply(urlpage, '//body/div[@class="w1000"]/div[@class="lt-list"]'))
Alph = XML::xpathSApply(urlpage, '//body/div[@class="w1000"]/div[@class="lt-list"]/div/a', fun = xmlValue)
Abbreviation = '//body/div[@class="w1000"]/div[position()='
for (i in 1:NumAlph){
  # browser()
  NumMainBrand = length(XML::xpathSApply(urlpage, str_c(Abbreviation, i+1, ']//dl')))
  MainBrand = XML::xpathSApply(urlpage, str_c(Abbreviation, i+1, ']//dl//a[@class="brand"]'), fun = xmlValue)
  for (j in 1:NumMainBrand){
    # browser()
    NumSubBrand = length(XML::xpathSApply(urlpage, str_c(Abbreviation, i+1, ']//dl[position()=', j, ']//div[@class="md-tit"]')))
    SubBrand = XML::xpathSApply(urlpage, str_c(Abbreviation, i+1, ']//dl[position()=', j, ']//div[@class="md-tit"]/a'), fun = xmlValue)
      for (k in 1:NumSubBrand){
        # browser()
        NumModelBrand = length(XML::xpathSApply(urlpage, str_c(Abbreviation, i+1,']//dl[position()=', j, ']//ul[position()=', k, ']/li')))
        ModelBrand = XML::xpathSApply(urlpage, str_c(Abbreviation, i+1, ']//dl[position()=', j, ']//ul[position()=', k, ']/li/a'), fun = xmlValue)
        ModelBrand.list[[k]] = data.frame(ModelBrand = ModelBrand, Alph = Alph[i], MainBrand = MainBrand[j], SubBrand = SubBrand[k], 
                                          stringsAsFactors = FALSE)
      }
    SubBrand.list[[j]] = plyr::rbind.fill(ModelBrand.list)
  }
  Brand.list[[i]] = plyr::rbind.fill(SubBrand.list)
}
Brand = plyr::rbind.fill(Brand.list)%>%
  group_by(Alph, MainBrand, SubBrand, ModelBrand)%>%
  summarise(n= n())

但是这个代码爬出来的数据总共只有1520条(2017年10月24日数据 ),跟实际的数据对不上啊,而且,我们的本意是通过上面的for循环之后的rbind.fill函数就能直接得出我们想要的data.frame格式的数据,但是为什么实际结果不是的呢?

其实上面真不是正确的code,那么正确的长啥样?LOOK!

setwd('C:\\ACYDrelation')
library(RCurl)
library(XML)
library(tidyverse)
library(stringr)

#定向解析网页
url = 'http://car.auto.ifeng.com/'
urlpage = XML::htmlParse(url)

Brand.list = list()
# SubBrand.list = list()
# ModelBrand.list = list()
NumAlph = length(XML::xpathSApply(urlpage, '//body/div[@class="w1000"]/div[@class="lt-list"]'))
Alph = XML::xpathSApply(urlpage, '//body/div[@class="w1000"]/div[@class="lt-list"]/div/a', fun = xmlValue)
Abbreviation = '//body/div[@class="w1000"]/div[position()='
for (i in 1:NumAlph){
  SubBrand.list = list()
  # browser()
  NumMainBrand = length(XML::xpathSApply(urlpage, str_c(Abbreviation, i+1, ']//dl')))
  MainBrand = XML::xpathSApply(urlpage, str_c(Abbreviation, i+1, ']//dl//a[@class="brand"]'), fun = xmlValue)
  for (j in 1:NumMainBrand){
    ModelBrand.list = list()
    # browser()
    NumSubBrand = length(XML::xpathSApply(urlpage, str_c(Abbreviation, i+1, ']//dl[position()=', j, ']//div[@class="md-tit"]')))
    SubBrand = XML::xpathSApply(urlpage, str_c(Abbreviation, i+1, ']//dl[position()=', j, ']//div[@class="md-tit"]/a'), fun = xmlValue)
      for (k in 1:NumSubBrand){
        # browser()
        NumModelBrand = length(XML::xpathSApply(urlpage, str_c(Abbreviation, i+1, ']//dl[position()=', j, ']//ul[position()=', k, ']/li')))
        ModelBrand = XML::xpathSApply(urlpage, str_c(Abbreviation, i+1, ']//dl[position()=', j, ']//ul[position()=', k, ']/li/a'), fun = xmlValue)
        ModelBrand.list[[k]] = data.frame(ModelBrand = ModelBrand, 
                                          Alph = Alph[i], 
                                          MainBrand = MainBrand[j], 
                                          SubBrand = SubBrand[k], 
                                          stringsAsFactors = FALSE)
      }
    SubBrand.list[[j]] = plyr::rbind.fill(ModelBrand.list)
  }
  Brand.list[[i]] = plyr::rbind.fill(SubBrand.list)
}
Brand = plyr::rbind.fill(Brand.list)

请注意第二段代码里面除browser()之外的注释部分,因为它们长错了地方!

browser()为了调试用,我们在i=2时发现了第一段代码的问题

为什么不能放在如第一段代码的位置?因为它没法在i或者j或者k变化的时候适时清空重建,从而导致数据混杂了。
第二段代码得到了1522条数据。这才是正确的结果。

RCurl+XML结果

接下来,再用Rvest来完成一次。这次我们不再得到完整的结果。

代码如下:

#=====================================================
#我们试着再用rvest package来解析上面的网页
#=====================================================
library(tidyverse)
library(stringr)
library(rvest)
url = 'http://car.auto.ifeng.com/'
urlpage = read_html(url)  #

#有多少个字母打头
rvest.Alph = html_nodes(urlpage, xpath = '//body/div[@class="w1000"]/div[@class="lt-list"]')%>%length()

#方便以后
rvest.Abbreviation = '//body/div[@class="w1000"]/div[position()='

#计算每一个打头字母下有多少个主品牌
rvest.Main.Num = c()
for (i in 1:rvest.Alph){
  rvest.Main.Num[i] = html_nodes(urlpage, xpath = str_c(rvest.Abbreviation, i+1,']/dl'))%>%length()
}

#计算每一个主品牌下面有多少个子品牌
rvest.Sub.Num = list()
for (i in 1:rvest.Alph){
  middle = c()
  for (j in 1:rvest.Main.Num[i]){
    middle[j] = html_nodes(urlpage, 
                           xpath = str_c(rvest.Abbreviation, 
                                         i+1,
                                         ']//dl[position()=', 
                                         j, 
                                         ']//div[@class="md-tit"]/a'))%>%length()
  }
  rvest.Sub.Num[[i]] = middle
}
#sum(unlist(rvest.Sub.Num))     #总共多少个子品牌209
#length(unlist(rvest.Sub.Num))  #总共多少个主品牌153

#计算每个子品牌下面有多少个车型
rvest.Model.Num = list()
for (i in 1:rvest.Alph){
  middle2 = list()
  for (j in 1:rvest.Main.Num[i]){
    middle = c()
    for (k in 1:rvest.Sub.Num[[i]][j]){
      middle[k] = html_nodes(urlpage, 
                             xpath = str_c(rvest.Abbreviation, 
                                           i+1,
                                           ']//dl[position()=', 
                                           j, 
                                           ']//ul[position()=', 
                                           k, 
                                           ']/li'))%>%length()
    }
    middle2[[j]] = middle
  }
  rvest.Model.Num[[i]] = middle2
}
#sum(unlist(rvest.Model.Num))     #总共多少个车型  1522
#length(unlist(rvest.Model.Num))  #总共多少个子品牌209

我们简单的看看这段代码的结果:

rvest解析结果

======================================================
以上只是本人对XPath的简单体会,至于解析的过程并不简约和完美,也希望有大能能提出指正。

全文比较散乱,唯一在于真实,其间个人倒腾无数,各种坑乱入乱出...

上一篇 下一篇

猜你喜欢

热点阅读