简述(Python)
Srapy是一个应用框架用于爬取网站和提取结构化数据,被很多有用的应用所使用,就像数据挖掘,信息处理或者历史档案。
即使虽然Scrapy一开始是用来做网络爬虫的,它也可以用来提取数据通过api就像Amazon Associates Web Services,或者作为一个普通用途的爬虫。
查看一个爬虫的例子
为了展示一个Srapy是怎么发送你们的表单,我们会查看这个一个爬虫例子,通过最简单的方法去运行一个爬虫。
以下是爬行器的代码,它从http://quotes.toscrape.com网站上抓取著名的引用,如下所示:
import scrapy
class QuotesSpider(scrapy.Spider): # 继承于Spider
name = 'quotes' # 名字
start_urls = [ # 起始链接
'http://quotes.toscrape.com/tag/humor/',
]
def parse(self, response):
for quote in response.css('div.quote'):
yield {
'text': quote.css('span.text::text').get(),
'author': quote.xpath('span/small/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)
Scrapy 向导
我们要去爬取quotes.toscrape.com,一个列出著名作家名言的网站。
本教程将指导您完成以下任务:
- 创建一个新的杂乱的项目
- 编写一个爬行器来爬行站点并提取数据
- 使用命令行导出抓取的数据
- 将爬行器改为递归跟踪链接
- 使用蜘蛛参数
创建一个项目
在您开始爬之前,您必须建立一个新的刮削项目。输入一个目录,你想在其中存储你的代码,并运行:
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
middlewares.py # project middlewares file
pipelines.py # project pipelines file
settings.py # project settings file
spiders/ # a directory where you'll later put your spiders
__init__.py
我的第一个spider
spider是您定义的类,它用于从网站(或一组网站)中抓取信息。它们必须是scrapy的子类。爬行并定义要发出的初始请求,可以选择如何跟踪页面中的链接,以及如何解析下载的页面内容以提取数据。
这是我们第一个Spider的代码。将它保存在一个名为quotes_spider.py的文件中,该文件位于项目的tutorial/spider目录下:
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。并定义了一些属性和方法:
- name:标识蜘蛛。它在项目中必须是惟一的,也就是说,不能为不同的爬行器设置相同的名称。
- start_requests():必须返回一个可迭代的请求(您可以返回一个请求列表或编写一个生成器函数),爬行器将开始从中爬行。后续请求将从这些初始请求中依次生成。
- parse():一个方法,它将被调用来处理为每个请求下载的响应。response参数是TextResponse的一个实例,它包含页面内容,并且有进一步有用的方法来处理它。
- parse()方法通常解析响应,将抓取的数据提取为dict,并找到要跟踪的新url,并从中创建新的请求(request)。
运行一个spider
回到顶级目录,使用crawl指令,name=quotes
scrapy crawl quotes
这个命令行运行spider通过名称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的内容,正如我们的解析方法所指示的那样。
如果您想知道为什么我们还没有解析HTML,请稍等,我们将很快介绍。
在后台发生了什么
Scrapy调度了scrapy.Request 对象,通过spider的start_requests 方法返回的。在每个接受到了各个响应,调用回调方法在关联request(在这个方案中,parse方法)传递response作为参数。
start requests方法的快捷方法
而不是实现生成scrapy的start_requests()方法。从url请求对象,您可以使用url列表定义start_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)
将调用parse()方法来处理这些url的每个请求,尽管我们没有显式地告诉Scrapy这样做。这是因为parse()是Scrapy的默认回调方法,它在没有显式分配回调的情况下调用请求。
提取数据
学习如何使用Scrapy提取数据的最佳方法是尝试使用shell 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,您可以尝试使用带有响应对象的CSS选择元素:
>>> response.css('title')
[<Selector xpath='descendant-or-self::title' data='<title>Quotes to Scrape</title>'>]
运行response.css(‘title’)的结果是一个类似列表的对象SelectorList,它表示一个选择器对象列表,这些选择器对象包围XML/HTML元素,允许您运行进一步的查询来细化选择或提取数据。
去获取title:text文本
>>> response.css('title::text').getall()
['Quotes to Scrape']
这里需要注意两件事:一是我们向CSS查询添加了::text,这意味着我们只想直接选择
>>> response.css('title').getall()
['<title>Quotes to Scrape</title>']
另一件事是调用.getall()的结果是一个列表:选择器可能返回多个结果,因此我们提取所有结果。当您知道您只想要第一个结果时,就像在本例中一样,您可以这样做:
>>> response.css('title::text').get()
'Quotes to Scrape'
或者,
>>> response.css('title::text')[0].get()
'Quotes to Scrape'
但是,直接在SelectorList实例上使用.get()可以避免使用IndexError,当它没有找到匹配选择的元素时,返回None。
这里有一个教训:对于大多数抓取代码,您希望它对页面上没有找到的内容所导致的错误具有弹性,这样即使某些部分没有被抓取,您至少可以获得一些数据。
除了getall()和get()方法外,还可以使用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选择器,您可能会发现使用view(response)从web浏览器的shell中打开响应页面很有用。您可以使用浏览器开发人员工具来检查HTML并提出一个选择器(请参阅有关使用浏览器开发人员工具进行抓取的部分)。
选择器小工具也是一个很好的工具,可以快速找到可视选择元素的CSS选择器,它可以在许多浏览器中工作。
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表达式非常强大,是粗糙选择器的基础。实际上,CSS选择器被转换为XPath底层。如果仔细阅读shell中选择器对象的文本表示形式,就会发现这一点。
虽然XPath表达式可能不像CSS选择器那么流行,但它提供了更强大的功能,因为除了导航结构之外,它还可以查看内容。使用XPath,您可以选择如下内容:选择包含文本“Next Page”的链接。这使得XPath非常适合于抓取任务,我们鼓励您学习XPath,即使您已经知道如何构造CSS选择器,它也会使抓取更加容易。
我们不会在这里过多地介绍XPath,但是您可以在这里阅读关于使用带有粗糙选择器的XPath的更多信息。要了解更多关于XPath的知识,我们建议本教程通过示例学习XPath,本教程学习“如何用XPath思考”。
提取引用和作者
现在您已经了解了一些关于选择和提取的知识,让我们通过编写从web页面提取引号的代码来完成爬行器。
http://quotes.toscrape.com中的每个引用都由如下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元素就像:
>>> response.css("div.quote")
上面查询返回的每个选择器允许我们在其子元素上运行进一步的查询。让我们把第一个选择器分配给一个变量,这样我们就可以直接在一个特定的引用上运行我们的CSS选择器:
>>> quote = response.css("div.quote")[0]
现在,让我们使用刚才创建的quote对象从引用中提取title、author和tags:
>>> title = quote.css("span.text::text").get()
>>> 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").get()
>>> author
'Albert Einstein'
>>> tags = quote.css("div.tags a.tag::text").getall()
>>> tags
['change', 'deep-thoughts', 'thinking', 'world']
就像这样提取出来,然后加入到字典中:
>>> 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中提出数据。
让我们回到我们的蜘蛛。到目前为止,它还没有提取任何特定的数据,只是将整个HTML页面保存到一个本地文件中。让我们将上面的提取逻辑集成到我们的spider中。
一个Scrap的蜘蛛通常会生成许多字典,其中包含从页面中提取的数据。为此,我们在回调中使用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').get(),
'author': quote.css('small.author::text').get(),
'tags': quote.css('div.tags a.tag::text').getall(),
}
如果你运行这个spider,他们会输出到控制台以下的输出:
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导出,并使用以下命令:
scrapy crawl quotes -o quotes.json
这将生成一个引号。包含所有被抓取项的json文件,用json序列化。
由于历史原因,Scrapy附加到给定文件而不是覆盖其内容。如果您两次运行这个命令而没有在第二次之前删除文件,那么您将得到一个损坏的JSON文件。
你也能够使用其他的样式,就像json行。
scrapy crawl quotes -o quotes.jl
JSON行格式非常有用,因为它类似于流,您可以轻松地向其添加新记录。当您运行两次时,它没有JSON的相同问题。另外,由于每条记录都是单独的一行,您可以处理大文件,而不需要将所有内容都放入内存中,因此有一些像JQ这样的工具可以帮助您在命令行中实现这一点。
在小型项目中(如本教程中的项目),这就足够了。
但是,如果您想要使用被抓取的项执行更复杂的操作,您可以编写一个项管道。
在创建项目时,在tutorial/pipeline .py中为项目管道设置了一个占位符文件。
不过,如果只想存储被擦除的项,则不需要实现任何项管道。
下面的链接
比方说,我们不只是从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').get()
'<a href="/page/2/">Next <span aria-hidden="true">→</span></a>'
这将获取anchor元素,但我们需要属性href。为此,Scrapy支持CSS扩展,让我们选择属性内容,如下所示:
使用a::attr(href)
>>> response.css('li.next a::attr(href)').get()
'/page/2/'
还有一个属性attrib可用(更多信息请参见选择元素属性):
>>> 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)
现在,在提取数据之后,parse()方法找到下一页的链接,创建一个完整的绝对URL使用了urljoin()方法(因为链接可以是相对的)然后为下一页产生一个request,将自己注册为回调函数,以处理下一页的数据提取,并保持爬行遍历所有页面。
您在这里看到的是Scrapy的以下链接机制:当您在回调方法中生成一个请求时,Scrapy将调度要发送的请求,并注册一个回调方法,以便在该请求完成时执行。
使用此功能,您可以构建复杂的爬虫程序,根据您定义的规则跟踪链接,并根据访问的页面提取不同类型的数据。
在我们的示例中,它创建了一种循环,跟踪到下一个页面的所有链接,直到它找不到一个链接为止——这对于爬行带有分页的博客、论坛和其他站点非常方便。
一个简单创建Request的方法
一个简单方法去创建一个request对象就是你能够使用response.follow:
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('span small::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:
yield response.follow(next_page, callback=self.parse)
不像scrapy。请求,响应。follow直接支持相对url—不需要调用urljoin。注意,响应。follow只返回一个请求实例;你还是得答应这个要求。
您还可以将选择器传递给response。跟随而不是字符串;这个选择器应该提取必要的属性:
for href in response.css('li.next a::attr(href)'):
yield response.follow(href, callback=self.parse)
对于元素,有一个快捷方式:response。**follow自动使用它们的href属性。**因此,代码可以进一步缩短:
for a in response.css('li.next a'):
yield response.follow(a, callback=self.parse)
**注意:**response.follow (response.css(‘li.next a’))无效,因为response.css为所有结果返回一个类列表对象,其中包含选择器,而不是单个选择器。就像上面例子中的for循环,或者response.follow(response.css(‘li))。下一个a’)[0])是好的。
更多的例子和模式
下面是另一个spider,它演示了回调和以下链接,这一次用于抓取作者信息:
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将从主页开始,它将跟随所有指向authors页面的链接,这些页面分别调用parse_author回调函数,以及前面看到的使用解析回调函数的分页链接。
这里我们将回调传递给response。跟随位置参数使代码更短;它也适用于scratch . request。
parse_author回调函数定义了一个helper函数,用于从CSS查询中提取和清理数据,并使用author数据生成Python dict。
这个spider演示的另一个有趣的事情是,即使有许多来自同一作者的引用,我们也不需要担心多次访问同一作者的页面。默认情况下,Scrapy会过滤已访问url的重复请求,从而避免由于编程错误而过多地攻击服务器。这可以通过设置DUPEFILTER_CLASS来配置。
希望现在您已经很好地理解了如何使用带有Scrapy的跟踪链接和回调机制。
作为另一个利用以下链接机制的爬行器示例,检查CrawlSpider类,它是一个通用的爬行器,它实现了一个小型规则引擎,您可以使用它在上面编写spider。
此外,一个常见的模式是使用来自多个页面的数据构建一个项目,使用一个技巧将额外的数据传递给回调。
使用spider的参数
你可以提供命令行参数给你的spider通过使用-a选项使用它们:
scrapy crawl quotes -o quotes-humor.json -a tag=humor
这些参数被传递到爬行器的_init__方法,并在缺省情况下成为爬行器的属性。
在本例中,为tag参数提供的值将通过self.tag可用。你可以用这个让你的spider抓取只有特定标签的引号,建立基于参数的URL:
获取属性:
import scrapy
class QuotesSpider(scrapy.Spider):
name = "quotes"
def start_requests(self):
url = 'http://quotes.toscrape.com/'
# 使用tag属性。
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)
如果将tag=humor参数传递给这个爬行器,您将注意到它只访问来自幽默标记的url,比如http://quotes.toscrape.com/tag/humor。