今日から始めるPython Scrapyを利用したスクレイピング

## なんでScrapyをやろうと思ったか
世は2019年GWまっただ中。今後の仕事の効率化のため、この時間を活用してスクレーピングやらない手はない。何を効率化するかって？そんなことは、できるようになってから考えるとしよう。

## Scrapyとは
> An open source and collaborative framework for extracting the data you need from websites.In a fast, simple, yet extensible way.

[Scrapy本家サイト](https://scrapy.org/)

Webサイトから簡単に欲しい情報を抽出させてやるための仕組み。ということです。ざっくりでも何のためにあるか確認すると理解の助けになりますよね。

## 前提
* OS：macOS High Sierra 10.13.6
* Python：3.6.1
* Scrapy：1.6.0

## 準備
### 仮想環境作成
```
$ cd [project dir]
$ python -m venv [venv name]
```
ここでは、[project dir]を`scrapy`、[venv name]を`venv`にします。
以降は`(venv)`は仮想環境に切り替えている前提とします。（[venv name]を別名にしている場合置き換えて下さい。）
ちなみに、仮想環境を作成するコマンド`venv`はPython3.3以上の標準とのことなので、基本的にはこのコマンドを利用するようにする。

仮想環境に切り替える。

```
$ source venv/bin/activate
```
プロンプトの先頭が`(venv)`になっていることを確認する。

scrapyをインストールします。

```
(venv)$ pip install scrapy
```
もしかしたら依存関係でエラーになるかもしれないが、落ち着いて必要なライブラリをインストールしましょう。

インストールしたらScrapyのバージョンを確認します。

```
(venv)$ scrapy version
Scrapy 1.6.0
```

試しのScrapyに記載されているサンプルコード`myspider.py`を作成します。

```python:myspider.py
import scrapy

class BlogSpider(scrapy.Spider):
    name = 'blogspider'
    start_urls = ['https://blog.scrapinghub.com']

    def parse(self, response):
        """
        blogタイトルを抽出し次のページに移動していく。
        """
        for title in response.css('.post-header>h2'):
            yield {'title': title.css('a::text').get()}

        for next_page in response.css('a.next-posts-link'):
            yield response.follow(next_page, self.parse)
```

* `parse():`リクエストに対してダウンロードされたレスポンスを受け取るための呼び出されるメソッド。`response`パラメータはページコンテンツを保持する`TextResponse`のインスタンスです。

[Scrapyのドキュメント](https://docs.scrapy.org/en/latest/index.html)
[日本語訳Scrapyドキュメント](https://scrapy-ja.readthedocs.io/ja/latest/index.html)


作成したmyspider.pyを実行します。

```
(venv)$ scrapy runspider myspider.py -0 sample.jl
```
* `jl`:JSON Lines形式のファイル。一度に1レコードずつ処理できる構造化データを格納するのに便利なフォーマットです。（jsonファイルだと、配列かつカンマ区切り列形式でファイル出力されます）

以下出力内容の`sample.jl`ファイルが作成されます。

```json:sample.jl
{"title": "From The Creators Of Scrapy: Artificial Intelligence Data Extraction API"}
{"title": "Scrapinghub\u2019s New AI Powered Developer Data Extraction API for E-Commerce & Article Extraction"}
 :
```

## Scrapyの開始

Yahoo!ニュースをスクレイピングしてみます。
ScrapyではクロールやスクレイピングにSpiderを利用します。


### Spiderのスクレイピングライフサイクル
> 1. 最初のURLをクロールする最初のリクエストを生成し, それらのリクエストからダウンロードされたレスポンスで呼び出されるコールバック関数を指定します.
     * 実行する最初のリクエストは, `start_requests()` メソッドを呼び出すことによって取得されます. このメソッドは, デフォルトで `start_urls` で指定されたURLと `Request` のコールバック関数として呼ばれる `parse` メソッドで, `Request` を生成します. 
> 2. コールバック関数では, レスポンス（Webページ）を解析し, 抽出されたデータ, `Item` オブジェクト, `Request` オブジェクト, またはこれらのオブジェクトの反復可能なものを返します これらのリクエストには, コールバックが含まれ, Scrapyによってダウンロードされ, その後指定されたコールバックによってリクエストが処理されます.
> 3. コールバック関数では, セレクタ を使用してページの内容を解析します（ただし, BeautifulSoup, lxmlなどの任意のメカニズムを使用することもできます）.
> 4. 最後に, スパイダーから返されたアイテムは, 通常, データベース（一部の アイテムパイプライン 内）に永続化されるか, または フィードのエクスポート を使用してファイルに書き込まれます.

引用：https://scrapy-ja.readthedocs.io/ja/latest/topics/spiders.html


### Scrapyプロジェクトを作成
以下コマンドを実行してプロジェクトを作成します。
ここでは[project name]を`myproject`としてます。

```
(venv)$ scrapy startproject [project name]
...
(venv)$ cd myproject
(venv)$ tree .
.
├── myproject
│   ├── __init__.py
│   ├── __pycache__
│   ├── items.py
│   ├── middlewares.py
│   ├── pipelines.py
│   ├── settings.py
│   └── spiders
│       ├── __init__.py
│       └── __pycache__
└── scrapy.cfg
```

まずは、最低限の環境設定を行います。
settings.pyの`DOWNLOAD_DELAY`のコメントを削除しWebサイトへのリクエストの間隔を秒単位に設定します。（接続先に負荷を与えないためにも必ず設定すること）

```python:settings.py

#DOWNLOAD_DELAY = 3
 ↓
DOWNLOAD_DELAY = 3
```

また、スクレイピングの結果をファイル出力する際の文字コードを追記し指定します。

```python:settings.py
FEED_EXPORT_ENCODING = 'utf-8'
```

### Itemを作成
スクレイピングは構造化されていないソース（通常はWebサイト）から構造化データを抽出することである。Scrapyでは`Item`クラスを利用することでPythonの扱いやすい構造に収める事ができます。`Item`クラスは`scrapy.Item`を継承します。

```python:items.py
import scrapy

class NewsTopics(scrapy.Item):
    title = scrapy.Field()
    summary = scrapy.Field()
```

### Spiderを作成
```
(venv)$ scrapy genspider news news.yahoo.co.jp
Created spider 'news' using template 'basic' in module:
  myproject.spiders.news
```

spidersフォルダに`news.py`ファイルが作成されます。

```python:news.py
# -*- coding: utf-8 -*-
import scrapy

class NewsSpider(scrapy.Spider):
    name = 'news'
    allowed_domains = ['news.yahoo.co.jp']
    start_urls = ['http://news.yahoo.co.jp/']

    def parse(self, response):
        for url in response.css('ul.topicsList_main a::attr("href")').re(r'/pickup/\d+$'):
            yield scrapy.Request(response.urljoin(url), self.parse_topics)

    def parse_topics(self, response):
        item = Headline()
        item['title'] = response.css('title ::text').extract_first()
        item['summary'] = response.css('.tpcNews_summary').xpath(
            'string()').extract_first()
        yield item
```

### Spiderの実行
```
(venv)$ scrapy crawl news -o topics.jl
```

`topics.jl`にスクレイピングした結果が出力されていることを確認します。

ここまでで、Scrapyを利用したクローリングとスクレイピングのやり方になります。
書簡としては、WebアプリケーションでいうとこのDjangoのように、スクレイピングのフレームワークだけあり小さく始めるにもとっつきやすいと思います。他にもパイプラインやシェルなどの仕組みがあるみたい。

---
今後調べたいこと。

* 機械学習用のデータを収集方法について
* クローリングとスクレイピングは機構を分けたほうがよいので、Scrapyを利用した分離方法について
* SPAやログインが必要なサイトのクローリング方法について

終了