在本教程中,我们假设Scrapy已经安装在您的系统上。如果不是这样,请参阅 安装指南 。
我们将要爬取 quotes.toscrape.com ,一个包含众多著名作家名言的网站。
本教程将引导您完成以下任务:
- 创建一个新的 Scrapy 项目
- 编写 爬虫 以抓取网站并提取数据
- 使用命令行导出已爬取的数据
- 将爬虫更改为递归跟进链接
- 使用爬虫参数
Scrapy 是由 Python 编写的,如果您刚接触并且好奇这门语言的特性。
对于已经熟悉其他语言,并且想要快速学习 Python 的人,我们建议您通过 Dive Into Python 3 阅读。或者,您可以按照 Python教程 。
对于刚开始接触编程,想从 Python 开始人,您可以找有用的在线书 Learn Python The Hard Way 。您还可以查看 非程序员的Python资源列表 。
创建项目
在开始爬取之前,您必须设置一个新的Scrapy项目。进入您要存储代码的目录,并运行如下命令:
scrapy startproject tutorial
这将创建一个具有以下内容的 tutorial
目录:
tutorial/
scrapy.cfg # 部署配置文件
tutorial/ # 项目的 Python 模块,您将从这里加入您的代码
__init__.py
items.py # 项目的 item 定义文件
pipelines.py # 项目的 pipelines 文件
settings.py # 项目的 settings 文件
spiders/ # 放置spider代码的目录.
__init__.py
我们的第一个爬虫
爬虫是您定义的类,Scrapy用它从网站(或一组网站)中抓取信息。它们必须子类化 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)
如你所见,我们的 Spider 继承了 scrapy.Spider 并定义了一些属性和方法:
name
:标识爬虫。它在项目中必须是唯一的,也就是说,您不能为不同的 Spider 设置相同的名称。start_requests()
:必须返回一个可迭代的 Requests(您可以返回一个 request 列表或写一个生成器函数),Spider将开始抓取。后续请求将从这些初始请求中连续生成。parse()
:被调用来处理 response 的方法, response 由每个 request 下载生成。 response 参数是一个TextResponse
的实例,它保存页面内容,并具有更多有用的方法来处理它。
parse()
方法通常解析 response ,将抓取的数据提取为 dicts,并查找要跟进的新 URL 并从中创建新请求(Request
)。
如何运行我们的爬虫
要让我们的蜘蛛工作,进入项目的根目录并运行:
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,内容分别对应各自的 URL ,作为我们的 parse
方法指示。
注意!
如果你想知道为什么我们还没有解析 HTML,稍等一会,我们马上将解决。
Scrapy 调度由 Spider 的 start_requests
方法返回的 scrapy.Request
对象。在接收到每个 response 时,它实例化 Response
对象并调用与 request 相关联的回调方法(在这儿是 parse
方法)将 response 作为参数传递。
start_requests 的快捷方式
您可以使用一个 URL 列表定义一个 start_urls
类属性,来代替实现一个从 URLs 生成 scrapy.Request
对象的 start_requests()
方法, 此列表将由默认实现的 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)
将调用 parse()
方法来处理对这些 URL 的每个请求,即使我们没有明确告诉 Scrapy 这样做。发生这种情况是因为 parse()
是 Scrapy 的默认回调方法,它为没有显式分配的回调的请求调用。
提取数据
学习如何使用 Scrapy 提取数据的最好方法是尝试使用 Scrapy 终端(Scrapy Shell)
的选择器(selectors)。跑:
scrapy shell 'http://quotes.toscrape.com/page/1/'
注意!
当您在终端运行 Scrapy 时,请一定记得给 url 地址加上引号,否则包含参数的 url (例如
&
字符)会导致 Scrapy 运行失败。在Windows上,请使用双引号:
scrapy shell "http://quotes.toscrape.com/page/1/"
你会看到类似:
[ ... 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
的类似列表的对象,它表示包含 XML / HTML 元素的 Selector
对象列表,允许您运行进一步的查询以精细选择或提取数据。
要从上面的标题中提取文本,您可以:
>>> response.css('title::text').extract()
['Quotes to Scrape']
这里有两个要注意的事情:一个是我们在CSS查询中添加了 ::text
,这意味着我们只想直接在 <title>
元素中选择文本元素。如果我们不指定 ::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_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选择器使用,您可能会发现有用的打开响应页面从您的Web浏览器中使用 view(response)
。您可以使用浏览器开发人员工具或扩展(如 Firebug)(有关 使用Firebug进行抓取? 和 借助Firefox进行抓取 的部分)。
Selector Gadget 也是一个很好的工具,可以快速找到CSS选择器的视觉选择的元素,这在许多浏览器。
除了 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,您可以选择以下内容:选择包含文本“下一页”的链接。这使得 XPath 非常适合于抓取的任务,我们鼓励您学习 XPath ,即使您已经知道如何构造CSS选择器,它将使抓取更容易。
我们不会在这里介绍 XPath 的很多,但是你可以阅读更多关于 使用 XPath 与 Scrapy 选择器? 。要了解有关 XPath 的更多信息,我们建议 本教程通过示例学习 XPath ,以及 本教程学习如何在XPath中思考 。
现在您已经对选择(select)和提取(extract)有一定的了解,让我们通过编写代码从网页提取quote来完成我们的爬虫。
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'
我们得到一个带有 HTML 元素的 quote 列表:
>>> response.css("div.quote")
通过上面的查询返回的每个选择器允许我们对它们的子元素进行进一步的查询。让我们将第一个选择器分配给一个变量,以便我们可以直接对特定的引用运行我们的CSS选择器:
>>> quote = response.css("div.quote")[0]
现在,让我们使用刚刚创建的 quote
对象从该报价中提取 title
, author
和 tags
:
>>> 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']
在找出了如何提取每个位之后,我们现在可以遍历所有的引号元素,并将它们放在一起成为一个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页面保存到本地文件。让我们将上面的提取逻辑集成到我们的爬虫中。
Scrapy 爬虫通常会生成许多包含从页面中提取的数据的字典。为此,我们在回调中使用 Python 的 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').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 导出(Feed exports) ,使用以下命令:
scrapy crawl quotes -o quotes.json
这将生成一个 quotes.json
文件,其中包含所有被抓取的项目,以 JSON 序列化。
出于历史原因,Scrapy将附加到给定文件,而不是覆盖其内容。如果您运行这个命令两次,而且在第二次运行之前没有删除文件,您会得到一个不合法的JSON文件。
您还可以使用其他格式,如JSON Lines:
scrapy crawl quotes -o quotes.jl
JSON Lines 格式很有用,因为它是流式的,你可以轻松地添加新的记录到它。当你运行两次它没有相同的 JSON 问题。此外,由于每条记录都是单独的行,因此您可以处理大文件,而无需将所有内容都放在内存中,有像 JQ 这样的工具可以帮助在命令行执行。
在小项目(如本教程中的一个)中,这应该足够了。但是,如果要对已抓取的 Item 执行更复杂的操作,则可以编写 Item Pipeline 。在创建项目时,已经在 tutorial / pipelines.py
中为您创建了 Item Pipeline 的占位符文件。如果您只想存储被抓取的 Item ,您不需要实现任何 Item Pipeline。
跟进链接
如果我们不只是从 http://quotes.toscrape.com 的前两页中提取内容,而是想从网站的所有页面提取 quote。
现在您已经知道如何从网页中提取数据,让我们看看如何跟进他们的链接。
首先是提取我们要关注的网页的链接。检查我们的页面,我们可以看到有一个链接到下一页与下面的标记:
<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。为此,Scrapy 支持一个 CSS 扩展,让您选择属性内容,如下所示:
>>> response.css('li.next a::attr(href)').extract_first()
'/page/2/'
让我们看看现在我们的爬虫被修改为递归地跟进到下一页的链接,并从中提取数据:
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。
另一个有趣的事情,这个爬虫演示的是,即使有很多来自同一作者的quote,我们不需要担心访问同一作者页面多次。默认情况下,Scrapy 会过滤掉已访问过的网址的重复请求,从而避免由于编程错误而导致服务器过多的问题。这可以通过设置 DUPEFILTER_CLASS
进行配置。
希望现在您对如何使用 Scrapy 的跟进链接和回调的机制有一个很好的理解。
作为利用跟进链接的机制另一个示例爬虫,请查看一个通用爬虫 CrawlSpider
类,它实现了一个小规则引擎(small rules engine),您可以用它写你的爬虫。
此外,一个常见的模式是使用一个 把额外的数据传递给回调函数的技巧 来构建一个包含多个页面的数据的 Item。
使用爬虫参数
在运行爬虫时,可以使用 -a
选项为您的爬虫提供命令行参数:
scrapy crawl quotes -o quotes-humor.json -a tag=humor
这些参数传递给 Spider 的 __init__
方法,默认成为spider属性。
在此示例中,为 tag
参数提供的值将通过 self.tag
提供。您可以使用此方法使您的爬虫根据参数构建 URL来实现仅抓取带有特定tag的数据:
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').extract_first(),
'author': quote.css('span small a::text').extract_first(),
}
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, self.parse)
如果您向此爬虫传递 tag=humor
参数,您会注意到它只会访问 humor
标记中的网址,例如 http://quotes.toscrape.com/tag/humor。