這是一篇簡單文章,主要目的在于展示XPath的不同使用方法檩奠,當然掐松,因為個人的喜好,所以示例當然是通過R語言來實現(xiàn)罢杉,順帶也簡單的介紹了通過RCurl
配合XML
或者rvest
這幾個package來從網(wǎng)頁獲取簡單數(shù)據(jù)趟畏,不涉及復雜數(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路徑:
看看下面的截圖:
我們對我們關心的簡單的分析一下:
一級品牌名稱
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頁面有基本的認識,單純從頁面的展示上明白我們需要提取的內容大概有多少層級
我們在觀察了幾個一級品牌名稱之后而叼,發(fā)現(xiàn)類似于
html/body/div[7]/div[2]/dl[1]/dt/a[2]
之類的xpath可以變化為html/body/div/div/dl[i]/dt/a
這樣的形式身笤,其中i
表示第幾個一級品牌在結合一級品牌的分析結論上,分析了二級品牌的xpath之后葵陵,我們發(fā)現(xiàn)二級品牌可以歸納為
html/body/div/div/dl[i]/dd/div[j]/a
液荸,其中i
表示第幾個一級品牌,而j
的存在提示了一級品牌下存在二級品牌埃难,如果沒有j
的存在莹弊,那么二級品牌和一級品牌基本類似涤久,但是無法肯定如果j
不存在的時候,二級品牌一定與一級品牌一致忍弛,所以响迂,不可在這種情況下,直接用一級品牌替代二級品牌結合前面關于一級和二級品牌的分析之后细疚,我們發(fā)現(xiàn)三級品牌也有類似的規(guī)律蔗彤,一般可以歸納為
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
不是完全對應的姨裸。
以上的分析結論可以為我們對本次提取任務有一個大概的認知秧倾,我們需要在這個基礎上進行分析和驗證,最終得到我們需要的方法傀缩。
除了使用絕對路徑之外那先,我們還可以使用相對路徑以及謂詞等來實現(xiàn)提取的過程。
在這里赡艰,我們不推薦使用相對路徑售淡,對于較小的html文件,我們可以使用相對路徑慷垮,因為這不會導致計算量的增加揖闸,但是在解析大型網(wǎng)頁的時候,使用絕對路徑是比較安全和便捷的方法料身,這樣并不會增加計算量楔壤,從而導致解析的時間大大縮短。至于謂詞以及繼承關系等等其它的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]的話,會導致后面處理的時候有其它問題出現(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ù)對照(對應fun = xmlvlue
)
以上雖然實現(xiàn)了提取的過程乡恕,但是很明顯言询,這種結果不是我們需要的,我們無法將各級品牌以及車型對應起來傲宜。那么唯一能做的就是用函數(shù)來實現(xiàn)提取的過程运杭。稍后,我們會編寫自己的代碼來實現(xiàn)這個過程『洌現(xiàn)在讓我們仔細來回顧下前面的提取過程辆憔。
讓我們仔細分析下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]
或者其它數(shù)字血筑? - 為什么
div[7]
之后的節(jié)點有些節(jié)點后面沒有序號绘沉?
要解答上面的問題,我們首先看看這個webpage的整體情況吧:
接下來再看看我們的webpage的html分析的總體結果:
我們總共發(fā)現(xiàn)了8個
div
節(jié)點云挟,那么div[7]
是不是就是我們需要的第7個div節(jié)點呢梆砸?我們點擊這個我們猜測中的 正確 的div節(jié)點前面的+
,展開它园欣,然后把鼠標放上去看看帖世?看起來這個<div class="w1000">
節(jié)點包含了我們需要的數(shù)據(jù)呀 We are so wise!7锌荨日矫! 接下來的其它分析也是如此的順利成章了。現(xiàn)在讓我們來一一回答上面的幾個問題:
-
body
前面的html
可以去掉绑榴,在整個頁面上哪轿,只有一個body
,我們可以方便的選擇這個節(jié)點 -
//body
表示了我們以body
作為頁面提取的第一個根節(jié)點翔怎,事實上窃诉,我們也沒必要從html
節(jié)點開始,這樣顯得我們很愚蠢一樣 - 我們需要提取的數(shù)據(jù)就在
div[7]
這個節(jié)點里面赤套,那么當然不能是div[8]
或者其它的飘痛,甚至不應該是div
,因為這樣同樣顯得我們很愚蠢容握,這導致了我們需要從body
節(jié)點開始探索每一個div
節(jié)點 - 看懂了第三點的宣脉,現(xiàn)在對第四點應該沒問題了吧,至于我們?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é)點關系彭则,我們展開
div[7]
可以看到下一級節(jié)點里面有很多的div
子節(jié)點,那么我們任意選擇一個當前div[7]
的div
子節(jié)點占遥,然后用節(jié)點關系來尋找我們需要的表達方式俯抖,我們可以用//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é)點负芋,注意里面表達繼承關系的/
符號漫蛔;在本例中也可以表達為//body//div[@class="lt-list"]/parent::*//dl/dt/a[2]
,里面的*
本意為子節(jié)點的任意父節(jié)點旧蛾,本例即為div[7]
莽龟;關于繼承關系的圖見圖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]
...這特么有點繞迄靠,請大家原諒我的語文學得不好,表達能力有巨大的問題喇辽。
讓我們再看看XPath相關的兩個介紹圖
以及
好了,截至到目前,我們沒有對該頁面有任何實質性的進展亚皂,那么茁彭,在了解了如何使用XPath之后,我們分別用RCurl+XML以及RVEST這兩種方式來分別實現(xiàn)一次對我們關心的數(shù)據(jù)的解析吧抽米。
以下的實際代碼中XPath并不是上述的方法特占,大家可以自行比較優(yōu)劣
首先來看RCurl+XML的方法:
整體的解析規(guī)則:
- 總共有a個字母打頭的(本例有22個不同的英文字母打頭)
- 每個字母打頭可能的主品牌不一樣,某一個字母可能有b個主品牌
- 每個主品牌的子品牌數(shù)目可能不一樣云茸,每一個主品牌可能有c個子品牌
- 每個子品牌的具體車型數(shù)目可能不一樣是目,每一個子品牌可能有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)
每個主品牌對應多少個子品牌:
下列語句解析了第一個字母對應的其中一個主品牌的子品牌的個數(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)
接下里我們需要分析每一個子品牌對應的車型的具體數(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'))
相應的,下列語句表示為字母為"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ù)阱佛,但是為什么實際結果不是的呢?
其實上面真不是正確的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()為了調試用淮逊,我們在i=2時發(fā)現(xiàn)了第一段代碼的問題
為什么不能放在如第一段代碼的位置?因為它沒法在i
或者j
或者k
變化的時候適時清空重建扶踊,從而導致數(shù)據(jù)混雜了泄鹏。
第二段代碼得到了1522條數(shù)據(jù)。這才是正確的結果秧耗。
接下來备籽,再用Rvest來完成一次。這次我們不再得到完整的結果。
代碼如下:
#=====================================================
#我們試著再用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
我們簡單的看看這段代碼的結果:
======================================================
以上只是本人對XPath的簡單體會车猬,至于解析的過程并不簡約和完美霉猛,也希望有大能能提出指正。
全文比較散亂珠闰,唯一在于真實惜浅,其間個人倒騰無數(shù),各種坑亂入亂出...