2024年7月16日 星期二

Python 學習筆記 : 網頁爬蟲框架 Scrapy 用法 (二)

在上一篇 Scrapy 測試中我們使用 BeautifulSoup 來剖析目標網頁以擷取目標資料, 其實 Scrapy 本身有內建一個選擇器類別 Selector 可用 XPath 或 CSS 選擇器來定位與擷取目標資料, 毋須假手 BeautifulSoup 之協助, 本篇旨在測試如何利用 Selector 物件的 xpath() 與 css() 方法來擷取目標資料. 

本系列之前的筆記參考 : 



七. 用 Selector 物件剖析網頁 :

Scrapy 套件的 selector 模組中提供了一個 Selector 類別可用來從 HTTP 回應網頁中剖析 HTML 文件, 透過 XPath 或 CSS 選擇器定位目標元素與取得目標資料. 此 Selector 類別乃是基於 lxml 套件而建構的, 具有剖析速度快的優點 (BeautifulSoup 若指定使用 lxml 作為剖析器亦可加速). 

關於 Selector 物件詳細用法參考官網教學 :


底下先來檢視一下 Selector 類別的位置 : 

>>> import scrapy   
>>> dir(scrapy)       # scrapy 套件織成員
['Field', 'FormRequest', 'Item', 'Request', 'Selector', 'Spider', '__all__', '__builtins__', '__cached__', '__doc__', '__file__', '__loader__', '__name__', '__package__', '__path__', '__spec__', '__version__', '_txv', 'exceptions', 'http', 'item', 'link', 'linkextractors', 'selector', 'signals', 'spiders', 'twisted_version', 'utils', 'version_info']
>>> dir(scrapy.selector)       # selector 模組織成員
['Selector', 'SelectorList', '__builtins__', '__cached__', '__doc__', '__file__', '__loader__', '__name__', '__package__', '__path__', '__spec__', 'unified']
>>> type(scrapy.selector.Selector)      # Selector 是一個類別
<class 'type'>

建立 Selector 物件的方法是將 HTML 文件傳給其建構式 Selector(), 例如 :

>>> from scrapy.selector import Selector     
>>> text='''<!doctype html>   
<html>
 <head>
  <meta charset="UTF-8">
  <meta http-equiv="cache-control" content="no-cache">
  <meta name="viewport" content="width=device-width,initial-scale=1">
  <title></title>
 </head>
 <body>  
   <h1>Hello World</h1>
   <h2>你是在說哈囉嗎?</h2>
   <ul>
     <li>軍師中郎將諸葛亮</li>
     <li>虎威將軍趙雲</li>
     <li>漢壽亭侯關羽</li>
     <li>車騎將軍張飛</li>
   </ul>
 </body>
</html>'''

此網頁文件若存成 HTM 檔於瀏覽器中顯示如下 :


將網頁內容傳給建構式 Selector() 的 text 參數即可建立代表整份文件的 Selector 物件 :

>>> selector=Selector(text=text)     # 傳入 HTML 文件建立 Selector 物件
>>> type(selector)     
<class 'scrapy.selector.unified.Selector'>   

注意, 呼叫 Selector() 時關鍵字 text 一定要給, 不可以直接傳入文本. 

檢視 Selector 物件成員 :

>>> dir(selector)    
['__bool__', '__class__', '__delattr__', '__dir__', '__doc__', '__eq__', '__format__', '__ge__', '__getattribute__', '__getstate__', '__gt__', '__hash__', '__init__', '__init_subclass__', '__le__', '__lt__', '__module__', '__ne__', '__new__', '__nonzero__', '__reduce__', '__reduce_ex__', '__repr__', '__setattr__', '__sizeof__', '__slots__', '__str__', '__subclasshook__', '__weakref__', '_css2xpath', '_default_namespaces', '_expr', '_get_root', '_huge_tree', '_lxml_smart_strings', '_text', 'attrib', 'body', 'css', 'drop', 'extract', 'get', 'getall', 'jmespath', 'namespaces', 're', 're_first', 'register_namespace', 'remove', 'remove_namespaces', 'response', 'root', 'selectorlist_cls', 'type', 'xpath']

Selector 物件常用屬性如下表 :


 Selector 物件常用屬性 說明
 type 解析的文件型態, 例如 'html' 或 'xml'
 response 代表 HTTP 回應的 HtmlResponse 物件
 attrib 元素的屬性字典, 可用 attrib['屬性'] 取得屬性值


以上面的 HTML 字串所建立的 selector 物件來說 : 

>>> selector.type   
'html'
>>> selector.attrib   
{}
>>> selector.response   
<200 about:blank>
>>> type(selector.response)   
<class 'scrapy.http.response.html.HtmlResponse'>

因為這是代表整份文件的 selector 物件而非個別元素的 Selector 物件, 因此其 attrib 屬性值為空字典, response 屬性值中的 200 表示 HTTP 回應 OK.

Selector 物件常用方法如下表 :


 Selector 物件常用方法 說明
 css(css_sel_str) 依據 CSS 選擇器字串搜尋元素, 傳回其 Selector 物件串列
 xpath(xpath_sel_str) 依據 XPath 選擇器字串搜尋元素, 傳回其 Selector 物件串列
 get() 傳回 Selector 物件(群) 的內容 (字串), 多物件時傳回第一個的內容
 getall() 取得 Selector 物件(群) 的內容 (串列)
 extract() 功能與 get() 相同的舊方法, 但目前仍可繼續使用


定位目標元素首先是呼叫代表整份文件的 Selector 物件之 css() 或 xpath() 方法, 這會傳回一個由Selector 物件組成的 SelectorList 串列物件, 此 SelectorList 物件也有 css() 與 xpath() 方法, 呼叫它們時會逐一呼叫串列裡面每一個 Selector 物件的 css() 與 xpath() 方法, 並將傳回值組成一個新的 SelectorList 物件後回傳. 


1. 使用 XPath 選擇器定位元素 :

XPath 是 XML 的路徑語言, 用來定位 XML 文件語法樹中的元素節點; 由於 HTML 係 XML 的子集, 故也可以用 XPath 來定位網頁中的元素. 關於 XML 參考 :


在 Scrapy 中呼叫 Selector 物件的 xpath() 方法並傳入 XPath 選擇器作為參數, xpath() 會傳回所有符合之元素節點 Selector 物件組成之 SelectorList 物件. 

XPath 選擇器的常用語法如下表 : 


 XPath 常用語法 說明
 / 文件的根結點 (root)
 tag 搜尋標籤名稱為 tag 的所有子節點
 * 所有子節點
 . 目前節點
 .. 父節點 (上一層)
 //  全部子孫節點
 [@attr] 搜尋全部含有屬性 attr 的節點
 [@attr='value'] 搜尋全部含有屬性 attr, 且值為 value 的節點
 [tag] 搜尋標籤名稱為 tag 的所有子節點
 [tag='text'] 搜尋全部子孫節點中標籤名稱為 tag, 且內容為 text 的所有子節點
 [index] 搜尋位置索引 (1 起始) 為 index 的子節點
 [@id] 選取所有含有 id 屬性的子節點
 text()    選取含有文字內容的所有子結點
 last()    選取最後一個子結點
 position() 傳回所有子節點位置, 通常搭配關係運算子例如 [position()==2]


可見 XPath 語法除了可指定節點與屬性外, 還提供了函式 (有 100 多個), 其中 text() 是擷取資料最重要的函式, 用來取得目標元素的文字內容. 中括號 [] 用來尋找符合條件之節點, 例如指定之索引 index, 標籤名稱 tag, 標籤文字內容 text, 屬性名稱 (@) 與其值等. XPath 詳細語法參考 :


實務上 XPath 選擇器不用自行撰寫, 在瀏覽器的開發人員工具視窗中即可取得, 以 Chrome 瀏覽器為例, 按 F12 就會開啟開發人員工具視窗, 切換至 Element 頁籤透過搜尋與左上角的 Select & Inspect 按鈕即可找到目標元素, 然後移動滑鼠至目標元素上面, 按滑鼠右鍵點選 "Copy/Copy XPath" 即可複製取得該元素的 XPath : 




以上面的測試網頁文件 text 為例 :

>>> h1=selector.xpath('/html/body/h1')     
>>> type(h1)   
<class 'scrapy.selector.unified.SelectorList'>   # 傳回 Selector 物件串列  
>>> len(h1)     # 只有一個 Selector 物件元素
1
>>> h1[0]  
<Selector query='/html/body/h1' data='<h1>Hello World</h1>'>

由於此網頁中只有一個 h1 元素, 故 xpath() 方法傳回的 SelectorList 串列中只有一個元素, 元素的 HTML 碼會放在此 Selector 物件的 data 屬性中, 呼叫其 get() 或 extract() 方法即可取得此元素的 HTML 碼 :

>>> h1[0].get()   
'<h1>Hello World</h1>'
>>> h1[0].extract()     
'<h1>Hello World</h1>'

呼叫 getall() 方法則會以串列傳回 : 

>>> h1[0].getall()   
['<h1>Hello World</h1>']    

也可以直接用 SelectorList 物件 h1 去呼叫這 3 個方法 :

>>> h1.get()   
'<h1>Hello World</h1>'
>>> h1.extract()    
['<h1>Hello World</h1>']
>>> h1.getall()    
['<h1>Hello World</h1>']

可見這時 extract() 與 getall() 都傳回串列, 而 get() 仍然傳回字串. 

如果要取出元素的文字內容, 則要在 XPath 最後面加上 text() 函式 :

>>> h1=selector.xpath('/html/body/h1/text()')   
>>> h1.get()     
'Hello World'
>>> h1.extract()    
['Hello World']
>>> h1.getall()    
['Hello World']

接著來看看含有多元素的 Selector 物件, 在上面的範例網頁中含有 4 個 li 元素, 選取這四個 li 元素的 XPath 選擇器為 '/html/body/ul/li' : 

>>> lis=selector.xpath('/html/body/ul/li')    
>>> type(lis)     
<class 'scrapy.selector.unified.SelectorList'>   
>>> len(lis)       # 此串列含有 4 個 Selector 物件
4
>>> lis.get()      # 多元素時 get() 會傳回第一個 Selector 物件的內容
'<li>軍師中郎將諸葛亮</li>'
>>> lis.extract()      # 傳回元素串列
['<li>軍師中郎將諸葛亮</li>', '<li>虎威將軍趙雲</li>', '<li>漢壽亭侯關羽</li>', '<li>車騎將軍張飛</li>']
>>> lis.getall()        # 傳回元素串列
['<li>軍師中郎將諸葛亮</li>', '<li>虎威將軍趙雲</li>', '<li>漢壽亭侯關羽</li>', '<li>車騎將軍張飛</li>']  

同樣地, 若要取得元素的文字內容, 要在 XPath 選擇器定位到元素後以 text() 取出, 例如 :

>>> lis=selector.xpath('/html/body/ul/li/text()')     # 用 text() 擷取元素文字內容
>>> lis.get()   
'軍師中郎將諸葛亮'
>>> lis.extract()    
['軍師中郎將諸葛亮', '虎威將軍趙雲', '漢壽亭侯關羽', '車騎將軍張飛']
>>> lis.getall()   
['軍師中郎將諸葛亮', '虎威將軍趙雲', '漢壽亭侯關羽', '車騎將軍張飛']

如果要取得多個元素中的最後一個, 可以將 last() 函式放在中括號 [] 內指定 :

>>> lis=selector.xpath('/html/body/ul/li[last()]/text()')   # 指定最後一個 li 元素
>>> lis.get()  
'車騎將軍張飛'
>>> lis.extract()  
['車騎將軍張飛'] 
>>> lis.getall()  
['車騎將軍張飛']

也可以用 position() 函式指定要定位第幾個元素, 例如 :

>>> lis=selector.xpath('/html/body/ul/li[position()=2]/text()')    # 指定第二個 li
>>> lis.get()   
'虎威將軍趙雲'
>>> lis.extract()   
['虎威將軍趙雲']
>>> lis.getall()   
['虎威將軍趙雲']

或者後兩個元素 :

>>> lis=selector.xpath('/html/body/ul/li[position()>=3]/text()')   
>>> lis.get()           # get() 只傳回搜尋到的第一個而非全部
'漢壽亭侯關羽'   
>>> lis.extract()   
['漢壽亭侯關羽', '車騎將軍張飛']
>>> lis.getall()   
['漢壽亭侯關羽', '車騎將軍張飛']


2. 使用 CSS 選擇器定位元素 :

CSS (串接樣式表) 是專門用在 HTML 網頁元素排版與外觀的語法, 也可以用來定位網頁元素, 其語法較 XPath 簡單, 事實上 Scrapy 是將 CSS 選擇器轉換成 XPath 語法後傳給 Selector 物件的 xpath() 去查詢, 因此效能上比直接用功能強大的 XPath 選擇器要低一些. 

CSS 選擇器常用語法摘要如下表 :


 CSS 選擇器常用語法 說明
 .class 選取樣式類別為 classe 之所有元素 (即 class='class')
 #id 選取 id 屬性值為 id 的元素 (即 id='id', 理論上只有一個)
 * 選取所有元素
 // 全部子孫節點
 tag 選取標籤為 tag 之所有元素 (例如 p, div 等) 
 tag1, tag2 選取所有 tag1 與 tag2 元素 (列舉)
 tag1 tag2 選取 tag1 子孫 (後代) 元素中的所有 tag2 元素
 tag1 > tag2 選取 tag1 子元素中的所有 tag2 元素
 tag1 + tag2 選取與 tag1 兄弟元素中所有的 tag2 元素
 [attr] 選取全部有 attr 屬性的元素, 例如 [class] 選取有 class 屬性之所有元素
 [attr=value] 選取全部有 attr 屬性且值為 value 之元素, 例如 [class=odd]
 [attr~=value] 選取全部有 attr 屬性且值包含 value 之元素, 例如 [class~=odd]
 [attr$=value] 選取全部有 attr 屬性且值以 value 開頭之元素, 例如 [class$=odd]
 [attr^=value] 選取全部有 attr 屬性且值以 value 結尾之元素, 例如 [class^=odd]
 tag:nth-child(n) 選取標籤為 tag 且是第 n 個子元素之節點
 tag:nth-last-child(n) 選取標籤為 tag 且是倒數第 n 個子元素之節點
 tag:first-child 選取標籤為 tag 且是第 1 個子元素之節點
 tag:last-child 選取標籤為 tag 且是倒數第 1 個子元素之節點
 tag::text 選取 tag 元素的文字節點


更多 CSS 選擇器用法參考 :


同樣地, 其實我們也不用真的去撰寫 CSS 選擇器, 在 Chrome 瀏覽器的開發人員工具的 Element 頁籤中可以於任何元素上按滑鼠右鍵點選 "Copy/Copy XPATH" 或 "Copy/Copy Selector" 輕鬆取得該元素的 CSS 選擇器 : 




以上面的測試網頁文件 text 為例, h1 標籤的 CSS 選擇器為 'body > h1' :

>>> h1=selector.css('body>h1')   
>>> h1  
[<Selector query='descendant-or-self::body/h1' data='<h1>Hello World</h1>'>]  
>>> len(h1)       # 網頁中只有一個 h1 元素
1
>>> type(h1)  
<class 'scrapy.selector.unified.SelectorList'>     # css() 傳回值為 Selector 物件串列
>>> h1.get()                          # get() 傳回第一個符合之物件內容
'<h1>Hello World</h1>'       
>>> h1.extract()                   # extract() 傳回內容字串之串列
['<h1>Hello World</h1>']
>>> h1.getall()                      # getall() 傳回內容字串之串列
['<h1>Hello World</h1>']

上面的選擇器取得的是元素的 Selector 物件 (包含元素標籤), 如果只要取得文字內容, 則後面要添加 ::text 選擇器 (注意是兩個連續冒號), 例如 :

>>> h1=selector.css('body>h1::text')   
>>> h1   
[<Selector query='descendant-or-self::body/h1/text()' data='Hello World'>]
>>> h1.get()   
'Hello World'
>>> h1.extract()      
['Hello World']
>>> h1.getall()   
['Hello World']

接下來是多元素的情況, 範例網頁中有 4 個 li 元素, 其 CSS 選擇器為 'body > ul > li', 將此選擇器傳入 css() 會得到一個 SelectorList 物件 :

>>> lis=selector.css('body > ul > li')   
>>> lis     
[<Selector query='descendant-or-self::body/ul/li' data='<li>軍師中郎將諸葛亮</li>'>, <Selector query='descendant-or-self::body/ul/li' data='<li>虎威將軍趙雲</li>'>, <Selector query='descendant-or-self::body/ul/li' data='<li>漢壽亭侯關羽</li>'>, <Selector query='descendant-or-self::body/ul/li' data='<li>車騎將軍張飛</li>'>]
>>> type(lis)   
<class 'scrapy.selector.unified.SelectorList'>
>>> lis.get()    
'<li>軍師中郎將諸葛亮</li>'
>>> lis.extract()    
['<li>軍師中郎將諸葛亮</li>', '<li>虎威將軍趙雲</li>', '<li>漢壽亭侯關羽</li>', '<li>車騎將軍張飛</li>']
>>> lis.getall()   
['<li>軍師中郎將諸葛亮</li>', '<li>虎威將軍趙雲</li>', '<li>漢壽亭侯關羽</li>', '<li>車騎將軍張飛</li>']

如果只要取得元素的內容字串, 只要在上面的選擇器後面添加 "::text" 即可 :

>>> lis=selector.css('body > ul > li::text')   
>>> lis.get()   
'軍師中郎將諸葛亮'
>>> lis.extract()   
['軍師中郎將諸葛亮', '虎威將軍趙雲', '漢壽亭侯關羽', '車騎將軍張飛']
>>> lis.getall()  
['軍師中郎將諸葛亮', '虎威將軍趙雲', '漢壽亭侯關羽', '車騎將軍張飛']

可見只要使用 Selector 物件的 xpath() 與 css() 方法即可剖析網頁內容取得目標資料, 不一定需要 BeautifulSoup 的協助. 


3. 呼叫 Response 物件的 css() 與 xpath() 定位元素 :

在 Scrapy 的爬蟲實務中通常並不需要像上面那樣自行建立 Selector 物件, 在爬蟲主程式中的 parse(self, response) 剖析方法中傳入的第二參數 response 是一個代表 HTTP 回應的 Response 物件, 它也提供了 Selector 物件的 css() 與 xpath() 方法, 直接呼叫這兩個方法即可從 Response 物件中定位目標元素與擷取目標資料 :

# myspider.py
from scrapy.spiders import Spider

class MySpider(Spider):
    name='project_name'     # 填入專案名稱
    start_urls=[url1, url2, url3, ...]     # 起始網址
    def parse(self, response):
        selector1=response.xpath(xpath_str)  
        item1=selector1.get()
        selector2=response.css(css_str) 
        item2=selector2.get()
        ...
        yield {
            'item1': item1,
            'item2': item2,
            ...
            }

不過在實際建立爬蟲專案撰寫 parse() 函式之前, 我會將目標網頁另存為本機的 .htm 檔, 然後先在互動環境中用 open() 函式開啟 .htm 檔進行 CSS 或 XPath 測試, 確認可正確取得後再將程式碼複製到 parse() 函式中. 測試程式範例如下 :

>>> with open('test3.htm', 'r', encoding='utf-8') as f:    
    text=f.read()       # 讀取 HTML 網頁碼 
    selector=Selector(text=text)      # 建立 Selector 物件
    xpath=' .... '         # 填入 XPath 
    sel=selector.xpath(xpath)          # 搜尋符合之元素, 取得 SelectorList 物件
    print(sel.getall())      # 檢視結果是否正確

參考 :


沒有留言:

張貼留言