用Xpath選擇器解析網頁(lxml)


《爬蟲基礎以及一個簡單的實例》一文中,我們使用了正則表達式來解析爬取的網頁。但是正則表達式有些繁瑣,使用起來不是那么方便。這次我們試一下用Xpath選擇器來解析網頁。

 

首先,什么是XPath?XPathXML路徑語言(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)

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM