这是一篇简单文章,主要目的在于展示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路径:
看看下面的截图:
我们对我们关心的简单的分析一下:
一级品牌名称
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
)
以上虽然实现了提取的过程,但是很明显,这种结果不是我们需要的,我们无法将各级品牌以及车型对应起来。那么唯一能做的就是用函数来实现提取的过程。稍后,我们会编写自己的代码来实现这个过程。现在让我们仔细来回顾下前面的提取过程。
让我们仔细分析下MainBrand的提取过程:
- 我们利用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的整体情况吧:
接下来再看看我们的webpage的html分析的总体结果:
我们总共发现了8个
div
节点,那么
div[7]
是不是就是我们需要的第7个div节点呢?我们点击这个我们猜测中的
正确 的div节点前面的
+
,展开它,然后把鼠标放上去看看?看起来这个
节点包含了我们需要的数据呀
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-list
的div
节点,然后我们再在这个div
子节点上翻它的父节点parent
,也就是我们需要表达的div[7]
这个节点,注意两个地方://body//div[@class="lt-list"]
的第二个//
的意思是body
节点的任意下级节点,div[@class="lt-list"]/parent::div
的意思是带有属性为class
的div
节点的父辈(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条数据。这才是正确的结果。
接下来,再用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
我们简单的看看这段代码的结果:
======================================================
以上只是本人对XPath的简单体会,至于解析的过程并不简约和完美,也希望有大能能提出指正。
全文比较散乱,唯一在于真实,其间个人倒腾无数,各种坑乱入乱出...