關於爬蟲中常見的兩個網頁解析工具的分析 —— lxml / xpath 與 bs4 / BeautifulSoup

  讀者可能會奇怪我標題怎麼理成這個鬼樣子,主要是單單寫 lxml 與 bs4 這兩個 py 模塊名可能並不能一下引發大衆的注意,通常講到網頁解析技術,提到的關鍵詞更多的是 BeautifulSoup 和 xpath ,而它們各自所在的模塊(python 中是叫作模塊,但其餘平臺下更多地是稱做庫),不多被拿到明面上來談論。下面我將從效率、複雜度等多個角度來對比 xpath 與 beautifulsoup 的區別。html

效率

  從效率上來說,xpath 確實比 BeautifulSoup 高效得多,每次分步調試時,soup 對象的生成有很明顯的延遲,而 lxml.etree.HTML(html) 方式則在 step over 的一瞬間便構建成功了一個可執行 xpath 操做的對象,速度驚人。原理上來說,bs4 是用 python 寫的,lxml 是 c 語言實現的,並且 BeautifulSoup 是基於 DOM 的,會載入整個文檔,解析整個DOM樹,所以時間和內存開銷都會大不少。而lxml只會進行局部遍歷。
 

使用複雜度

  從使用複雜度來說,beautifulsoup 的 find 方法要比 xpath 簡單,後者不只要求通曉 xpath 語法,並且 xpath 方法的返回對象始終是一個 list,這使得對於頁面中一些惟一元素的處理有些尷尬,好比根據 id 獲取頁面某一標籤,下面我用兩種方式實現一個獲取網頁導航欄的方法 (註釋部分爲 bs4 的實現):
    def get_nav(self,response):
        # soup = BeautifulSoup(response.body_as_unicode(), 'lxml')
        # nav_list = soup.find('ul', id='nav').find_all('li')
        model = etree.HTML(response.body_as_unicode())
        nav_list = model.xpath('//ul[@id="nav"]/li')
        for nav in nav_list[1:]:
            # href = nav.find('a').get('href')
            href = nav.xpath('./a/@href')[0]
yield Request(href, callback=self.get_url)

  能夠看到 xpath 除了其特殊的語法看上去有些彆扭(跟正則表達式似的)之外,它在代碼簡潔度上仍是可觀的,只是全部 xpath 方法的返回結果都是一個 list ,若是匹配目標是單個元素,對於無腦下標取0的操做,強迫症患者可能有些難受。相比之下,BeautifulSoup 這一長串的 find 與 find_all 方法顯得有些呆板,若是碰到搜索路線比較曲折的,好比:python

# href = article.find('div', class_='txt').find('p', class_='tit blue').find('span').find('em').find('a').get('href')
href = article.xpath('./div[@class="txt"]//p[@class="tit blue"]/span/em/a/@href')[0]

  這種狀況下,BeautifulSoup 的寫法就顯得有些讓人反胃了,固然通常狀況下不會出現這麼長的路徑定位。正則表達式

 

功能缺陷總結——BeautifulSoup

   BeautifulSoup 在使用上的一個短板,就是在嵌套列表中去匹配元素的時候會顯得很無力,下面是一個例子(具體網頁結構可根據 index_page 在瀏覽器打開進行審查):api

class RankSpider(spider):
    name = 'PCauto_rank'
    index_page = 'http://price.pcauto.com.cn/top/hot/s1-t1.html'
    api_url = 'http://price.pcauto.com.cn%s'

    def start_requests(self):
        yield Request(self.index_page, callback=self.get_left_nav)

    # 測試 BeautifulSoup 是否能連續使用兩個 find_all 方法
    def get_left_nav(self,response):
        # model = etree.HTML(response.body_as_unicode())
        # nav_list = model.xpath('//div[@id="leftNav"]/ul[@class="pb200"]/li//a[@class="dd "]')
        soup = BeautifulSoup(response.body_as_unicode(), 'lxml')
        nav_list = soup.find('div', id='leftNav').find('ul', class_='pb200').find_all('li').find_all('a', class_='dd')
        for sub_nav in nav_list:
            href = self.api_url % sub_nav.xpath('./@href')[0]
            yield Request(href, callback=self.get_url)

    def get_url(self):
        pass

   使用註釋部分的 xpath 寫法沒什麼問題,可實現準肯定位,但用到 BeautifulSoup 去實現相應邏輯的時候,就要連續使用兩個 find_all 方法 ,顯然這種寫法不符合規範,運行的時候會報 AttributeError: 'ResultSet' object has no attribute 'find_all' 錯誤,這時候咱們要實現這種匹配,只能先去遍歷各個 li ,而後調 find_all 方法找到 li 下的各個 a 標籤,實在繁瑣,因此這種場景用 xpath 來解決會省下很多麻煩。瀏覽器

  固然這裏我只是單單爲了詮釋這麼個問題纔在故意在拿目標 url 時分這麼多級的,實際開發中我這裏用的是:ide

        # nav_list = model.xpath('//div[@id="leftNav"]///a[@class="dd "]')
        nav_list = soup.find('div', id='leftNav').find_all('a', class_='dd')

  但若是說咱們的目標不是全部的 li 下面的 a 標籤,而是部分 class="*" 的 li 下面的 a 標籤,這時候咱們就只能選擇使用 xpath 來達到目的,固然若是你喜歡寫遍歷,以爲這樣寫出來邏輯展現更清晰,那你能夠跳過這一節。測試

 

功能缺陷總結——xpath

  xpath 的類選擇器在作公共類名選擇時有短板,也勉強把它算做功能缺陷吧,好比:     
 model = etree.HTML(response.body_as_unicode())
model.xpath('//div[@class="box box-2 box-4"]')
  沒法定位 html 中 class 爲 box box-2 box-4 mt25 與 box box-2 box-4 mt17 的兩個 div,必須分別以: 
model.xpath('//div[@class="box box-2 box-4 mt25"]') 
model.xpath('//div[@class="box box-2 box-4 mt17"]')

  來匹配目標,這可能要歸結於 xpath 在設計的時候自己就是以類名的徹底匹配來肯定目標的,哪怕多一個空格:網站

  頁面中一個 a 標籤是這樣寫的:  <a href="/top/hot/s1-t1.html" class="dd ">5萬如下</a> 用 xpath 去選擇,寫做:

      model.xpath('//a[@class="dd"]')ui

  死活匹配不到(當時真的是蠻懵逼的),必需要在後面加空格,但在經過 js 控制檯 a.dd 這個類選擇器又能夠定位到目標,並且 BeautifulSoup 調 find_all('a', class_='dd') 也是沒有問題的,這種應用場景下的 xpath 就略顯死板。

 

文本獲取

  xpath 目標結點的 text 屬性值對應的只是當前匹配元素下面的文本信息,如要獲取該結點下面包括子結點在內的全部文本內容要使用 .xpath('string()') 的方式:url

       model = etree.HTML(response.body_as_unicode())
place = model.xpath('//div[@class="guide"]')
# nav and aiticle
        if place:
            mark = place[0].xpath('./span[@class="mark"]')
            if mark:
                # text = mark[0].text.strip().replace('\n','').replace('\r','')  # false
                text = mark[0].xpath('string()')
                result['address'] = text

 

其餘方面比較

   從參考資料上來說,bs4 有詳細中/英文版官方幫助文檔,lxml 好像 document 相對少。另外 BeautifulSoup 的結點對象在 Debugger 下面對於變量內容的監視更友好它直接顯示匹配的 html 字符串,而 lxml 就是一個相似這種表示的對象: <Element html at 0x####>,不是很友好,但這都不重要,筆者仍是更喜歡 xpath 的高效,簡潔,一步到位
  
整理了一下午,感受這兩種技術在應用中的細節對比網上應該比這寫得更詳細的了,但願對你們有幫助吧。另外,說一點題外話,最近發現一些網站未經容許引用本人的文章 (以"懂客"爲表明) ,在這裏我仍是呼籲你們更多地關注源出處,關注正經博客網站,好比博客園,嘿嘿~
相關文章
相關標籤/搜索