本文是昨晚睡不着,然后查看
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
直接获取,另一种是通过scrapy
的attrib[]
来获取:
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']