Python + Scrapyで画像を巡回取得する

# この記事について
- Python用のスクレイピングフレームワークであるScrapyを使用します
- Scrapyを使用して、Webサイトをクローリングして、画像を取得、保存する方法のメモです
- 保存するファイル名はSHA1 hashではなく、ちゃんと個別にファイル名を付けて保存します
- 画像保存の方法にフォーカスしたいため、フィルタ設定などはしません。また、以下のような単純なサイトにのみ対応します
	- リンクは素直に、htmlファイルに対して`<a>`タグで実装されている
	- 画像は`<img>`タグで実装されている
- この記事では、ヤフーニュースのトップからすべてのページ、画像を巡回する例を挙げます

# Scrapyプロジェクトを作る
```
scrapy startproject test_scrapy
cd test_scrapy
```

# Spiderを用意して実装する
下記コマンドで、save_yahoo_image.pyを生成します。

```
scrapy genspider save_yahoo_image news.yahoo.co.jp
```

下記のように実装します。実装内容としては、`http://https://news.yahoo.co.jp`からすべてのリンクを巡回しながらパース(`parse_page`につっこむ)します。 
`parse_page`では、取得したページ内のすべての`<img>`タグの`src`を取得して画像のURLを取得します。その他、必要な情報を後で用意するImageItemクラスに入れます。 
ImageItemクラスには、画像を保存するディレクトリ名(`image_directory_name`、ここでは、URLのファイル名の一つ上のアドレスとしている)と、画像のURLリスト(`image_urls`)を格納します。ImageItemクラスは後で実装します。  

```python:save_yahoo_image.py
# -*- coding: utf-8 -*-
import scrapy
from scrapy.spiders import CrawlSpider, Rule
from scrapy.linkextractors import LinkExtractor

from test_scrapy.items import ImageItem

class SaveYahooImageSpider(CrawlSpider):
    name = 'save_yahoo_image'
    allowed_domains = ["news.yahoo.co.jp"]
    start_urls = ["https://news.yahoo.co.jp"]

    rules = (
        Rule(LinkExtractor(allow=( )), callback="parse_page", follow=True),
    )

    def parse_page(self, response):
        print("\n>>> Parse " + response.url + " <<<")
        # print(response.url.rsplit("/", 1)[0])
        item = ImageItem()
        item["image_directory_name"] = self.start_urls[0].rsplit("/", 1)[1]
        item["image_urls"] = []
        for image_url in response.xpath("//img/@src").extract():
            if "http" not in image_url:
                item["image_urls"].append(response.url.rsplit("/", 1)[0] + "/" + image_url)
            else:
                item["image_urls"].append(image_url)
        # print(vars(item))
        return item
```

# ImageItemを実装する
items.pyにImageItemクラスを追加します。格納するフィールドを定義しているだけです。

```python:items.py
# -*- coding: utf-8 -*-

import scrapy
from scrapy.item import Item, Field

class ImageItem(Item):
    image_directory_name = Field()
    image_urls = Field()
    images = Field()
```

# Imageパイプラインの設定をする
先ほど実装した`parse_page`処理が終わった後に、画像を保存するような設定をします。  
まず、settings.pyに以下の2行を追加します。後で作成するMyImagesPipelineクラスを使用し、画像の保存先を./savedImagesにするという指定をします。

```python:settings.py
ITEM_PIPELINES = {'test_scrapy.pipelines.MyImagesPipeline': 1}
# ITEM_PIPELINES = {'scrapy.contrib.pipeline.images.ImagesPipeline': 1}
IMAGES_STORE = './savedImages'
```

pipelines.pyに画像を保存するための処理を追加します。これは必須ではなく、標準のImagesPipelineを使用しても良いです(そのときは、上述のsettings.pyのコメントアウトを切り替える)。ただし、ImagesPipelineだと保存される画像ファイルのファイル名がSHA1のハッシュ値になってしまうという問題があります。それだとわかりづらいので、指定したディレクトリ下に、URLの最後の部分(XXX.jpg)をファイル名として保存するようにしたクラスMyImagesPipelineを用意して、それを使います。

```python:pipelines.py
# -*- coding: utf-8 -*-

import scrapy
from scrapy.pipelines.images import ImagesPipeline
from scrapy.utils.misc import md5sum

# refer: https://stackoverflow.com/questions/31779995/how-to-give-custom-name-to-images-when-downloading-through-scrapy
class MyImagesPipeline(ImagesPipeline):
    def get_media_requests(self, item, info):
        for image_url in item['image_urls']:
            yield scrapy.Request(image_url, meta={'image_directory_name': item["image_directory_name"]})

    def image_downloaded(self, response, request, info):
        checksum = None
        for path, image, buf in self.get_images(response, request, info):
            if checksum is None:
                buf.seek(0)
                checksum = md5sum(buf)
            width, height = image.size
            filename = request._url.rsplit("/", 1)[1]
            path = 'full/%s/%s' % (response.meta['image_directory_name'], filename)
            self.store.persist_file(
                path, buf, info,
                meta={'width': width, 'height': height})
        return checksum
```

# 実行する
下記コマンドで実行すると、`savedImages\full\news.yahoo.co.jp`に画像が保存されていくはずです。

```
scrapy crawl save_yahoo_image
```

# 注意
ヤフーニュースを使用したのは、あくまで例です。本作業はサーバに多大な負荷をかけますので、動作確認出来たら、すぐに停止するようにしてください。本プログラムはフィルタや終了条件を指定していないので、延々とクローリング、ダウンロードを行います。

# 投稿後に気付いた
何も考えずに記事を書いて、投稿をしてから気づいたのですが、[こちら](https://qiita.com/watame/items/f71a8ad93bce9b8d12ab)とだいぶ内容がかぶっていました。まあ、本記事は画像の保存方法に特化した内容ということで。。。 (やっぱり皆さん、題材にはヤフーニュースを使うのですね)