Scrapy入門


<a href="https://unsplash.com/ja/s/%E5%86%99%E7%9C%9F/crawl?utm_source=unsplash&utm_medium=referral&utm_content=creditCopyText">Unsplash</a>の<a href="https://unsplash.com/@epan5?utm_source=unsplash&utm_medium=referral&utm_content=creditCopyText">Krzysztof  Niewolny</a>が撮影した写真
  

Scrapyは非常に便利だが、何かをスクレイピングしたいというニーズは、そんなに頻繁に起こるわけではないので、そんなことがあるたびに、毎回、使い方をググっている。いい加減めんどうくさいので、簡単な使い方を、ここにまとめておくことにした。

ここでは、ざっくりとした手順だけを書いておき、各々のステップの詳細は別途ググるつもりで書いている。

B-Leagueの試合日程を公式ページからスクレイピングするのに使ったプロジェクトをサンプルとしておいておく[link][sample]。
以下のコマンド実行例やコードのサンプルは、このサンプルを念頭に書かれている。

## 主な手順

Scrapyでスクレピングをするときには、以下のようなステップを踏むことになる。
Scrapyプロジェクトの構成や、Item・Spider・PipelineといったScrapy用語については、以下に述べるステップの詳細の中で説明する。

1. プロジェクトの作成
1. Spiderの作成
1. Itemの定義
1. Pipelineの定義
1. 設定ファイルの調整

<!-- link -->
[sample]: https://github.com/hotoku/scraping-bleague

## プロジェクトの作成

1. プロジェクト用のディレクトリを作る
1. poetryで仮想環境を作る
1. scrapyをインストールする
1. poetryの仮想環境に入る

``` shell
$ mkdir scraping-bleague
$ cd scraping-bleague
$ poetry init
$ poetry add scrapy
$ poetry shell
```

すると`scrapy`コマンドが使えるようになる。これを使って、プロジェクトを初期化する。

``` shell
$ scrapy startproject bleague
```

これによって、Scrapyプロジェクトの雛形が出来上がる。

Scrapyプロジェクトは、主に

- 設定ファイル
- Spiderの定義: どのURLを訪問するか・そのURLが指すリソースからどうやって情報を取得するか、などを書くところ
- Itemの定義: 取得していようとしている情報の形を定義するところ
- Pipelineの定義: 取得した情報を、どうやって処理する(DBに保存したらファイルに書いたり統計処理をしたり)かを書くところ

といった構成要素からなる。上のコマンドで、spider以外の雛形が作成される。
以下のような雛形が作成されるはず。

``` shell
scraping-bleague
├── bleague
│   ├── bleague
│   │   ├── __init__.py
│   │   ├── items.py
│   │   ├── middlewares.py
│   │   ├── pipelines.py
│   │   ├── settings.py
│   │   └── spiders
│   │       └── __init__.py
│   └── scrapy.cfg
├── poetry.lock
└── pyproject.toml
```

追記: 上のコマンドだと、scraping-bleague/bleague/bleague以下に.pyファイルを配置していくことになる。深すぎる場合は、pyproject.toml, poetry.lockファイルと同じ階層にコードを移してしまっても良い。

以下のコマンドは、全て`scrapy.cfg`ファイルが存在する場所で実行する・・ので、そこに移動しておく。

``` shell
cd bleague
```

## Spiderの作成

Scrapyでは、訪問するURLや、得られたリソースをどうやってパースするかといった情報はSpiderクラスの子クラスとして定義する。
以下のコマンドで、`www.bleague.jp`ドメイン以下のURLからスクレピングするためのSpiderの雛形が作成される。

``` shell
$ scrapy genspider bleague-match www.bleague.jp
```

これによって、以下のような`scraping-bleague/bleague/bleague/spiders/bleague_match.py`ファイルができる。

``` bleague_match.py
import scrapy

class BleagueMatchSpider(scrapy.Spider):
    name = 'bleague-match'
    allowed_domains = ['www.bleague.jp']
    start_urls = ['http://www.bleague.jp/']

    def parse(self, response):
        pass

```

この段階で、とりあえずクローラを走らせることができる。

``` shell
$ scrapy crawl bleague-match
```

### spiderの書き方
spiderを書くにあたって必要な基礎知識を箇条書きしておく。

- spiderは、最初に`start_urls`にあるurlをgetする
- getの結果が、`response`オブジェクトに入った状態で`parse`が呼ばれる
  - `response.xpath`などで要素にアクセスできる
- 必要な情報を取得したら、それを辞書またはItem(詳細は後述)クラスのオブジェクトにまとめて`yield`する
- 訪問したページからリンクを取得して別のページを訪問したい場合は`response.follow()`を`yield`する
- `scrapy crawl <spider name> -a <key>=<value>`という形で、コマンド引数を利用できる。ここで与えた値は、spiderの`__init__`の引数として受け取ることができる。コマンドで与える`key`が`__init__`の引数名になる。この値は、文字列として渡ってくるので、適宜パースが必要

**B-leagueのトップページからaタグを2つ抜き出し、そのリンク先のタイトルを取得する例**

``` python
class BleagueMatchSpider(scrapy.Spider):
    name = 'bleague-match'
    allowed_domains = ['www.bleague.jp']
    start_urls = ['http://www.bleague.jp/']

    def parse(self, response):
        links = response.xpath("//a")

        cnt = 0
        i = 0
        while cnt < 2:
            url = links[i].attrib["href"]
            if any(map(lambda d: d in url, self.allowed_domains)):
                cnt += 1
                yield response.follow(url, self.get_title)
            i += 1

    def get_title(self, response):
        title = response.xpath("//title/text()").get()
        yield dict(title=title)
```

### 開発用のrepl

``` python
from scrapy.shell import inspect_response
inspect_response(response, self)
```

というコードを書いておくと、クロール中に上記のコードが評価される時点で、対話環境に入ることができる。この中で、`response`の中身などを見ることができるので便利。`view(response)`で取得したhtmlをブラウザで見ることができる。

## Itemの定義

自動生成される雛形の中に、`scraping-bleague/bleague/bleague/items.py`というファイルがある。
この中に、スクレイピングによって取得したデータの形式を`Item`クラスのサブクラスとして定義する。
ここにItemとして定義せずに、`dict`を利用して任意のデータ形式を使うこともできる。

## Pipelineの定義
自動生成される雛形の中に、`scraping-bleague/bleague/bleague/pipelines.py`というファイルがある。
この中に、取得した`Item`(または`dict`)をどうやって処理するかを書く。

### Pipelineの書き方
pipelineを書くにあたって必要な基礎知識を箇条書きしておく。

- `item`に、spiderがyieldしたオブジェクトが渡される
- `spider`には、それをyieldしたSpiderオブジェクトが渡される
- pipelineは(名前から想像されるように)、複数定義して、それらを順番に適用するように想定されている。そのため、`process_item`の最後に、渡されたitemを`return`して、次の処理にオブジェクトを渡す
- 適用されるPipelineや、それらの順番は、`scraping-bleague/bleague/bleague/settings.py`の`ITEM_PIPELINES`で指定する
  - なので、Pipelineを定義したら、この項目を編集しなければ処理に反映されない

**Itemからtitleを抜き出して、/tmp/titles.txtに追記していく例**

``` python
class BleaguePipeline:
    def process_item(self, item, spider):
        title = item["title"]
        with open("/tmp/titles.txt", "a") as fp:
            fp.write(title + "\n")
        return item
```

ここまでを実装して、`scrapy crawl bleague-match`を実行すると`/tmp/titles.txt`に値が入るはず。

## 設定の調整

クローラの設定は、`scraping-bleague/bleague/bleague/settings.py`で記述される。

- robots.txtに従うか or Not
- 同一ドメインに同時に投げるリクエストの数

などを、ここでコントロールできる。

## トラブルシュート

Mac OS + pyenvで作業した際に、_lzmaというパッケージがないというエラーを吐いた。この方法で対処[link](https://stackoverflow.com/questions/59690698/modulenotfounderror-no-module-named-lzma-when-building-python-using-pyenv-on)