本文是昨晚睡不着,然后查看Scrapy官网文档做的一些笔记,收获颇多,填了很多坑。

英文官方链接:https://docs.scrapy.org/en/latest/topics/selectors.html

打开shell终端

在终端中运行scrapy模块的shell

PS C:\Users\myxc> scrapy shell https://docs.scrapy.org/en/latest/_static/selectors-sample1.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>

获取DOM文本值

直接打印xpath获取的对象:

In [6]: response.xpath('//title/text()')
Out[6]: [<Selector xpath='//title/text()' data='Example website'>]
    
In [7]: response.xpath('//title/text()')[0]
Out[7]: <Selector xpath='//title/text()' data='Example website'>

In [8]: print(response.xpath('//title/text()')[0])
<Selector xpath='//title/text()' data='Example website'>

In [9]: print(type(response.xpath('//title/text()')[0]))
<class 'scrapy.selector.unified.Selector'>

会发现返回的是一个list,同时,里面成员为对象。

提取元素的文本内容,可以使用 .get().getall() 方法:

In [10]: response.xpath('//title/text()').getall()
Out[10]: ['Example website']

In [11]: response.xpath('//title/text()').get()
Out[11]: 'Example website'

可以发现 .getall()获取的对象为list,而.get()获取的是字符串,这是因为该xpath选择器只是选择了一个DOM对象,下面我们在看下当xpath获取多个对象时它们两者的不同:

In [13]: response.xpath('//a/@href')
Out[13]:
[<Selector xpath='//a/@href' data='image1.html'>,
 <Selector xpath='//a/@href' data='image2.html'>,
 <Selector xpath='//a/@href' data='image3.html'>,
 <Selector xpath='//a/@href' data='image4.html'>,
 <Selector xpath='//a/@href' data='image5.html'>]

In [14]: response.xpath('//a/@href').get()
Out[14]: 'image1.html'

In [15]: response.xpath('//a/@href').getall()
Out[15]: ['image1.html', 'image2.html', 'image3.html', 'image4.html', 'image5.html']

由上述代码可知:当xpath获取DOM对象为多个时,.get()只返回第一个元素的文本值,而.getall()可以返回一个列表,该列表中包含所有元素的文本值。

xpath获取的DOM元素中还有子节点时,两个方法可以获取该节点内的所有文本值,包括html子节点:

In [16]: response.xpath('//a')
Out[16]:
[<Selector xpath='//a' data='<a href="image1.html">Name: My image ...'>,
 <Selector xpath='//a' data='<a href="image2.html">Name: My image ...'>,
 <Selector xpath='//a' data='<a href="image3.html">Name: My image ...'>,
 <Selector xpath='//a' data='<a href="image4.html">Name: My image ...'>,
 <Selector xpath='//a' data='<a href="image5.html">Name: My image ...'>]

In [17]: response.xpath('//a').get()
Out[17]: '<a href="image1.html">Name: My image 1 <br><img src="image1_thumb.jpg"></a>'

In [18]: response.xpath('//a').getall()
Out[18]:
['<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>']

问题来了:如何获取DOM节点中所有文本值而不包括HTML标签呢?

我们可以使用xpath中的string()方法解决这个问题:

In [19]: response.xpath('string(//a)')
Out[19]: [<Selector xpath='string(//a)' data='Name: My image 1 '>]

In [20]: response.xpath('string(//a)').get()
Out[20]: 'Name: My image 1 '

注意:该方法只能获取元素中只有一个子节点的情况!(请看下文常见错误中的一个实例)

你可能听说过这个方法:extract_first(),这个方法存在于老版本的scrapy中,它完全等同于get()

In [24]: response.xpath('//a').get()
Out[24]: '<a href="image1.html">Name: My image 1 <br><img src="image1_thumb.jpg"></a>'

In [25]: response.xpath('//a').extract_first()
Out[25]: '<a href="image1.html">Name: My image 1 <br><img src="image1_thumb.jpg"></a>'

xpath选择的元素不存在时,get()方法将会返回None,这一点非常重要,这意味着程序并不会因为xpath未选择到元素就报错停止运行:

In [27]: print(response.xpath('//demo').get())
None

In [28]: print(response.xpath('//demo').get() is None)
True

如果你不想返回None,你可以自定义该方法的返回值:

In [29]: response.xpath('//demo').get(default='not-found')
Out[29]: 'not-found'

获取元素的属性值

获取元素属性值的方法有两种:一种是通过xpath直接获取,另一种是通过scrapyattrib[]来获取:

In [30]: response.xpath('//a/@href')
Out[30]:
[<Selector xpath='//a/@href' data='image1.html'>,
 <Selector xpath='//a/@href' data='image2.html'>,
 <Selector xpath='//a/@href' data='image3.html'>,
 <Selector xpath='//a/@href' data='image4.html'>,
 <Selector xpath='//a/@href' data='image5.html'>]

In [31]: response.xpath('//a').attrib['href']
Out[31]: 'image1.html'

显然,这两种方法由很大不同,/@href可以以列表的形式获取;但是element.attrib['href']只能获取选择器的第一个对象的属性值。

In [37]: first_a = response.xpath("//a")

In [38]: first_a.attrib
Out[38]: {'href': 'image1.html'}

直接使用element.attrib可以返回一个字典,该字典包含该节点的所有属性与属性值。

所以,当我们想要获取的属性值仅仅是一个DOM对象时,就可以使用这种方法,如果我们想要同时获取多个DOM对象的属性值,那么我觉得还是使用xpath比较方便:

In [32]: response.xpath('//a/@href').getall()
Out[32]: ['image1.html', 'image2.html', 'image3.html', 'image4.html', 'image5.html']

xpath的优点:It is a standard XPath feature, and @attributes can be used in other parts of an XPath expression - e.g. it is possible to filter by attribute value.

当然,除了上述的两种方法,适用CSS选择器也是可以获取属性值的,点击英文官方文档查看。

选择器的嵌套使用

当然,xpath选择器也可以在嵌套数据(nested data)中使用:

In [21]: a_list = response.xpath('//a')
    
In [23]: for item in a_list:
    ...:     print(item.xpath('./img/@src').get())
    ...:
    ...:
image1_thumb.jpg
image2_thumb.jpg
image3_thumb.jpg
image4_thumb.jpg
image5_thumb.jpg

正则的使用

scrapy框架中同样集成了正则表达式re模块的使用:

In [39]: a_text = response.xpath("//a/text()")

In [40]: a_text
Out[40]:
[<Selector xpath='//a/text()' data='Name: My image 1 '>,
 <Selector xpath='//a/text()' data='Name: My image 2 '>,
 <Selector xpath='//a/text()' data='Name: My image 3 '>,
 <Selector xpath='//a/text()' data='Name: My image 4 '>,
 <Selector xpath='//a/text()' data='Name: My image 5 '>]

In [41]: a_text = response.xpath("//a/text()").re("Name: (.*)")

In [42]: a_text
Out[42]: ['My image 1 ', 'My image 2 ', 'My image 3 ', 'My image 4 ', 'My image 5 ']

注意:使用正则时,返回的对象为字符串形式,这意味着你无法在正则中使用嵌套选择器。

类似于.get().extract_first()) ,在正则模块中 .re()也有一个相似的方法.re_first(),可以只获取列表元素的第一个值。

In [43]: a_text = response.xpath("//a/text()").re_first("Name: (.*)")

In [44]: a_text
Out[44]: 'My image 1 '

两个老方法

如果你是Scrapy的老用户了,那么你一定会知道.extract().extract_first(),直到今天,依然有很多博客论坛教程在使用这两个方法,Scrapy也会一直支持这两个方法,暂时没有弃用的想法。

但是Scrapy官方推荐你使用.get().getall() 这两个方法,因为使用它们明显会使你的程序更加简介,并且可读性更高。

常见错误

Xpath的相对路径选择

如果你想提取某个div内的所有p标签,获取你会使用这样的方法:

>>> divs = response.xpath('//div')

>>> for p in divs.xpath('//p'):  # this is wrong - gets all <p> from the whole document
...     print(p.get())

但是这显然是一种错误的方法,这样你得到的是页面内所有的p标签,而不是你所指定的div内的p标签。

正确的方法应该是:

>>> for p in divs.xpath('.//p'):  # extracts all <p> inside
...     print(p.get())

注意前缀 .//p

还有一种简洁的方法:

>>> for p in divs.xpath('p'):
...     print(p.get())

//node[1] 和 (//node)[1]的不同

举例:

>>> 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).getall()

获取每一个节点下的第一个li元素:

>>> xp("//li[1]")
['<li>1</li>', '<li>4</li>']

获取页面中所有li中的第一个:

>>> xp("(//li)[1]")
['<li>1</li>']

正确获取嵌套元素的文本值

导入实例:

In [1]: from scrapy import Selector

In [2]: sel = Selector(text='<a href="#">Click here to go to the <strong>Next Page</strong></a>')

首先想到的方法是XPath中的text()方法:

In [3]: sel.xpath('//a//text()').getall() # take a peek at the node-set
Out[3]: ['Click here to go to the ', 'Next Page']

然后尝试 string()方法:

In [4]: sel.xpath("string(//a[1]//text())").getall() # convert it to string
Out[4]: ['Click here to go to the ']

正确的方法应该是:

In [6]: sel.xpath("string(//a/.)").getall() # convert it to string
Out[6]: ['Click here to go to the Next Page']