Scrapyﾁｮｯﾄﾃﾞｷﾙ

* これはとある会社さんで、「Scrapyを使った所感を発表してよ」と言われて、断る理由も見当たらないので発表した時の資料の原案です。
* 2019/3時点の内容です。
* 結局最終版はGoogleスライドに移行。（公開予定なし。案件について書いてあるし）
* だけどScrapyに関する内容はだいたい一緒。


----

## アジェンダ
- 私のスペック
- Scrapyを使った案件
- Scrapyとは
- Scrapyの重要な要素
- これだけ覚えればだいたい問題ない！
- スクレイピングを行う上で考慮したい点

----

## 私のスペック
- 情報系の大学でプログラムの基本的ことを学ぶ
- 2005年から社会人
  - Java、VBA、Seleniumとか
  - プライベートではPHPによるスクレイピングを無理やりやったことあり
- 初めてのPythonは2015年に半年いたDjangoのプロジェクト
  - 設計中心の担当で、Pythonのソースにはバグ修正の時に手伝ったくらい
- その後、組み込み系Linux？の上でpythonスクリプトを動かしたり
  - 単純なバッチ的なもの
- さらにその後半年以上前から2019/3時点でDjangoのプロジェクトに参画

----

## 私のスペック

- 総じていうと、Pythonについてはペーペー
- 見よう見まねのPythonコーダー

----

## Scrapyを使った案件

**某社さんのご依頼**

「とあるサイトからできるだけ多くの情報を持ってきてほしい」

※対象サイトにはスクレイピングさせてくださいと連絡しているとのこと


----

## Scrapyとは

- 「Python スクレイピング」で検索すると最初に現れるのはBeautifulSoup
  - BeautifulSoupは画面から項目を取得するところのみ
- Scrapyは、スクレイピングの「フレームワーク」
  - 「フレームワーク」であること！はよく言われる
  - 画面から項目を取得するところ、アウトプットの作り方をフレームワーク化している
 
アウトプットの作り方もフレームワーク化が他のスクレイピングのものと異なるところ

----

## 重要な要素
![アーキテクチャ](https://docs.scrapy.org/en/latest/_images/scrapy_architecture_02.png)

- Spider
    - ある意味これが全て
    - ItemやPipelineも使わずにこれだけで完結することも可能
    - スクレイピングする対象の定義も、取得したHTMLから項目の取得も実装
- Item
    - データ項目を定義したクラス
    - ItemLoaderなど様々機能を使わない限りは、ディクショナリと大差はないように思う
- Pipeline
    - Spiderでreturn/yieldされたものは、ここに渡される
    - 取得したデータはここで煮るなり焼くなり

----

## インストール

* 2019/3時点の最新バージョンは1.6.0
* Python2.7、3.4以上で動くとのこと

```
pip install Scrapy
scrapy startproject test_scraping
cd test_scraping
tree

test_scraping
    │  items.py
    │  middlewares.py
    │  pipelines.py
    │  settings.py
    │  __init__.py
    │
    ├─spiders
    │  │  __init__.py
    │  │
    │  └─__pycache__
    └─__pycache__
```

----

##Spiderの作成
* スクレイピングする内容を記述するところ

```
scrapy genspider [クラス名] [対象のドメイン]
vim test_scraping/spiders/[クラス名].py
```

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


class [クラス名]Spider(scrapy.Spider):
    name = '[クラス名]'
    allowed_domains = ['[対象のドメイン]']
    start_urls = ['http://[対象のドメイン]/']

    def parse(self, response):
        pass 
```

----

## Itemの作成
* 既にファイルは存在している
* コメントの例のように、 `name = scrapy.Field()` で項目名のフィールドを必要なだけ作る

```
vim test_scraping/items.py
```

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

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

import scrapy


class TestScrapingItem(scrapy.Item):
    # define the fields for your item here like:
    # name = scrapy.Field()
    pass
```

----

##Pipeline
* 既にファイルは存在している
* `process_item`の中で色々やるのが基本
    * `item`がSpiderから渡されてくるので、抽出したものを色々どうにかできる
* 実は、スクレイピングの開始時や終了時のイベントで処理を記述する（例えばDBのセッション開始とか）こともここでできる

```
vim test_scraping/pipelines.py
```

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

# Define your item pipelines here
#
# Don't forget to add your pipeline to the ITEM_PIPELINES setting
# See: https://doc.scrapy.org/en/latest/topics/item-pipeline.html


class TestScrapingPipeline(object):
    def process_item(self, item, spider):
        return item
```

----

## これだけ覚えればだいたい問題ない！
- Request and Response
- Spider Middleware
- Feed Export
- yieldを使う
- HTML構造
- `scrapy shell [URL]` によるデバッグ

----

## Request and Response
- 全てはResponseオブジェクトから
  - response.url、response.url、response.xpathなどとにかくResponseがなんでも持ってる
- 新たなリクエストの生成はResponse.followと、Requestオブジェクト
  - Requestオブジェクトを生成する際に、URLと、実行するメソッドをコールバックで指定する
  - Response.followは、Requestオブジェクトの生成を省略したもの（相対パスが使える）
  - リクエスト間の値の引継ぎは、metaプロパティに持たせる
- FormRequestを使えばログインのようなセッションを使ったリクエストも可能だとか（今回は使っていない）

```python
class [クラス名]Spider(scrapy.Spider):
    name = '[クラス名]'
    allowed_domains = ['[対象のドメイン]']
    start_urls = ['http://[対象のドメイン]/']


    def parse(self, response):
        item = TestScrapingItem()

        # 既にstart_urlsのレスポンスを持っている状態なのでresponseが使える
        item['h1'] = response.xpath('//h1[@class="title"]/text()').extract_first()

        # 前リクエストの値を引き継ぐ
        if response.meta.has_key('from'):
            item['from'] = response.meta['from']

        # 見つけたリンクでスクレイピング
        for href in response.xpath('//a/@href').extract():
            # parseを指定して再帰（値もmetaで渡す）
            yield response.follow(href, self.parse, meta={'from': item['h1']})

        yield item
```

※実行して確認してないので、確認は各自で


----

## Spider Middleware
- リクエストの開始時点、例外発生時など各種イベントが発生した時の動作にひと手間加えたいときに使う

## Feed Export
- Scrapyではデフォルトでいくつかエクスポートの種類（CSVやJSON）が用意されている
- Pipelineを使わずにこれだけでアウトプットもできる

----

## yieldを使う
- 複数のURLにアクセスする場合、次のように数珠つなぎでアクセスする作り方になる

```
start_page（for文でsecond_page、third_pageをリクエスト）
　└ second_page
  └ third_page
```

- returnを使うと`third_page`が終わった段階で`second_page`の結果が出力される
    - エラーで止まった場合に途中までの実行が残らない
    - メモリリークしそう
- 上記のことからyieldを使う

----

## HTML構造

- CSS形式とXPATH形式での画面項目の指定が可能
  - CSS指定は中身でXPATHに変換されていたので、私はXPATHを使っていた
- （XPATHの場合は）とにかく階層の表し方を色々駆使することになる
  - 参考にしていたサイト：
    http://itref.fc2web.com/xml/xpath.html

----

## `scrapy shell [URL]` によるデバッグ

- とにかくXPATHの指定は、たくさん試す必要がある
- そのためには`scrapy shell [URL]`を使うことは必須

----

## スクレイピングを行う上で考慮したい点

- スクレイピング対象のサイトにリクエストを出しまくることになるので、Scrapyのキャッシュやリクエスト間隔の設定は適宜設定するべき
- XPATHの指定は厳し過ぎず優し過ぎず
    - 無駄に指定を多くすると「これも取りたかった」が取れない場合がある
    - 指定が少なすぎで余計なものが取れてしまう場合もある
    - 職人の気分になってくる
    - いずれにしてもできるだけ`scrapy shell [URL]`や取得された結果を確認して、想定している対象が取れているかの確認が重要
- 実行環境でProxyが必要だったりしないか

----

# 以上