当你在爬取网页的时候,最常见的任务就是从HTML源代码中提取数据。
有几个库可以实现这个功能:
BeautifulSoup
BeautifulSoup 在Python程序员中是一个非常受欢迎的网页爬取库, 它基于HTML代码的结构设计了一些能够很好处理坏标记(bad markup)的Python对象, 然而, 它的缺点就是——慢。
lxml
lxml 是一个XML解析库(当然也可以解析HTML), 它使用基于 ElementTree 的 pythonic API。(lxml不是Python标准库的一部分)
Scrapy 有自己的提取数据机制(mechanism技巧)。 他们被称为 selectors (选择器), 因为它们由 XPath 或 CSS 表达式选择HTML文件特定的部分。
XPath 是一种选择XML文件结点的语言, 也能用于HTML。 CSS 是一种将样式应用于HTML的语言。 它定义了选择器来将这些样式与特定的HTML元素关联起来。
Scrapy selectors 建立在lxml库的基础上, 也就是说,他们在速度与解析精确度(accuracy)上相近。
这一章解释了选择器是如何工作的, 并且描述了他们小型、简单的API, 而不像lxml API 那么大, 因为除了选择标记文档以外 lxml 库还可以应用于很多其他的任务。
Scrapy 选择器是 Selector
类的实例, 通过传递 text 或 TextResponse
对象构造。 它基于输入类型自动地选择最好的解析规则(XML vs HTML):
>>> from scrapy.selector import Selector
>>> from scrapy.http import HtmlResponse
>>> # 从text中构造:
>>> body = '<html><body><span>good</span></body></html>'
>>> Selector(text=body).xpath('//span/text()').extract()
['good']
>>> # 为了方便, response对象在.selector属性中显示一个选择器, 完全可以用下面的捷径:
>>> response.selector.xpath('//span/text()').extract()
['good']
为了解释如何使用选择器, 我们将用 Scrapy shell 和一个位于Scrapy服务器的用于举例子的网页:
源代码:
<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
变量, 并且它的可以使用response.selector
属性,比如response.xpath...
因为我们正在处理HTML, 所以选择器会自动的使用 HTML parser。
所以,通过查看HTML的源代码,我们就可以构建一个选择title标签的 XPath 路径:
>>> response.xpath('//title/text()')
[<Selector (text) xpath=//title/text()>]
CSS与XPath都可以使用:
>>> response.xpath('//title/text()')
[<Selector (text) xpath=//title/text()>]
>>> response.css('title::text')
[<Selector (text) xpath=//title/text()>]
如你所见,.xpath()
和 .css()
方法返回一个SelectorList
实例, 它是一系列新的选择器。
这个API可以用于快速选择嵌套的数据:
>>> response.css('img').xpath('@src').extract()
['image1_thumb.jpg', 'image2_thumb.jpg', 'image3_thumb.jpg', 'image4_thumb.jpg', 'image5_thumb.jpg']
为了完全提取text的数据, 你必须调用.extract()
方法, 如下:
>>> response.xpath('//title/text()').extract()
['Example website']
如果你只想提取第一个匹配的元素, 你可以调用 .extract_first()
>>> response.xpath('//div[@id="images"]/a/text()').extract_first()
'Name: My image 1 '
如果它没有找到元素则返回None:
>>> response.xpath('//div[@id="not-exists"]/text()').extract_first() is None
True
默认的返回值可以被提供为一个参数, 来替换 None:
response.xpath('//div[@id="non-exists"/text()').extract_first(default='not-found')
'not-found'
注意CSS 选择器可以使用 CSS3 pseudo-elements 选择 text 或者 属性结点:
response.css('title::text').extract()
['Example website']
接下来我们将得到 base 的URL链接和一些图片的链接:
>>> response.xpath('//base/@href').extract()
['http://example.com/']
>>> response.css('base::attr(href)').extract()
['http://example.com/']
>>> response.xpath('//a[contains(@href, "image")]/@href').extract()
['image1.html', 'image2.html', 'image3.html', 'image4.html', 'image5.html']
>>> response.css('a[href*=image]::attr(href)').extract()
['image1.html', 'image2.html', 'image3.html', 'image4.html', 'image5.html']
>>> response.xpath('//a[contains(@href, "image")]/img/@src').extract()
['image1_thumb.jpg', 'image2_thumb.jpg', 'image3_thumb.jpg', 'image4_thumb.jpg', 'image5_thumb.jpg']
>>> response.css('a[href*=image] img::attr(src)').extract()
['image1_thumb.jpg', 'image2_thumb.jpg', 'image3_thumb.jpg', 'image4_thumb.jpg', 'image5_thumb.jpg']
选择器的方法(.xpath()
or .css()
) 返回一个同类型的选择器列表, 所以你也可以对这些选择器调用选择器的方法:
>>> links = response.xpath('//a[contains(@href, "image")]')
>>> links.extract()
['<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>']
>>> 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)
...
Link number 0 points to url ['image1.html'] and image ['image1_thumb.jpg']
Link number 1 points to url ['image2.html'] and image ['image2_thumb.jpg']
Link number 2 points to url ['image3.html'] and image ['image3_thumb.jpg']
Link number 3 points to url ['image4.html'] and image ['image4_thumb.jpg']
Link number 4 points to url ['image5.html'] and image ['image5_thumb.jpg']
Selectors
也有一个.re()
方法使用正则表达式来提取数据。 然而,不像使用.xpath()
或者 .css()
方法, .re()
返回一个unicode字符串列表, 所以你不能构造嵌套的 .re()
方法
下面有一个从HTML 源代码提取图片名字的例子:
>>> response.xpath('//a[contains(@href, "image")]/text()').re(r'Name:\s*(.+)')
['My image 1 ', 'My image 2 ', 'My image 3 ', 'My image 4 ', 'My image 5 ']
有一个附加的辅助方法,与 .extract_first()
类似的 .re_first()
方法。 使用它来提取第一次匹配到的字符串:
>>> response.xpath('//a[contains(@href, "image")]/text()').re_first(r'Name:\s*(.*)')
'My image 1 '
下面这一点一定要牢记于心!!!
如果你在使用嵌套选择器并且使用一个以/
开始的 XPath, 这个XPath是绝对的 document, 而不是相对于调用它的选择器。
(注: document这里指response的内容,可以去了解XPath以获得更多的知识xpath-1.0、xpath-3.1)
比如, 假设你想提取所有在<div>
标签中的<p>
标签(假设document与div中都有p), 首先, 你需要得到所有的div标签:
divs = response.xpath('//div')
首先,你可能会尝试使用以下方法,然而这是错误的,因为它实际上从document中提取了所有的 <p>
元素,不仅是那些在<div>
内部的元素:
for p in divs.xpath('//p'): # 这是错误的, 提取了所有的 <p> 元素
... print(p.extract())
下面是正确的方法1(使用.//p
XPath,多了一个点):
>>> for p in divs.xpath('.//p'): # 正确
... print(p.extract())
方法2:
>>> for p in divs.xpath('p'):
... print(p.extract())
更多的方法见Location Paths
XPath允许你在表达式中引用变量, 使用 $somevariable
语法, 这有点类似于SQL world中的参数化查询或已经准备好的语句并且可以在其中替换一些参数,你对占位符的查询 ?
然后,将这些值替换为通过查询传递的值, 其实就类似于格式化输出。
下面这个例子将会”动态”匹配一个基于@id
属性的元素, 而不像以前那样静态地写上路径:
>>> # '$val' 在表达式中使用, 一个 'val' 参数需要被传入:
>>> response.xpath('//div[@id=$val]/a/text()', val='images').extract_first()
['Name: My image 1 ', 'Name: My image 2 ', 'Name: My image 3 ', 'Name: My image 4 ', 'Name: My image 5 ']
下面是另一个例子, 找到包含五个<a>
子标签的<div>
中的”@id”属性(在这里将整数5(integer)传入):
>>> response.xpath('//div[count(a)=$cnt]/@id', cnt=5).extract_first()
'images'
在调用 xpath()
时所有引用的变量必须有一个绑定的值对应, 否则你会报出 ValueError: XPath error: exception 。
parsel,
XPath variables
由于Scrapy的选择器基于lxml, 所以也支持一些EXSLT extensions和这些已经预先注册的命名空间 用于XPath 表达式:
prefix | namespace | usage |
---|---|---|
re | http://exslt.org/regexp/index.html | regular expressions |
set | http://exslt.org/set/index.html | set manipilation |
regexp:match()
regexp:replace()
regexp:test()
方法 | 语法 | 说明 |
---|---|---|
re:test() | re:test(string, string, string?) | 第一个string是要匹配的对象,这里是标签的属性, 第二个是正则表达式的字符串, 第三个是控制标记 |
re:match() | re:match(string, string, string?) | 我感觉和test()没啥区别, 感觉和re里面的match方法有出入, 而且它不仅可以匹配开头 |
re:replace() | re:replace(string, string, string, string) | 第一个string是要匹配的字符串,这里就是属性, 第二个string是正则表达式语法, 第三个是将要替换的新的字符串, 第四个就是控制标记(讲道理特别迷,不存在什么替换) |
不过,放弃吧,使用
re:test()
就可以了, 不过它返回的是字符串,讲道理这三个都是字符串。 匹配到你想要的标签的字符串后, 再用sel = Selector(text=$你匹配到的字符串)
, 这样sel就有.xpath
属性了,你就可以接着解析。
当XPath
的 starts-with()
或 contains()
方法不够用时, test()
方法显得十分地有用。
比如说选择class
属性结尾有 数字 的链接:
>>> from scrapy import Selector
>>> doc = """
... <div>
... <url>
... <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()
['link1.html', 'link2.html', 'link3.html', 'link4.html', 'link5.html
>>> sel.xpath('//li[re:test(@class, "item-\d$")]//@href').extract()
['link1.html', 'link2.html', 'link4.html', 'link5.html']
>>>
注意: C语言的library中
libxslt
不支持 EXSLT 正则表达式, 所以lxml的实现是使用python的re
模块, 因此在XPath中使用正则表达式会使性能稍微降低。
实现说明, 如果想熟练 set 的话, 就要对XPath 有一定的了解, xpath-1.0、xpath-3.1), 我只看了1.0版本的XPath, 因为3.1版本的介绍好多又难懂, 相对1.0版本来说难。
方法 | 语法 | 说明 |
---|---|---|
set:difference | set:difference(node-set, node-set) | 取两个XPath的并集减去交集, 也就是相异的部分 |
set:intersection | set:intersection(node-set, node-set) | 交集, 相同的部分 |
set:distinct | set:distinct(node-set) | 去重, 即去掉相同的部分 |
set:has-same-node | set:has-same-node(node-set, node-set) | 集合:如果作为第一个参数传递的节点集与第二个参数传递的节点集至少有一个节点相同,则has-same-node函数将返回true。 如果两个节点集中没有相同的节点,则返回false。 |
set:leading | set:leading(node-set, node-set) | 传递两个结点集合(也可以是单独的结点), 根据源代码的结点顺序, 如果第二个结点集合中最前面的一个结点(用A代替名字)在第一个结点集合中, 那么第一个结点集合中在A 前面 的所有结点都会被返回(不包括A), 如果A不在第一个结点集合中, 那么就返回空列表, 如果第二个集合为空(也就是XPath没有找到任何结点), 那么第一个结点集合会被返回 |
set:trailing | set:trailing(node-set, node-set) | 传递两个结点集合(也可以是单独的结点), 根据源代码的结点顺序, 如果第二个结点集合中最前面的一个结点(用A代替名字)在第一个结点集合中, 那么第一个结点集合中在A 后面 的所有结点都会被返回(不包括A), 如果A不在第一个结点集合中, 那么就返回空列表, 如果第二个集合为空(也就是XPath没有找到任何结点), 那么第一个结点集合会被返回 |
下面的这些例子在提取元素之前有效地排除 document tree 的一部分。
例如在提取一些微数据时(从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>
下面是python shell:
>>> from scrapy import Selector
>>> doc = """
... <div itemscope itemtype="http://schema.org/Product">
... <span itemprop="name">Kenmore White 17"
... ... # 省略一部分, 就是上面的源代码
... </div>
... """
>>> sel = Selector(text=doc, type="html")
>>> for scope in sel.xpath('//div[@itemscope]'):
... print("current scope:", scope.xpath('@itemtype').extract())
... props = scope.xpath('set:difference(./descendant::*/@itemprop, .//*[@itemscope]/*/@itemprop)')
... print("\tproperties:", props.extract())
... print()
current scope: ['http://schema.org/Product']
properties: ['name', 'aggregateRating', 'offers', 'description', 'review', 'review']
current scope: ['http://schema.org/AggregateRating']
properties: ['ratingValue', 'reviewCount']
current scope: ['http://schema.org/Offer']
properties: ['price', 'availability']
current scope: ['http://schema.org/Review']
properties: ['name', 'author', 'datePublished', 'reviewRating', 'description']
current scope: ['http://schema.org/Rating']
properties: ['worstRating', 'ratingValue', 'bestRating']
current scope: ['http://schema.org/Review']
properties: ['name', 'author', 'datePublished', 'reviewRating', 'description']
current scope: ['http://schema.org/Rating']
properties: ['worstRating', 'ratingValue', 'bestRating']
我们先通过迭代所有的itemscope
元素, 然后对于每一个, 我们寻找所有的itemprops
元素并且排除那些在其他itemscope的元素, 最终得到的是两个xpath中不同的属性(你可以通过将两个XPath都打印一遍来观察有什么区别, 事实上set:difference方法就是找出他们相异的属性)
这里有一些建议, 你可以使用XPath更加的有效, 基于this post from ScrapingHub’s blog, 更详细的内容还是在XPath-1.0
当你需要使用text 内容作为参数到 XPath string function时, 避免使用 .//text()
, 而应该使用 .
来代替。
这是因为表达式 text()
yield 一个text 元素的collection, 也就是结点集合(简称结点集), 当一个结点集传入string时, 也就是当传入字符串给contains()
方法或者 starts-with()
方法等, 它会导致在文本中只显示第一个元素。
例子:
>>> from scrapy import Selector
>>> sel = Selector(text='<a href="#">Click here to go to the <strong>Next Page</strong></a>')
>>> # 传入一个字符串到结点集中:
>>> sel.xpath('//a//text()').extract() # 查看结点集的内容
['Click here to go to the ', 'Next Page']
>>> sel.xpath('string(//a[1]//text())').extract() # 将它传入string
['Click here to go to the ']
>>> # 然而,一个转换成字符串的节点将其本身的文本和它的所有后代组合在一起
>>> sel.xpath('(//a[1])').extract() # 查看第一个结点
['<a href="#">Click here to go to the <strong>Next Page</strong></a>']
>>> sel.xpath('string(//a[1])').extract() # 将它传给string
['Click here to go to the Next Page']
>>> # 使用 .//text() 结点集不会选择任何东西:
>>> sel.xpath('//a[contains(.//text(), "Next Page")]').extract()
[]
>>> # 使用 . 来起作用:
>>> sel.xpath('//a[contains(., "Next Page")]').extract()
['<a href="#">Click here to go to the <strong>Next Page</strong></a>']
//node[1]
与 (//node)[1]
的区别//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).extract() # 为了使表达简单
>>> xp("//li[1]") # 选择所有ul父类下的第一个li结点
['<li>1</li>', '<li>4</li>']
>>> xp("(//li)[1]") # 选择所有的li结点, 然后返回第一个
['<li>1</li>']
>>> xp("//ul/li[1]") # 所有ul结点下的第一个li结点
['<li>1</li>', '<li>4</li>']
>>> xp("(//ul/li)[1]") # 选择所有ul下的的li结点, 返回第一个
['<li>1</li>']
因为一个元素可以包含很多的 CSS classes, 使用XPath通过class来选择元素十分地冗长(注意这里的括号, 官方文档写错了):
*[contains(concat(' ', normalize-space(@class, ' ')), 'someclass ')]
如果你使用 @class="someclass"
你可能失去拥有该类的同时有其他类的元素, 并且如果你仅仅使用 contains(@class, "someclass")
来补偿, 你会找到额外的更多的元素
事实证明, Scrapy 选择器允许你使用链式选择器(即一个用完再接着一个), 所以你大多数时候可以在 XPath 与 CSS 中交替使用, 当选择类的时候切到CSS即可, 平时依然可以使用XPath:
>>> from scrapy import Selector
>>> sel = Selector(text='<div class="herp shout"><time datetime="2014-07-23 19:00"
>Special date</time></div>')
>>> sel.css('.shout').xpath('./time/@datetime').extract()
['2014-07-23 19:00']
这比使用冗长的XPath来选择类简单多了, 记住在 XPath 中使用 .
来接着前面的css。
class scrapy.selector.Selector(response=None, text=None, type=None)
Selector 类的实例是一个response包装器(wrapper) 来选择内容的一部分。
response 是一个 HtmlResponse
或 XmlResponse
对象, 将从它解析提取数据。
text 是一个 unicode 字符串或者 utf-8 编码的文本, 当response没法找到, 或者用一段HTML文本来测试 可以设置text, 就如上面的那些例子一样。 不可以同时使用 response 与 text。
type 定义了选择器的类型, 可以是 html, xml 或者 None(默认)
如果 type 是None, 选择器会自动根据response的类型选择最适合的类型, 如果是 text 类型的话默认是 html
如果 type 是None 并且有response传入, 选择器的类型就会被推测为一下类型:
html 对应 HtmlResponse
xml 对应 XmlResponse
html 对应 任何其他类型
如果 type 被设置, 那么选择器会被强制使用该类型, 不会自动检测。
xpath(query)
找到匹配XPath 查询(query)的结点 并且将查到的元素以SelectorList
对象返回结果。 List 元素也实现了 Selector 接口。
该方法可以用response.xpath(query) 调用
css(query)
用CSS选择器查询元素并返回一个 SelectorList 实例
该方法可以用response.css(query) 调用
extract()
序列化并返回由匹配的节点组成unicode字符串的列表。百分比编码的内容不被引用。
re(regex)
使用给定的regex表达式匹配Unicode字符串并作为列表返回。
re()
与re_first()
都转码为 HTML entities(除了<
与&
)
register_namespace(prefix, uri)
注册在选择器中将要使用的命名空间(namespace)。 不注册namespace的话你不能从不标准的namespace选择或者提取数据。
remove_namespaces()
移除所有的 namespaces, 允许使用名称空间较少的xpath遍历(traverse)文档
nonzero()
如果选择了任何真实的内容返回True, 否则返回False。 换言之, 选择器的 boolean 由所选内容决定。
class scrapy.selector.SelectorList
SelectorList 类是python内置类 list
的一个子类, 同时它提供了一些额外的方法。
xpath(query)
对每个列表中的元素调用 .xpath()
方法并以新的 SelectorList 返回他们的结果。
css(query)
对每个列表中的元素调用 .css()
方法并以新的 SelectorList 返回他们的结果
extract()
对每个列表中的元素调用 .extract()
方法并以 unicode 字符串列表返回
re()
对每个列表中的元素调用 .re()
方法并以 unicode 字符串列表返回
接下来几个小例子来说明几个概念(to illustrate several concepts), 对所有的情况, 我们假设已经有了一个 HtmlResponse
对象:
sel = Selector(html_response)
从HTML response body 中选择所有的<h1>
元素:
sel.xpath('//h1')
提取所有的h1
元素:
sel.xpath('//h1').extract()
sel.xpath('//h1/text()').extract()
遍历所有的 <p>
标签并打印他们的 class 属性:
for node in sel.xpath(//p'):
print(node.xpath('@class').extract())
接下来几个小例子来说明几个概念。 假设已经有了一个 XmlResponse
对象:
sel = Selector(xml_response)
从 XML response body 选择所有的 <product>
元素, 返回SelectorList 对象:
sel.xpath('//product')
从 Google Base XML feed提取所有的价格属性, 要求注册namespace:
sel.register_namespace("g", "http://base.google.com/ns/1.0")
sel.xpath('//g:price').extract()
在scrapy 项目中, 除掉 namespaces 而仅仅处理他们的元素名字是十分方便的, 来写更简单的 XPath。 你可以使用 Selector.remove_namespaces()
方法
让我们举个例子,用GitHub博客atom来说明这一点。
首先进入shell:
>>> response.xpath('//link')
[]
>>> response.selector.remove_namespaces()
>>> response.xpath('//link')
[<Selector xpath='//link' data='<link xmlns="http://www.w3.org/2005/Atom'>,
<Selector xpath='//link' data='<link xmlns="http://www.w3.org/2005/Atom'>,
<Selector xpath='//link' data='<link xmlns="http://www.w3.org/2005/Atom'>,
...
你必须调用 response.selector.remove_namespaces()
才能调用, 原因如下:
除掉 namespaces 后就可以迭代并修改所有的结点, 这对于Scrapy 爬下来的document 来说代价相当高
允许使用 namespaces 事实上应该有几种情况, 防止一些元素名在 namespaces 中相互冲突, 但这些情况很少见。
下面我们利用今天所学的知识写一个小爬虫,爬取的内容自己爬了就知道。
先定义我们的items.py
:
import scrapy
class ZhongzisouItem(scrapy.Item):
# seed 的 title
title = scrapy.Field()
# seed 的 magnet
magnet = scrapy.Field()
# resource 的 size
size = scrapy.Field()
看到magnet,大概就知道要爬取什么了吧
接下来定义pipelines.py
:
import json
class ZhongzisouPipeline(object):
def open_spider(self, spider):
self.file = open('items.jl', 'w', encoding='utf-8')
def close_spider(self, spider):
self.file.close()
def process_item(self, item, spider):
line = json.dumps(dict(item), ensure_ascii=False) + '\n'
self.file.write(line)
return item
解释一下: 在 open_spider
中我们定义打开的文件为 items.jl
(别忘了导入json库), 文件为覆盖写入, 编码为utf-8, 在process_item
中 line 就是每一行的数据, 利用 dump方法 来将它转格式, ensure_ascii=False
就是将强制ascii码关闭,这样就可以显示中文了。
然后就是我们spider了:
# -*- coding: utf-8 -*-
import scrapy
from Zhongzisou.items import ZhongzisouItem
class ZhongzisoSpider(scrapy.Spider):
name = 'zhongziso'
def __init__(self, keyword=None):
if keyword is not None:
self.start_urls = ['https://zhongziso.com/list/' + keyword + '/1']
else:
raise ValueError('You must specify a keyword using `-a keyword=your_keyword`')
def parse(self, response):
item = ZhongzisouItem()
for table in response.xpath('//table[@class="table table-bordered table-striped"]'):
item['title'] = ''.join(table.xpath('.//a/text()').extract())[:-10]
item['magnet'] = table.xpath('.//a[contains(., "磁力")]/@href').extract_first()
item['size'] = table.xpath('.//td[contains(., "大小")]/strong/text()').extract_first()
yield item
next_page = response.xpath('//a[contains(., "下一页")]/@href').extract_first()
if next_page is not None:
next_page = 'https://zhongziso.com' + next_page
next_page = response.urljoin(next_page)
yield scrapy.Request(next_page, callback=self.parse)
为大家解释一下:
首先记得导入你的item类, 然后再看我们写的spider, 它的name就是 zhongziso, 然后看__init__
, 我在里面添加了keyword=None
, 也就是说在运行该爬虫时要求指定一个关键字参数, 也就是我们要爬取内容的关键字, 如果没有给该参数传递值, 就会引起 ValueError 异常。
然后解释一下 parse
方法, 首先创建一个item对象, 然后就是今天所学的关键部分了, 我们首先指定一个范围, 也就是for语句那一行, 下面items['title']
后面的路径中, 看到了我使用 .//
这样的方式, 这是为了在我们指定的范围内提取, 如果没有那个 .
的话, 就会从整个网页上提取所有的路径。后面的 [:-10]
是为了过滤掉一些没用的信息
items[‘magnet’] 就是提取内容包含 “磁力” 的链接, items[‘size’] 就是提取内容包含 “大小” 的标签里面的内容, 也就是具体的参数啦
next_page 就是下一页的链接, 很明显, 直接指定链接的内容包含”下一页”即可。
好了, 以上是简单的说明, 爬取之前记得在 settings.py
把 ITEM_PIPELINES
取消注释, 我们来运行爬虫试试, 在命令行中使用如下的命令:
scrapy crawl zhongziso -a keyword=刀剑神域
然后, 你就可以看到相应的结果, 没错, 就是动漫刀剑神域的 magnet 链接, 你可以通过改变参数来爬取你想要的内容, 你当然可以换其他网站试试来满足你的好奇心