【スターターキットNo.1】Scrapy&MariaDB&Django&Dockerでデータ自動収集ボットシステムを構築する


![screencapture-192-168-99-102-admin-nutrition-item-2019-03-04-15_04_19-1024x686.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/514619/8e65d631-de2a-b0f4-18a4-b427fb78e439.png)

# 背景

世の中にあるWebサービスのデータベースを自動で同期して、本家にはない付加価値をつけることによって、手軽にニーズのあるWebサービスを作ることができます。

例えばECサイトのデータをスクレイピングして自前でデータベースとして持っておき、それに対して本家にはない検索方法を提供して、リンクを貼り、アフィリエイトで稼ぐみたいな軽量なビジネスモデルが個人事業のレベルで可能です。

このようなパターンはいくらでも考えられるのですが、とにかくまずはスクレイピングスクリプトを書いて、自動でデータ収集して、きちんと構造化して、それをなるべく最新の状態に保てるようなボットとインフラが必要になるわけです。今回はどのようなパターンであれ、アイデアを思いついてから、立ち上げまで作業を効率化できるようにサンプルテンプレートを作ってみました。

テンプレートといっても必要な以下のようなミドルウェアやフレームワーク込みでDockerで環境構築するところまでやってみようと思います。従ってDockerが使える人なら読み飛ばしてもこちらのコードさえあれば即実行できます。

- Scrapy
- MariaDB
- Django

今回は、海外のサプリを輸入代行する大手ECサイト[IHerb](https://iherb.com)をターゲットにDBを構築し、Django Adminでデータの内容を閲覧できるところまで説明していきます。 

# とりあえず動かしたい人へ

```bash
git clone xxxx
cd xxxx
docker-compose up -d --build
./start.sh
```


Githubにソースコード置いてます。スターターキットとしてお使いください。
https://github.com/makotunes/scrapy-django-example

Docker環境をローカルにフォワーディングしている場合は、http://localhost/admin にアクセスします。初期ユーザーはroot、パスワードはinitpassとなっています。ホスト名はDockerの依存する環境によって違うので注意してください。スクレイピングと同時並行で取得されたデータを確認することができます。


![screencapture-192-168-99-102-admin-nutrition-item-2019-03-04-15_04_19-1024x686.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/514619/8e65d631-de2a-b0f4-18a4-b427fb78e439.png)



今回のサンプルとして、iHerbの商品の価格や栄養素を取得していくコードを書いていきます。第一段階として、サイトのHTML構造を観察し、必要なデータを取り出すスクレイピングについて見ていきます。まずiHerbの商品ページのルールは「  https://www.iherb.com/pr/pr/[ID] 」という規則に従っています。1からインクリメントで順番になめていけば良いはずです。IDが欠番になっていたり在庫切れの場合は取得できないですから、これもハンドリングしておく必要があるでしょう。以降、ざっくり主要なコンポーネントの実装について簡単に触れていきます。



# Scrapyの基本的な使い方

ScrapyはPythonの多機能なスクレイピングライブラリです。まず基本的な作法として以下のようにscrapy.Spiderを継承したクラスを作っておきます。start_urlsにリスト型でスクレイピングするターゲットを入れておきます。そして、parseメソッドをオーバーライドして、responseを受け取りSelectorに変換します。



```Python
import os
import scrapy
from scrapy.selector import Selector

class Spider(scrapy.Spider):
  name = 'items'
  start = int(os.getenv('SCRAPY_START_INDEX', 22419))
  target_range = int(os.getenv('SCRAPY_NUM_ITEMS', 1000))
  start_urls = ['https://www.iherb.com/pr/pr/' + str(x) for x in range(start, start + target_range) ]    

  def parse(self, response):
    time.sleep(random.randint(2, 3))

    product_url = response.url
    self.logger.info('url=%s', product_url)

    sel = Selector(response)
```

selectorはHTMLの木構造を保持していて、任意のノードを取り出せるわけです。ここで、XPathは木構造の中から特定のノードを指定する文字列表現になります。

```Python
  def get_product_name(sel):
    product_name = sel.xpath('//*[@id="name"]/text()').extract_first()
    self.logger.info('product_name = %s', product_name)
    return product_name
```

必要な要素を表現するXPath文字列を取置します。難しく考える必要はありません。簡単な方法としては、Chromeで任意の要素で右クリックしたときに「検証」というメニューがあるのでそれを押してみてください。すると開発者コンソールを表示され、Elementsタブ内でHTML要素が青く選択されます。ここでさらに右クリックして、「Copy」 ⇛ 「Copy XPath」を押すとクリックボードに文字列がコピーされます。これをさきほどのクラスの中に書いていけばいいだけです。

# Djangoについて

スクレイピングで値が取れれば、そのままファイルに書き出しで任務完了でも良いのですが、このデータを使ってアプリケーションを作ろうと考えているなら、最初からDBに保存して置いたほうが良いと思います。特に、一度取得して終わりではなく、定期的に取得して最新の状態を保つ必要のあるアプリケーションの場合は、取得・保管・利用のサイクルが効率的に回るように、スクレイピングモジュールの開発段階からしっかりRDBでモデルを定義していくことを私はおすすめします。


Djangoには中心的な概念を説簡単にご紹介しておきます。



| ファイル | 機能 |
|:--|:--|
| settings.py | アプリ全体の設定を定義します。 |
| models.py | データベースの構造を定義します。 |
| views.py | 機能そのものを記述します。 |
| urls.py | URLと機能のマッピングを担当します。 |
| admin.py | 管理者用サイトの機能を生成します。 |
| permissions.py | 機能に対する権限をユーザーごとに記述します。 |
| serializers.py | Rest API用にデータ構造を定義します。 |


Djangoの開発は上記のようなスクリプトを実装します。settings.py、models.py、views.py、urls.pyの４つが必須要素ですが、admin.pyはデータベースに簡単にアクセスするWebサイトを簡単に構築でき、大変便利なので私は使用しています。この役目はphpMyAdminのようなものを使用しても問題ないでしょう。permissions.py、serializers.pyはRest APIを生成するDjangoの拡張ライブラリであるdjangorestframeworkによって使用されます。



スクレイピングしたデータを保存して可視化するという点で重要なのはmodels.pyだけであり、ここさえしっかり書ければ、あとの実装はこの段階では問題ないです。この記事ではmodels.pyの部分だけご紹介しておきます。


### データモデル

| テーブル名 | 機能 |
|:--|:--|
| Item | 商品テーブル |
| Composition | 栄養素とその成分量のテーブル |
| Nutrition | 栄養素テーブル |

Item,Composition,Nutritionの３つのテーブルで構成されます。必要に応じて正規化していきます。この作業はデータ取得後になると難しくなってくるので、スクレイピング実装の段階でモデル実装もちゃんとやっておこうと思います。サンプルコードでは、第２正規化までやっておきました。

このモデルについてですが、DjangoにはDBのモデル構造を表現できるORマッパーが含まれています。従って、直接DBにSQL操作を行う必要はありません。ただし、データ構造が変更さびに、マイグレーションというテーブル構造の変更に対応するSQL操作を実行する必要がありますが、ORマッパーはこの点もスクリプトで表現されたモデルに従って自動でSQLを生成し、コマンドから一発で実行してくれます。この記事のサンプルコードでは、以下のスクリプトで、簡単に呼び出せるようにしてあります。


```shell
./migrate.sh
```

```Python
from django.db import models

class Nutrition(models.Model):
    id = models.AutoField('ID', primary_key=True)
    name = models.CharField('Name', max_length=255, blank=True, null=True)
    description = models.TextField('Description', blank=True, null=True)
    create_date_time = models.DateTimeField('Date', auto_now=True)

    def __str__(self):
        return str(self.name)

class Composition(models.Model):
    id = models.AutoField('ID', primary_key=True)
    name = models.ForeignKey('Nutrition', related_name='composition_nutrition_id')
    amount = models.IntegerField('Amount', default=0, blank=False, null=False)
    unit = models.CharField('Unit', max_length=20,blank=True, null=True)
    create_date_time = models.DateTimeField('Date', auto_now=True)

    def __str__(self):
        return str(self.name) + ':' + str(self.amount) + ' ' + str(self.unit)

class Item(models.Model):
    id = models.AutoField('ID', primary_key=True)

    product_name = models.CharField('Name', max_length=100,blank=True, null=True)
    product_url = models.CharField('Product URL', max_length=999,blank=True, null=True)
    company = models.CharField('Company', max_length=100,blank=True, null=True)
    amount = models.IntegerField('Amount', default=0, blank=False, null=True)
    capsule_type = models.CharField('Capsule Type', max_length=20,blank=True, null=True)
    rating_count = models.IntegerField('Rating Count', default=0, blank=False, null=False)
    rating = models.DecimalField('Rating', max_digits=32, decimal_places=16, default=0.0)
    price = models.IntegerField('Price', default=0, blank=False, null=True)
    product_code = models.CharField('Product Code', max_length=100,blank=True, null=True)
    serving_size = models.IntegerField('Serving Size', default=1, blank=False, null=True)
    composition = models.ManyToManyField(Composition)
    create_date_time =  models.DateTimeField('Date', auto_now=True)
    
    def __str__(self):
        return str(self.product_name)
```


## データの保存

スクレイピングできた値、データ・セットを順番にMariaDBに保存していきます。簡略化して書くと以下ようになります。models.pyで定義したORマッパークラスのインスタンスに値を渡していき、save()メソッドでDBに反映します。以下のコードは簡略化して書いてあります。実際の実装についてはGit Hubで提供しているサンプルコードを見てください。


```Python
from nutrition.models import Item
from scrapy.selector import Selector

sel = Selector(response)
product_name = get_product_name(sel)
item = Item()
item.product_name = product_name
item.save()
```

実際やってみると分かりますが、Webサイトのデータというのは完璧なものはなく、少なからず表記の不統一などがあり、一発で綺麗なデータ・セットにはならないことがほとんどです。従って表記の不統一を吸収するような実装が必要になってきます。モデルを厳密に定義しながら実装することはスクレイピングの精度を高めていく過程そのもと言えるでしょう。

ここまでできたら、次はアプリ側を実装していくフェイズになります。Djangoにはテンプレートエンジン、つまりサーバーサイドで動的にHTMLをレンダリングする機構がありますが、私はモバイル用途やSPA(Reactなど)などを想定して、Rest APIによる構成を中核に位置づけています。実はソースコードはサンプルコードにすでに含まれています。機会があればこの辺についても詳しく書いていきます。

#　おわりに
読んで頂きありがとうございました。
最後に開発中の個人アプリの紹介をさせて下さい。

Mockers
https://mockers.io

「Mockers」は、「危険すぎる」と話題の大規模教師なし機械学習技術「GPT-2」を搭載した多言語対応オンライン自動テキスト生成ツールです。 「Mockers」を使用すると、この素晴らしい技術をWeb上で簡単に使用できるだけでなく、Webサイトの記事やTwitterのツイートを学習して、そのスタイルやコンテキストを模倣して、関連性の高い文章を自動生成し、WordpressやTwitterに自動的に投稿することができます。