這是一篇簡單文章,主要目的在于展示XPath的不同使用方法,當然,因為個人的喜好,所以示例當然是通過R語言來實現(xiàn),順帶也簡單的介紹了通過RCurl配合XML或者rvest這幾個package來從網(wǎng)頁獲取簡單數(shù)據(jù),不涉及復(fù)雜數(shù)據(jù)的獲取。本文的主要實例來自于鳳凰網(wǎng)的汽車板塊。
以下是我的簡單的初始代碼
library(RCurl)
library(XML)
library(tidyverse)
library(stringr)
#定向解析網(wǎng)頁
url = 'http://car.auto.ifeng.com/'
urlpage = XML::htmlParse(url)
我在這里用XML::htmlParse(url)的方式來表示對某個特定package的具體函數(shù)的引用,這樣方便我們以后能清晰的記得某個函數(shù)的具體來源,做為新手,這是一個較好的建議,事實上,我在看網(wǎng)上代碼的時候經(jīng)常對某個函數(shù)的來源非常疑惑。
我們利用firefox瀏覽器的firebug插件查看隨意的兩個我們需要提取的汽車品牌名稱,可以發(fā)現(xiàn)以下的xpath路徑:
看看下面的截圖:
我們對我們關(guān)心的簡單的分析一下:
一級品牌名稱
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的絕對路徑的規(guī)律:
首先我們需要對整個html頁面有基本的認識,單純從頁面的展示上明白我們需要提取的內(nèi)容大概有多少層級
我們在觀察了幾個一級品牌名稱之后,發(fā)現(xiàn)類似于
html/body/div[7]/div[2]/dl[1]/dt/a[2]之類的xpath可以變化為html/body/div/div/dl[i]/dt/a這樣的形式,其中i表示第幾個一級品牌在結(jié)合一級品牌的分析結(jié)論上,分析了二級品牌的xpath之后,我們發(fā)現(xiàn)二級品牌可以歸納為
html/body/div/div/dl[i]/dd/div[j]/a,其中i表示第幾個一級品牌,而j的存在提示了一級品牌下存在二級品牌,如果沒有j的存在,那么二級品牌和一級品牌基本類似,但是無法肯定如果j不存在的時候,二級品牌一定與一級品牌一致,所以,不可在這種情況下,直接用一級品牌替代二級品牌結(jié)合前面關(guān)于一級和二級品牌的分析之后,我們發(fā)現(xiàn)三級品牌也有類似的規(guī)律,一般可以歸納為
html/body/div/div/dl[i]/dd/ul[m]/li[k]/a,其中k表示三級車型的序號,可以存在,也可能不存在,如Aplina品牌,在國內(nèi)銷售就只有一種車型,所以其xpath的絕對路徑就為html/body/div/div/dl[i]/dd/ul/li/a;并且ul后面的序號m與其對應(yīng)的二級k不是完全對應(yīng)的。
以上的分析結(jié)論可以為我們對本次提取任務(wù)有一個大概的認知,我們需要在這個基礎(chǔ)上進行分析和驗證,最終得到我們需要的方法。
除了使用絕對路徑之外,我們還可以使用相對路徑以及謂詞等來實現(xiàn)提取的過程。
在這里,我們不推薦使用相對路徑,對于較小的html文件,我們可以使用相對路徑,因為這不會導(dǎo)致計算量的增加,但是在解析大型網(wǎng)頁的時候,使用絕對路徑是比較安全和便捷的方法,這樣并不會增加計算量,從而導(dǎo)致解析的時間大大縮短。至于謂詞以及繼承關(guān)系等等其它的xpath方式,我們接下來盡量一一實現(xiàn)一次。
我們使用以下的語句來實現(xiàn)提取汽車一級、二級以及三級品牌的過程:
# 利用XML package的xpathSApply函數(shù)來解決直接讀取鳳凰網(wǎng)汽車板塊的所有汽車品牌名稱
# 第一個參數(shù)是已經(jīng)解析的網(wǎng)頁,第二參數(shù)是xpath的絕對路徑,第三個參數(shù)是指定需要獲取的節(jié)點的具體部分,xmlvalue指取該節(jié)點的參數(shù)
# 節(jié)點參數(shù)見下表
# 注意a[2]這個寫法,如果不加入[2]的話,會導(dǎo)致后面處理的時候有其它問題出現(xiàn),可以試著不加[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)
簡單的瀏覽下節(jié)點參數(shù)對照(對應(yīng)fun = xmlvlue)
以上雖然實現(xiàn)了提取的過程,但是很明顯,這種結(jié)果不是我們需要的,我們無法將各級品牌以及車型對應(yīng)起來。那么唯一能做的就是用函數(shù)來實現(xiàn)提取的過程。稍后,我們會編寫自己的代碼來實現(xiàn)這個過程?,F(xiàn)在讓我們仔細來回顧下前面的提取過程。
讓我們仔細分析下MainBrand的提取過程:
- 我們利用FirePath提取的絕對路徑是類似于
html/body/div[7]/div[2]/dl[1]/dt/a[2]這樣的,但是我們的提取并不是這樣的過程,而是類似于//body/div[7]/div/dl/dt/a[2]這樣的結(jié)構(gòu),我們來仔細解讀下:
- 為什么開始的
html不見了?有什么影響么? - 為什么
body前面多了//? -
div[7]是什么意思?為什么不是div或者div[8]或者其它數(shù)字? - 為什么
div[7]之后的節(jié)點有些節(jié)點后面沒有序號?
要解答上面的問題,我們首先看看這個webpage的整體情況吧:
接下來再看看我們的webpage的html分析的總體結(jié)果:
我們總共發(fā)現(xiàn)了8個
div節(jié)點,那么div[7]是不是就是我們需要的第7個div節(jié)點呢?我們點擊這個我們猜測中的 正確 的div節(jié)點前面的+,展開它,然后把鼠標放上去看看?看起來這個<div class="w1000">節(jié)點包含了我們需要的數(shù)據(jù)呀 We are so wise?。?! 接下來的其它分析也是如此的順利成章了。現(xiàn)在讓我們來一一回答上面的幾個問題:
-
body前面的html可以去掉,在整個頁面上,只有一個body,我們可以方便的選擇這個節(jié)點 -
//body表示了我們以body作為頁面提取的第一個根節(jié)點,事實上,我們也沒必要從html節(jié)點開始,這樣顯得我們很愚蠢一樣 - 我們需要提取的數(shù)據(jù)就在
div[7]這個節(jié)點里面,那么當然不能是div[8]或者其它的,甚至不應(yīng)該是div,因為這樣同樣顯得我們很愚蠢,這導(dǎo)致了我們需要從body節(jié)點開始探索每一個div節(jié)點 - 看懂了第三點的,現(xiàn)在對第四點應(yīng)該沒問題了吧,至于我們?yōu)槭裁葱枰谧詈笾付?code>a[2],大家可以試著去掉
[2]看看... The conclusion is so obviously
既然說到了這里,那么我們就干脆先放下我們的終極目標--獲取汽車品牌及車型,我們先好好對這個div[7]嘮嗑嘮嗑
我們首先想到的:div[7]難道就因為它是body的第7個子節(jié)點并且我們的數(shù)據(jù)在里面,so,我們就只能用這一種寫法?

那么我們能夠用哪些方法來表述這div[7]呢?
- 第一種方法是使用文本謂語,我們可以看到
div[7]有一個class屬性,那么我們直接用div[@class="w1000"]來替代div[7]; - 第二種方法是使用數(shù)字謂語,我們知道
div[7]是指的body節(jié)點的第7個子節(jié)點,那么我們使用div[position()=7]一樣可以來替代它; - 第三種方法是使用節(jié)點關(guān)系,我們展開
div[7]可以看到下一級節(jié)點里面有很多的div子節(jié)點,那么我們?nèi)我膺x擇一個當前div[7]的div子節(jié)點,然后用節(jié)點關(guān)系來尋找我們需要的表達方式,我們可以用//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節(jié)點下面的任意一層存在的div節(jié)點,我們需要選除body節(jié)點下面的任何一層具有class屬性,且class屬性為lt-list的div節(jié)點,然后我們再在這個div子節(jié)點上翻它的父節(jié)點parent,也就是我們需要表達的div[7]這個節(jié)點,注意兩個地方://body//div[@class="lt-list"]的第二個//的意思是body節(jié)點的任意下級節(jié)點,div[@class="lt-list"]/parent::div的意思是帶有屬性為class的div節(jié)點的父輩(parent)名為div的節(jié)點,注意里面表達繼承關(guān)系的/符號;在本例中也可以表達為//body//div[@class="lt-list"]/parent::*//dl/dt/a[2],里面的*本意為子節(jié)點的任意父節(jié)點,本例即為div[7];關(guān)于繼承關(guān)系的圖見圖3及4; - 接下來這種其實也是數(shù)字謂語,但是有裝逼的嫌疑:
//body/div[count(./div)>10]。可是:count是什么鬼?./div又是什么鬼?為什么是10?好吧,我們用通順的語言來解釋下這段代碼:body節(jié)點下的div節(jié)點中,如果該div節(jié)點的下級節(jié)點是div并且div子節(jié)點的數(shù)目多于10個,那好,這就是我們要找的body下的div子節(jié)點了,注意:不是div節(jié)點的子節(jié)點,而是body節(jié)點的子節(jié)點,也就是我們的div[7]...這特么有點繞,請大家原諒我的語文學(xué)得不好,表達能力有巨大的問題。
讓我們再看看XPath相關(guān)的兩個介紹圖
以及
好了,截至到目前,我們沒有對該頁面有任何實質(zhì)性的進展,那么,在了解了如何使用XPath之后,我們分別用RCurl+XML以及RVEST這兩種方式來分別實現(xiàn)一次對我們關(guān)心的數(shù)據(jù)的解析吧。
以下的實際代碼中XPath并不是上述的方法,大家可以自行比較優(yōu)劣
首先來看RCurl+XML的方法:
整體的解析規(guī)則:
- 總共有a個字母打頭的(本例有22個不同的英文字母打頭)
- 每個字母打頭可能的主品牌不一樣,某一個字母可能有b個主品牌
- 每個主品牌的子品牌數(shù)目可能不一樣,每一個主品牌可能有c個子品牌
- 每個子品牌的具體車型數(shù)目可能不一樣,每一個子品牌可能有d個不同車型
我們可以通過以下代碼段知道有多少個字母(本例總共應(yīng)該有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)
關(guān)于每個打頭字母下分別對應(yīng)有多少個主品牌,我們的示例代碼如下:
此處的
div[@class="w1000"]/div[position()=2]必須從position()=2開始,從2開始,到23結(jié)束
NumMainBrand = length(XML::xpathSApply(urlpage, '//body/div[@class="w1000"]/div[position()=2]//dl'))
接下來的代碼試著獲取了字母A對應(yīng)的主品牌的名稱:
XML::xpathSApply(urlpage, '//body/div[@class="w1000"]/div[position()=2]//dl//a[@class="brand"]', fun = xmlValue)
每個主品牌對應(yīng)多少個子品牌:
下列語句解析了第一個字母對應(yīng)的其中一個主品牌的子品牌的個數(shù)
本例為字母為"A"開頭的(div[position()=2])(總共有5個主品牌)主品牌,第4個主品牌(dl[position()=4])的子品牌數(shù)目
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)
接下里我們需要分析每一個子品牌對應(yīng)的車型的具體數(shù)量
下列語句表示為字母為"A"開頭的(
div[position()=2])(總共有5個主品牌)主品牌,第4個主品牌(dl[position()=4])的第一個子品牌(ul[position()=1])的具體車型數(shù)量
NumModelBrand = length(XML::xpathSApply(urlpage, '//body/div[@class="w1000"]/div[position()=2]//dl[position()=4]//ul[position()=1]/li'))
相應(yīng)的,下列語句表示為字母為"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)
#定向解析網(wǎng)頁
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())
但是這個代碼爬出來的數(shù)據(jù)總共只有1520條(2017年10月24日數(shù)據(jù) ),跟實際的數(shù)據(jù)對不上啊,而且,我們的本意是通過上面的for循環(huán)之后的rbind.fill函數(shù)就能直接得出我們想要的data.frame格式的數(shù)據(jù),但是為什么實際結(jié)果不是的呢?
其實上面真不是正確的code,那么正確的長啥樣?LOOK!
setwd('C:\\ACYDrelation')
library(RCurl)
library(XML)
library(tidyverse)
library(stringr)
#定向解析網(wǎng)頁
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()為了調(diào)試用,我們在i=2時發(fā)現(xiàn)了第一段代碼的問題
為什么不能放在如第一段代碼的位置?因為它沒法在i或者j或者k變化的時候適時清空重建,從而導(dǎo)致數(shù)據(jù)混雜了。
第二段代碼得到了1522條數(shù)據(jù)。這才是正確的結(jié)果。
接下來,再用Rvest來完成一次。這次我們不再得到完整的結(jié)果。
代碼如下:
#=====================================================
#我們試著再用rvest package來解析上面的網(wǎng)頁
#=====================================================
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
我們簡單的看看這段代碼的結(jié)果:
======================================================
以上只是本人對XPath的簡單體會,至于解析的過程并不簡約和完美,也希望有大能能提出指正。
全文比較散亂,唯一在于真實,其間個人倒騰無數(shù),各種坑亂入亂出...