Python 爬虫框架Scrapy入门 官方手册翻译版

将以 'quotes.toscrape.com' 网站作为爬取的对象。

在这个教程中将围绕如下内容展开:

  1. 创建一个新的 Scrapy 项目
  2. 编写一个 spider 去爬网站,提取数据
  3. 使用命令行导出抓取数据
  4. 修改爬虫递归下一个链接
  5. 使用 spider 属性

创建项目

进入目标项目文件夹,执行以下代码:

scrapy startproject tutorial

这会创建一个 tutorial 路径,包含以下内容

turorial/
    scrapy.cfg        # 部署配置的文件
    
    tutorial/         # 项目的 Python 模块,import 导入的代码在模块中
        __init__.py
        
        items.py      # 项目的 items(条目)自定义文件
        
        middlewares.py # 项目的中间件文件
        
        pipelines.py   # 项目的管道文件
        
        settings.py    # 配置文件
        
        spiders/       # 后期放 spider 的文件
            __init__.py

第一个项目

自定义的 Spider 类用于从网站(一组网站)中抓取数据,它们必须为 scrapy.Spider 子类,初始化的请求为必选项,可选项包括对下一页的相关处理、从解析下载的页面内容提取数据的细节。

我们的第一个 Spider 保存在 quotes_spider.py 文件中,在 tutorial/spiders 路径下:

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:唯一识别名,在当前项目中必须唯一。
  • start_requests():必须返回请求的迭代(可以是请求列表或生成器函数),Spider 从这里开始爬。
  • parse(): 每个请求发起后调用,用于处理响应。response 参数是 TextResponse 的实例,它包含了页面内容,内容随后会被处理。parse() 方法实现许多功能,包括解析响应,提取爬取的数据并解析为字典,寻找下一个爬取的 URL 并对此链接发起新的请求。

如何执行我们的 Spider

为了使 Spider 生效,需要进入项目顶层路径,并执行如下命令:

scrapy crawl quotes

这个命令以 quotes 名称作为参数执行,并且会对 qotes.toscrape.com 域名的网站发起请求。

执行命令后,可以发现在当前路径下,有两个新文件: quotes-1.html quotes-2.html

刚才发生了什么

Scrapy 会规划 Spider 中 start_requests 返回的 scrapy.Request。一旦接受到每个请求的响应,就会实例化 Response 对象,将响应实例作为回调方法的参数(在这个例子中,是 parse 方法),回调与请求 Request 关联。

start_requests 方法的简写

我们可以仅仅定义 start_urls 类属性,它是 URL 的列表组合,用于替代 start_requests() 中通过生成 scrapy.Reuqest 对象的做法。定义了 start_urls 后,会使用默认的 start_requests() 来创建 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)

变化在于 start_urls 替代了 start_requests() 方法,只提供 URL 列表,其余的初始化请求操作交给父类中默认的 start_requests() 方法。

parse() 方法会被在每个 URL 被请求时调用,即使我们没有明确的告诉 Scrapy 这么去做,这是因为 parse() 是 Scrapy 默认自动调用的方法。

提取数据

使用 Scrapy 提取数据最好的调试方式:Scrapy shell,执行:

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

使用 shell,你可以尝试从响应中选择元素,以 CSS 选择器为例:

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

返回的结果 SelectorList,类列表对象,它扩展了list功能,可以理解为 Selector 的列表。这个列表对象被 XML/HTML 元素包裹,允许你执行进一步的查询和更细微力度的选择。

提取标签文本

比如,从刚才的 title 对象中提取文本,可以这么做:

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

这里有两点要说明:

  1. 在 CSS 查询中添加了 '::text',这意味着只提取 <title> 中的文本。如果定义 '::text',会返回整个 title 元素内容(包括标签):
>>> response.css('title').getall()
['<title>Quotes to Scrape</title>']
  1. 调用 '.getall()',它返回一个列表:这意味着选择器可能返回一个或多个结果;如果你只需要第一个结果,可以使用 '.get()',也可以通过 python 代码先选择第一个结果:
response.css("title::text")[0].get()

但是,如果直接对 SelectorList 实例使用 get() 方法可以避免 'IndexError' 错误,它会处理成 None 返回,所以推荐直接使用 '.get()'

正则匹配

除了使用 'getall()' 和 'get()' 方法,可以使用 're()' 方法来提取数据

浏览器查看响应结果

为了更好的使用合适的 CSS 选择器,可以使用 view(response) 来查看响应。它会在浏览器中显示响应内容,你可以使用浏览器开发者工具来检测 HTML 并选择合适的选择器。

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 表达式非常强大,是 Scrapy 选择器的基石。实际上,CSS选择器最终会被转换成 XPath。

尽管 XPath 表达式没有 CSS 活跃,但是它更有用,因为除了导航定位文件结构,它还能获取文本内容。
使用 XPath,你可以选择这样的内容:选择一个包含 "Next Page" 的链接。
这使得 XPath 足以胜任爬虫的任务,我们鼓励你去学习 XPath ,即使你已经了解了如何构建 CSS 选择器。

更多 XPath 使用方法可以查看 http://zvon.org/comp/r/tut-XPath_1.htmlhttp://plasmasturm.org/log/xpath101/

提取 quotes 和 authors

现在你已经知道了一点选择和提取数据的方法,继续完善代码。

>>> 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 提取数据

上面都是在 shell 中调试,在返回 spider 中。目前 Spider 没有提取任何数据,只是保存了整个 HTML 文件到本地。
接下来整合提取数据的逻辑到 spider 中。

一个 Scrapy spider 可以生成许多包含已提取数据的字典。
为做到这个目的,我们使用 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').get(),
                'author': quote.css('small.author::text').get(),
                'tags': quote.css('div.tags a.tag::text').getall(),
            }

对比之前的 spider 代码,在这里主要发生变化的是 yield 子句。是一个调试的三个选择器,我们获取了每个 quote 的指定内容。

存储爬取的数据

最简单的存储方式是使用 'Feed exports',命令如下:

scrapy crawl -o quotes.json

这会生成 quotes.json 文件,包含爬取的项目,以 JSON 序列化。

由于历史原因,Scrapy 使用附加的方式而非替代原文件内容生成文件。也就是说,如果你执行两次存储名命令,会再添加内容到文件中。由于 JSON 格式问题,存储两次,将破坏 JSON 的格式。

其他格式 JSON Lines

scrapy crawl -o quotes.jl

'JSON Lines' 是一种流类型结构,可以轻易的添加新内容到文件中,而不用担心执行了两次破坏文件格式。
因为每个记录都以单独的一行记录。
JL 当可以搭配工具,如 'JQ' 来辅助执行这样的命令。

项目管道 Item Pipeline

在小项目中,上面提到才这些内容都足够了。然而,如果你想执行更加复杂的爬取,你可以写一个 'tiem pipleline'

「下一页」链接

除了仅仅抓取第一页和第二页的内容,你想要网页中所有的内容也可以。

在上面已经了解到如何提取页面上的链接,接下来看看如何获取并进入下一页链接

提取下一页标签

首先观察一下页面中下一页内容

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

需要提取的内容是 href 的属性值,它告诉我们下一页的相对地址

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

>>> 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)

这里的关键在于:一、判断是否有下一页,二、如果有,如何拼接 URL 递归请求访问和解析新页面内容。

现在,已经提取了数据,parse() 中拼接绝对路径的 URL 使用了 urljoin() 方法,并且生成一个新的请求到下一页;它将自身注册作为回调,以处理下一页的数据,并且持续爬取新页面。

总结一下 Scrapy 的下一页获取机制:如果你在回调函数中生成了一个请求,Scrapy 会适配已发送的请求,并在请求完成注册一个要执行的回调方法。

基于这个机制,你可以构建复杂的爬虫自定义点击超链接,也可以根据访问的页面不同自定义提取规则。

在这个例子中,我们创建了一个循环,一直进入下一页界面直到最后一页。这个功能非常适合爬取博客、论坛或者拥有分页的网站。

一个简单的方式创建请求

我们可以使用 response.follow 来请求下一个页面

...
        next_page = response.css('li.next a::attr(href)').get()
        if next_page is not None:
            yield response.follow(next_page, callback=self.parse)

将上面例子的最后两行改成 response.follow() 请求网站,不同于 scrapy.Request,response.follow 提供相对路径请求,这意味着你不需要再添加 urljoin 拼接 URL。
记住,response.follow 返回的还是请求实例,所以需要 yield 生成这个请求。

由于 <a> 标签是一个链接,所以 response.follow 自动使用 href 属性,所以代码还可以简写为:

        for a in response.css('li.next a'):
            yield response.follow(a, 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)'):
            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 开始于主页面,为所有的作家页面调用 parse_author 回调,分页链接的 parse 回调。

parse_author 回调定义了一个方法从 CSS 查询中提取和清理数据,再产生作者资料的字典。

在这个 spider 中展示的另一个有趣的事情:即使有许多 quotes 对应一个 author,我们也不用担心会多次访问同一个 author 界面。
默认情况下,Scrapy 过滤重复URL请求,避免因访问服务器过多而产生问题。
可以在设置中配置参数 DUPFILTER_CLASS

使用 spider 参数

提供命令行参数给 spider,添加 '-a' 选项即可:

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

这些参数会传递给 Spider 类的 '__init__' 方法,并作为 spider 实例的默认属性。

在这个例子中,提供给 tag 属性的值可以使用 self.tag 获取。
添加 tag 选项后,spider 抓取的内容被限制在特定的标签中:

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').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)

在这里,添加了 humor 标签/tag 的 spider 访问的 URL 变成了 'http://quotes.toscrape.com/tag/humor'

[1] http://docs.scrapy.org/en/latest/

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