前言
这是 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 | '//div') divs = response.xpath( |
再次,看看SelectorList
的源码,
1 | class SelectorList(list): |
由此可见,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 | '//div[count(a)=$cnt]/@id', cnt=5).extract_first() response.xpath( |
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 非常的罕见;