Gunosyデータ分析ブログ

Gunosyで働くデータエンジニアが知見を共有するブログです。

Scrapy + Scrapy Cloudで快適Pythonクロール+スクレイピングライフを送る

はじめに

こんにちは、データ分析部の久保 (@beatinaniwa) です。

今日は義務教育で教えても良いんじゃないかとよく思うWebクロールとスクレイピングの話です。

私自身、日頃は社内に蓄積されるニュース記事データや行動ログをSQLやPythonを使って取得・分析することが多いですが、Web上にある外部データを使って分析に役立てたいというシーンはままあります。

単独のページをガリガリスクレイピングしたいときなどは、下の1年半ぐらい前の会社アドベントカレンダーに書いたような方法でやっていけば良いんですが、いくつもの階層にわかれたニュースポータルサイトやグルメポータルサイトを効率よくクロール+スクレイピングするためには、それに適したツールを使うのがすごく便利です。

qiita.com

そこでPython用スクレイピングフレームワークScrapyの登場です。 f:id:beatinaniwa:20160817174214p:plain

Scrapy | A Fast and Powerful Scraping and Web Crawling Framework

Scrapyとは

Scrapyを使えばweb spiderと呼ばれる、サイトをクロール+スクレイピングする処理を簡潔に記述することができ、さらに後述するようにそれをScrapy Cloudにデプロイすることによって、自分がほしいサイトの情報を自動的に(それも最低限であれば無料で!)、取得することができます。

今年の5月にリリースされた1.1.0(最新バージョンはこの記事を書いている時点で1.1.1)からはついにPython3系がサポートされ(まだベータサポートということでWindowsではPython2系のみの対応ですが...)、心を落ち着かせて使うことができます。

Scrapyの使い方

インストール

まずインストールはいつもどおりpipで

$ pip install scrapy

前準備

今回は実際のクロール+スクレイピングを行うニュースポータルサイトとしてWeb版のグノシーを題材にしたいと思います。

グノシー 無料で読めるニュースまとめ

グノシーのトップページはこのようになっていて、大きくトップ、エンタメ、スポーツ、... グルメのように分かれています。 f:id:beatinaniwa:20160807181332p:plain

今回はエンタメからグルメまでの各カテゴリの記事をScrapyを使って集めてみることにします。各記事のタイトル、URL、サブカテゴリ名を取得するターゲットとします。

f:id:beatinaniwa:20160817111219p:plain

プロジェクト作成

まずはScrapyのプロジェクトを作ります。

$ scrapy startproject gunosynews
$ cd gunosynews

そうするとこんなファイルがディレクトリ以下に作られます。

.
├── gunosynews
│   ├── __init__.py
│   ├── __pycache__
│   ├── items.py
│   ├── pipelines.py
│   ├── settings.py
│   └── spiders
│       ├── __init__.py
│       └── __pycache__
└── scrapy.cfg

gunosynews/items.py を編集して

# -*- coding: utf-8 -*-

# Define here the models for your scraped items
#
# See documentation in:
# http://doc.scrapy.org/en/latest/topics/items.html

import scrapy


class GunosynewsItem(scrapy.Item):
    title = scrapy.Field()
    url = scrapy.Field()
    subcategory = scrapy.Field()

次に要のweb spider部分を作ります、scrapyで用意されているコマンドを実行することで雛形を作成してれます。

$ scrapy genspider gunosy gunosy.com

これはgunosy.comドメインをクロールするgunosyという名前のspider(クローラ)を作成する、という意味になります。 早速できた雛形を編集します。

gunosynews/spiders/gunosy.py

# -*- coding: utf-8 -*-
import scrapy

from gunosynews.items import GunosynewsItem


class GunosynewsSpider(scrapy.Spider):
    name = "gunosy"
    allowed_domains = ["gunosy.com"]

    start_urls = (
        'https://gunosy.com/categories/1',  # エンタメ
        'https://gunosy.com/categories/2',  # スポーツ
        'https://gunosy.com/categories/3',  # おもしろ
        'https://gunosy.com/categories/4',  # 国内
        'https://gunosy.com/categories/5',  # 海外
        'https://gunosy.com/categories/6',  # コラム
        'https://gunosy.com/categories/7',  # IT・科学
        'https://gunosy.com/categories/8',  # グルメ
    )

    def parse(self, response):
        for sel in response.css("div.list_content"):
            article = GunosynewsItem()
            article['title'] = sel.css("div.list_title > a::text").extract_first()
            article['url'] = sel.css("div.list_title > a::attr('href')").extract_first()
            article['subcategory'] = sel.css("div.list_text > a::text").extract_first()
            yield article

        next_page = response.css("div.page-link-option > a::attr('href')")
        if next_page:
            url = response.urljoin(next_page[0].extract())
            yield scrapy.Request(url, callback=self.parse)

順番に説明します。

  • start_urls にはクロールの起点となるURLを記述していきます。今回はエンタメ~グルメ各カテゴリの記事を集めたかったので、各カテゴリのトップをクロールの起点としました。
  • parse() メソッドが今回のクローラの中心となるメソッドです。先ほど指定したstart_urlがダウンロードされるとそのレスポンスが引数となってこのメソッドに渡されることになります。
  • parse() メソッドの中では与えられたレスポンスから取り出したい要素の箇所をCSSセレクタで指定しています。記事数の分だけfor文で処理を繰り返しています。 next_page~ 以降の箇所に注目してください。ここでは記事一覧ページの下部にある「次のページをみる」リンクの要素をCSSセレクタによって選択し、もし「次のページをみる」リンクが存在すればそのURLを scrapy.Request オブジェクトに渡して、callback引数に先ほどの parse() メソッドを指定することによって再帰的に次のページをたどることが可能になっています。こうすることでScrapyは自動的に次のページがなくなるまで、各カテゴリの記事リストをたどってくれます。

ここまでで一通りクロール+スクレイピングする処理ができました。いよいよ実行してみるところですが、その前に設定ファイルを見てみます。設定の中身は gunosynews/settings.py に記述されています。DOWNLOAD_DELAY = 3 と書かれているところに注目してください。ここではクローラが各ページをダウンロードする間隔(秒)を指定できます。デフォルトではコメントアウトされていますがクロール先に迷惑をかけないためにもコメントアウトを外しておきましょう。 その他細かい設定などについては本家ドキュメントを参照してください。

Settings — Scrapy 1.1.1 documentation

実行&結果

おつかれさまでした、実行してみます。

$ scrapy crawl gunosy

するとクロール+スクレイピングが始まりたとえば下のような実行結果がつらつらと出てくると思います。

2016-08-17 11:46:55 [scrapy] DEBUG: Scraped from <200 https://gunosy.com/categories/8>
{'subcategory': 'お店',
 'title': '「刀削麺って何?」そんなアナタに『冷やし野菜刀削麺』はいかがでしょう? @『唐家』',
 'url': 'https://gunosy.com/articles/R0MAi'}
2016-08-17 11:46:55 [scrapy] DEBUG: Scraped from <200 https://gunosy.com/categories/8>
{'subcategory': 'グルメ総合',
 'title': 'ベルギーで1年に2回だけの味!お祭りと共にやってくる絶品スイーツ「スマウトボゥ」',
 'url': 'https://gunosy.com/articles/RWeO2'}
2016-08-17 11:46:55 [scrapy] DEBUG: Scraped from <200 https://gunosy.com/categories/8>
{'subcategory': 'お店',
 'title': '「空間と時間」日本と違うアプローチ/記者ルポ',
 'url': 'https://gunosy.com/articles/Ri3TP'}

目的の要素が正しく取得できていることがわかります。

Scrapy Cloudで簡単クローラ管理

導入

さて目的の情報を取得するところまでできましたが、重要なのはこれらのデータをどうやって保存しておくのか、またどうやってクローラの実行を管理するかということです。そこでScrapy Cloudの登場です。

scrapinghub.com

Scrapy Cloudでは先ほど作成したクローラをデプロイし、ブラウザの画面から簡単に管理することができます。 クローラを一つ作って動かすのなら無料で使うことができ、クレジットカードの登録なども必要ありません(データの保持期限など無料ならではの制限は当然ながらあります)。 サインアップしてOrganizationを適当に設定すると下のような画面になると思います。 f:id:beatinaniwa:20160817120615p:plain

上のメニューバーから Scrapy Cloud > Create Project を選択し、プロジェクトを作成します。 f:id:beatinaniwa:20160817121132p:plain

無事作成されました。 f:id:beatinaniwa:20160817121254p:plain

すると左のサイドバーに Code & Deploys という項目があるのでそれをクリックして、APIキーとプロジェクトIDをメモっておきます。

デプロイ

いよいよデプロイです。デプロイツール用のライブラリが用意されているのでそれを利用します。

$ pip install shub

Scrapy Cloud — Scrapinghub documentation

ログインします。

$ shub login

ログイン時にAPIキーを聞かれるのでさきほどメモったAPIキーを入力すると ~/.scrapiinghub.yml にAPIキーが保存され、次回以降ログインするときには自動的にこれが使われるようになります。

実際のデプロイです。ここでは対象プロジェクトIDが聞かれるので、同様にさきほどメモったプロジェクトIDを入力します。

$ shub deploy

デプロイが完了するといろいろなディレクトリやファイルが追加されているのがわかります。

.
├── build
│   ├── bdist.macosx-10.10-x86_64
│   └── lib
│       └── gunosynews
│           ├── __init__.py
│           ├── items.py
│           ├── pipelines.py
│           ├── settings.py
│           └── spiders
│               ├── __init__.py
│               └── gunosy.py
├── gunosynews
│   ├── __init__.py
│   ├── __pycache__
│   │   ├── __init__.cpython-35.pyc
│   │   ├── items.cpython-35.pyc
│   │   └── settings.cpython-35.pyc
│   ├── items.py
│   ├── pipelines.py
│   ├── settings.py
│   └── spiders
│       ├── __init__.py
│       ├── __pycache__
│       │   ├── __init__.cpython-35.pyc
│       │   └── gunosy.cpython-35.pyc
│       └── gunosy.py
├── project.egg-info
│   ├── PKG-INFO
│   ├── SOURCES.txt
│   ├── dependency_links.txt
│   ├── entry_points.txt
│   └── top_level.txt
├── scrapinghub.yml
├── scrapy.cfg
└── setup.py

実行&結果

実際にScrapy Cloudの左サイドバーから Spiders > Dashboard からデプロイしたクローラ一覧を見ることができ、クローラ名をクリックすることでクローラ詳細画面に行くことができます。 f:id:beatinaniwa:20160817124539p:plain f:id:beatinaniwa:20160817124858p:plain

右上のRun Spiderボタンを押して、もう一度開いたウィンドウでRun Spiderボタンを押すことで実際のクローラの実行が始まります、押してみましょう! f:id:beatinaniwa:20160817125029p:plain

するとRunning Jobsのところにみるみるうちにアイテムが溜まっていきます。 f:id:beatinaniwa:20160817125628p:plain

アイテム数のところをクリックすることで、実際にスクレイピングした項目をチェックすることができ、さらにはCSVなどでダウンロードすることができます。 f:id:beatinaniwa:20160817125830p:plain f:id:beatinaniwa:20160817125850p:plain 超便利!!

最後に

いかがでしたでしょうか、クローラ+スクレイピングは自分で一から書くとなると、コードのメンテナンスや実際のクローラの監視、データの保存などいろいろな箇所がネックとなってきますが、Scrapy + Scrapy Cloudを使うとかなり負担が少なくなることがわかると思います。 Scrapyには今回紹介しきれなかった便利機能がたくさんあり、ドキュメントもしっかりしているので詳しく知りたい方はぜひ読んでみてください。

Scrapy 1.1 documentation — Scrapy 1.1.1 documentation

それでは良いスクレイピングライフを。