Scrapy&MariaDB&Djangoでデータ自動収集ボットをDockerで構築する

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

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

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

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

  • Scrapy
  • MariaDB
  • Django

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

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

Githubにソースコード置いてます。(準備中)


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

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

http://localhost/admin

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

Scrapyの基本的な使い方

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


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は木構造の中から特定のノードを指定する文字列表現になります。


  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.pyURLと機能のマッピングを担当します。
admin.py管理者用サイトの機能を生成します。
permissions.py機能に対する権限をユーザーごとに記述します。
serializers.pyRest API用にデータ構造を定義します。

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

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

データモデル

Item商品テーブル
Composition栄養素とその成分量のテーブル
Nutrition栄養素テーブル

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

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


./migrate.sh

サンプルコードのpython3/mysite/models.pyは以下のようになっています。ここでテーブルの列の型や、テーブル間の関係性を定義しています。


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で提供しているサンプルコードを見てください。


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による構成を中核に位置づけています。実はソースコードはサンプルコードにすでに含まれています。機会があればこの辺についても詳しく書いていきます。

最後に

質問や、誤り、書いてほしい記事などありましたらお気軽にコメントください。今回ご紹介した記事の内容に関係するお仕事の依頼も受けつけています。


Follow me!

コメントを残す

メールアドレスが公開されることはありません。 * が付いている欄は必須項目です