PythonでAmazonの価格を監視

#PythonでAmazonの価格を監視
　[前回](https://qiita.com/kawa-Kotaro/items/9933f56abd53a09826d9)と[前々回](https://qiita.com/kawa-Kotaro/items/460977f050bf0e2828f2)の内容を用いてAmazonの価格を監視するシステムを構築します．またWebスクレイピングの技術も用いるため[こちら](https://qiita.com/kawa-Kotaro/items/2c9121eeb384ba348db4)も参照してください．今回は内容が多いため，少々難易度が高い内容になります．分かりにくい部分は適宜上記のページを参照して理解を深めてください．
　使用する環境はPython3.7，Pycharmの仮想環境です．

##概要
　今回実装する動作のうち，定期的に実行する部分を以下に示します．

 - Amazonのページから価格を取得
 - 価格が下がっていたらメールで通知する
 - データベースから以前の価格を取り出す
 - 価格を更新する

　それ以外の部分を以下に示します．

 - ユーザーの名前，メールアドレスを登録する
 - 新しい商品を登録する
 - 商品を削除する

今回は繰り返し実行する部分を作成します．

##Amazonのページから価格を取得
　まずAmazonの価格を取得します．価格を取得する関数を以下に示します．

```python:get_price.py
def get_price(page_url):
    res = requests.get(page_url)
    soup = bs4.BeautifulSoup(res.text, features="lxml")
    selected_html = soup.select('.a-span12 span.a-color-price')

    if not selected_html:
        selected_html = soup.select('.a-color-base span.a-color-price')

    pattern = r'\d*,?\d*,?\d*\d'
    regex = re.compile(pattern)
    matches = re.findall(regex, selected_html[0].text)
    price = matches[0].replace(',', '')
    return int(price)
```

　requestsとbs4を用いて商品の価格を取得します．商品によって価格表示部分のhtmlが異なるため，数種類のセレクタを用意する必要があります．if文を用いて１つ目のセレクタで引っかからない場合にはもう１つのセレクタを使用するようにしました．実はこれでも不足する分は同じ形式でセレクタを追加することでカバーできます．
　セレクタで取得できるデータは空白や「,」などを含む文字列です．これを数字に直すために正規マッチングを使用しました．正規マッチングにはreライブラリを使用します．正規表現について以下の表に示します．

|　　　記号　　　|　　　　 　　 意味　　　　　　|
|:----------------:|:------------------------------:|
| \d               |    任意の数字とマッチする    |
| *                |直前の文字の0回以上の繰り返しとマッチする |
|?                 |直前の文字の0回か1回の繰り返しとマッチする|

　難しい書き方をしましたが，＊や？はあってもなくてもいいものにつけます．？は多くても１回のもの，＊は２回以上あっても構わないものにつけます．このほかにも多くの表現があります．そのほかの表現については[こちら](http://gimite.net/help/devas-ja/all_regex.html)を参照ください．価格を取得するためには１つ以上の数字を含み，３文字ごとに「,」を持つパターンとマッチする必要があります．そのため次のようになります．

```python:pattern.py
r'\d*,?\d*,?\d*\d'
```

＊を使用しているため，３文字以上の数字ともマッチしますが，価格の表示で３文字以上数字が繰り返されることはないため，これで問題ありません．今回用意したパターンは「,」の数が２つであるため，10億円以上の商品に対しては使用できません．10億円以上の商品を購入したい場合にはパターンを書き換えていただく必要があります．
　このパターンをコンパイルし，findallメソッドを用いてパターンに合致する文字列を探索します．
　intに変換するには「,」を取り除かなければならないため，replaceメソッドを用いて「,」を空文字列に置き換えます．これで価格を取得することができました．

同様に商品名も取得しましょう．関数は次のようになります．

```python:get_title.py
def get_title(page_url):
    res = requests.get(page_url)
    soup = bs4.BeautifulSoup(res.text, features="lxml")
    selected_html = soup.select('#productTitle')
    title = selected_html[0].text
    title = title.replace(' ', '')
    title = title.replace('\n', '')
    return title
```

これで必要な情報をすべて取得することができました．
##メールを送信
　メール送信には[前々回](https://qiita.com/kawa-Kotaro/items/460977f050bf0e2828f2)用意した関数を使用します．

```python:functions.py
def create_message(from_addr, to_addr, subject, body):
    msg = MIMEText(body)
    msg['Subject'] = subject
    msg['From'] = from_addr
    msg['To'] = to_addr
    msg['Date'] = formatdate()
    return msg


def send_mail(from_addr, to_addr, body_msg):
    smtpobj = smtplib.SMTP('smtp.gmail.com', 587)
    smtpobj.ehlo()
    smtpobj.starttls()
    smtpobj.ehlo()
    smtpobj.login(FROM_ADDRESS, MY_PASSWORD)
    smtpobj.sendmail(from_addr, to_addr, body_msg.as_string())
    smtpobj.close()
```

関数の詳細は[前々回](https://qiita.com/kawa-Kotaro/items/460977f050bf0e2828f2)の記事をご覧ください．これで関数の定義は終わりました．

##データベースの処理
　これ以降の処理はmain関数で行います．main関数を以下に示します．

```python:main.py
if __name__ == '__main__':
    db = 'price_checker.db'
    temp_msg = '''{}さん！\n{}の値段が昨日より下がって{}円になりました！'''
    mail_subject = '値下げのお知らせ'

    with closing(sqlite3.connect(db)) as conn:
        c = conn.cursor()
        select_sql = 'select * from users'
        for row in c.execute(select_sql):
            url = row[2]
            current_price = get_price(url)
            if current_price < row[3]:
                mail_body = temp_msg.format(row[0], row[1], current_price)
                message = create_message(FROM_ADDRESS, row[4], mail_subject, mail_body)
                send(FROM_ADDRESS, row[4], message)

            if current_price != row[3]:
                update_sql = 'update users set price = ? where url = ?'
                c.execute(update_sql, (current_price, row[2]))
                conn.commit()
        conn.commit()
        conn.close()
```

データの並びは　名前, 商品名, URL, 前回の価格, メールアドレスの順です．main関数では，データベースのの全データを操作し，URLから商品の価格を取得し，前回の価格より低ければメールを送信，そして前回から価格が変わっていればデータを更新するようになっています．データの取得は'select * from users'で行います．ここではレコードがタプルとして渡されるため，添え字で任意のデータにアクセスします．
　メールの本文はあらかじめテンプレートを用意しておき，ユーザーと商品，価格を埋め込んでメッセージを完成させます．
　データの更新は'update users set price = ? where url = ?'で行います．
　以上で価格をチェックしメールを送信するプログラムは完成しました．

##おわりに
　これでプログラムはほとんど完成です．残る作業はユーザーの登録，データの登録，削除の処理，および繰り返し実行の処理です．頑張っていきましょう!!