在上一篇 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()) # 檢視結果是否正確
參考 :
沒有留言:
張貼留言