前言
这是 Scrapy 系列学习文章之一,本章主要介绍 Selectors 相关的内容;
本文为作者的原创作品,转载需注明出处;
简介
Scrapy 定义了自己的提取数据的机制,该机制被称作 Selector,该 Selector 是根据 XPath 或者 CSS 标准语言进行定义的;
简而言之,XPath 是一门用来从 XML 文档中选取元素的语言,不过它也可以使用在 HTML 文档中提取元素;
CSS 是一门用来将 style 应用到 HTML 中的一门语言,它为特定的 HTML 元素关联对应 style 而定义了 selectors;
Scrapy selectors 是基于 lxml 类库而构建的;更多有关信息请参考 Selector API
使用 Selectors
构建 selectors
通过向 Selector 类的构造函数传入 text 或者是 TextResponse 对象来构造 selectors 实例;它会根据传入的类型(input type)自动的去选择最佳的解析规则(XML vs HTML);
1 | from scrapy.selector import Selector |
通过参数 text 来构建,
1 | '<html><body><span>good</span></body></html>' body = |
通过 response 来构建,
1 | 'http://example.com', body=body) response = HtmlResponse(url= |
为了使用方便,response 对象直接通过 .selector 暴露一个 selector 实例,
1 | '//span/text()').extract() response.selector.xpath( |
使用 selectors
本小节我们将通过 Scrapy shell 来模拟爬取网站 https://doc.scrapy.org/en/latest/_static/selectors-sample1.html 中的内容,如下,是相关的 HTML 代码,
1 | <html> |
首先,让我们来打开该 shell,
1 | scrapy shell http://doc.scrapy.org/en/latest/_static/selectors-sample1.html |
然后,当 shell 的加载结束以后,你将会通过 shell 变量response
来获取相应的 Response,然后通过response.selector
来获取 Selector 对象;
因为我们处理的是 HTML,所以该 selector 将会自动的使用 HTML parser;
通过上述的 HTML code,我们通过构建 XPath 来从 title 标签中去选择文本:
1 | '//title/text()') response.selector.xpath( |
正是因为通过使用selector
的xpath()
和css()
来查询 Response 是非常常用的做法,因此通过response
简化了相关的调用流程,包含了两个方法response.xpath()
和response.css()
来分别取代response.selector.xpath()
以及responose.selector.css()
方法:
1 | '//title/text()') response.xpath( |
从上述的结果中可以非常明确的看到,返回的是一个 SelectorList 实例,该实例中包含了一组 selectors;通过调用 SelectorList 的相关接口使得我们可以迅速的获取到每一个 selector 元素的相关内容:
1 | 'img').xpath('@src').extract() response.css( |
可见,通过 extract()
方法便可以从 selector 中提取出所要的文本,再看一个例子,
1 | '//title/text()').extract() response.xpath( |
如果你只想去提取第一个相匹配的元素,你可以直接使用.extract_first()
,
1 | '//div[@id="images"]/a/text()').extract_first() response.xpath( |
如果没有找到对应的元素,将会返回None
;
1 | '//div[@id="not-exists"]/text()').extract_first() is None response.xpath( |
可以设置一个 default value 来取代None
1 | '//div[@id="not-exists"]/text()').extract_first(default='not-found') response.xpath( |
注意
,CSS selectors 可以通过 CSS3 pseudo-elements 来选择文本或者是属性;
1 | 'title::text').extract() response.css( |
现在,我们将会去获取一些基本的 URL 和一些 images links,
1 | '//base/@href').extract() response.xpath( |
SelectorList
通过上面的描述,我们清楚的知道,response.selector.xpath()
或者response.xpath()
当然还包含css()
的情况,返回的是一个包含多个 selectors 的列表对象 SelectorList,那么 SelectorList 是什么东西呢?为什么直接可以在该对象上直接调用xpath()
和css()
方法呢?
首先,看看response.xpath()
所返回的类型,1
2
3'//div') divs = response.xpath(
type(divs)
<class 'scrapy.selector.unified.SelectorList'>
再次,看看SelectorList
的源码,1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90class SelectorList(list):
"""
The :class:`SelectorList` class is a subclass of the builtin ``list``
class, which provides a few additional methods.
"""
# __getslice__ is deprecated but `list` builtin implements it only in Py2
def __getslice__(self, i, j):
o = super(SelectorList, self).__getslice__(i, j)
return self.__class__(o)
def __getitem__(self, pos):
o = super(SelectorList, self).__getitem__(pos)
return self.__class__(o) if isinstance(pos, slice) else o
def xpath(self, xpath, namespaces=None, **kwargs):
"""
Call the ``.xpath()`` method for each element in this list and return
their results flattened as another :class:`SelectorList`.
``query`` is the same argument as the one in :meth:`Selector.xpath`
``namespaces`` is an optional ``prefix: namespace-uri`` mapping (dict)
for additional prefixes to those registered with ``register_namespace(prefix, uri)``.
Contrary to ``register_namespace()``, these prefixes are not
saved for future calls.
Any additional named arguments can be used to pass values for XPath
variables in the XPath expression, e.g.::
selector.xpath('//a[href=$url]', url="http://www.example.com")
"""
return self.__class__(flatten([x.xpath(xpath, namespaces=namespaces, **kwargs) for x in self]))
def css(self, query):
"""
Call the ``.css()`` method for each element in this list and return
their results flattened as another :class:`SelectorList`.
``query`` is the same argument as the one in :meth:`Selector.css`
"""
return self.__class__(flatten([x.css(query) for x in self]))
def re(self, regex, replace_entities=True):
"""
Call the ``.re()`` method for each element in this list and return
their results flattened, as a list of unicode strings.
By default, character entity references are replaced by their
corresponding character (except for ``&`` and ``<``.
Passing ``replace_entities`` as ``False`` switches off these
replacements.
"""
return flatten([x.re(regex, replace_entities=replace_entities) for x in self])
def re_first(self, regex, default=None, replace_entities=True):
"""
Call the ``.re()`` method for the first element in this list and
return the result in an unicode string. If the list is empty or the
regex doesn't match anything, return the default value (``None`` if
the argument is not provided).
By default, character entity references are replaced by their
corresponding character (except for ``&`` and ``<``.
Passing ``replace_entities`` as ``False`` switches off these
replacements.
"""
for el in iflatten(x.re(regex, replace_entities=replace_entities) for x in self):
return el
else:
return default
def extract(self):
"""
Call the ``.extract()`` method for each element is this list and return
their results flattened, as a list of unicode strings.
"""
return [x.extract() for x in self]
getall = extract
def extract_first(self, default=None):
"""
Return the result of ``.extract()`` for the first element in this list.
If the list is empty, return the default value.
"""
for x in self:
return x.extract()
else:
return default
get = extract_first
由此可见,SelectorList 内部实现了和 Selector 相同的接口方法,唯一不同的就是,SelectorList 所对应的xpath()
、css()
、re()
等方法内部的实现是遍历 SelectorList 里面的每一个 selector 对象,然后分别执行其对应的xpath()
、css()
、re()
等方法;
Nesting selectors
通过.xpath()
或者.css()
方法返回的是一个包含相同类型的 selectos 的队列,所以,我们仍然可以对返回的 selector 执行.xpath()
和.css()
方法;
1 | '//a[contains(@href, "image")]') links = response.xpath( |
来解读下上面的用例,response.xpath()
等价于执行response.selector.xpath()
,可见执行的是 Selector 的xpath()
方法,该方法返回的是一个selector
列表;通过遍历该列表,得到每一个selector
元素,然后可以继续针对该selector
元素执行xpath()
和css()
方法并获得一个新的包含 Selector 的列表,然后,我们可以遍历该 Selector 列表,然后得到每一个selector
元素,然后可以继续….. 这就是 Scrapy 中有关 Selector 有趣的地方了;
通过正则表达式使用 selectors (.re())
Selector 有一个.re()
方法,通过该方法可以使用正则表达式来提取数据;但是,不同于.xpath()
和.css()
的是,.re()
返回的是一个 unicode strings 的列表;所以,你不能像 Nesting selectors 那样构建.re()
的嵌入式调用方式;
下面的这个例子将会演示如何从上面的 HTML 代码片段中获取 image names
1 | '//a[contains(@href, "image")]/text()').re(r'Name:\s*(.*)') response.xpath( |
使用相对的 XPaths
记住,如果你在使用 nesting selector 操作的同时使用了一个起始符为/
的 XPath,该 XPath 将会是绝对路径而不再与当前的Selector
具有相对性质;
举个例子,假定你想从一个<div>
的元素中去提取所有<p>
元素,首先,你会去提取所有的<div>
元素:
1 | '//div') divs = response.xpath( |
一开始,你或许试图使用如下的方式,但是,这种使用方式是错误的,它的确获取到了<p>
元素,但是它将会返回所有的<p>
元素而不仅仅是<div>
中的<p>
;
1 | for p in divs.xpath('//p'): # this is wrong - gets all <p> from the whole document |
正确的方式是,使用.//p
作为xpath()
中的参数,这样,它将会获取所有<div>
元素内部的<p>
元素
1 | for p in divs.xpath('.//p'): # extracts all <p> inside |
还有一种非常常用的方式,就是获取<div>
的所有直接
子元素<p>
,
1 | for p in divs.xpath('p'): |
XPath 表达式中的参数
XPath 允许你引用 XPath 表达式中的参数,使用$somevariable
;下面我们来通过一个例子看看是如何通过表达式参数来为”id”属性赋值的,而无需 hard code;
1 | # `$val` used in the expression, a `val` argument needs to be passed |
另外的一个例子,找到这样的一个<div>
元素,包含 5 个<a>
子元素且其属性值为”id”的例子,1
2'//div[count(a)=$cnt]/@id', cnt=5).extract_first() response.xpath(
u'images'
parsel,该类库壮大了 Scrapy selectors,更多的相关例子可以参考 XPath variables;
使用 EXSLT 扩展
Scrapy 支持 EXSLT 以及一些相关的 namespaces,
prefix | namespace | usage |
---|---|---|
re | http://exslt.org/regular-expressions | regular expressions |
set | http://exslt.org/sets | set manipulation |
正则表达式 regular expressions
当 XPath 的starts-with()
或者contains()
方法的功能不能满足需要的时候,test()
方法就派上用场了;看下面这个例子,
通过匹配条件,后缀为数字的 “class” 属性,来获取所有相关的 links
1 | from scrapy import Selector |
可以看到,通过在//li
的匹配规则后面通过test()
方法增强了过滤条件,既是要求class
属性值的后缀必须是数字才能被匹配;最后,上面这个例子非常好的诠释了,如果我要在本地构建一些 HTML 代码如何用于快速测试的方式,通过构建一个 doc 文本,将该文本用来构建我们所需要的 Selector 实例;
Set 操作
Set 操作可以在提取 text 元素之前非常方便的从 document tree 中排除部分元素;
下面这个例子通过对 itemscopes 和对应的 itemprops 进行分组去提取微观的数据(该部分内容是从 http://schema.org/Product 中提取)
1 | >>> doc = """ |
1 | "html") sel = Selector(text=doc, type= |
上述代码的执行逻辑是,首先通过遍历所有的itemscope
元素,然后针对每一个itemscope
元素,我们将会查找到所有的 itemprops
,但是不包含itemprops
中的子itemprops
元素,ok,这个逻辑是通过下面这行核心代码所实现的,
1 | ''' props = scope.xpath( |
一些有关 XPath 有用的提示
这里有一些关于使用 XPath 的有用提示,参考 ScrapingHub’s blog;如果你对 XPath 不熟悉,可以从这里开始 XPath tutorial
在某些条件中使用文本节点
当你需要使用 text 文本内容作为参数去使用 XPath string function的时候,避免使用.//text()
的方式,而应当仅仅使用.
即可;因为.//text()
将会产生一个 text elements 的集合既是一个 node-set;而当一个 node-set 要转换为一个 string 的时候,转换的过程必须通过调用 string()、contains() 或者 start-with() 方法才能进行转换,并且只有第一个元素
会被转换,其余的元素并不会被转换,这点尤其需要注意;
举一个例子,
1 | from scrapy import Selector |
得到一个 node-set
1 | '//a//text()').extract() # take a peek at the node-set sel.xpath( |
可以看到,该 node-set 由两个 text element 组成;那么当我们试图要获取其中一个 text element,
1 | "string(//a[1])").extract() # convert it to string sel.xpath( |
可见,利用 string() 方法只能将 node-set 中的第一个元素转换成 string;试图转换第二个元素返回为空;
同理可以推导出,在a
元素中使用.//text()
得到的包含两个 text node 的 node-set 对象,同理将其通过 contains 方法转换为 string 以后并不会得到所有的 text node 的值,同理,只会返回第一个 text node 的值,既是 ‘Click here to go to the’,因此,下面这个查询是不会返回任何结果的,
1 | "//a[contains(.//text(), 'Next Page')]").extract() sel.xpath( |
但是,如果我们使用.
作为参数,则表示当前 node,所以,下面这个方式是生效的,
1 | "//a[contains(., 'Next Page')]").extract() sel.xpath( |
注意 //node[1] 和 (//node)[1] 的区别
//node[1]
从所有匹配的父元素开始检索,并依次返回其第一个子元素;(//node)[1]
从当前文档的所有 nodes 中进行检索,然后只取其中第一个值;
例子,
1 | from scrapy import Selector |
返回相关父元素的所有第一个子元素,
1 | "//li[1]") xp( |
只会去当前文档中的第一个元素<li>
1 | "(//li)[1]") xp( |
返回<ul>
中所有的第一个子元素
1 | "//ul/li[1]") xp( |
返回当前文档中第一个<ul>
元素的第一个子元素<li>
1 | "(//ul/li)[1]") xp( |
如果是通过 class 进行查询,使用 CSS
因为一个元素可以包含多个 CSS classes,这种情况下使用 XPath 将会显得非常的麻烦,
1 | *[contains(concat(' ', normalize-space(@class), ' '), ' someclass ')] |
如果你使用@class='someclass'
,你将可能失去其它的 classes,如果你仅仅使用contains(@class, 'someclass')
那么你可能会得到更多的但并不需要的元素;
不过,Scrapy selectors 允许你使用 selectors 所组成的链条 (chain),所以大多数的时候,你可以先使用 CSS 检索 class 的方式,当需要的时候,再转换到使用 XPath 的方式;
1 | from scrapy import Selector |
这比使用 xpath 的方式要简单许多,但是要记住的是,在 XPath 表达式中使用.
表示紧随上一个匹配的元素,这里指的就是<div class="hero shout/>
;
内置的 Selectors 对象
Selector
1 | class scrapy.selector.Selector(response=None, text=None, type=None) |
Selector 是一个对 response 进行包装的实例,用来去检索与其内容所相关的部分;
相关参数解释如下,
response
是一个 HtmlResponse 或者是一个 XmlResponse 对象;
text
是一个 unicode string 或者是 utf-8 编码的 string,当 response 为 None 的时候生效,如果text
和response
同时输入,则是一个未被定义的行为;
type
定义了 selector 类型,有html
、xml
以及Node
(默认)三种类型
- 如果
type
是None
,text
不为None
,response
为None
,则默认选择html
- 如果
type
是None
,text
为None
,response
不为None
,那么type
的选择将会受到 response 类型的影响;- 如果 response 类型是 HtmlResponse,那么使用
html
- 如果 response 类型是 XmlResponse,那么使用
xml
- 如果 response 类型是其它类型,那么使用其它;
- 如果 response 类型是 HtmlResponse,那么使用
- 如果
type
不为None
,那么会强制使用用户自定义的type
;
xpath(query)
通过 xpath 的查询方式来查找对应的 nodes,当命中以后,将会返回一个 SelectorList 实例;
css(query)
不再赘述
extract()
通过一组 unicode strings 来返回被匹配的 nodes;
re(regex)
通过正则表达式进行匹配并返回被匹配的 unicode strings;需要注意的是,re()
和re_first()
都会将 HTML 元素进行解码操作;
register_namespace(prefix, uri)
为当前的 Selector 注册指定的 namespace;如果不指定相关的 namespaces,你将不能爬取提取那些使用了非标准 namespaces 网页中的数据;看后面的例子,
remove_namespaces()
将删除所有的 namespaces,允许通过 namespace-less xpaths 来进行文档检索;看后面的例子;
nonzero()
如果这里有任何真实的内容被检索将返回True
否则返回False
;
SelectorList
该部分参考 SelectorList
例子
HTML Response
在讲解这个例子之前,我们假定已经有一个 Selector 实例通过 HTMLResponse 进行实例化了
1 | sel = Selector(html_response) |
从 html_response 中检索所有
<h1>
的元素并返回一组 Selector 对象( SelectorList )1
sel.xpath("//h1")
从 html_response 中提取所有的
<h1>
元素的文本内容,返回 unicode strings1
2sel.xpath("//h1").extract() # this includes the h1 tag
sel.xpath("//h1/text()").extract() # this excludes the h1 tag注意,第一个是直接从
<h1>
元素中通过 extract() 方法提取出了其文本内容;第二个是从<h1>
的 text node 中提取出文本内容;- 遍历所有的
<p>
标签,然后打印出他们所有的属性:1
2for node in sel.xpath("//p"):
print node.xpath("@class").extract()
XML Response
1 | sel = Selector(xml_response) |
从 XML response body 中检索所有的
<product>
元素,返回一个 SelectorList 对象1
sel.xpath("//product")
从 Google Base XML Feed 中提取所有的 prices,注意,该用例需要注册一个 namespace;
1
2sel.register_namespace("g", "http://base.google.com/ns/1.0")
sel.xpath("//g:price").extract()
Removing namespaces
如果想写一个写更简单更方便的 XPaths,你可以使用Selector.remove_namespaces()
方法;
让我们通过 Github blog atom feed 作为例子来进行演示,
首先,打开 Scrapy shell
1 | $ scrapy shell https://github.com/blog.atom |
然后,可以看到,我们试图通过 xpath 来检索匹配的 <link>
元素,但并未成功,因为 Atom 的 XML namespace 使得它们的 nodes 含糊不清,既是没有使用标准的 namespaces;
1 | "//link") response.xpath( |
但是,一旦我们通过Selector.remove_namespaces()
方法将其所有的 namespaces 删除掉以后,所有的检索便可以生效了,
1 | response.selector.remove_namespaces() |
你也许想知道为什么 remove namespaces 的行为不是默认被删掉,而是需要手动删掉?原因有二,
- 删除 namespaces 需要遍历文档中的所有元素并对其进行修改,所以,它是一个非常耗时的操作;
- 有些时候,使用 namespaces 是必要的,当万一有些元素的命名发生了冲突,这个时候就必须使用 namespaces,不过这个 cases 非常的罕见;