1. 程式人生 > >爬蟲:Scrapy5 - 選擇器Selectors

爬蟲:Scrapy5 - 選擇器Selectors

服務器 更多 stars 文件中 alt nic data bar nts

當抓取網頁時,常見的任務是從HTML源碼中提取數據。現有的一些庫可以達到這個目的:

  • BeautifulSoup
  • lxml

Scrapy 提取數據有自己的一套機制。它們被稱作選擇器(seletors),因為他們通過特定的 XPath 或者 CSS 表達式來“選擇” HTML 文件中的某個部分。

構造選擇器

Scrapy selector 是以 文字(Text)或 TextResponse 構造的 Selector。其根據輸入類型自動選擇最優的分析方法(XML vs HTML):

>>> from scrapy.selector import Selector
>>> from scrapy.http import HtmlResponse

以文字構造:

>>> body = ‘<html><body><span>good</span></body></html>‘
>>> Selector(text=body).xpath(‘//span/text()‘).extract()
[u‘good‘]

以 response 構造:

>>> response = HtmlResponse(url=‘http://example.com‘, body=body)
>>> Selector(response=response).xpath(‘//span/text()‘).extract()
[u‘good‘]

為了方便起見,response 對象以 .selector 屬性提供了一個 selector:

>>> response.selector.xpath(‘//span/text()‘).extract()
[u‘good‘]

使用選擇器

使用 Scrapy Shell 和 Scrapy 文檔服務器的一個樣例頁面,來解釋如何使用選擇器:

http://doc.scrapy.org/en/latest/_static/selectors-sample1.html

技術分享
<html>
 <head>
  <base href=‘http://example.com/‘ />
  <title>Example website</title>
 </head>
 <body>
  <div id=‘images‘>
   <a href=‘image1.html‘>Name: My image 1 <br /><img src=‘image1_thumb.jpg‘ /></a>
   <a href=‘image2.html‘>Name: My image 2 <br /><img src=‘image2_thumb.jpg‘ /></a>
   <a href=‘image3.html‘>Name: My image 3 <br /><img src=‘image3_thumb.jpg‘ /></a>
   <a href=‘image4.html‘>Name: My image 4 <br /><img src=‘image4_thumb.jpg‘ /></a>
   <a href=‘image5.html‘>Name: My image 5 <br /><img src=‘image5_thumb.jpg‘ /></a>
  </div>
 </body>
</html>
技術分享

首先打開 Shell:

scrapy shell http://doc.scrapy.org/en/latest/_static/selectors-sample1.html

當 shell 載入後,將獲得名為 response 的 shell 變量,其為響應的 response,並且在其 response.selector 屬性上綁定了一個 selector。

因為處理的是 HTML,選擇器自動使用 HTML 語法分析。

那麽,通過查看 HTML code 該頁面的源碼,可以構建一個 XPath 來選擇 title 標簽內的文字:

>>> response.selector.xpath(‘//title/text()‘)
[<Selector (text) xpath=//title/text()>]

由於在 response 中使用 XPath、CSS 查詢十分普遍,因此,Scrapy 提供了兩個實用的快捷方式:

response.xpath()

response.css()

>>> response.xpath(‘//title/text()‘)
[<Selector (text) xpath=//title/text()>]
>>> response.css(‘title::text‘)
[<Selector (text) xpath=//title/text()>]

.xpath() 以及 .css() 方法返回一個類 SelectList 的實例,它是一個新選擇器的列表。這個 API 可以用來快速的提取嵌套數據。

為了提取真實的原文數據,需要調用 .extract() 方法如下:

>>> response.xpath(‘//title/text()‘).extract()
[u‘Example website‘]

CSS 選擇器可以使用 CSS3 偽元素(pseudo-elements)來選擇文字或者屬性節點:

>>> response.css(‘title::text‘).extract()
[u‘Example website‘]

獲取得到根 URL(base URL)和一些圖片鏈接:

技術分享
>>> response.xpath([email protected]).extract()
[u‘http://example.com/‘]

>>> response.css(‘base::attr(href)‘).extract()
[u‘http://example.com/‘]

>>> response.xpath(‘//a[contains(@href, "image")][email protected]).extract()
[u‘image1.html‘,
 u‘image2.html‘,
 u‘image3.html‘,
 u‘image4.html‘,
 u‘image5.html‘]

>>> response.css(‘a[href*=image]::attr(href)‘).extract()
[u‘image1.html‘,
 u‘image2.html‘,
 u‘image3.html‘,
 u‘image4.html‘,
 u‘image5.html‘]

>>> response.xpath(‘//a[contains(@href, "image")][email protected]).extract()
[u‘image1_thumb.jpg‘,
 u‘image2_thumb.jpg‘,
 u‘image3_thumb.jpg‘,
 u‘image4_thumb.jpg‘,
 u‘image5_thumb.jpg‘]

>>> response.css(‘a[href*=image] img::attr(src)‘).extract()
[u‘image1_thumb.jpg‘,
 u‘image2_thumb.jpg‘,
 u‘image3_thumb.jpg‘,
 u‘image4_thumb.jpg‘,
 u‘image5_thumb.jpg‘]
技術分享

嵌套選擇器

選擇器方法(.xpath() or css())返回相同類型的選擇器列表,因此也可以對這些選擇器調用選擇器方法:

技術分享
>>> links = response.xpath(‘//a[contains(@href, "image")]‘)
>>> links.extract()
[u‘<a href="image1.html">Name: My image 1 <br><img src="image1_thumb.jpg"></a>‘,
 u‘<a href="image2.html">Name: My image 2 <br><img src="image2_thumb.jpg"></a>‘,
 u‘<a href="image3.html">Name: My image 3 <br><img src="image3_thumb.jpg"></a>‘,
 u‘<a href="image4.html">Name: My image 4 <br><img src="image4_thumb.jpg"></a>‘,
 u‘<a href="image5.html">Name: My image 5 <br><img src="image5_thumb.jpg"></a>‘]

>>> for index, link in enumerate(links):
        args = (index, link.xpath([email protected]).extract(), link.xpath([email protected]).extract())
        print ‘Link number %d points to url %s and image %s‘ % args

Link number 0 points to url [u‘image1.html‘] and image [u‘image1_thumb.jpg‘]
Link number 1 points to url [u‘image2.html‘] and image [u‘image2_thumb.jpg‘]
Link number 2 points to url [u‘image3.html‘] and image [u‘image3_thumb.jpg‘]
Link number 3 points to url [u‘image4.html‘] and image [u‘image4_thumb.jpg‘]
Link number 4 points to url [u‘image5.html‘] and image [u‘image5_thumb.jpg‘]
技術分享

結合正則表達式使用選擇器

Selector 也有一個 .re() 方法,用來通過正則表達式來提取數據。然而,不同於使用 .xpath() 或者 .css() 方法,.re() 方法返回 Unicode 字符串的列表。所以無法構造嵌套式的 .re() 調用。

>>> response.xpath(‘//a[contains(@href, "image")]/text()‘).re(r‘Name:\s*(.*)‘)
[u‘My image 1‘,
 u‘My image 2‘,
 u‘My image 3‘,
 u‘My image 4‘,
 u‘My image 5‘]

使用相對 XPaths

如果使用嵌套的選擇器,並且使用起始為 / 的 XPath, 那麽該 XPath 將對文檔使用絕對路徑,而且對於你調用的 Selector 不是相對路徑。

比如,假設要提取 <div> 元素中所有的 <p> 元素。首先,先得到所有的 <div> 元素:

>>> divs = response.xpath(‘//div‘)

開始時,你也許會嘗試使用下面的錯誤的方法,因為它其實是從整篇文檔中,而不是僅僅從那些 <div> 元素內部提取所有的 <p> 元素:

>>> for p in divs.xpath(‘//p‘):  # this is wrong - gets all <p> from the whole document
...     print p.extract()

下面是比較適合的處理方法(註意 .//p XPath的點前綴):

>>> for p in divs.xpath(‘.//p‘):  # extracts all <p> inside
...     print p.extract()

另一種常見的情況是提取所有直系的 <p> 元素:

>>> for p in divs.xpath(‘p‘):
...     print p.extract()

使用 EXSLT 擴展

因建於 lxml 之上,Scrapy 選擇器也支持一些 EXSLT 擴展,可以在 XPath 表達式中使用這些預先指定的命名空間:

前綴 命名空間 用途
re http://exslt.org/regular-expressions 正則表達式
set http://exslt.org/sets 集合操作

正則表達式

例如在 XPath 的 starts-with() 或 contains() 無法滿足需求時,test() 函數可以非常有用。

例如在列表中選擇有 “class” 元素且結尾為一個數字的鏈接:

技術分享
>>> from scrapy import Selector
>>> doc = """
... <div>
...     <ul>
...         <li class="item-0"><a href="link1.html">first item</a></li>
...         <li class="item-1"><a href="link2.html">second item</a></li>
...         <li class="item-inactive"><a href="link3.html">third item</a></li>
...         <li class="item-1"><a href="link4.html">fourth item</a></li>
...         <li class="item-0"><a href="link5.html">fifth item</a></li>
...     </ul>
... </div>
... """
>>> sel = Selector(text=doc, type="html")
>>> sel.xpath([email protected]).extract()
[u‘link1.html‘, u‘link2.html‘, u‘link3.html‘, u‘link4.html‘, u‘link5.html‘]
>>> sel.xpath(‘//li[re:test(@class, "item-\d$")][email protected]).extract()
[u‘link1.html‘, u‘link2.html‘, u‘link4.html‘, u‘link5.html‘]
>>>
技術分享

警告:C 語言庫 libxslt 不支持 EXSLT 正則表達式,因此 lxml 在實現時使用了 Python re 模塊的鉤子。因此,在 XPath 表達式中使用 regexp 函數可能會犧牲少量的性能。

集合操作

集合操作可以方便地用於在提取文字元素前從文檔樹種去除一些部分。

例如使用 itemscopes 組合對應的 itemprops 來提取微數據(來自 http://schema.org/Product 的樣本內容)

技術分享
>>> doc = """
... <div itemscope itemtype="http://schema.org/Product">
...   <span itemprop="name">Kenmore White 17" Microwave</span>
...   <img src="kenmore-microwave-17in.jpg" alt=‘Kenmore 17" Microwave‘ />
...   <div itemprop="aggregateRating"
...     itemscope itemtype="http://schema.org/AggregateRating">
...    Rated <span itemprop="ratingValue">3.5</span>/5
...    based on <span itemprop="reviewCount">11</span> customer reviews
...   </div>
...
...   <div itemprop="offers" itemscope itemtype="http://schema.org/Offer">
...     <span itemprop="price">$55.00</span>
...     <link itemprop="availability" href="http://schema.org/InStock" />In stock
...   </div>
...
...   Product description:
...   <span itemprop="description">0.7 cubic feet countertop microwave.
...   Has six preset cooking categories and convenience features like
...   Add-A-Minute and Child Lock.</span>
...
...   Customer reviews:
...
...   <div itemprop="review" itemscope itemtype="http://schema.org/Review">
...     <span itemprop="name">Not a happy camper</span> -
...     by <span itemprop="author">Ellie</span>,
...     <meta itemprop="datePublished" content="2011-04-01">April 1, 2011
...     <div itemprop="reviewRating" itemscope itemtype="http://schema.org/Rating">
...       <meta itemprop="worstRating" content = "1">
...       <span itemprop="ratingValue">1</span>/
...       <span itemprop="bestRating">5</span>stars
...     </div>
...     <span itemprop="description">The lamp burned out and now I have to replace
...     it. </span>
...   </div>
...
...   <div itemprop="review" itemscope itemtype="http://schema.org/Review">
...     <span itemprop="name">Value purchase</span> -
...     by <span itemprop="author">Lucas</span>,
...     <meta itemprop="datePublished" content="2011-03-25">March 25, 2011
...     <div itemprop="reviewRating" itemscope itemtype="http://schema.org/Rating">
...       <meta itemprop="worstRating" content = "1"/>
...       <span itemprop="ratingValue">4</span>/
...       <span itemprop="bestRating">5</span>stars
...     </div>
...     <span itemprop="description">Great microwave for the price. It is small and
...     fits in my apartment.</span>
...   </div>
...   ...
... </div>
... """
>>>
>>> for scope in sel.xpath(‘//div[@itemscope]‘):
...     print "current scope:", scope.xpath([email protected]).extract()
...     props = scope.xpath(‘‘‘
...                 set:difference(./descendant::[email protected],
...                                .//*[@itemscope][email protected])‘‘‘)
...     print "    properties:", props.extract()
...     print
...

current scope: [u‘http://schema.org/Product‘]
    properties: [u‘name‘, u‘aggregateRating‘, u‘offers‘, u‘description‘, u‘review‘, u‘review‘]

current scope: [u‘http://schema.org/AggregateRating‘]
    properties: [u‘ratingValue‘, u‘reviewCount‘]

current scope: [u‘http://schema.org/Offer‘]
    properties: [u‘price‘, u‘availability‘]

current scope: [u‘http://schema.org/Review‘]
    properties: [u‘name‘, u‘author‘, u‘datePublished‘, u‘reviewRating‘, u‘description‘]

current scope: [u‘http://schema.org/Rating‘]
    properties: [u‘worstRating‘, u‘ratingValue‘, u‘bestRating‘]

current scope: [u‘http://schema.org/Review‘]
    properties: [u‘name‘, u‘author‘, u‘datePublished‘, u‘reviewRating‘, u‘description‘]

current scope: [u‘http://schema.org/Rating‘]
    properties: [u‘worstRating‘, u‘ratingValue‘, u‘bestRating‘]

>>>
技術分享

在這裏,首先在 itemscope 元素上叠代,對於其中的每一個元素,我們尋找所有的 itemprops 元素,並排除那些身在另一個 itemscope 內的元素。

內建選擇器的參考

class scrapy.selector.Selector(response=None, text=None, type=None)

Selector 的實例時對選擇某些內容響應的封裝。

response 是 HTMLResponse 或 XMLResponse 的一個對象,將被用來選擇和提取數據。

text 是在 response 不可用時的一個 unicode 字符串或 utf-8 編碼的文字。將 text 和 response 一起使用時未定義行為。

type 定義了選擇器類型,可以是 html, xml 或 None(默認)。

如果 type 是 None,選擇器會根據 response 類型(參見下面)自動選擇最佳類型,或者在和 text 一起使用時,默認為 html。

如果 type 是None,並傳遞了一個 response,選擇器類型將從 response 類型中推導如下:

  • "html" for HtmlResponse type
  • "xml" for XmlResponse type
  • "html" for anything else

其他情況下,如果設定了 type ,選擇器類型將被強制設定,而不進行檢測。

xpath(query)

尋找可以匹配 xpath query 的節點,並返回一個 SelectorList 的一個實例結果,單一化其所有元素。列表元素也實現了 Selector 的接口。

query 是包含 XPath 查詢請求的字符串。

方便起見該方法也可以通過 response.xpath() 調用。

css(query)

css(query)

應用給定的 CSS 選擇器,返回 SelectorList 的一個實例。

query是一個包含 CSS 選擇器的字符串。

在後臺,通過 cssselect 庫和運行 .xpath() 方法,CSS 查詢會被轉換為 XPath 查詢。

方便起見該方法也可以通過 response.css() 調用。

extract()

串行化並將匹配到的節點返回一個 unicode 字符串列表。 結尾是編碼內容的百分比。

re(regex)

應用給定的 regex,並返回匹配到的 unicode 字符串列表。

regex可以是一個已編譯的正則表達式,也可以是一個將被 re.compile(regex) 編譯為正則表達式的字符串。

register_namespace(prefix, uri)

註冊給定的命名空間,其將在 Selector 中使用。 不註冊命名空間,你將無法從非標準命名空間中選擇或提取數據。

remove_namespaces()

移除所有的命名空間,允許使用少量的命名空間 xpaths 遍歷文檔。參加下面的例子。

__nonzero__()

如果選擇了任意的真實文檔,將返回 True ,否則返回 False 。 也就是說, Selector 的布爾值是通過它選擇的內容確定的。

SelectorList 對象

class scrapy.selector.SelectorList

SelectorList 類是內建 list 類的子類,提供了一些額外的方法。

xpath(query)

對列表中的每個元素調用.xpath()方法,返回結果為另一個單一化的 SelectorList。

query 和 Selector.xpath() 中的參數相同。

css(query)

對列表中的各個元素調用.css() 方法,返回結果為另一個單一化的 SelectorList。

query 和 Selector.css() 中的參數相同。

extract()

對列表中的各個元素調用.extract()方法,返回結果為單一化的 unicode 字符串列表。

re()

對列表中的各個元素調用 .re() 方法,返回結果為單一化的 unicode 字符串列表。

__nonzero__()

列表非空則返回 True,否則返回 False。

在 HTML 響應上的選擇器樣例

這裏是一些 Selector 的樣例,用來說明一些概念。在所有的例子中,假設已經有一個通過 HtmlResponse 對象實例化的 Selector,如下:

sel = Selector(html_response)
技術分享
#從 HTML 響應主體中提取所有的<h1>元素,返回:class:Selector 對象(即 SelectorList 的一個對象)的列表:

sel.xpath("//h1")

#從 HTML 響應主體上提取所有<h1>元素的文字,返回一個 unicode 字符串的列表:

sel.xpath("//h1").extract()         # this includes the h1 tag
sel.xpath("//h1/text()").extract()  # this excludes the h1 tag

#在所有<p>標簽上叠代,打印它們的類屬性:

for node in sel.xpath("//p"):
    print node.xpath("@class").extract()
技術分享

在 XML 響應上的選擇器樣例

這裏是一些樣例,用來說明一些概念。在兩個例子中,我們假設已經有一個通過 XmlResponse 對象實例化的 Selector ,如下:

sel = Selector(xml_response)
技術分享
#從 XML 響應主體中選擇所有的 product 元素,返回 Selector 對象(即 SelectorList 對象)的列表:

sel.xpath("//product")

#從 Google Base XML feed 中提取所有的價錢,這需要註冊一個命名空間:

sel.register_namespace("g", "http://base.google.com/ns/1.0")
sel.xpath("//g:price").extract()
技術分享

移除命名空間

在處理爬蟲項目時,完全去掉命名空間而僅僅處理元素名字,寫更多簡單/實用的 XPath 會方便很多。你可以為此實用 Selector.remove_namespaces() 方法。

以 Github 博客的 atom 訂閱來解釋這個情況:

$ scrapy shell https://github.com/blog.atom

一旦進入 shell,我們可以嘗試選擇所有的 <link> 對象,可以看到沒有結果(因為 Atom XML 命名空間混淆了這些節點):

>>> response.xpath("//link")
[]

但一旦調用 Selector.remove_namespaces() 方法,所有節點都可以直接通過他們的名字來訪問:

>>> response.selector.remove_namespaces()
>>> response.xpath("//link")
[<Selector xpath=‘//link‘ data=u‘<link xmlns="http://www.w3.org/2005/Atom‘>,
 <Selector xpath=‘//link‘ data=u‘<link xmlns="http://www.w3.org/2005/Atom‘>,
 ...

如果你對為什麽命名空間移除操作並不總是被調用,而需要手動調用有疑惑。這是因為存在如下兩個原因,按照相關順序如下:

  1. 移除命名空間需要叠代並修改文件的所有節點,而這對於 Scrapy 爬取的所有文檔操作需要一定的性能消耗
  2. 會存在這樣的情況,確實需要使用命名空間,但有些元素的名字與命名空間沖突。盡管這些情況非常少見。

爬蟲:Scrapy5 - 選擇器Selectors