在《爬蟲基礎以及一個簡單的實例》一文中,我們使用了正則表達式來解析爬取的網頁。但是正則表達式有些繁瑣,使用起來不是那么方便。這次我們試一下用Xpath選擇器來解析網頁。
首先,什么是XPath?XPath即XML路徑語言(XML Path Language),用於在XML文檔中查找信息(在XML文檔中對元素和屬性進行遍歷),也適用於HTML文檔。
那么,怎樣來選擇我們想要的內容呢?常用的規則如下:(以下摘自:https://cuiqingcai.com/2621.html)
選取節點:使用路徑表達式
表達式 | 描述 |
---|---|
nodename | 選取此節點的所有子節點。 |
/ | 從根節點選取。 |
// | 從匹配選擇的當前節點選擇文檔中的節點,而不考慮它們的位置。 |
. | 選取當前節點。 |
.. | 選取當前節點的父節點。 |
@ | 選取屬性。 |
查找某個特定的節點或者包含某個指定的值的節點:使用謂語(注:謂語被嵌在方括號中)
路徑表達式 | 結果 |
---|---|
/bookstore/book[1] | 選取屬於 bookstore 子元素的第一個 book 元素。 |
/bookstore/book[last()] | 選取屬於 bookstore 子元素的最后一個 book 元素。 |
/bookstore/book[last()-1] | 選取屬於 bookstore 子元素的倒數第二個 book 元素。 |
/bookstore/book[position()<3] | 選取最前面的兩個屬於 bookstore 元素的子元素的 book 元素。 |
//title[@lang] | 選取所有擁有名為 lang 的屬性的 title 元素。 |
//title[@lang=’eng’] | 選取所有 title 元素,且這些元素擁有值為 eng 的 lang 屬性。 |
/bookstore/book[price>35.00] | 選取 bookstore 元素的所有 book 元素,且其中的 price 元素的值須大於 35.00。 |
/bookstore/book[price>35.00]/title | 選取 bookstore 元素中的 book 元素的所有 title 元素,且其中的 price 元素的值須大於 35.00。 |
選取未知節點:使用通配符
通配符 | 描述 |
---|---|
* | 匹配任何元素節點。 |
@* | 匹配任何屬性節點。 |
node() | 匹配任何類型的節點。 |
Xpath運算符:
運算符 | 描述 | 實例 | 返回值 |
---|---|---|---|
| | 計算兩個節點集 | //book | //cd | 返回所有擁有 book 和 cd 元素的節點集 |
+ | 加法 | 6 + 4 | 10 |
– | 減法 | 6 – 4 | 2 |
* | 乘法 | 6 * 4 | 24 |
div | 除法 | 8 div 4 | 2 |
= | 等於 | price=9.80 | 如果 price 是 9.80,則返回 true。如果 price 是 9.90,則返回 false。 |
!= | 不等於 | price!=9.80 | 如果 price 是 9.90,則返回 true。如果 price 是 9.80,則返回 false。 |
< | 小於 | price<9.80 | 如果 price 是 9.00,則返回 true。如果 price 是 9.90,則返回 false。 |
<= | 小於或等於 | price<=9.80 | 如果 price 是 9.00,則返回 true。如果 price 是 9.90,則返回 false。 |
> | 大於 | price>9.80 | 如果 price 是 9.90,則返回 true。如果 price 是 9.80,則返回 false。 |
>= | 大於或等於 | price>=9.80 | 如果 price 是 9.90,則返回 true。如果 price 是 9.70,則返回 false。 |
or | 或 | price=9.80 or price=9.70 | 如果 price 是 9.80,則返回 true。如果 price 是 9.50,則返回 false。 |
and | 與 | price>9.00 and price<9.90 | 如果 price 是 9.80,則返回 true。如果 price 是 8.50,則返回 false。 |
mod | 計算除法的余數 | 5 mod 2 | 1 |
節點之間的關系:這部分比較簡單,稍微看一下https://cuiqingcai.com/2621.html上的例子就明白了。
1, 父(Parent)
2. 子(Children)
3. 同胞(Sibling)
4. 先輩(Ancestor) --- 包括父和父的父
5. 后代(Descendant) --- 包括子和子的子
一些路徑表達式的例子:(摘自:https://www.jianshu.com/p/89c10770d72c)
使用絕對路徑:/html/body/div/form/input
絕對路徑是從網頁起始標簽開始一直到要定位的元素的路徑,如果要定位的元素在頁面最下面,則這個Xpath路徑會非常長。如果在要定位的元素與頁面開始之間的元素有任何增減,元素定位就會失敗。
使用相對路徑://input
相對路徑一般只包含與被定位元素關系最近的幾層元素,相對路徑寫的好的話,頁面變動影響最小,而且定位准確。
使用索引定位元素,索引的初始值為1://input[2]
如果一個頁面中有多個相似的元素,或是一個層下面有多個同樣的元素的時候,需要用索引的方法來定位,否則無法區分。
結合屬性值來定位元素://input[@id='username']
屬性定位也是比較常用的方法,如果元素中沒有常見的id,name,class等直接有方法可調用的屬性,也可以查找元素中是否有其他能唯一標識元素的屬性,如果有,就可以用此方法定位。
使用多個屬性定位元素://input[@id='username' and @name='userID']
多個屬性聯合定位,更能准確定位到元素。(注意:匹配多個屬性:用and連接; 匹配屬性的多個值:contains(..., ...))
使用屬性名來定位元素://input[@button]
此方法可以區分同一種標簽,含有不同屬性名的元素。定位相對簡單一些兒,但也同樣存在着無法區分同種標簽含有同種屬性名的多個元素,這個時候要配合索引定位才行。
使用部分屬性值匹配元素,用starts-with(),ends-with(),contains()://input[stars-with(@id,'user')];
//input[ends-with(@id,'name')];
//input[contains(@id,"ernam")]
此方法更加靈活,可以定位屬性值不太規律,或是部分變動,中間有空格的情況。
使用任意屬性值匹配元素://input[@*='username']
此方法相當於模糊查詢,只要欲定位的標簽,如input中任何屬性值等於‘username’,就能匹配成功。缺點是可能會匹配含有這個屬性值的其他元素,所以我們在定位的時候要查看一下這個元素值在頁面中是否唯一。
使用文本匹配元素://input[contains(text(),'text')]
(注:獲取元素的內容用text())
總結:用Xpath定位時,先看這個元素是否有明顯的,唯一的屬性值。如果有,我們就用相對路徑加屬性值定位,這是最簡單准確的定位方法。如果要定位的元素不符合這個特征,例如:元素屬性是動態的,無法區分這個元素,屬性值中間有空格,等等。那么應該從此元素的上一層開始查找。當遇到了一個符合條件的元素時,對其寫Xpath。然后從這個元素開始,一級級往下寫,直到要定位的元素為止。
在python中使用Xpath選擇器,我們需要安裝lxml庫。下面是經常用到的一些語法:
導入lxml的etree庫: from lxml import etree
讀取需要進行解析的網頁:
1. 從字符串讀取:html=etree.HTML(text)
2. 從文件讀取:html=etree.parse(file_path)
輸出修正后的html:result=etree.tostring(html)
選取所需的節點:result=html.xpath(...)
了解了以上的知識后,我們就可以開始進行實際操練了。還是用之前的那個例子,實例網址:https://maoyan.com/board/4。
實例目標:用requests庫爬取貓眼電影網上top100的電影(排名,圖片,電影名稱,上映時間,評分),用Xpath進行解析,然后把數據保存到MongoDB。
首先,導入requests庫,lxml的etree庫和pymongo庫:
from lxml import etree import requests import pymongo
爬取單個網頁還是用原來的代碼:
def get_one_page(url): try: headers={'User-Agent': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_13_3) \ AppleWebKit/537.36 (KHTML, like Gecko) Chrome/65.0.3325.162 Safari/537.36'} response=requests.get(url, headers=headers) if response.status_code==200: return response.text return None except requests.RequestException: print("Fail")
接下來用瀏覽器打開網頁,然后在瀏覽器里面選擇開發者工具,在Network里查看網頁源代碼。下面截取一部分:
<div class="content"> <div class="wrapper"> <div class="main"> <p class="update-time">2018-12-30<span class="has-fresh-text">已更新</span></p> <p class="board-content">榜單規則:將貓眼電影庫中的經典影片,按照評分和評分人數從高到低綜合排序取前100名,每天上午10點更新。相關數據來源於“貓眼電影庫”。</p> <dl class="board-wrapper"> <dd> <i class="board-index board-index-1">1</i> <a href="/films/1203" title="霸王別姬" class="image-link" data-act="boarditem-click" data-val="{movieId:1203}"> <img src="//ms0.meituan.net/mywww/image/loading_2.e3d934bf.png" alt="" class="poster-default" /> <img data-src="https://p1.meituan.net/movie/20803f59291c47e1e116c11963ce019e68711.jpg@160w_220h_1e_1c" alt="霸王別姬" class="board-img" /> </a> <div class="board-item-main"> <div class="board-item-content"> <div class="movie-item-info"> <p class="name"><a href="/films/1203" title="霸王別姬" data-act="boarditem-click" data-val="{movieId:1203}">霸王別姬</a></p> <p class="star"> 主演:張國榮,張豐毅,鞏俐 </p> <p class="releasetime">上映時間:1993-01-01</p> </div> <div class="movie-item-number score-num"> <p class="score"><i class="integer">9.</i><i class="fraction">5</i></p>
可以看到,電影的排名在一個dd節點下面,緊接着還有一個i節點,我們需要以"board-index"開頭的class屬性的文本:
<dd> <i class="board-index board-index-1">1</i>
因此,相應的路徑可以寫為://dd/i[starts-with(@class,'board-index')]/text()
接下來,我們發現圖片在一個a節點下面,但是有兩張圖片。經過檢查,第二個img節點下的data-src屬性是圖片的鏈接:
<img data-src="https://p1.meituan.net/movie/20803f59291c47e1e116c11963ce019e68711.jpg@160w_220h_1e_1c" alt="霸王別姬" class="board-img" />
因此,相應的路徑可以寫為://a/img[2]/@data-src
再接下來,電影的名稱,在一個p節點下面,class為"name",下面還有一個a節點:
<p class="name"><a href="/films/1203" title="霸王別姬" data-act="boarditem-click" data-val="{movieId:1203}">霸王別姬</a></p>
相應的路徑可以寫為://p[@class='name']/a/@title
上映時間,在一個p節點下面,class為"releasetime":
<p class="releasetime">上映時間:1993-01-01</p>
相應的路徑可以寫為://p[@class='releasetime']/text()
評分,在一個p節點下面,class為"score",下面還有一個i節點:
<p class="score"><i class="integer">9.</i><i class="fraction">5</i></p>
相應的路徑可以寫為://p[@class='score']/i/text()
完整的路徑如下(用|連接):
//dd/i[starts-with(@class,'board-index')]/text()|//a/img[2]/@data-src|//p[@class='name']/a/@title|//p[@class='releasetime']/text()|//p[@class='score
下面,我們再定義一個解析網頁的方法:
def parse_one_page(html): result=html.xpath("//dd/i[starts-with(@class,'board-index')]/text()|//a/img[2]/@data-src|//p[@class='name']/a/@title|//p[@class='releasetime']/text()|//p[@class='score']/i/text()") return result
輸出的匹配結果如下:
['1', 'https://p1.meituan.net/movie/20803f59291c47e1e116c11963ce019e68711.jpg@160w_220h_1e_1c', '霸王別姬', '上映時間:1993-01-01', '9.', '5', '2', 'https://p0.meituan.net/movie/283292171619cdfd5b240c8fd093f1eb255670.jpg@160w_220h_1e_1c', '肖申克的救贖', '上映時間:1994-09-10(加拿大)', '9.', '5', '3', 'https://p0.meituan.net/movie/289f98ceaa8a0ae737d3dc01cd05ab052213631.jpg@160w_220h_1e_1c', '羅馬假日', '上映時間:1953-09-02(美國)', '9.', '1', '4', 'https://p1.meituan.net/movie/6bea9af4524dfbd0b668eaa7e187c3df767253.jpg@160w_220h_1e_1c', '這個殺手不太冷', '上映時間:1994-09-14(法國)', '9.', '5', '5', 'https://p1.meituan.net/movie/b607fba7513e7f15eab170aac1e1400d878112.jpg@160w_220h_1e_1c', '泰坦尼克號', '上映時間:1998-04-03', '9.', '5', '6', 'https://p0.meituan.net/movie/da64660f82b98cdc1b8a3804e69609e041108.jpg@160w_220h_1e_1c', '唐伯虎點秋香', '上映時間:1993-07-01(中國香港)', '9.', '1', '7', 'https://p0.meituan.net/movie/46c29a8b8d8424bdda7715e6fd779c66235684.jpg@160w_220h_1e_1c', '魂斷藍橋', '上映時間:1940-05-17(美國)', '9.', '2', '8', 'https://p0.meituan.net/movie/223c3e186db3ab4ea3bb14508c709400427933.jpg@160w_220h_1e_1c', '亂世佳人', '上映時間:1939-12-15(美國)', '9.', '1', '9', 'https://p1.meituan.net/movie/ba1ed511668402605ed369350ab779d6319397.jpg@160w_220h_1e_1c', '天空之城', '上映時間:1992', '9.', '1', '10', 'https://p0.meituan.net/movie/b0d986a8bf89278afbb19f6abaef70f31206570.jpg@160w_220h_1e_1c', '辛德勒的名單', '上映時間:1993-12-15(美國)', '9.', '2']
可以看出,上述的格式還是有些雜亂,讓我們修改一下解析網頁的方法,使其變為整齊的結構化數據:
def parse_one_page(html): result=html.xpath("//dd/i[starts-with(@class,'board-index')]/text()|//a/img[2]/@data-src|//p[@class='name']/a/@title|//p[@class='releasetime']/text()|//p[@class='score']/i/text()") for i in range(0,55,6): yield {"index": result[i], "movie_name": result[i+2],\ "pic": result[i+1], "release": result[i+3],\ "score": result[i+4]+result[i+5]}
現在匹配結果變成了字典格式:
{'index': '1', 'movie_name': '霸王別姬', 'pic': 'https://p1.meituan.net/movie/20803f59291c47e1e116c11963ce019e68711.jpg@160w_220h_1e_1c', 'release': '上映時間:1993-01-01', 'score': '9.5'} {'index': '2', 'movie_name': '肖申克的救贖', 'pic': 'https://p0.meituan.net/movie/283292171619cdfd5b240c8fd093f1eb255670.jpg@160w_220h_1e_1c', 'release': '上映時間:1994-09-10(加拿大)', 'score': '9.5'} {'index': '3', 'movie_name': '羅馬假日', 'pic': 'https://p0.meituan.net/movie/289f98ceaa8a0ae737d3dc01cd05ab052213631.jpg@160w_220h_1e_1c', 'release': '上映時間:1953-09-02(美國)', 'score': '9.1'} {'index': '4', 'movie_name': '這個殺手不太冷', 'pic': 'https://p1.meituan.net/movie/6bea9af4524dfbd0b668eaa7e187c3df767253.jpg@160w_220h_1e_1c', 'release': '上映時間:1994-09-14(法國)', 'score': '9.5'} {'index': '5', 'movie_name': '泰坦尼克號', 'pic': 'https://p1.meituan.net/movie/b607fba7513e7f15eab170aac1e1400d878112.jpg@160w_220h_1e_1c', 'release': '上映時間:1998-04-03', 'score': '9.5'} {'index': '6', 'movie_name': '唐伯虎點秋香', 'pic': 'https://p0.meituan.net/movie/da64660f82b98cdc1b8a3804e69609e041108.jpg@160w_220h_1e_1c', 'release': '上映時間:1993-07-01(中國香港)', 'score': '9.1'} {'index': '7', 'movie_name': '魂斷藍橋', 'pic': 'https://p0.meituan.net/movie/46c29a8b8d8424bdda7715e6fd779c66235684.jpg@160w_220h_1e_1c', 'release': '上映時間:1940-05-17(美國)', 'score': '9.2'} {'index': '8', 'movie_name': '亂世佳人', 'pic': 'https://p0.meituan.net/movie/223c3e186db3ab4ea3bb14508c709400427933.jpg@160w_220h_1e_1c', 'release': '上映時間:1939-12-15(美國)', 'score': '9.1'} {'index': '9', 'movie_name': '天空之城', 'pic': 'https://p1.meituan.net/movie/ba1ed511668402605ed369350ab779d6319397.jpg@160w_220h_1e_1c', 'release': '上映時間:1992', 'score': '9.1'} {'index': '10', 'movie_name': '辛德勒的名單', 'pic': 'https://p0.meituan.net/movie/b0d986a8bf89278afbb19f6abaef70f31206570.jpg@160w_220h_1e_1c', 'release': '上映時間:1993-12-15(美國)', 'score': '9.2'}
接下來將結果保存到MongoDB,先寫一個保存到mongo數據庫的方法:
def write_to_mongo(result): query=result collection.update_one(query,{'$set':result},upsert=True)
注:為了避免保存重復的數據,這里把upsert改為True。
其他步驟還和以前一樣,完整代碼如下:
from lxml import etree import requests import pymongo import time def get_one_page(url): try: headers={'User-Agent': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_13_3) \ AppleWebKit/537.36 (KHTML, like Gecko) Chrome/65.0.3325.162 Safari/537.36'} response=requests.get(url, headers=headers) if response.status_code==200: return response.text return None except requests.RequestException: print("Fail") def parse_one_page(html): result=html.xpath("//dd/i[starts-with(@class,'board-index')]/text()|//a/img[2]/@data-src|//p[@class='name']/a/@title|//p[@class='releasetime']/text()|//p[@class='score']/i/text()") for i in range(0,55,6): yield {"index": result[i], "movie_name": result[i+2],\ "pic": result[i+1], "release": result[i+3],\ "score": result[i+4]+result[i+5]} def write_to_mongo(result): query=result collection.update_one(query,{'$set':result},upsert=True) def main(offset): url="https://maoyan.com/board/4?offset={}".format(offset) html=get_one_page(url) html=etree.HTML(html) result=parse_one_page(html) for i in result: write_to_mongo(i) if __name__=='__main__': client=pymongo.MongoClient(host='localhost',port=27017) db=client['test'] collection=db['top100_movies'] for i in range(10): main(offset=i*10) time.sleep(1)