将以 'quotes.toscrape.com' 网站作为爬取的对象。
在这个教程中将围绕如下内容展开:
- 创建一个新的 Scrapy 项目
- 编写一个 spider 去爬网站,提取数据
- 使用命令行导出抓取数据
- 修改爬虫递归下一个链接
- 使用 spider 属性
创建项目
进入目标项目文件夹,执行以下代码:
scrapy startproject tutorial
这会创建一个 tutorial 路径,包含以下内容
turorial/
scrapy.cfg # 部署配置的文件
tutorial/ # 项目的 Python 模块,import 导入的代码在模块中
__init__.py
items.py # 项目的 items(条目)自定义文件
middlewares.py # 项目的中间件文件
pipelines.py # 项目的管道文件
settings.py # 配置文件
spiders/ # 后期放 spider 的文件
__init__.py
第一个项目
自定义的 Spider 类用于从网站(一组网站)中抓取数据,它们必须为 scrapy.Spider 子类,初始化的请求为必选项,可选项包括对下一页的相关处理、从解析下载的页面内容提取数据的细节。
我们的第一个 Spider 保存在 quotes_spider.py 文件中,在 tutorial/spiders 路径下:
import scrapy
class QuotesSpider(scrapy.Spider):
name = "quotes"
def start_requests(self):
urls = [
'http://quotes.toscrape.com/page/1/',
'http://quotes.toscrape.com/page/2/',
]
for url in urls:
yield scrapy.Request(url=url, callback=self.parse)
def parse(self, response):
page = response.url.split("/")[-2]
filename = 'quotes-%s.html' % page
with open(filename, 'wb') as f:
f.write(response.body)
self.log('Saved file %s' % filename)
可以看出来,我们定义的 Spider 是 scrapy.Spider 的子类,并且定义了一些属性和方法:
- name:唯一识别名,在当前项目中必须唯一。
- start_requests():必须返回请求的迭代(可以是请求列表或生成器函数),Spider 从这里开始爬。
- parse(): 每个请求发起后调用,用于处理响应。response 参数是 TextResponse 的实例,它包含了页面内容,内容随后会被处理。parse() 方法实现许多功能,包括解析响应,提取爬取的数据并解析为字典,寻找下一个爬取的 URL 并对此链接发起新的请求。
如何执行我们的 Spider
为了使 Spider 生效,需要进入项目顶层路径,并执行如下命令:
scrapy crawl quotes
这个命令以 quotes 名称作为参数执行,并且会对 qotes.toscrape.com 域名的网站发起请求。
执行命令后,可以发现在当前路径下,有两个新文件: quotes-1.html quotes-2.html
刚才发生了什么
Scrapy 会规划 Spider 中 start_requests 返回的 scrapy.Request。一旦接受到每个请求的响应,就会实例化 Response 对象,将响应实例作为回调方法的参数(在这个例子中,是 parse 方法),回调与请求 Request 关联。
start_requests 方法的简写
我们可以仅仅定义 start_urls 类属性,它是 URL 的列表组合,用于替代 start_requests() 中通过生成 scrapy.Reuqest 对象的做法。定义了 start_urls 后,会使用默认的 start_requests() 来创建 Spider 中的初始化请求。
于是代码可以改成:
import scrapy
class QuotesSpider(scrapy.Spider):
name = "quotes"
start_urls = [
'http://quotes.toscrape.com/page/1/',
'http://quotes.toscrape.com/page/2/',
]
def parse(self, response):
page = response.url.split("/")[-2]
filename = 'quotes-%s.html' % page
with open(filename, 'wb') as f:
f.write(response.body)
变化在于 start_urls 替代了 start_requests() 方法,只提供 URL 列表,其余的初始化请求操作交给父类中默认的 start_requests() 方法。
parse() 方法会被在每个 URL 被请求时调用,即使我们没有明确的告诉 Scrapy 这么去做,这是因为 parse() 是 Scrapy 默认自动调用的方法。
提取数据
使用 Scrapy 提取数据最好的调试方式:Scrapy shell,执行:
scrapy shell 'http://quotes.toscrape.com/page/1'
使用 shell,你可以尝试从响应中选择元素,以 CSS 选择器为例:
In [4]: response.css('title')
Out[4]: [<Selector xpath='descendant-or-self::title' data='<title>Quotes to Scrape</title>'>]
返回的结果 SelectorList,类列表对象,它扩展了list功能,可以理解为 Selector 的列表。这个列表对象被 XML/HTML 元素包裹,允许你执行进一步的查询和更细微力度的选择。
提取标签文本
比如,从刚才的 title 对象中提取文本,可以这么做:
>>> response.css('title::text').getall()
['Quotes to Scrape']
这里有两点要说明:
- 在 CSS 查询中添加了 '::text',这意味着只提取 <title> 中的文本。如果定义 '::text',会返回整个 title 元素内容(包括标签):
>>> response.css('title').getall()
['<title>Quotes to Scrape</title>']
- 调用 '.getall()',它返回一个列表:这意味着选择器可能返回一个或多个结果;如果你只需要第一个结果,可以使用 '.get()',也可以通过 python 代码先选择第一个结果:
response.css("title::text")[0].get()
但是,如果直接对 SelectorList 实例使用 get() 方法可以避免 'IndexError' 错误,它会处理成 None 返回,所以推荐直接使用 '.get()'
正则匹配
除了使用 'getall()' 和 'get()' 方法,可以使用 're()' 方法来提取数据
浏览器查看响应结果
为了更好的使用合适的 CSS 选择器,可以使用 view(response) 来查看响应。它会在浏览器中显示响应内容,你可以使用浏览器开发者工具来检测 HTML 并选择合适的选择器。
XPath 介绍
除了 CSS,Scrapy 选择器还支持使用 XPath 表达式:
>>> response.xpath('//title')
[<Selector xpath='//title' data='<title>Quotes to Scrape</title>'>]
>>> response.xpath('//title/text()').get()
'Quotes to Scrape'
XPath 表达式非常强大,是 Scrapy 选择器的基石。实际上,CSS选择器最终会被转换成 XPath。
尽管 XPath 表达式没有 CSS 活跃,但是它更有用,因为除了导航定位文件结构,它还能获取文本内容。
使用 XPath,你可以选择这样的内容:选择一个包含 "Next Page" 的链接。
这使得 XPath 足以胜任爬虫的任务,我们鼓励你去学习 XPath ,即使你已经了解了如何构建 CSS 选择器。
更多 XPath 使用方法可以查看 http://zvon.org/comp/r/tut-XPath_1.html 和 http://plasmasturm.org/log/xpath101/
提取 quotes 和 authors
现在你已经知道了一点选择和提取数据的方法,继续完善代码。
>>> for quote in response.css("div.quote"):
... text = quote.css("span.text::text").get()
... author = quote.css("small.author::text").get()
... tags = quote.css("div.tags a.tag::text").getall()
... print(dict(text=text, author=author, tags=tags))
{'tags': ['change', 'deep-thoughts', 'thinking', 'world'], 'author': 'Albert Einstein', 'text': '“The world as we have created it is a process of our thinking. It cannot be changed without changing our thinking.”'}
{'tags': ['abilities', 'choices'], 'author': 'J.K. Rowling', 'text': '“It is our choices, Harry, that show what we truly are, far more than our abilities.”'}
... a few more of these, omitted for brevity
>>>
使用 spider 提取数据
上面都是在 shell 中调试,在返回 spider 中。目前 Spider 没有提取任何数据,只是保存了整个 HTML 文件到本地。
接下来整合提取数据的逻辑到 spider 中。
一个 Scrapy spider 可以生成许多包含已提取数据的字典。
为做到这个目的,我们使用 yield 关键字来构建生成器:
import scrapy
class QuotesSpider(scrapy.Spider):
name = "quotes"
start_urls = [
'http://quotes.toscrape.com/page/1/',
'http://quotes.toscrape.com/page/2/',
]
def parse(self, response):
for quote in response.css('div.quote'):
yield {
'text': quote.css('span.text::text').get(),
'author': quote.css('small.author::text').get(),
'tags': quote.css('div.tags a.tag::text').getall(),
}
对比之前的 spider 代码,在这里主要发生变化的是 yield 子句。是一个调试的三个选择器,我们获取了每个 quote 的指定内容。
存储爬取的数据
最简单的存储方式是使用 'Feed exports',命令如下:
scrapy crawl -o quotes.json
这会生成 quotes.json 文件,包含爬取的项目,以 JSON 序列化。
由于历史原因,Scrapy 使用附加的方式而非替代原文件内容生成文件。也就是说,如果你执行两次存储名命令,会再添加内容到文件中。由于 JSON 格式问题,存储两次,将破坏 JSON 的格式。
其他格式 JSON Lines
scrapy crawl -o quotes.jl
'JSON Lines' 是一种流类型结构,可以轻易的添加新内容到文件中,而不用担心执行了两次破坏文件格式。
因为每个记录都以单独的一行记录。
JL 当可以搭配工具,如 'JQ' 来辅助执行这样的命令。
项目管道 Item Pipeline
在小项目中,上面提到才这些内容都足够了。然而,如果你想执行更加复杂的爬取,你可以写一个 'tiem pipleline'
「下一页」链接
除了仅仅抓取第一页和第二页的内容,你想要网页中所有的内容也可以。
在上面已经了解到如何提取页面上的链接,接下来看看如何获取并进入下一页链接
提取下一页标签
首先观察一下页面中下一页内容
<ul class="pager">
<li class="next">
<a href="/page/2/">Next <span aria-hidden="true">→</span></a>
</li>
</ul>
需要提取的内容是 href 的属性值,它告诉我们下一页的相对地址
>>> response.css('li.next a::attr(href)').get()
'/page/2/'
或
>>> response.css('li.next a').attrib['href']
'/page/2'
那么如何递归所有的下一页链接呢,spider 内容可以这样改动:
import scrapy
class QuotesSpider(scrapy.Spider):
name = "quotes"
start_urls = [
'http://quotes.toscrape.com/page/1/',
]
def parse(self, response):
for quote in response.css('div.quote'):
yield {
'text': quote.css('span.text::text').get(),
'author': quote.css('small.author::text').get(),
'tags': quote.css('div.tags a.tag::text').getall(),
}
next_page = response.css('li.next a::attr(href)').get()
if next_page is not None:
next_page = response.urljoin(next_page)
yield scrapy.Request(next_page, callback=self.parse)
这里的关键在于:一、判断是否有下一页,二、如果有,如何拼接 URL 递归请求访问和解析新页面内容。
现在,已经提取了数据,parse() 中拼接绝对路径的 URL 使用了 urljoin() 方法,并且生成一个新的请求到下一页;它将自身注册作为回调,以处理下一页的数据,并且持续爬取新页面。
总结一下 Scrapy 的下一页获取机制:如果你在回调函数中生成了一个请求,Scrapy 会适配已发送的请求,并在请求完成注册一个要执行的回调方法。
基于这个机制,你可以构建复杂的爬虫自定义点击超链接,也可以根据访问的页面不同自定义提取规则。
在这个例子中,我们创建了一个循环,一直进入下一页界面直到最后一页。这个功能非常适合爬取博客、论坛或者拥有分页的网站。
一个简单的方式创建请求
我们可以使用 response.follow 来请求下一个页面
...
next_page = response.css('li.next a::attr(href)').get()
if next_page is not None:
yield response.follow(next_page, callback=self.parse)
将上面例子的最后两行改成 response.follow() 请求网站,不同于 scrapy.Request,response.follow 提供相对路径请求,这意味着你不需要再添加 urljoin 拼接 URL。
记住,response.follow 返回的还是请求实例,所以需要 yield 生成这个请求。
由于 <a> 标签是一个链接,所以 response.follow 自动使用 href 属性,所以代码还可以简写为:
for a in response.css('li.next a'):
yield response.follow(a, callback=self.parse)
更多的例子和模型
这里继续将回调和更多操作的内容,这次爬取作者信息
import scrapy
class AuthorSpider(scrapy.Spider):
name = 'author'
start_urls = ['http://quotes.toscrape.com/']
def parse(self, response):
# follow links to author pages
for href in response.css('.author + a::attr(href)'):
yield response.follow(href, self.parse_author)
# follow pagination links
for href in response.css('li.next a::attr(href)'):
yield response.follow(href, self.parse)
def parse_author(self, response):
def extract_with_css(query):
return response.css(query).get(default='').strip()
yield {
'name': extract_with_css('h3.author-title::text'),
'birthdate': extract_with_css('.author-born-date::text'),
'bio': extract_with_css('.author-description::text'),
}
这个 spider 开始于主页面,为所有的作家页面调用 parse_author 回调,分页链接的 parse 回调。
parse_author 回调定义了一个方法从 CSS 查询中提取和清理数据,再产生作者资料的字典。
在这个 spider 中展示的另一个有趣的事情:即使有许多 quotes 对应一个 author,我们也不用担心会多次访问同一个 author 界面。
默认情况下,Scrapy 过滤重复URL请求,避免因访问服务器过多而产生问题。
可以在设置中配置参数 DUPFILTER_CLASS
使用 spider 参数
提供命令行参数给 spider,添加 '-a' 选项即可:
scrapy crawl quotes -o quotes-humor.json -a tag=humor
这些参数会传递给 Spider 类的 '__init__' 方法,并作为 spider 实例的默认属性。
在这个例子中,提供给 tag 属性的值可以使用 self.tag 获取。
添加 tag 选项后,spider 抓取的内容被限制在特定的标签中:
import scrapy
class QuotesSpider(scrapy.Spider):
name = "quotes"
def start_requests(self):
url = 'http://quotes.toscrape.com/'
tag = getattr(self, 'tag', None)
if tag is not None:
url = url + 'tag/' + tag
yield scrapy.Request(url, self.parse)
def parse(self, response):
for quote in response.css('div.quote'):
yield {
'text': quote.css('span.text::text').get(),
'author': quote.css('small.author::text').get(),
}
next_page = response.css('li.next a::attr(href)').get()
if next_page is not None:
yield response.follow(next_page, self.parse)
在这里,添加了 humor 标签/tag 的 spider 访问的 URL 变成了 'http://quotes.toscrape.com/tag/humor'