Scrapy食用指南4

Scrapy文档笔记-4

提取数据

学习提取数据的最好的方式是使用Scrapy shell的选择器,运行:

1
scrapy shell 'http://quotes.toscrape.com/page/1'

注意:

当你在终端运行Scrapy时,请一定记得给url地址加上引号,否则包含参数的url(例如&字符)会导致Scrapy运行失败。

在Windows上请记得使用双引号:

1
2
3
>scrapy shell "http://quotes.toscrape.com/page/1/"
>
>

你会看到类似

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
2018-05-30 15:27:22 [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 0x0000017F75D44828>
[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 0x0000017F77086860>
[s] spider <DefaultSpider 'default' at 0x17f7732c278>
[s] Useful shortcuts:
[s] fetch(url[, redirect=True]) Fetch URL and update local objects (by default, redirects are followed)
[s] fetch(req) Fetch a scrapy.Request and update local objects
[s] shelp() Shell help (print this help)
[s] view(response) View response in a browser
>>>

使用shell,你可以通过使用带有response对象的CSS

1
2
>>> response.css('title')
[<Selector xpath='descendant-or-self::title' data='<title>Quotes to Scrape</title>'>]

运行response.css('title')的结果是一个名为SelectorList的类似列表的对象,它表示包含XML/HTML元素的Selector对象列表,允许您运行进一步的查询以精细选择或提取数据。

要从上边的标题中提取文本,您可以:

1
2
>>> response.css('title::text').extract()
['Quotes to Scrape']

这里要注意两件事

一是我们在CSS查询中添加了::text,这意味着我们只想在<title>元素中选择文本元素。如果我们不指定::text,我们将获得完整的 title 元素,包括其标签

1
2
>>> response.css("title").extract()
['<title>Quotes to Scrape</title>']

另一件事是调用.extract()的结果是一个列表,因为我们处理的是SelectorList的一个实例。如果你知道你只是想要第一个结果时,可以输入以下命令

1
2
>>> response.css("title::text").extract_first()
'Quotes to Scrape'

你也可以这样做:

1
2
>>> response.css("title::text")[0].extract()
'Quotes to Scrape'

但是使用.extract_first()会避免IndexError,并且在找不到与选择匹配的元素时返回None

这里有一个经验:对于大多数爬虫代码,你希望它的容错性比较高,因为在被爬取的页面上有时候会找不到我们想要的数据。所以即使有一部分没有被爬取到,你可以至少获得一些数据。

除了extractextract_first方法之外,还可以使用re方法使用正则表达式进行提取

1
2
3
4
5
6
7
8
>>> 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+) (\w+)')
['Quotes', 'to']
>>> response.css("title::text").re(r'(\w+) to (\w+)')
['Quotes', 'Scrape']

为了找到正确的CSS选择器使用,可以使用view(response).也可以使用浏览器开发人员工具或拓展(如 Firebug 下一篇教程会详细写Firebug).

Selector Gadget也是一个很好的工具,可以快速找到CSS选择器的视觉选择元素,适用于许多浏览器.

XPath简介

除了CSS,Scrapy选择器也支持使用XPath表达式

1
2
>>> response.xpath("//title")
[<Selector xpath='//title' data='<title>Quotes to Scrape</title>'>]

XPath表达式非常强大并且是 Scrapy Selectors的基础.事实上,CSS选择器的底层实现是由XPath来完成(待商榷).如果您仔细阅读在 shell 中的选择器对象的文本表示您将会看到它。 (selector对象在这个Scrapy版本没有显示出来,即sel)

虽然可能不像CSS选择器那样流行,XPath 表达式提供了更多的功能,因为除了导航结构之外,它还可以查看内容。使用 XPath,您可以选择以下内容:选择包含文本“下一页”的链接。这使得 XPath 非常适合于抓取的任务,我们鼓励您学习 XPath ,即使您已经知道如何构造CSS选择器,它将使抓取更容易。

我们不会在这里介绍 XPath 的很多,但是你可以阅读更多关于 使用 XPath 与 Scrapy 选择器? 。要了解有关 XPath 的更多信息,我们建议 本教程通过示例学习 XPath ,以及 本教程学习如何在XPath中思考

提取 quotes 和 authors

现在您已经对选择(select)和提取(extract)有一定的了解,让我们通过编写代码从网页提取quote来完成我们的爬虫。

http://quotes.toscrape.com中的每个 quote 都由如下所示的HTML元素表示

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<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来探索一下如何提取我们想要的数据

1
$ scrapy shell "http://quotes.toscrape.com"

我们得到一个带有 HTML 元素的选择器列表

1
2
3
4
5
6
7
8
9
10
>>> response.css("div.quote")
[<Selector xpath="descendant-or-self::div[@class and contains(concat(' ', normalize-space(@class), ' '), ' quote ')]" data='<div class="quote" itemscope itemtype="h'>, <Selector xpath="descendant-or-self::div[@class and cont
ains(concat(' ', normalize-space(@class), ' '), ' quote ')]" data='<div class="quote" itemscope itemtype="h'>, <Selector xpath="descendant-or-self::div[@class and contains(concat(' ', normalize-space(@class), ' '), ' quote '
)]" data='<div class="quote" itemscope itemtype="h'>, <Selector xpath="descendant-or-self::div[@class and contains(concat(' ', normalize-space(@class), ' '), ' quote ')]" data='<div class="quote" itemscope itemtype="h'>, <Se
lector xpath="descendant-or-self::div[@class and contains(concat(' ', normalize-space(@class), ' '), ' quote ')]" data='<div class="quote" itemscope itemtype="h'>, <Selector xpath="descendant-or-self::div[@class and contains
(concat(' ', normalize-space(@class), ' '), ' quote ')]" data='<div class="quote" itemscope itemtype="h'>, <Selector xpath="descendant-or-self::div[@class and contains(concat(' ', normalize-space(@class), ' '), ' quote ')]"
data='<div class="quote" itemscope itemtype="h'>, <Selector xpath="descendant-or-self::div[@class and contains(concat(' ', normalize-space(@class), ' '), ' quote ')]" data='<div class="quote" itemscope itemtype="h'>, <Select
or xpath="descendant-or-self::div[@class and contains(concat(' ', normalize-space(@class), ' '), ' quote ')]" data='<div class="quote" itemscope itemtype="h'>, <Selector xpath="descendant-or-self::div[@class and contains(con
cat(' ', normalize-space(@class), ' '), ' quote ')]" data='<div class="quote" itemscope itemtype="h'>]
>>>

通过上面的查询返回的每个选择器允许我们对他们的子元素进行进一步查询。让我们将第一个选择器分配给一个变量,以便我们可以直接对特定的引用运行我们的CSS选择器:

1
>>> quote = response.css("div.quote")[0]

现在,让我们使用刚刚创建的 quote 对象从该报价中提取 titleauthortags:

1
2
3
4
5
6
7
8
9
10
11
12
13
>>> quote = response.css("div.quote")[0]
>>> 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 = response.css("small.author::text").extract_first()
>>> author
'Albert Einstein'
>>> tags = response.css("a.tag::text").extract()
>>> tags
['change', 'deep-thoughts', 'thinking', 'world', 'abilities', 'choices', 'inspirational', 'life', 'live', 'miracle', 'miracles', 'aliteracy', 'books', 'classic', 'humor', 'be-yourself', 'inspirational', 'adulthood', 'success
', 'value', 'life', 'love', 'edison', 'failure', 'inspirational', 'paraphrased', 'misattributed-eleanor-roosevelt', 'humor', 'obvious', 'simile', 'love', 'inspirational', 'life', 'humor', 'books', 'reading', 'friendship', 'f
riends', 'truth', 'simile']
>>>

搞清楚如何提取每个位之后,我们现在可以遍历所有的引号元素,并将它们放在一起成为一个Python字典

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
>>> for quote in response.css("div.quote"):
... title = quote.css("span.text::text").extract_first()
... author = quote.css("small.author::text").extract_first()
... tags = quote.css("a.tag::text").extract()
... print(dict(text=title,author=author,tags=tags))
...
{'text': '“The world as we have created it is a process of our thinking. It cannot be changed without changing our thinking.”', 'author': 'Albert Einstein', 'tags': ['change', 'deep-thoughts', 'thinking', 'world']}
{'text': '“It is our choices, Harry, that show what we truly are, far more than our abilities.”', 'author': 'J.K. Rowling', 'tags': ['abilities', 'choices']}
{'text': '“There are only two ways to live your life. One is as though nothing is a miracle. The other is as though everything is a miracle.”', 'author': 'Albert Einstein', 'tags': ['inspirational', 'life', 'live', 'miracl
e', 'miracles']}
{'text': '“The person, be it gentleman or lady, who has not pleasure in a good novel, must be intolerably stupid.”', 'author': 'Jane Austen', 'tags': ['aliteracy', 'books', 'classic', 'humor']}
{'text': "“Imperfection is beauty, madness is genius and it's better to be absolutely ridiculous than absolutely boring.”", 'author': 'Marilyn Monroe', 'tags': ['be-yourself', 'inspirational']}
{'text': '“Try not to become a man of success. Rather become a man of value.”', 'author': 'Albert Einstein', 'tags': ['adulthood', 'success', 'value']}
{'text': '“It is better to be hated for what you are than to be loved for what you are not.”', 'author': 'André Gide', 'tags': ['life', 'love']}
{'text': "“I have not failed. I've just found 10,000 ways that won't work.”", 'author': 'Thomas A. Edison', 'tags': ['edison', 'failure', 'inspirational', 'paraphrased']}
{'text': "“A woman is like a tea bag; you never know how strong it is until it's in hot water.”", 'author': 'Eleanor Roosevelt', 'tags': ['misattributed-eleanor-roosevelt']}
{'text': '“A day without sunshine is like, you know, night.”', 'author': 'Steve Martin', 'tags': ['humor', 'obvious', 'simile']}

在我们的爬虫中提取数据

让我们回到爬虫,到现在为止它还没有提取任何特别的数据,只是将整个HTML页面保存到本地文件上,让我们将上面的提取逻辑集成到我们的爬虫中.

Scrapy 爬虫通常会生成许多包含从页面中提取的数据的字典。为此,我们在回调中使用 Python 的 yield 关键字,如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
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):
for quote in response.css("div.quote"):
yield {
'text':quote.css('span.text::text').extract_first(),
'author':quote.css("small.author::text").extract_first(),
'tags':quote.css("a.tag::text").extract()
}

运行爬虫,你会得到类似下面的输出

1
2
3
4
5
6
7
2018-05-30 19:28:19 [scrapy.core.scraper] DEBUG: Scraped from <200 http://quotes.toscrape.com/page/1/>
{'text': '“The world as we have created it is a process of our thinking. It cannot be changed without changing our thinking.”', 'author': 'Albert Einstein', 'tags': ['change', 'deep-thoughts', 'thinking', 'world']}
2018-05-30 19:28:19 [scrapy.core.scraper] DEBUG: Scraped from <200 http://quotes.toscrape.com/page/1/>
{'text': '“It is our choices, Harry, that show what we truly are, far more than our abilities.”', 'author': 'J.K. Rowling', 'tags': ['abilities', 'choices']}
2018-05-30 19:28:19 [scrapy.core.scraper] DEBUG: Scraped from <200 http://quotes.toscrape.com/page/1/>
{'text': '“There are only two ways to live your life. One is as though nothing is a miracle. The other is as though everything is a miracle.”', 'author': 'Albert Einstein', 'tags': ['inspirational', 'life', 'live', 'miracl
e', 'miracles']}

存储爬虫数据

最简单的存储爬虫数据的方式是使用 Feed exports(在下下篇博客会详细说明),使用下面的命令

1
scrapy crawl quotes -o quotes.json

这将生成一个 quotes.json 文件,其中包含所有被抓取的项目,以 JSON 序列化。

出于历史原因,Scrapy将附加到给定文件,而不是覆盖其内容。如果您运行这个命令两次,而且在第二次运行之前没有删除文件,您会得到一个broken的JSON文件。

也可以使用其他格式的文件,例如 JSON Lines

1
scrapy crawl quotes -o quotes,j1

JSON Lines 格式很有用,因为它是流式的,你可以轻松地添加新的记录到它。当你运行两次它没有相同的 JSON 问题。此外,由于每条记录都是单独的行,因此您可以处理大文件,而无需将所有内容都放在内存中,有像 JQ 这样的工具可以帮助在命令行执行。

在小项目(如本教程中的一个)中,这应该足够了。但是,如果要对已抓取的 Item 执行更复杂的操作,则可以编写 Item Pipeline 。在创建项目时,已经在 tutorial / pipelines.py 中为您创建了 Item Pipeline 的占位符文件。如果您只想存储被抓取的 Item ,您不需要实现任何 Item Pipeline。

如果说我们不只是想从http://quotes.toscrape.com爬取前两页的内容,而是想爬取这个网站的所有页面,该怎么办?

现在您已经知道如何从网页中提取数据,让我们看看如何跟进他们的链接。

首先是提取我们要关注的网页的链接。检查我们的页面,我们可以看到有一个链接到下一页与下面的标记:

1
2
3
4
5
<ul class="pager">
<li class="next">
<a href="/page/2/">Next <span aria-hidden="true">&rarr;</span></a>
</li>
</ul>

我们可以尝试在shell中提取它:

1
2
>>> response.css('li.next a').extract_first()
'<a href="/page/2/">Next <span aria-hidden="true">→</span></a>'

上面的操作得到了锚( anchor )元素,但我们想要属性 href.为此,Scrapy支持了一个CSS拓展,允许让我们选择属性内容,如下所示:

1
2
>>> response.css("li.next a::attr(href)").extract_first()
'/page/2/'

现在我们的爬虫被修改为递归地跟进到下一页的链接,并从中提取数据:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
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 会调度要发送的请求,并注册一个回调方法。

使用它,您可以构建复杂的抓取工具,根据您定义的规则跟进链接,并根据访问的网页提取不同类型的数据。

在我们的示例中,它创建一个循环,所有的链接到下一页,直到它找不到一个可以抓取的博客,论坛和其他网站分页

创建请求的快捷方式

你可以使用response.follow作为创建Request对象的快捷方式

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
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:
yield response.follow(next_page, callback=self.parse)

不同于scrapy.Request,response.follow直接支持相对 URLs,不需要调用urljoin.注意response.follow仅仅返回一个Request实例,你仍然需要调用yield去返回这个实例.

你也可以给response.follow传递一个选择器作为参数而不是一个字符串,这个选择器应该能够提取出正确的属性:

1
2
for href in response.css("li.next a::attr(href)"):
yield response.follow(a,callback=self.parse)

对于<a>元素也存在着一个快捷方式:response.follow会自动使用他们的 href 属性,所以代码可以更短:

1
2
for a in response.css("li.next a"):
yield response.follow(a,callback=self.parse)

注意:

response.follow(response.css('li.next a')) is not valid becauseresponse.css returns a list-like object with selectors for all results, not a single selector. A for loop like in the example above, orresponse.follow(response.css('li.next a')[0]) is fine.

更多的示例和模式

这里是另一个爬虫,用来说明回调和跟进链接,这一次是抓取作者信息:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
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).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。

使用 spider 参数

在运行爬虫时,可以使用 -a 选项为您的爬虫提供命令行参数:

1
scrapy crawl quotes -o quotes-humor.json -a tag=humor

这些参数传递给 Spider 的 __init__ 方法,默认成为spider属性。

在此示例中,为 tag 参数提供的值将通过 self.tag 提供。您可以使用此方法使您的爬虫根据参数构建 URL来实现仅抓取带有特定tag的数据:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
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。

您可以 在此处了解有关处理spider参数的更多信息

参考文档-EN

参考文档-CH