Scrapy教程

假设你已经装好了Scrapy,如果没有请查看安装指南.。

将要抓取 quotes.toscrape.com网站。

此教程指导你完成以下任务:

  1. 新建Scrapy工程
  2. 编写一个spider爬网站提取数据
  3. 用命令行导出爬取的数据
  4. 改变spider递归爬行链接
  5. 使用spider参数

创建一个项目

在抓取之前,先要构建Scrapy项目:
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

第一个爬虫

Spider是定义为爬取网站信息的类。必须继承自scrapy.Spider,定义初始请求,如何选择页面爬取url,以及如何解析页面内容提取数据。

这是我们第一个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。在项目中必须是唯一的,不能给不同的spider设置相同的名称。
  • start_requests():必须返回一个请求的迭代(可以返回一个请求的列表或者写一个生成器函数),spider从这里开始爬去。子序列请求从这些初始的请求自动生成。
  • parse():在每个请求完成时回掉方法。response参数是TextResponse类的实例,包含页面内容和一些选择器等函数操作。

parse()函数通常解析html,把抓到的数据提取为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的响应数据。

内部机制是什么

Scrapy调用蜘蛛的start_requests方法,一旦接收到一个响应,立马初始化Response对象然后调用请求的回掉函数(在此例中,时parse()函数)把response对象作为参数。

start_requests函数简写

作为start_requests函数的替代,可以定义start_urls的种子列表。默认的start_requests()函数实现中会使用start_urls创建初始请求。

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()。这是因为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'

也可以使用python的列表写法:

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

但是,使用extract()extract_first()方法可以在没有找到任何匹配元素时返回None,避免IndexError

除了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,authortags使用我们刚创建的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
>>>

使用spider导出数据

让我们回到spider。直到现在,仍然没有导出任何数据,只是把HTML页面保存到本地文件中。我们把导出逻辑集成到spider中。

一个Scrapy蜘蛛通常包含多个页面抓取数据的字典。这样,我们可以使用在回调函数中使用yieldPython关键字,如下所示:

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类,它是一个通用的蜘蛛实现了一个小的规则引擎,你可以在这之上编写自己的爬虫。

https://doc.scrapy.org/en/latest/intro/tutorial.html

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 204,445评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,889评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 151,047评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,760评论 1 276
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,745评论 5 367
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,638评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,011评论 3 398
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,669评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,923评论 1 299
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,655评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,740评论 1 330
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,406评论 4 320
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,995评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,961评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,197评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,023评论 2 350
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,483评论 2 342

推荐阅读更多精彩内容

  • Scrapy 教程 本文翻译自scrapy的最新官方教程,觉得有帮助的朋友可以小小打赏一下,谢谢。 首先,用户需要...
    fromradio阅读 1,511评论 2 14
  • 引用Scrapy 官方教程 一,概述 Scrapy 是一个为了爬取网站数据,提取结构性数据而编写的应用框架,可以使...
    一曲广陵散阅读 864评论 0 0
  • Python版本管理:pyenv和pyenv-virtualenvScrapy爬虫入门教程一 安装和基本使用Scr...
    inke阅读 61,917评论 12 130
  • 序言第1章 Scrapy介绍第2章 理解HTML和XPath第3章 爬虫基础第4章 从Scrapy到移动应用第5章...
    SeanCheney阅读 15,031评论 13 61
  • (一)客去 近岸千层岭,穿林一片风。 苍苍隔明月,飒飒绕孤蓬。 客去秋水满,春来诗酒空。 争知独归处,遥念五云中。...
    高天洁雨18阅读 151评论 0 3