选择器(Selectors)
当您抓取网页时,您需要执行的最常见任务是从HTML源中提取数据。有几个库可以实现这一点:
- BeautifulSoup 是Python 程序员中非常流行的网络抓取库,它基于HTML代码的结构构建一个Python对象,对不良标记的处理也非常合理,但它有一个缺点:它很慢。
- lxml 是一个基于ElementTree 的pythonic API(lxml不是Python标准库的一部分。)XML解析库(也解析HTML).
Scrapy 自带了提取数据的机制。它们称为选择器,因为它们由 XPath 或 CSS 表达式“选择”的HTML文档的某些部分。
使用选择器
构造选择器
Scrapy选择器是通过文本(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代码:
<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:
接着,当shell载入后,您将获得名为 response
的shell变量用来表示响应, 并且在其 response.selector
属性上绑定了一个selector。
由于我们处理的是HTML,所以选择器会自动使用HTML解析器。
因此,通过查看该页面的HTML源码,我们构建一个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()
方法返回一个 SelectorList
实例,它是一个新的选择器列表。此API可用于快速提取嵌套数据:
>>> response.css('img').xpath('@src').extract()
[u'image1_thumb.jpg',
u'image2_thumb.jpg',
u'image3_thumb.jpg',
u'image4_thumb.jpg',
u'image5_thumb.jpg']
为了实际提取文本数据,必须调用选择器 .extract()
方法,如下所示:
>>> response.xpath('//title/text()').extract()
[u'Example website']
如果只想提取第一个匹配的元素,可以调用 .extract_first()
:
>>> response.xpath('//div[@id="images"]/a/text()').extract_first()
u'Name: My image 1 '
如果没有匹配的元素,则返回 None:
>>> response.xpath('//div[@id="not-exists"]/text()').extract_first() is None
True
可以提供默认返回值作为参数,而不使用None:
>>> response.xpath('//div[@id="not-exists"]/text()').extract_first(default='not-found')
'not-found'
注意CSS选择器可以使用CSS3伪元素(pseudo-elements)来选择文字或者属性节点:
>>> response.css('title::text').extract()
[u'Example website']
现在我们要获取 base URL 和一些图像链接:
>>> response.xpath('//base/@href').extract()
[u'http://example.com/']
>>> response.css('base::attr(href)').extract()
[u'http://example.com/']
>>> response.xpath('//a[contains(@href, "image")]/@href').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")]/img/@src').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('@href').extract(), link.xpath('img/@src').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()
调用。
以下是用于从上面的HTML代码中提取图片名称的示例:
>>> 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']
还有一个额外的辅助函数 .extract_first()
for .re()
,命名为 .re_first()
。使用它只提取第一个匹配的字符串:
>>> response.xpath('//a[contains(@href, "image")]/text()').re_first(r'Name:\s*(.*)')
u'My image 1'
使用相对XPaths
请记住,如果您嵌套选择器并使用起始为 /
的 XPath,该XPath将对文档使用绝对路径,而且对于你调用的 Selector 不是相对路径。
例如,假设要提取
元素。首先,你会得到所有的
>>> divs = response.xpath('//div')
首先,你可能会使用下面的方法,这是错误的,因为它实际上从文档中提取所有 <p>
元素,而不仅仅是 <div>
元素内的元素:
>>> 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()
有关相对XPath的更多详细信息,请参阅XPath规范中的 位置路径 部分。
使用EXSLT扩展
因构建在 lxml 之上,Scrapy选择器也支持一些 EXSLT 扩展,可以在XPath表达式中使用这些预先制定的命名空间:
前缀 | 命名空间 | 用法 |
---|---|---|
re | http://exslt.org/regular-expressions | http://exslt.org/regexp/index.html |
set | http://exslt.org/sets | http://exslt.org/set/index.html |
正则表达式
例如,当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()
[u'link1.html', u'link2.html', u'link3.html', u'link4.html', u'link5.html']
>>> sel.xpath('//li[re:test(@class, "item-\d$")]//@href').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>
... """
>>> sel = Selector(text=doc, type="html")
>>> 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 内的元素。
一些XPath提示
您可以根据 ScrapingHub的博客 的帖子发现和XPath与Scrapy选择器有用的知识。如果您不太熟悉XPath,你可能想先看看这个 XPath教程。
在条件中使用文本结点(text node)
当您需要使用文本内容作为XPath字符串函数的参数时,请使用 .
代替.//text()
。
这是因为表达式 //text()
产生一组文本元素 —— 一个 node-set。一个 node-set 被转换为一个字符串,当它作为参数传递给一个字符串函数,如contains()
或 starts-with()
时,会产生第一个元素的文本。
示例:
>>> from scrapy import Selector
>>> sel = Selector(text='<a href="#">Click here to go to the <strong>Next Page</strong></a>')
将 node-set 转换为字符串:
>>> sel.xpath('//a//text()').extract() # take a peek at the node-set
[u'Click here to go to the ', u'Next Page']
>>> sel.xpath("string(//a[1]//text())").extract() # convert it to string
[u'Click here to go to the ']
然而,转换为字符串的节点会将自身的文本加上其所有后代:
>>> 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()
[]
But using the .
to mean the node, works:
>>> sel.xpath("//a[contains(., 'Next Page')]").extract()
[u'<a href="#">Click here to go to the <strong>Next Page</strong></a>']
注意 //node[1] 和 (//node)[1] 的区别
//node[1]
选择在其各自父节点下首先出现的所有 node。
(//node)[1]
选择文档中的所有 node,然后只获取其中的第一个节点。
示例:
>>> from scrapy import Selector
>>> sel = Selector(text="""
....: <ul class="list">
....: <li>1</li>
....: <li>2</li>
....: <li>3</li>
....: </ul>
....: <ul class="list">
....: <li>4</li>
....: <li>5</li>
....: <li>6</li>
....: </ul>""")
>>> xp = lambda x: sel.xpath(x).extract()
This gets all first <li>
elements under whatever it is its parent:
>>> xp("//li[1]")
[u'<li>1</li>', u'<li>4</li>']
And this gets the first <li>
element in the whole document:
>>> xp("(//li)[1]")
[u'<li>1</li>']
This gets all first <li>
elements under an <ul>
parent:
>>> xp("//ul/li[1]")
[u'<li>1</li>', u'<li>4</li>']
And this gets the first<li>
element under an <ul>
parent in the whole document:
>>> xp("(//ul/li)[1]")
[u'<li>1</li>']
当按类查询时,请考虑使用CSS
因为一个元素可以包含多个CSS类,XPath选择元素的方法是相当冗长:
*[contains(concat(' ', normalize-space(@class), ' '), ' someclass ')]
如果您使用 @class='someclass'
,你可能会丢失有其他类的元素,如果你只是使用 contains(@class,'someclass')
来弥补,您可能会得到更多的您想要的元素,前提是他们的类名中都包含字符串 someclass
。
事实证明,Scrapy 选择器允许你链接选择器,所以大多数时候你可以使用CSS选择类,然后在需要时切换到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技巧更清晰。记得在XPath表达式中使用 .
来表示相对路径。
内建选择器参考
class scrapy.selector.Selector(response=None, text=None, type=None)
Selector 实例是一个封装了 response 的封装器,可以选择所封装的 response 的部分内容。
response
是将用于选择和提取数据的HtmlResponse
或XmlResponse
对象。text
是unicode字符串或utf-8编码文本,用于response
不可用时的情况。一起使用text
和response
是未定义的行为。type
定义选择器类型,它可以是“html”,“xml”或None(默认)。如果
type
为None,选择器会根据response
类型(见下文)自动选择最佳类型,或者在与text
一起使用时默认为“html”。如果 type 为None并且传递了
response
,则从响应类型推断选择器类型如下:- "html" for
HtmlResponse
type - "xml" for
XmlResponse
type - "html" for anything else
否则,如果设置了
type
,则强制使用指定类型,而不会发生检测。查找与 xpath
query
匹配的节点,并将结果作为SelectorList
实例返回,并将所有元素单一化。列表元素也实现了Selector
接口。query
是一个包含要应用的XPATH查询的字符串。注意!
为了方便起见,该方法也可以通过
response.css()
调用应用给定的CSS选择器并返回
SelectorList
实例。query是一个包含要应用的CSS选择器的字符串。在后台,CSS查询使用
cssselect
库转换为XPath查询并运行.xpath()
方法。注意!
为了方便起见,该方法也可以通过
response.css()
调用序列化(Serialize )并返回匹配的节点作为unicode字符串列表。结尾是编码内容的百分比。
应用给定的正则表达式并返回一个包含匹配项的unicode字符串的列表。
regex
可以已编译的正则表达式,也可以是将被re.compile(regex)
编译为正则表达式的字符串。注册要在此
Selector
中使用的给定命名空间。如果不注册命名空间,则无法从非标准命名空间中选择或提取数据。参见下面的例子。删除所有命名空间,允许使用少量的命名空间xpaths遍历文档。参见下面的例子。
如果选择了任何真实的内容,则返回
True
;否则返回False
。换句话说,Selector
的布尔值由它选择的内容确定。- "html" for
SelectorList 对象
class scrapy.selector.SelectorList
SelectorList
类是内置list
类的子类,它提供了一些额外的方法。为此列表中的每个元素调用
.xpath()
方法,并将其结果作为另一个单一化的SelectorList
返回。query
是与Selector.xpath()
中的参数相同的参数。为此列表中的每个元素调用
.css()
方法,并将其结果作为另一个单一化的SelectorList
返回。query
是与 Selector.css()
中的参数相同的参数。对列表中的各个元素调用
.extract()
方法,返回结果为单一化的unicode字符串列表。为此列表中的每个元素调用
.re()
方法,返回结果为单一化的unicode字符串列表。列表非空则返回True,否则返回False。
HTML响应的选择器示例
这里有几个 Selector
示例来说明几个概念。在所有情况下,我们假设已经有一个通过 HtmlResponse
对象实例化的Selector
,像这样:
sel = Selector(html_response)
从HTML响应正文中选择所有
<h1>
元素,返回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)
从HTML响应正文中选择所有
<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订阅来解释这个情况。
首先,我们使用想爬取的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'>,
...
如果你对为什么命名空间移除操作并不总是被调用,而需要手动调用有疑惑。这是因为存在如下两个原因,按照相关顺序如下:
- 移除命名空间需要迭代并修改文件的所有节点,而这对于Scrapy爬取的所有文档操作需要一定的性能消耗。
- 会存在这样的情况,确实需要使用命名空间,但有些元素的名字与命名空间冲突。尽管这些情况非常少见。