Scrapy教程
原文地址https://doc.scrapy.org/en/latest/intro/tutorial.html
此教程我们假设你已经装好了Scrapy,如果没有请查看安装指南.。
我们将要抓取 quotes.toscrape.com网站,这个网站展示了很多名人名言。
此教程指导你完成一下任务:
- 新建一个Scrapy工程
- 编写一个spider爬网站提取数据
- 用命令行导出爬取的数据
- 改变spider递归爬去链接
- 使用spider参数
Scrapy是python编写。如果你是python新手,你可能先要知道这门语言大概是什么样的,才能直到Scrapy的更多东西。
如果你熟悉其他编程语言,想快速学习python,我们建议你使用 Dive Into Python 3或者Python Tutorial。
如果你刚接触编程语言想学习python,可以使用Learn Python The Hard Way。或者看this list of Python resources for non-programmers。
创建一个项目
在抓取之前,你必须构建一个新的Scrapy project。到你先要存储的目录运行:scrapy startproject tutorial
这将创建一个含有以下内容的tutorial
目录:
tutorial/
scrapy.cfg # deploy configuration file
tutorial/ # project's Python module, you'll import your code from here
__init__.py
items.py # project items definition file
pipelines.py # project pipelines file
settings.py # project settings file
spiders/ # a directory where you'll later put your spiders
__init__.py
我们第一个爬虫
蜘蛛是你定义的一些去爬取网站信息的类。他们必须继承自scrapy.Spider
,定义初始请求,如何选择页面爬取这是可选的,以及如何解析下载页面的内容提取数据。
这是我们第一个蜘蛛的代码,把它保存在tutorial/spiders
目录的 quotes_spider.py
中文件。
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)
如你所见,我们的蜘蛛继承自 scrapy.Spider 并且定义了一些属性和方法。
name
:标识这个蜘蛛。在一个项目中必须时唯一的,意味着你不能给不同的蜘蛛设置相同的名称。start_requests()
:必须返回一个请求的迭代(可以返回一个请求的列表或者写一个生成器函数),蜘蛛从这里开始爬去。子序列请求从这些初始的请求自动生成。parse()
:在每个请求的相应完成时调用的方法。response参数是TextResponse
的一个实例,拥有页面内容和更多有用的函数操作。
parse()
函数通常解析响应内容,把抓到的数据提取为dicts随后查找新的URLS创建新的请求。
如何运行我们的蜘蛛
为了让我们的蜘蛛工作,到项目的最顶层目录运行:
scrapy crawl quotes
这条命令运行我们刚添加的名为quotes
的蜘蛛。它发送一些请求到quotes.toscrape.com
。你将得到如下输出:
... (omitted for brevity)
2016-12-16 21:24:05 [scrapy.core.engine] INFO: Spider opened
2016-12-16 21:24:05 [scrapy.extensions.logstats] INFO: Crawled 0 pages (at 0 pages/min), scraped 0 items (at 0 items/min)
2016-12-16 21:24:05 [scrapy.extensions.telnet] DEBUG: Telnet console listening on 127.0.0.1:6023
2016-12-16 21:24:05 [scrapy.core.engine] DEBUG: Crawled (404) <GET http://quotes.toscrape.com/robots.txt> (referer: None)
2016-12-16 21:24:05 [scrapy.core.engine] DEBUG: Crawled (200) <GET http://quotes.toscrape.com/page/1/> (referer: None)
2016-12-16 21:24:05 [scrapy.core.engine] DEBUG: Crawled (200) <GET http://quotes.toscrape.com/page/2/> (referer: None)
2016-12-16 21:24:05 [quotes] DEBUG: Saved file quotes-1.html
2016-12-16 21:24:05 [quotes] DEBUG: Saved file quotes-2.html
2016-12-16 21:24:05 [scrapy.core.engine] INFO: Closing spider (finished)
...
现在,检查当前目录。你会注意到创建了两个新文件quotes-1.html 和quotes-2.html,里面包含了urls的响应数据。
提示
如果你奇怪为什么我们还没有解析HTML,淡定,很快会讲到。
内部机制是什么
Scrapy调用蜘蛛的start_requests
方法,一旦接收到一个响应,立马初始化Response对象然后调用请求的回掉函数(在此例中,时parse()
函数)把response对象作为参数。
start_requests函数简写
作为start_requests
函数的替代实现,你可以仅定义一个名为start_urls
的urls列表属性。词列表将在默认的start_requests()
函数实现中被使用为你的蜘蛛创建出事请求。
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)
urls的每次请求都将调用parse()函数,即使我们没有显示告诉Scrapy这么做。这是因为parse()是Scrapy在没有显示给回掉函数赋值时的默认回掉函数。
提取数据
最好的学习使用Scrapy的选择器的方式是使用Scrapy shell。
scrapy shell 'http://quotes.toscrape.com/page/1/'
提示
记住使用单引号包裹地址否则包含参数(如&字符)将不会工作
在windows中,使用双引号
你将看到:
[ ... Scrapy log here ... ]
2016-09-19 12:09:27 [scrapy.core.engine] DEBUG: Crawled (200) <GET http://quotes.toscrape.com/page/1/> (referer: None)
[s] Available Scrapy objects:
[s] scrapy scrapy module (contains scrapy.Request, scrapy.Selector, etc)
[s] crawler <scrapy.crawler.Crawler object at 0x7fa91d888c90>
[s] item {}
[s] request <GET http://quotes.toscrape.com/page/1/>
[s] response <200 http://quotes.toscrape.com/page/1/>
[s] settings <scrapy.settings.Settings object at 0x7fa91d888c10>
[s] spider <DefaultSpider 'default' at 0x7fa91c8af990>
[s] Useful shortcuts:
[s] shelp() Shell help (print this help)
[s] fetch(req_or_url) Fetch request (or URL) and update local objects
[s] view(response) View response in a browser
>>>
使用shell,你可以使用response对象的CSS 函数选择元素。
>>> response.css('title')
[<Selector xpath='descendant-or-self::title' data='<title>Quotes to Scrape</title>'>]
response.css('title')
的运行结果是一个名为SelectorList的list-like对象,它是包含XML/HTML元素的 Selector 对象列表允许你进一步查询选择和提取数据。
为了导出title的文本,你可以:
>>> response.css('title::text').extract()
['Quotes to Scrape']
此处有两点要注意:一、我们添加了::text
到CSS查询中,意味着我们只选择了<title>
玄素的text元素。如果我们不指定::text
,我们会得到含有标记的整个title元素。
>>> response.css('title').extract()
['<title>Quotes to Scrape</title>']
二、.extract()
调用结果是一个列表,因为我们处理的是SelectorList
对象。当你知道你只需要第一个结果时,你可以:
>>> response.css('title::text').extract_first()
'Quotes to Scrape'
作为一种替换方法,你可以这么写:
>>> response.css('title::text')[0].extract()
'Quotes to Scrape'
然而,使用extract()
和extract_first()
方法避免了IndexError
并且在没有找到任何匹配元素时返回None
。
这有个教训,对于大多数抓取代码,你想要在页面不能找到元素时有伸缩性,以至于即使在抓取数据时发生错误,你依然可以得到一些数据。
除了extract()
和extract_first()
方法,你还可以使用re()
的正则表达式方法。
>>> response.css('title::text').re(r'Quotes.*')
['Quotes to Scrape']
>>> response.css('title::text').re(r'Q\w+')
['Quotes']
>>> response.css('title::text').re(r'(\w+) to (\w+)')
['Quotes', 'Scrape']
为了找到适当的CSS选择器,你可从shell中使用view(response)浏览响应界面。你可以使用浏览器开发工具或插件如Firebug(此处请看使用Firebug 抓取和使用FireFox抓取)。
选择器小工具也是一个查找CSS选择器很好的工具,可以可视化的查找元素,可在很多浏览器中工作。
XPATH:简介
除了css,Scrapy选择器也支持XPath表达式:
>>> response.xpath('//title')
[<Selector xpath='//title' data='<title>Quotes to Scrape</title>'>]
>>> response.xpath('//title/text()').extract_first()
'Quotes to Scrape'
XPATH表达式很强大,是Scrapy选择器的基础。事实上,CSS选择器在内部转换为Xpath。你可以在shell查看文本选择器的对象类型。
尽管不如CSS选择器流行,Xpath表达式却更强大。它除了可以导航到结构也可以查找内容。使用xpath,你能这么选择如:选择包含Next Page的文本连接。这使得xpath非常适合抓取,我们鼓励你学习Xpath,即使你已经知道如何构造CSS选择器,它会更简单。
我们在这不会涉及XPath太多,你可以阅读使用XPath.为了学习Xpath,我们建议通过例子学习XPath教程,和如何使用XPath思考。
提取quotes和authors
现在你知道了一点关于选择和提起的知识了,让我们完善我们的spider,写代码从网站页面提取quotes。
http://quotes.toscrape.com中的每个quote的HTML形式类似下面:
<div class="quote">
<span class="text">“The world as we have created it is a process of our
thinking. It cannot be changed without changing our thinking.”</span>
<span>
by <small class="author">Albert Einstein</small>
<a href="/author/Albert-Einstein">(about)</a>
</span>
<div class="tags">
Tags:
<a class="tag" href="/tag/change/page/1/">change</a>
<a class="tag" href="/tag/deep-thoughts/page/1/">deep-thoughts</a>
<a class="tag" href="/tag/thinking/page/1/">thinking</a>
<a class="tag" href="/tag/world/page/1/">world</a>
</div>
</div>
我们打开scrapy shell并做一些解决如何提取我们想要的数据的事。
$ scrapy shell 'http://quotes.toscrape.com'
我们使用下面语法得到一系列的quote元素的选择器:
>>> response.css("div.quote")
查询返回的每个选择器我们还可以查询它们的子元素。我们把第一个选择器赋值给变量,这样我们可以直接运行指定的quote选择器。
quote = response.css("div.quote")[0]
现在我们从quote导出title
,author
和tags
使用我们刚创建的quote
对象。当你知道你只需要第一个结果时,你可以:
>>> title = quote.css("span.text::text").extract_first()
>>> title
'“The world as we have created it is a process of our thinking. It cannot be changed without changing our thinking.”'
>>> author = quote.css("small.author::text").extract_first()
>>> author
'Albert Einstein'
考虑到标签是字符串列表,我们可以使用.extract()
方法获取他们。
>>> tags = quote.css("div.tags a.tag::text").extract()
>>> tags
['change', 'deep-thoughts', 'thinking', 'world']
解决了如何导出每个,我们现在可迭代所有quotes元素把他们保存到Python字典中。
>>> for quote in response.css("div.quote"):
... text = quote.css("span.text::text").extract_first()
... author = quote.css("small.author::text").extract_first()
... tags = quote.css("div.tags a.tag::text").extract()
... 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
>>>
在我们的蜘蛛里导出数据
让我们回到蜘蛛。直到现在,仍然没有导出任何数据,只是把HTML页面保存到本地文件中。我们把导出逻辑集成到spider中。
一个Scrapy蜘蛛通常包含多个页面抓取数据的字典。这样,我们可以使用在回调函数中使用yield
Python关键字,如下所示:
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').extract_first(),
'author': quote.css('span small::text').extract_first(),
'tags': quote.css('div.tags a.tag::text').extract(),
}
如果你运行这个蜘蛛,它把导出数据输出到日志中:
2016-09-19 18:57:19 [scrapy.core.scraper] DEBUG: Scraped from <200 http://quotes.toscrape.com/page/1/>
{'tags': ['life', 'love'], 'author': 'André Gide', 'text': '“It is better to be hated for what you are than to be loved for what you are not.”'}
2016-09-19 18:57:19 [scrapy.core.scraper] DEBUG: Scraped from <200 http://quotes.toscrape.com/page/1/>
{'tags': ['edison', 'failure', 'inspirational', 'paraphrased'], 'author': 'Thomas A. Edison', 'text': "“I have not failed. I've just found 10,000 ways that won't work.”"}
保存抓取到的数据
最简单的保存抓取数据是使用Feed exports, 使用下面的命令行:
scrapy crawl quotes -o quotes.json
这将生成一个quotes.json文件包含所有抓取像序列化为json。
由于历史原因,Scrapy使用追加而不是覆盖,如果你运行两次此命令而没有在第二次删除之前的文件,你将得到一个损毁的JSON文件。
你也可以使用其他格式,如Json Lines
scrapy crawl quotes -o quotes.jl
Json Lines格式很有用,因为她是stream-like。你可以往里面轻松的添加新纪录。他没有上面的JSON文件的问题当你运行两次的时候。同时,因为每条记录是一行,你可以处理超大文件而不必担心内存问题,有很多工具如JQ可在命令行处理。
在小项目里(例如此教程),这样就够了。然而,如果你想处理更复杂的抓取项,你可以编写[Item 管道]。当创建项目的时候,会在tutorial/pipelines.py
构建一个Item 管道文件。这样如果你只是想保存抓取到的项,就不需要实现任何的Item管道。
下面的连接
假如你不想仅抓取http://quotes.toscrape.com网站中的两个页面,而是想抓取所有的网站页面。
现在你知道如何从页面抓取数据,让我们看看下面的连接如何得到。
首先从页面中提取我们想要的连接。查看我们的页面,我们可以看见页面中的下一页连接如下所示标志:
<ul class="pager">
<li class="next">
<a href="/page/2/">Next <span aria-hidden="true">→</span></a>
</li>
</ul>
试着在shell中提取它:
>>> response.css('li.next a').extract_first()
'<a href="/page/2/">Next <span aria-hidden="true">→</span></a>'
这得到了整个anchor元素,但是我们想要href
属性。为了如此,Scrapu提供了CSS的扩展使你可以选择属性内容,如下:
>>> response.css('li.next a::attr(href)').extract_first()
'/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').extract_first(),
'author': quote.css('span small::text').extract_first(),
'tags': quote.css('div.tags a.tag::text').extract(),
}
next_page = response.css('li.next a::attr(href)').extract_first()
if next_page is not None:
next_page = response.urljoin(next_page)
yield scrapy.Request(next_page, callback=self.parse)
现在,导出数据后,parse()
函数查找下一页,使用urljoin
构建一个绝对路径URL并生成一个到下一页的新请求,把下一页的请求注册为回调使得蜘蛛可以爬到所有的页面。
这是Scrapy跟踪页面的机制:当你在回调中生成一个请求对象,Scrapy会安排请求发送并注册回调函数在请求结束时运行。
使用这些,你可以构建复杂的爬虫系统,链接规则可以自定义,根据访问页面导出各种各样的数据。
在我们的例子中,它创建了一系列循环跟踪所有的链接到下一页直到找不到任何连接——方便爬取博客,论坛或其他的导航网站。
更多示例和模式
这是另一个蜘蛛用来解释回调和跟踪连接,这次抓取作者信息:
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)').extract():
yield scrapy.Request(response.urljoin(href),
callback=self.parse_author)
# follow pagination links
next_page = response.css('li.next a::attr(href)').extract_first()
if next_page is not None:
next_page = response.urljoin(next_page)
yield scrapy.Request(next_page, callback=self.parse)
def parse_author(self, response):
def extract_with_css(query):
return response.css(query).extract_first().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'),
}
蜘蛛从主页面开始,使用parse_author
回调函数跟踪所有的作者页面连接,同时用parse
回调函数跟踪导航连接如我们之前看到的。
parse_author
回调函数定义了一个帮助方法,从CSS查询提取和清理并使用作者数据生成Python dict。
另一件关于蜘蛛的有趣的事情是,即使有很多名言出自同一作者,我们也不必担心多次访问相同作者的页面。默认情况下,Scrapy过滤掉重复的已访问的请求地址,避免程序太多次点击服务器的问题。这是用DUPEFILTER_CLASS配置。
希望你已理解了Scrapy如何跟踪页面和回调的机制。
这个程序利用跟踪链接机制实现,查看CrawlSpider类,它是一个通用的蜘蛛实现了一个小的规则引擎,你可以在这之上编写自己的爬虫。