scrapy英文文档 : https://doc.scrapy.org/en/1.3/index.html
scrapy中文文档: http://scrapy-chs.readthedocs.io/zh_CN/1.0/intro/overview.html
内容都是从官方文档整理过来的,只整理一部分,要想深入了解,可以看官方文档
初窥Scrapy
Scrapy是一个为了爬取网站数据,提取结构性数据而编写的应用框架。 可以应用在包括数据挖掘,信息处理或存储历史数据等一系列的程序中。其最初是为了 网络抓取 所设计的, 也可以应用在获取API所返回的数据(例如 Amazon Associates Web Services ) 或者通用的网络爬虫。
一窥示例spider
为了让您了解Scrapy提供了什么功能,我们将提供一个Scrapy Spider的示例,并且以最简单的方式启动该spider。以下的代码将跟进StackOverflow上具有投票数最多的链接,并且爬取其中的一些数据:
import scrapy class StackOverflowSpider(scrapy.Spider): name = 'stackoverflow' start_urls = ['http://stackoverflow.com/questions?sort=votes'] def parse(self, response): for href in response.css('.question-summary h3 a::attr(href)'): full_url = response.urljoin(href.extract()) yield scrapy.Request(full_url, callback=self.parse_question) def parse_question(self, response): yield { 'title': response.css('h1 a::text').extract()[0], 'votes': response.css('.question .vote-count-post::text').extract()[0], 'body': response.css('.question .post-text').extract()[0], 'tags': response.css('.question .post-tag::text').extract(), 'link': response.url, }
将上述代码存入到某个文件中,以类似于 stackoverflow_spider.py
命名, 并且使用 runspider
命令运行:
scrapy runspider stackoverflow_spider.py -o top-stackoverflow-questions.json
当命令执行完后,您将会得到 top-stackoverflow-questions.json
文件。 该文件以JSON格式保存了StackOverflow上获得upvote最多的问题, 包含了标题、链接、upvote的数目、相关的tags以及以HTML格式保存的问题内容, 看起来类似于这样(为了更容易阅读,对内容进行重新排版):
[{ "body": "... LONG HTML HERE ...", "link": "http://stackoverflow.com/questions/11227809/why-is-processing-a-sorted-array-faster-than-an-unsorted-array", "tags": ["java", "c++", "performance", "optimization"], "title": "Why is processing a sorted array faster than an unsorted array?", "votes": "9924" }, { "body": "... LONG HTML HERE ...", "link": "http://stackoverflow.com/questions/1260748/how-do-i-remove-a-git-submodule", "tags": ["git", "git-submodules"], "title": "How do I remove a Git submodule?", "votes": "1764" }, ...]
Here’s the code for a spider that scrapes famous quotes from website http://quotes.toscrape.com, following the pagination:
import scrapy class QuotesSpider(scrapy.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').extract_first(), 'author': quote.xpath('span/small/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, callback=self.parse)将上述代码存入到某个文件中,以类似于
quotes_spider.py
命名, 并且使用
runspider
命令运行:
scrapy runspider quotes_spider.py -o quotes.json
刚刚发生了什么?
当您运行 scrapy runspider somefile.py
命令时,Scrapy尝试从该文件中查找Spider的定义,并且在爬取引擎中运行它。
Scrapy首先读取定义在 start_urls
属性中的URL(在本示例中,就是StackOverflow的top question页面的URL), 创建请求,并且将接收到的response作为参数调用默认的回调函数 parse
,来启动爬取。 在回调函数 parse
中,我们使用CSS Selector来提取链接。接着,我们产生(yield)更多的请求, 注册 parse_question
作为这些请求完成时的回调函数。
这里,您可以注意到Scrapy的一个最主要的优势: 请求(request)是 被异步调度和处理的 。 这意味着,Scrapy并不需要等待一个请求(request)完成及处理,在此同时, 也发送其他请求或者做些其他事情。 这也意味着,当有些请求失败或者处理过程中出现错误时,其他的请求也能继续处理。
在允许您可以以非常快的速度进行爬取时(以容忍错误的方式同时发送多个request), Scrapy也通过 一些设置 来允许您控制其爬取的方式。 例如,您可以为两个request之间设置下载延迟, 限制单域名(domain)或单个IP的并发请求量,甚至可以 使用自动限制插件 来自动处理这些问题。
最终, parse_question
回调函数从每个页面中爬取到问题(question)的数据并产生了一个dict, Scrapy收集并按照终端(command line)的要求将这些结果写入到了JSON文件中。
注解
这里使用了 feed exports 来创建了JSON文件, 您可以很容易的改变导出的格式(比如XML或CSV)或者存储后端(例如FTP或者 Amazon S3)。 您也可以编写 item pipeline 来将item存储到数据库中。
还有什么?您已经了解了如何通过Scrapy提取存储网页中的信息,但这仅仅只是冰山一角。Scrapy提供了很多强大的特性来使得爬取更为简单高效, 例如:
- 对HTML, XML源数据 选择及提取 的内置支持, 提供了CSS选择器(selector)以及XPath表达式进行处理, 以及一些帮助函数(helper method)来使用正则表达式来提取数据.
- 提供 交互式shell终端 , 为您测试CSS及XPath表达式,编写和调试爬虫提供了极大的方便
- 通过 feed导出 提供了多格式(JSON、CSV、XML),多存储后端(FTP、S3、本地文件系统)的内置支持
- 提供了一系列在spider之间共享的可复用的过滤器(即 Item Loaders),对智能处理爬取数据提供了内置支持。
- 针对非英语语系中不标准或者错误的编码声明, 提供了自动检测以及健壮的编码支持。
- 高扩展性。您可以通过使用 signals ,设计好的API(中间件, extensions, pipelines)来定制实现您的功能。
- 内置的中间件及扩展为下列功能提供了支持: * cookies and session 处理 * HTTP 压缩 * HTTP 认证 * HTTP 缓存 * user-agent模拟 * robots.txt * 爬取深度限制 * 其他
- 内置 Telnet终端 ,通过在Scrapy进程中钩入Python终端,使您可以查看并且调试爬虫
- 以及其他一些特性,例如可重用的,从 Sitemaps 及 XML/CSV feeds中爬取网站的爬虫、 可以 自动下载 爬取到的数据中的图片(或者其他资源)的media pipeline、 带缓存的DNS解析器,以及更多的特性。
安装指南
安装Scrapy
Scrapy入门教程
在本篇教程中,我们假定您已经安装好Scrapy。
本篇教程中将带您完成下列任务:
- 创建一个Scrapy项目
- 定义提取的Item
- 编写爬取网站的 spider 并提取 Item
- 编写 Item Pipeline 来存储提取到的Item(即数据)
Scrapy由 Python 编写。如果您刚接触并且好奇这门语言的特性以及Scrapy的详情, 对于已经熟悉其他语言并且想快速学习Python的编程老手, 我们推荐 Learn Python The Hard Way 、 Dive Into Python 3,或者 跟随 Python Tutorial。 对于想从Python开始学习的编程新手, 非程序员的Python学习资料列表 将是您的选择。
创建项目
在开始爬取之前,您必须创建一个新的Scrapy项目。 进入您打算存储代码的目录中,运行下列命令:
scrapy startproject tutorial
该命令将会创建包含下列内容的 tutorial
目录:
tutorial/ scrapy.cfg tutorial/ __init__.py items.py pipelines.py settings.py spiders/ __init__.py ...
定义Item
Item 是保存爬取到的数据的容器;其使用方法和python字典类似。虽然您也可以在Scrapy中直接使用dict,但是 Item 提供了额外保护机制来避免拼写错误导致的未定义字段错误。 They can also be used with Item Loaders, a mechanism with helpers to conveniently populate Items.
类似在ORM中做的一样,您可以通过创建一个 scrapy.Item
类, 并且定义类型为 scrapy.Field
的类属性来定义一个Item。 (如果不了解ORM, 不用担心,您会发现这个步骤非常简单)
首先根据需要从dmoz.org获取到的数据对item进行建模。 我们需要从dmoz中获取名字,url,以及网站的描述。 对此,在item中定义相应的字段。编辑 tutorial
目录中的 items.py
文件:
import scrapy class DmozItem(scrapy.Item): title = scrapy.Field() link = scrapy.Field() desc = scrapy.Field()一开始这看起来可能有点复杂,但是通过定义item, 您可以很方便的使用Scrapy的其他方法。而这些方法需要知道您的item的定义。
编写第一个爬虫(Spider)
Spider是用户编写用于从单个网站(或者一些网站)爬取数据的类。其包含了一个用于下载的初始URL,如何跟进网页中的链接以及如何分析页面中的内容, 提取生成 item 的方法。为了创建一个Spider,您必须继承 scrapy.Spider
类, 且定义一些属性:
name
: 用于区别Spider。 该名字必须是唯一的,您不可以为不同的Spider设定相同的名字。start_urls
: 包含了Spider在启动时进行爬取的url列表。 因此,第一个被获取到的页面将是其中之一。 后续的URL则从初始的URL获取到的数据中提取。start_requests()
: must return an iterable of Requests (you can return a list of requests or write a generator function) which the Spider will begin to crawl from. Subsequent requests will be generated successively from these initial requests.parse()
是spider的一个方法。 被调用时,每个初始URL完成下载后生成的Response
对象将会作为唯一的参数传递给该函数。 该方法负责解析返回的数据(response data),提取数据(生成item)以及生成需要进一步处理的URL的Request
对象。
tutorial/spiders
目录下的
dmoz_spider.py
文件中:
import scrapy class DmozSpider(scrapy.Spider): name = "dmoz" allowed_domains = ["dmoz.org"] start_urls = [ "http://www.dmoz.org/Computers/Programming/Languages/Python/Books/", "http://www.dmoz.org/Computers/Programming/Languages/Python/Resources/" ] def parse(self, response): filename = response.url.split("/")[-2] + '.html' with open(filename, 'wb') as f: f.write(response.body)
第二个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 crawl dmoz 或者 scrapy crawl quotes
在项目的跟目录下多了两个HTML文件
继续在项目的根目录下运行 :scrapy crawl dmoz
刚才发生了什么?
Scrapy 通过 start_requests
方法调度 scrapy.Request
对象,然后根据 request 关联的 回调 函数,把每一个接收到response传给回调函数处理
一个快捷的处理
Instead of implementing a start_requests()
method that generates scrapy.Request
objects from URLs, you can just define a start_urls
class attribute with a list of URLs. This list will then be used by the default implementation of start_requests()
to create the initial requests for your 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)
The parse()
method will be called to handle each of the requests for those URLs, even though we haven’t explicitly told Scrapy to do so. This happens because parse()
is Scrapy’s default callback method, which is called for requests without an explicitly assigned callback.
提取Item
Selectors选择器简介
从网页中提取数据有很多方法。Scrapy使用了一种基于 XPath 和 CSS 表达式机制: Scrapy Selectors。 关于selector和其他提取机制的信息请参考 Selector文档 。
这里给出XPath表达式的例子及对应的含义:
/html/head/title
: 选择HTML文档中<head>
标签内的<title>
元素/html/head/title/text()
: 选择上面提到的<title>
元素的文字//td
: 选择所有的<td>
元素//div[@class="mine"]
: 选择所有具有class="mine"
属性的div
元素
CSS vs XPath: 您可以仅仅使用CSS Selector来从网页中 提取数据。不过, XPath提供了更强大的功能。其不仅仅能指明数据所在的路径, 还能查看数据: 比如,您可以这么进行选择: 包含文字 ‘Next Page’ 的链接 。 正因为如此,即使您已经了解如何使用 CSS selector, 我们仍推荐您使用XPath
为了配合CSS与XPath,Scrapy除了提供了 Selector
之外,还提供了方法来避免每次从response中提取数据时生成selector的麻烦。Selector有四个基本的方法(点击相应的方法可以看到详细的API文档):
xpath()
: 传入xpath表达式,返回该表达式所对应的所有节点的selector list列表 。css()
: 传入CSS表达式,返回该表达式所对应的所有节点的selector list列表.extract()
: 序列化该节点为unicode字符串并返回list。re()
: 根据传入的正则表达式对数据进行提取,返回unicode字符串list列表。
为了介绍Selector的使用方法,接下来我们将要使用内置的 Scrapy shell 。Scrapy Shell需要您预装好 IPython (一个扩展的Python终端)。您需要进入项目的根目录,执行下列命令来启动shell:
scrapy shell "http://www.dmoz.org/Computers/Programming/Languages/Python/Books/" 或者 scrapy shell "http://quotes.toscrape.com/page/1/"当您在终端运行Scrapy时,请一定记得给url地址加上引号,否则包含参数的url(例如
&
字符)会导致Scrapy运行失败。
当shell载入后,您将得到一个包含response数据的本地 response
变量。输入 response.body
将输出response的包体, 输出 response.headers
可以看到response的包头。
#TODO.. 更为重要的是, response
拥有一个 selector
属性, 该属性是以该特定 response
初始化的类 Selector
的对象。 您可以通过使用 response.selector.xpath()
或 response.selector.css()
来对 response
进行查询。 此外,scrapy也对 response.selector.xpath()
及 response.selector.css()
提供了一些快捷方式, 例如 response.xpath()
或 response.css()
,
同时,shell根据response提前初始化了变量 sel
。该selector根据response的类型自动选择最合适的分析规则(XML vs HTML)。
In [1]: response.xpath('//title') Out[1]: [<Selector xpath='//title' data=u'<title>Open Directory - Computers: Progr'>] In [2]: response.xpath('//title').extract() Out[2]: [u'<title>Open Directory - Computers: Programming: Languages: Python: Books</title>'] In [3]: response.xpath('//title/text()') Out[3]: [<Selector xpath='//title/text()' data=u'Open Directory - Computers: Programming:'>] In [4]: response.xpath('//title/text()').extract() Out[4]: [u'Open Directory - Computers: Programming: Languages: Python: Books'] In [5]: response.xpath('//title/text()').re('(\w+):') Out[5]: [u'Computers', u'Programming', u'Languages', u'Python']
response.css('title') response.css('title::text').extract() response.css('title').extract() response.css('title::text').extract_first() response.css('title::text')[0].extract() response.css('title::text').re(r'Quotes.*') response.css('title::text').re(r'Q\w+') response.css('title::text').re(r'(\w+) to (\w+)')之前提到过,每个
.xpath()
调用返回selector组成的list,因此我们可以拼接更多的 .xpath()
来进一步获取某个节点。我们将在下边使用这样的特性:
for sel in response.xpath('//ul/li'): title = sel.xpath('a/text()').extract() link = sel.xpath('a/@href').extract() desc = sel.xpath('text()').extract() print title, link, desc关于嵌套selctor的更多详细信息,请参考 嵌套选择器(selectors) 以及 选择器(Selectors) 文档中的 使用相对XPaths 部分。
在我们的spider中加入这段代码:
import scrapy class DmozSpider(scrapy.Spider): name = "dmoz" allowed_domains = ["dmoz.org"] start_urls = [ "http://www.dmoz.org/Computers/Programming/Languages/Python/Books/", "http://www.dmoz.org/Computers/Programming/Languages/Python/Resources/" ] def parse(self, response): for sel in response.xpath('//ul/li'): title = sel.xpath('a/text()').extract() link = sel.xpath('a/@href').extract() desc = sel.xpath('text()').extract() print title, link, desc
使用item
Item
对象是自定义的python字典。 您可以使用标准的字典语法来获取到其每个字段的值。(字段即是我们之前用Field赋值的属性):
>>> item = DmozItem() >>> item['title'] = 'Example title' >>> item['title'] 'Example title'为了将爬取的数据返回,我们最终的代码将是:
import scrapy from tutorial.items import DmozItem class DmozSpider(scrapy.Spider): name = "dmoz" allowed_domains = ["dmoz.org"] start_urls = [ "http://www.dmoz.org/Computers/Programming/Languages/Python/Books/", "http://www.dmoz.org/Computers/Programming/Languages/Python/Resources/" ] def parse(self, response): for sel in response.xpath('//ul/li'): item = DmozItem() item['title'] = sel.xpath('a/text()').extract() item['link'] = sel.xpath('a/@href').extract() item['desc'] = sel.xpath('text()').extract() yield item
您可以在 dirbot 项目中找到一个具有完整功能的spider。该项目可以通过 https://github.com/scrapy/dirbot 找到。
现在对dmoz.org进行爬取将会产生 DmozItem
对象:
提取数据
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('small.author::text').extract_first(), 'tags': quote.css('div.tags a.tag::text').extract(), }If you run this spider, it will output the extracted data with the log:
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.”"}存储数据
最简单的是通过命令行:scrapycrawlquotes-oquotes.json
该命令将采用 JSON 格式对爬取的数据进行序列化,生成 quotes.json
文件。
在类似本篇教程里这样小规模的项目中,这种存储方式已经足够。 如果需要对爬取到的item做更多更为复杂的操作,您可以编写 Item Pipeline 。 类似于我们在创建项目时对Item做的,用于您编写自己的 tutorial/pipelines.py
也被创建。 不过如果您仅仅想要保存item,您不需要实现任何的pipeline。
追踪链接(Following links)
既然已经能从页面上爬取数据了,为什么不提取您感兴趣的页面的链接,追踪他们, 读取这些链接的数据呢?
下面是实现这个功能的改进版spider:
import scrapy from tutorial.items import DmozItem class DmozSpider(scrapy.Spider): name = "dmoz" allowed_domains = ["dmoz.org"] start_urls = [ "http://www.dmoz.org/Computers/Programming/Languages/Python/", ] def parse(self, response): for href in response.css("ul.directory.dir-col > li > a::attr('href')"): url = response.urljoin(response.url, href.extract()) yield scrapy.Request(url, callback=self.parse_dir_contents) def parse_dir_contents(self, response): for sel in response.xpath('//ul/li'): item = DmozItem() item['title'] = sel.xpath('a/text()').extract() item['link'] = sel.xpath('a/@href').extract() item['desc'] = sel.xpath('text()').extract() yield item现在, parse() 仅仅从页面中提取我们感兴趣的链接,使用 response.urljoin 方法构造一个绝对路径的URL(页面上的链接都是相对路径的), 产生(yield)一个请求, 该请求使用 parse_dir_contents() 方法作为回调函数, 用于最终产生我们想要的数据.。
这里展现的即是Scrpay的追踪链接的机制: 当您在回调函数中yield一个Request后, Scrpay将会调度,发送该请求,并且在该请求完成时,调用所注册的回调函数。
基于此方法,您可以根据您所定义的跟进链接的规则,创建复杂的crawler,并且, 根据所访问的页面,提取不同的数据.
一种常见的方法是,回调函数负责提取一些item,查找能跟进的页面的链接, 并且使用相同的回调函数yield一个 Request:
def parse_articles_follow_next_page(self, response): for article in response.xpath("//article"): item = ArticleItem() ... extract article data here yield item next_page = response.css("ul.navigation > li.next-page > a::attr('href')") if next_page: url = response.urljoin(next_page[0].extract()) yield scrapy.Request(url, self.parse_articles_follow_next_page)上述代码将创建一个循环,跟进所有下一页的链接,直到找不到为止 – 对于爬取博客、论坛以及其他做了分页的网站十分有效
另一种常见的需求是从多个页面构建item的数据, 这可以使用 在回调函数中传递信息的技巧.
注解
上述代码仅仅作为阐述scrapy机制的样例spider, 想了解 如何实现一个拥有小型的规则引擎(rule engine)的通用spider 来构建您的crawler, 请查看 CrawlSpider
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('small.author::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)
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'), }
使用Spider参数
当运行爬虫时,可以使用 -a
选项,为爬虫提供命令行参数:
scrapy crawl quotes -o quotes-humor.json -a tag=humor
设置的参数通过 Spider 的 __init__
方法 来变成 的 spider attributes.
这个例子中, 提供给 tag
参数的值 可以通过 self.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('small.author::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
参数给 spider,就只会访问url的 “humor” 标签, such as http://quotes.toscrape.com/tag/humor
.
更过关于处理spider参数:https://doc.scrapy.org/en/1.3/topics/spiders.html#spiderargs
例子
学习的最好方法就是参考例子,Scrapy也不例外。Scrapy提供了一个叫做 dirbot 的样例项目供您把玩学习。其包含了在教程中介绍的dmoz spider。您可以通过 https://github.com/scrapy/dirbot 找到 dirbot 。README文件对项目内容进行了详细的介绍。
Scrapy project 另一个名叫 quotesbot 的样例项目, 包含两个 spiders 关于 http://quotes.toscrape.com, 一个是使用 CSS selectors 另一个是使用 XPath expressions.