1. 程式人生 > >Scrapy selector介紹

Scrapy selector介紹

從HTML原始檔庫中解析資料通常有以下常用的庫可以使用:

  • BeautifulSoup是在程式設計師間非常流行的網頁分析庫,它基於HTML程式碼的結構來構造一個Python物件, 對不良標記的處理也非常合理,但它有一個缺點:慢。
  • lxml是一個基於 ElementTree (不是Python標準庫的一部分)的python化的XML解析庫(也可以解析HTML)。

Scrapy提取資料有自己的一套機制。它們被稱作選擇器(seletors),因為他們通過特定的 XPath 或者 CSS 表示式來“選擇” HTML檔案中的某個部分。XPath 是一門用來在XML檔案中選擇節點的語言,也可以用在HTML上。 CSS 是一門將HTML文件樣式化的語言。選擇器由它定義,並與特定的HTML元素的樣式相關連。

Scrapy選擇器構建於 lxml 庫之上,這意味著它們在速度和解析準確性上非常相似。不同於 lxml API的臃腫,該API短小而簡潔。這是因為 lxml 庫除了用來選擇標記化文件外,還可以用到許多工上。

1. Using selectors

1.1 Constructing selectors

Scrapy selectors是Selector類的例項,通過傳入text或TextResponse來建立,它自動根據傳入的型別選擇解析規則(XML or HTML):

from scrapy.selecor import Selector
from scrapy.http import HtmlResponse

從text構建:

body = '<html><body><span>good</span></body></html>'
Selector(text=body).xpath('//span/text()').extract()

從response構建:

response = HtmlResponse(url='http://example.com', body=body)
Selector(response=response).xpath('//span/text()').extract()

response物件以 .selector

屬性提供了一個selector, 您可以隨時使用該快捷方法:

response.selector.xpath('//span/text()').extract()

1.2 Using selectors

以下面的文件來解釋如何使用選擇器:

<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.selector 屬性上綁定了一個selector。

檢視title內的文字:

response.selector.xpath('//title/text()')

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

response.xpath('//title/text()')
response.css('title::text')

.xpath().css() 方法返回一個類 SelectorList 的例項, 它是一個新選擇器的列表。這個API可以用來快速的提取巢狀資料。為了提取真實的原文資料,你需要呼叫 .extract() 方法如下:

response.css('img').xpath('@src').extract()

如果你只想要第一個匹配的元素,可以使用·.extract_first()·:

response.xpath('//div[@id="images"]/a/text()').extract_first()

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

response.css('title::text').extract()

現在我們將得到根URL(base URL)和一些圖片連結:

1. response.xpath('//base/@href').extract()
1. response.css('base::attr(href)').extract()
2. response.xpath('//a[contains(@href, "image")]/@href').extract()
2. response.css('a[href*=image]::attr(href)').extract()
3. response.xpath('//a[contains(@href, "image")]/img/@src').extract()
3. response.css('a[href*=image] img::attr(src)').extract()

1.3 Nesting selectors(巢狀選擇器)

選擇器方法返回相同型別的選擇器列表,因此你也可以對這些選擇器呼叫選擇器方法

links = response.xpath('//a[contains(@href, "image")]')
links.extract()
for index, link in enumerate(links):
    args = (index, link.xpath('@href').extract(), link.xpath('img/@src').extract())
    print 'Link number %d points to url %s and image %s' % args

1.4 Using selectors with regular expressions(正則表示式)

Selector 有一個 .re() 方法,用來通過正則表示式來提取資料。不同於使用 .xpath() 或者 .css() 方法, .re() 方法返回unicode字串的列表,所以無法構造巢狀式的 .re() 呼叫。

下面是一個例子,從上面的 HTML code 中提取影象名字:

response.xpath('//a[contains(@href, "image")]/text()').re(r'Name:\s*(.*)')

1.5 Working with relative XPaths(相對XPath)

如果你使用巢狀的選擇器,並使用起始為 / 的XPath,那麼該XPath將對文件使用絕對路徑。

比如,假設你想提取在 <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()

1.6 Using EXSLT extensions

因建於 lxml 之上, Scrapy選擇器也支援一些 EXSLT 擴充套件,可以在XPath表示式中使用這些預先制定的名稱空間:

正則表示式:

例如在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('//li//@href').extract()
sel.xpath('//li[re:test(@class, "item-\d$")]//@href').extract()

C語言庫 libxslt 不原生支援EXSLT正則表示式,因此 lxml 在實現時使用了Python re 模組的鉤子。 因此,在XPath表示式中使用regexp函式可能會犧牲少量的效能。

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

例如使用itemscopes組和對應的itemprops來提取微資料(microdata)(來自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('@itemtype').extract()
...     props = scope.xpath('''
...                 set:difference(./descendant::*/@itemprop,
...                                .//*[@itemscope]/*/@itemprop)''')
...     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 內的元素。

1.7 Some XPath tips

1.7.1 謹慎的使用text nodes

當你想要使用文字內容作為XPath函式的引數時,避免使用.//text(),採用.來替代。

這是因為.//text()會產生一個text元素的集合——一個節點集合。當一個node-set被轉換成一個string(例如,當它被當做引數傳遞給contains()或者start-with()函式的時候),它只返回第一個元素。

示例如下:

>>> from scrapy import Selector
>>> sel = Selector(text='<a href="#">Click here to go to the <strong>Next Page</strong></a>')

把一個node-set轉化成string:

>>> sel.xpath('//a//text()').extract() # 檢視一下node-set
[u'Click here to go to the ', u'Next Page']
>>> sel.xpath("string(//a[1]//text())").extract() #轉換成string
[u'Click here to go to the ']

節點被轉化成了string,但是,它本身以及子節點都放在了一起。

>>> sel.xpath("//a[1]").extract() # select the first node
[u'<a href="#">Click here to go to the <strong>Next Page</strong></a>']
>>> sel.xpath("string(//a[1])").extract() # convert it to string
[u'Click here to go to the Next Page']

因此,使用.//text()node-set不會得到任何結果:

>>> sel.xpath("//a[contains(.//text(), 'Next Page')]").extract()
[]

但是,使用.會奏效:

>>> sel.xpath("//a[contains(., 'Next Page')]").extract()
[u'<a href="#">Click here to go to the <strong>Next Page</strong></a>']

1.7.2 注意 //node[1](//node)[1]的區別

  • //node[1] 選擇它們的父節點的第一個子節點(occurring first under their respective parents)
  • (//node)[1] 選擇文件中的所有node,然後選取其中的第一個

1.7.3 當通過class查詢的時候, 考慮使用CSS
因為一個元素可能含有多個CSS class,用XPath的方式選擇元素會很冗長:

*[contains(concat(' ', normalize-space(@class), ' '), ' someclass ')]

如果使用@class='someclass'可能會遺漏含有其他class的元素,如果使用contains(@class, 'someclass')去補償的話,會發現其中包含了多餘的含有相同的someclass的元素。

因此,scrapy允許鏈式使用選擇器,因此多數情況下你可以先用CSS選擇class,再使用XPath:

>>> from scrapy import Selector
>>> sel = Selector(text='<div class="hero shout"><time datetime="2014-07-23 19:00">Special  date</time></div>')
>>> sel.css('.shout').xpath('./time/@datetime').extract()
[u'2014-07-23 19:00']

這比上面的冗長的XPath不知道高到哪裡去了。

2. 關於Selector的詳細介紹

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

Selector是對response的封裝,用於選取其中的特定內容。

下面是Selector的主要成員變數

  • response 一個HtmlResponse或者XmlResponse物件
  • text 一個unicode字串或者utf-8文字,當response為空的時候才有效。同時使用text和response是未定義行為
  • type 定義selector的型別,可以是htmlxmlNone(default)

    • 如果type為None,那麼selector會根據response自動選擇最佳的type,如果定義了text那麼預設成html型別
    • response的型別確定:
    • xmlXmlResponse
    • htmlHtmlResponse
    • html:其他型別

    • 如果已經設定了type那麼強制使用設定好的type。

主要成員函式

  • xpath() 尋找匹配xpath query 的節點,並返回 SelectorList 的一個例項結果,單一化其所有元素。返回的列表元素也實現了 Selector 的介面。query 是包含XPATH查詢請求的字串。

  • css() 應用給定的CSS選擇器,返回 SelectorList 的一個例項。在後臺,通過 cssselect 庫和執行 .xpath() 方法,CSS查詢會被轉換為XPath查詢

  • extract() 序列化並將匹配到的節點返回一個unicode字串列表。 結尾是編碼內容的百分比
  • reg(regex) 應用給定的regex,並返回匹配到的unicode字串列表。regex 可以是一個已編譯的正則表示式,也可以是一個將被 re.compile(regex) 編譯為正則表示式的字串。
  • register_namespaces(prefix, uri) 註冊給定的名稱空間,其將在 Selector 中使用。 不註冊名稱空間,你將無法從非標準名稱空間中選擇或提取資料。
  • remove_namespaces() 移除所有的名稱空間,允許使用少量的名稱空間xpaths遍歷文件
  • __nonzero__() 如果選擇了任意的真實文件,將返回 True ,否則返回 False 。 也就是說, Selector 的布林值是通過它選擇的內容確定的。

SelectorList物件

class scrapy.selector.SelectorList

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

  • xpath(query) 對列表中的每個元素呼叫 .xpath() 方法,返回結果為另一個單一化的 SelectorList
  • css(query) 對列表中的各個元素呼叫 .css() 方法,返回結果為另一個單一化的 SelectorList
  • extract() 對列表中的各個元素呼叫 .extract() 方法,返回結果為單一化的unicode字串列表
  • re() 對列表中的各個元素呼叫 .re() 方法,返回結果為單一化的unicode字串列表
  • __nonzero__() 列表非空則返回True,否則返回False

在XML響應上的選擇器樣例
我們假設已經有一個通過 XmlResponse 物件例項化的 Selector ,如下:

sel = Selector(xml_response)

選擇所有的 元素,返回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訂閱來解釋這個情況。

首先,我們使用想爬取的url來開啟shell:

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. 會存在這樣的情況,確實需要使用名稱空間,但有些元素的名字與名稱空間衝突。儘管這些情況非常少見。

xpath解析帶名稱空間頁面的方法:

If the XPath expression does not include a prefix, it is assumed that the namespace URI is the empty namespace. If your XML includes a default namespace, you must still add a prefix and namespace URI to the XmlNamespaceManager; otherwise, you will not get any nodes selected

上面一段話的意思就是:如果XPath沒有指定名稱空間的話,那麼它的名稱空間為空。如果待解析XML檔案含有預設名稱空間的話,那麼你必須新增那個名稱空間的字首,並且把名稱空間的URI新增到XmlNamespaceManager中,否則,你得不到任何查詢結果。

對於scrapy,這裡提供了register_namespaces(prefix, uri)remove_namespaces()兩個函式來解決這個問題。