从HTML源文件库中解析数据通常有以下常用的库可以使用:
Scrapy提取数据有自己的一套机制。它们被称作选择器(seletors),因为他们通过特定的 XPath 或者 CSS 表达式来“选择” HTML文件中的某个部分。XPath 是一门用来在XML文件中选择节点的语言,也可以用在HTML上。 CSS 是一门将HTML文档样式化的语言。选择器由它定义,并与特定的HTML元素的样式相关连。
Scrapy选择器构建于 lxml 库之上,这意味着它们在速度和解析准确性上非常相似。不同于 lxml API的臃肿,该API短小而简洁。这是因为 lxml 库除了用来选择标记化文档外,还可以用到许多任务上。
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()
以下面的文档来解释如何使用选择器:
<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()
选择器方法返回相同类型的选择器列表,因此你也可以对这些选择器调用选择器方法
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
Selector 有一个 .re()
方法,用来通过正则表达式来提取数据。不同于使用 .xpath()
或者 .css()
方法, .re()
方法返回unicode字符串的列表,所以无法构造嵌套式的 .re()
调用。
下面是一个例子,从上面的 HTML code 中提取图像名字:
response.xpath('//a[contains(@href, "image")]/text()').re(r'Name:\s*(.*)')
如果你使用嵌套的选择器,并使用起始为 /
的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()
因建于 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('//li//@href').extract()
sel.xpath('//li[re:test(@class, "item-\d$")]//@href').extract()
C语言库
libxslt
不原生支持EXSLT正则表达式,因此 lxml 在实现时使用了Pythonre
模块的钩子。 因此,在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.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不知道高到哪里去了。
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的类型,可以是html
、xml
或None
(default)
xml
:XmlResponse
html
:HtmlResponse
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() 方法,返回结果为另一个单一化的 SelectorListcss(query)
对列表中的各个元素调用 .css() 方法,返回结果为另一个单一化的 SelectorListextract()
对列表中的各个元素调用 .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'>,
...
如果你对为什么命名空间移除操作并不总是被调用,而需要手动调用有疑惑。这是因为存在如下两个原因,按照相关顺序如下:
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()
两个函数来解决这个问题。