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

这是一篇简单文章,主要目的在于展示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页面有基本的认识,单纯从页面的展示上明白我们需要提取的内容大概有多少层级

  • 我们在观察了几个一级品牌名称之后,发现类似于html/body/div[7]/div[2]/dl[1]/dt/a[2]之类的xpath可以变化为html/body/div/div/dl[i]/dt/a这样的形式,其中i表示第几个一级品牌

  • 在结合一级品牌的分析结论上,分析了二级品牌的xpath之后,我们发现二级品牌可以归纳为html/body/div/div/dl[i]/dd/div[j]/a,其中i表示第几个一级品牌,而j的存在提示了一级品牌下存在二级品牌,如果没有j的存在,那么二级品牌和一级品牌基本类似,但是无法肯定如果j不存在的时候,二级品牌一定与一级品牌一致,所以,不可在这种情况下,直接用一级品牌替代二级品牌

  • 结合前面关于一级和二级品牌的分析之后,我们发现三级品牌也有类似的规律,一般可以归纳为html/body/div/div/dl[i]/dd/ul[m]/li[k]/a,其中k表示三级车型的序号,可以存在,也可能不存在,如Aplina品牌,在国内销售就只有一种车型,所以其xpath的绝对路径就为html/body/div/div/dl[i]/dd/ul/li/a;并且ul后面的序号m与其对应的二级k不是完全对应的。

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

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

在这里,我们不推荐使用相对路径,对于较小的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]这样的结构,我们来仔细解读下:
  • 为什么开始的html不见了?有什么影响么?
  • 为什么body前面多了//
  • div[7]是什么意思?为什么不是div或者div[8]或者其它数字?
  • 为什么div[7]之后的节点有些节点后面没有序号?
    要解答上面的问题,我们首先看看这个webpage的整体情况吧:
TotalWebPage.JPG

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

TotalHtmlPageParse.JPG

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

  • body前面的html可以去掉,在整个页面上,只有一个body,我们可以方便的选择这个节点
  • //body表示了我们以body作为页面提取的第一个根节点,事实上,我们也没必要从html节点开始,这样显得我们很愚蠢一样
  • 我们需要提取的数据就在div[7]这个节点里面,那么当然不能是div[8]或者其它的,甚至不应该是div,因为这样同样显得我们很愚蠢,这导致了我们需要从body节点开始探索每一个div节点
  • 看懂了第三点的,现在对第四点应该没问题了吧,至于我们为什么需要在最后指定a[2],大家可以试着去掉[2]看看... The conclusion is so obviously

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

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

白眼送给你

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

  • 第一种方法是使用文本谓语,我们可以看到div[7]有一个class属性,那么我们直接用div[@class="w1000"]来替代div[7];
  • 第二种方法是使用数字谓语,我们知道div[7]是指的body节点的第7个子节点,那么我们使用div[position()=7]一样可以来替代它;
  • 第三种方法是使用节点关系,我们展开div[7]可以看到下一级节点里面有很多的div子节点,那么我们任意选择一个当前div[7]div子节点,然后用节点关系来寻找我们需要的表达方式,我们可以用//body//div[@class="lt-list"]/parent::div//dl/dt/a[2]来替代//body/div[7]/div[2]/dl[1]/dt/a[2],让我们来分析一下://body//div[@class="lt-list"]表示body节点下面的任意一层存在的div节点,我们需要选除body节点下面的任何一层具有class属性,且class属性为lt-listdiv节点,然后我们再在这个div子节点上翻它的父节点parent,也就是我们需要表达的div[7]这个节点,注意两个地方://body//div[@class="lt-list"]的第二个//的意思是body节点的任意下级节点,div[@class="lt-list"]/parent::div的意思是带有属性为classdiv节点的父辈(parent)名为div的节点,注意里面表达继承关系的/符号;在本例中也可以表达为//body//div[@class="lt-list"]/parent::*//dl/dt/a[2],里面的*本意为子节点的任意父节点,本例即为div[7];关于继承关系的图见图3及4;
  • 接下来这种其实也是数字谓语,但是有装逼的嫌疑://body/div[count(./div)>10]。可是:count是什么鬼?./div又是什么鬼?为什么是10?好吧,我们用通顺的语言来解释下这段代码:body节点下的div节点中,如果该div节点的下级节点是div并且div子节点的数目多于10个,那好,这就是我们要找的body下的div子节点了,注意:不是div节点的子节点,而是body节点的子节点,也就是我们的div[7]...这特么有点绕,请大家原谅我的语文学得不好,表达能力有巨大的问题。

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

节点继承关系图

以及

节点继承关系说明表

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

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

首先来看RCurl+XML的方法:

整体的解析规则:

  • 总共有a个字母打头的(本例有22个不同的英文字母打头)
  • 每个字母打头可能的主品牌不一样,某一个字母可能有b个主品牌
  • 每个主品牌的子品牌数目可能不一样,每一个主品牌可能有c个子品牌
  • 每个子品牌的具体车型数目可能不一样,每一个子品牌可能有d个不同车型

我们可以通过以下代码段知道有多少个字母(本例总共应该有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的简单体会,至于解析的过程并不简约和完美,也希望有大能能提出指正。

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

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 211,948评论 6 492
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,371评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 157,490评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,521评论 1 284
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,627评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,842评论 1 290
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,997评论 3 408
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,741评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,203评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,534评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,673评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,339评论 4 330
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,955评论 3 313
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,770评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,000评论 1 266
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,394评论 2 360
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,562评论 2 349

推荐阅读更多精彩内容