【Python】レシピサイトから「本当に求めているレシピ」を紹介するシステムを作ってみた！

#はじめに
こんにちは，HanBeiです。

[前回の記事](https://qiita.com/HanBei/items/91ed10df1f12f817b853) は機械学習用のデータ収集についてでしたが，今回も続きをやっていきます。

引き続き，[Google Colaboratory](https://colab.research.google.com/notebooks/intro.ipynb?hl=ja#recent=true)を使います。

興味を持たれたら，**コメント**や**LGTM**をお願いいたします！

##1-1. 目的
レシピサイトを使うけど**「量多いな」**，**「ホントにオススメの料理は美味しいのか？（失礼極まりない）」**と思ったので，自分が求めるレシピを探そうと思います。

##1-2. ターゲット（この記事を読む理由）
**Web上にあるデータを用いて，機械学習用のデータを手に入れたい人** の役に立てれば幸いです。

##1-3. 注意
スクレイピングは用法・用量を守って正しく守らないと**犯罪**になります。

「スクレイピングやりたいから，気にせずやっちゃえー！」
と楽観的な人や，心配な人は少なくとも下の2つの記事は目を通すことをオススメします。

[miyabisun](https://qiita.com/miyabisun)  様: 「[スクレイピングのやり方をQ&Aサイトで質問するな](https://qiita.com/miyabisun/items/9883f7b7006c09efa5a0)」
[nezuq](https://qiita.com/nezuq) 様: 「[Webスクレイピングの注意事項一覧](https://qiita.com/nezuq/items/c5e827e1827e7cb29011)」

##1-4. 確認事項
この記事はスクレイピングの方法を教えておりますが，**一切の責任を負いかねます**。

自分で考え，正しい倫理観をもって使ってください。

#2. 準備
##2-1. レシピサイトの検討
[クックパッド](https://cookpad.com/)，[Nadia](https://oceans-nadia.com/)，[白ごはん.com](https://www.sirogohan.com/)などオススメのレシピサイトはありますが，今回は[**楽天レシピ**](https://recipe.rakuten.co.jp/)を使用していきます。

理由は，
・「リピートしたい」，「簡単だったよ」，「節約できた」などの定量的なデータがある。
・レシピの件数が多い

です。

##2-2. Google Colaboratoryを使えるようにする
Google Colaboratoryを使うにあたってGoogleアカウントを作っていない人は，作成しましょう。

ノートブックを新規作成する方法は...
1. [Google Colaboratory](https://colab.research.google.com/notebooks/intro.ipynb?hl=ja#recent=true)をクリックして作業を始める
2. Googleドライブから新規作成する
　参考:　[shoji9x9](https://qiita.com/shoji9x9) 様 「[Google Colabの使い方まとめ](https://qiita.com/shoji9x9/items/0ff0f6f603df18d631ab)」

#3. 実践
ここからは実装の内容を書いていきます。

##3-1. はじめに
まず，ライブラリをインポートします。

```python
from bs4 import BeautifulSoup
from google.colab import drive
from google.colab import files
import urllib
import urllib.parse
import urllib.request as req
import csv
import random
import pandas as pd
import numpy as np
import time
import datetime
```

調べたい料理名を決めましょう！

```python
# 調べたい料理名
food_name = 'カレー'
```

##3-2. レシピのURLを取得
レシピのURLを取得する関数を作ります。

```python
# 各レシピのurlを格納する
recipe_url_lists = []

def GetRecipeURL(url):
  res = req.urlopen(url)
  soup = BeautifulSoup(res, 'html.parser')

  # レシピ一覧の範囲を選択
  recipe_text = str(soup.find_all('li', class_= 'clearfix'))
  # 取得したテキストを1行ずつ分割してリストに収納
  recipe_text_list = recipe_text.split('\n')

  # リストを1行ずづ読み込んで料理名が一致する行だけ抽出
  for text in recipe_text_list:
    # 各レシピのurl取得
    if 'a href="/recipe/' in text:
      #特定箇所を指定してurlにいれる
      recipe_url_id = text[16:27]
      # urlの結合
      recipe_url_list = 'https://recipe.rakuten.co.jp/recipe/' + recipe_url_id + '/?l-id=recipe_list_detail_recipe'
      # urlを格納
      recipe_url_lists.append(recipe_url_list)  

    # 各レシピのタイトル取得
    if 'h3' in text:
      print(text + ", " + recipe_url_list)
```

人気順のレシピを確認する

```python
# 調べたいページの量
page_count = 2

# 料理名をurlに入れるためエンコード
name_quote = urllib.parse.quote(food_name)

# urlを結合する（1ページのみurl）
# 人気順
base_url = 'https://recipe.rakuten.co.jp/search/' + name_quote
# 新着順
# base_url = 'https://recipe.rakuten.co.jp/search/' + name_quote + '/?s=0&v=0&t=2'

for num in range(page_count):
  #特定のページ以降を取得する場合
  # num = num + 50

  if num == 1:
    # urlを結合する（1ページのみurl）
   GetRecipeURL(base_url)

  if num  > 1:
    # urlを結合する（2ページ以降のurl）
    # 人気順
    base_url_other =  'https://recipe.rakuten.co.jp/search/' + name_quote + '/' + str(num) + '/?s=4&v=0&t=2'
    # 新着順
    # base_url_other =  'https://recipe.rakuten.co.jp/search/' + name_quote + '/' + str(num) + '/?s=0&v=0&t=2'
    GetRecipeURL(base_url_other)

  # スクレイピングの1秒ルールを適用
  time.sleep(1)
```

ここまでを実行すると，タイトルとレシピのURLが表示されます。
![Searching_of_Delicious_Food_ipynb_Colaboratory.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/375869/e1e329fb-1500-63e9-42ed-779ed45989f9.png)

ここで取得したレシピ数を確認してみましょう！

```python

# 取得したレシピ数
len(recipe_url_lists)
```

実行すると，17件と表示されます。


次に，各レシピから必要なデータを取得していきます。

```python

data_count = []
recipe_data_set = []

def SearchRecipeInfo(url, tag, id_name):
  res = req.urlopen(url)
  soup = BeautifulSoup(res, 'html.parser')

  for all_text in soup.find_all(tag, id= id_name):
    # ID
    for text in all_text.find_all('p', class_= 'rcpId'):
      recipe_id = text.get_text()[7:17]
      
    # 公開日
    for text in all_text.find_all('p', class_= 'openDate'):
      recipe_date = text.get_text()[4:14]

    # おいしかった, 簡単だった, 節約できたよの3種類のスタンプ数
    for text in all_text.find_all('div', class_= 'stampHead'):
      for tag in text.find_all('span', class_= 'stampCount'):
        data_count.append(tag.get_text())

    # つくったよレポート数
    for text in all_text.find_all('div', class_= 'recipeRepoBox'):
      for tag in text.find_all('h2'):
        # レポート数が0のとき
        if tag.find('span') == None:
          report = str(0)
        else:
          for el in tag.find('span'):
            report = el.replace('\n							', '').replace('件', '')

  print("ID: " + recipe_id + ", DATE: " + recipe_date + ", 作られた数: " + report + 
        ", リピートしたい: " + data_count[0] +
        ", 簡単だった: " + data_count[1] +
        ", 節約できたよ: " + data_count[2]+
        ", url: " + url)

  # csvファイルに書き込むために格納
  recipe_data_set.append([recipe_id, recipe_date, data_count[0], data_count[1], data_count[2], report, url])

  # スタンプ数を入れていた配列を空にする
  data_count.clear()

  # スクレイピングの制限
  time.sleep(1)
```

ここで，取得したデータを確認します。

```python

for num in range(len(recipe_url_lists)):
  SearchRecipeInfo(recipe_url_lists[num], 'div', 'detailContents')
```

実行すると，ちゃんと取得できていることだわかります。
![Searching_of_Delicious_Food_ipynb_Colaboratory (1).png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/375869/fab41ad8-4ab1-7f5d-dfea-9063d07f27ea.png)

##3-3. Google Driveにcsvファイルを出力

Google Driveにスプレットシートを作成し，データを出力していきます

```python
#使用したいディレクトリをマウントする
drive.mount('/content/drive')
```

Google Driveの任意のフォルダを選択，ファイル名を指定
「〇〇〇」は指定してください。

```python
# google drive上にフォルダ作成し保存先を指定
save_dir = "./drive/My Drive/Colab Notebooks/〇〇〇/"
# ファイル名を選択
data_name = '〇〇〇.csv'
# フォルダにcsvファイルを保存
data_dir = save_dir + data_name

# csvファイルに項目を追加
with open(data_dir, 'w', newline='') as file:
  writer = csv.writer(file, lineterminator='\n')
  writer.writerow(['ID','Release Date','Repeat','Easy','Economy','Report','URL'])

  for num in range(len(recipe_url_lists)):
    writer.writerow(recipe_data_set[num])

# 作製したファイルを保存
with open(data_dir, 'r') as file:
  sheet_info = file.read()
```

実行すると，指定のディレクトリに〇〇〇.csvが出力されます。
ここまでの内容を，簡単にスライドに纏めたのでご参考ください。
※今回は，新着順ではなく人気順で出力しています。

![2020_05_22_レシピ検索_Google_スライド.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/375869/3b99169c-2536-f35b-7e8b-df41deacc219.png)
![2020_05_22_レシピ検索_Google_スライド (1).png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/375869/d98009e7-4da1-ca4b-155c-2588f58a1f80.png)

##3-4. レシピデータの重み付け

Pandasで出力されたcsvファイルを確認します。

```python

# csvを読み込み
rakuten_recipes = pd.read_csv(data_dir, encoding="UTF-8")

# 列に追加する準備
df = pd.DataFrame(rakuten_recipes)

df
```

出力のイメージは省略します。

次にレシピの公開日から今日までの経過日数を算出します。


```python

# rakuten_recipes.csvからRelease Dateを抜き出す
date = np.array(rakuten_recipes['Release Date'])
# 現在の日にちを取得
today = datetime.date.today()

# 型を合わせる
df['Release Date'] = pd.to_datetime(df['Release Date'], format='%Y-%m-%d')
today = pd.to_datetime(today, format='%Y-%m-%d')

df['Elapsed Days'] = today - df['Release Date']

# 経過日数の値のみ取出してあげる
for num in range(len(df['Elapsed Days'])):
  df['Elapsed Days'][num] = df['Elapsed Days'][num].days

# 上から5行だけ確認
df.head()
```

すると，URLの列の隣に経過日数が出てきます。

次は，経過日数を**重み**とし，Repeat，Easy，Economyの3種類のスタンプに重み付けします。
その重み付けされたデータを，既存のレシピの列に追加していきます。

![2020_05_22_レシピ検索_Google_スライド (2).png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/375869/81d46714-8e9b-1c2d-ba06-c87954286719.png)


```python

# あまりにも小さい値にならないように修正
weighting = 1000000

# 3種類のスタンプとレポートの値を抜き出す
repeat_stamp = np.array(rakuten_recipes['Repeat'])
easy_stamp = np.array(rakuten_recipes['Easy'])
economy_stamp = np.array(rakuten_recipes['Economy'])
report_stamp = np.array(rakuten_recipes['Report'])

# 各スタンプとレポートの合計
repeat_stamp_sum = sum(repeat_stamp)
easy_stamp_sum = sum(easy_stamp)
economy_stamp_sum = sum(economy_stamp)
report_stamp_sum = sum(report_stamp)

# 重み付けした値の列を追加
'''
リピート重み付け = (リピートのスタンプ数 ÷ リピート合計) × (修正値 ÷ 公開日からの経過日数)
'''
df['Repeat WT'] = (df['Repeat'] / repeat_stamp_sum) * (weighting / df['Elapsed Days'])
df['Easy WT'] = (df['Easy'] / easy_stamp_sum) * (weighting / df['Elapsed Days'])
df['Economy WT'] = (df['Economy'] / economy_stamp_sum) * (weighting / df['Elapsed Days'])

# レポートの重要度（0～1の範囲）
proportions_rate = 0.5

# 重み付けした値の列を追加
'''
リピート重み付け = (リピート重み付け × (1 - 重要度)) × ((レポート数 ÷ レポート数の合計) × 重要度[%])
'''
df['Repeat WT'] = (df['Repeat WT'] * (1 - proportions_rate)) * ((df['Report'] / report_stamp_sum) * proportions_rate)
df['Easy WT'] = (df['Easy WT'] * (1 - proportions_rate)) * ((df['Easy WT'] / report_stamp_sum) * proportions_rate)
df['Economy WT'] = (df['Economy WT'] * (1 - proportions_rate)) * ((df['Economy WT'] / report_stamp_sum) * proportions_rate)
```

重み付けについて...
経過日数については，1ヵ月前と1年前の記事があるとして同じ100個のスタンプがついてるとします。
どちらが，レシピのオススメ度が高いかというと1か月前の方です。
なので，経過日数がある記事ほどスコアは低くなるようにしています。

重み付けした値の最大値，最小値の範囲を0～1に変えます。
参考にさせていただいたページを載せておきます。

[QUANON](https://qiita.com/QUANON)様：「[ある範囲における数値を別な範囲における数値に変換する](https://qiita.com/QUANON/items/188baf469fd493a2f1e6)」


```python

df['Repeat WT'] = (df['Repeat WT'] - np.min(df['Repeat WT'])) / (np.max(df['Repeat WT']) - np.min(df['Repeat WT']))
df['Easy WT'] = (df['Easy WT'] - np.min(df['Easy WT'])) / (np.max(df['Easy WT']) - np.min(df['Easy WT']))
df['Economy WT'] = (df['Economy WT'] - np.min(df['Economy WT'])) / (np.max(df['Economy WT']) - np.min(df['Economy WT']))

df.head()
```

実行結果です。
df.head()とやることでトップ5行だけ表示されます。
![Searching_of_Delicious_Food_ipynb_Colaboratory (2).png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/375869/26763248-02c5-22c1-4ec8-c7f7fd02f5e6.png)

##3-5. オススメのレシピを表示
ユーザから飛んできた点数を5段階評価にして検索を行う。

```python

# 範囲指定に使用（1: 0-0.2, 2: 0.2-0.4, 3: 0.4-0.6, 4: 0.6-0.8, 5: 0.8-1）
condition_num = 0.2

def PlugInScore(repeat, easy, economy):
  # 引数を指定の範囲内に
  if 1 >= repeat:
    repeat = 1
  if 5 <=repeat:
    repeat = 5
  if 1 >= easy:
    easy = 1
  if 5 <= easy:
    easy = 5
  if 1 >= economy:
    economy = 1
  if 5 <= economy:
    economy = 5

  # 3種類の得点から，レシピを絞り込む
  df_result =  df[((repeat*condition_num) - condition_num <= df['Repeat WT']) & (repeat*condition_num >= df['Repeat WT']) &
                  ((easy*condition_num) - condition_num <= df['Easy WT']) & (easy*condition_num >= df['Easy WT']) &
                  ((economy*condition_num) - condition_num <= df['Economy WT']) & (economy*condition_num >= df['Economy WT'])]
  # print(df_result)

  CsvOutput(df_result)
```

検索した結果をcsvファイルに出力する。
〇〇〇は任意の名前を入れてください！

```python

# ファイル名を選択
data_name = '〇〇〇_result.csv'
# フォルダにcsvファイルを保存
data_dir_result = save_dir + data_name

# csvを出力
def CsvOutput(df_result):
  # 絞り込んだ結果をcsvファイルに出力
  with open(data_dir_result, 'w', newline='') as file:
    writer = csv.writer(file, lineterminator='\n')
    # タイトル
    writer.writerow(df_result)
    # 各値
    for num in range(len(df_result)):
      writer.writerow(df_result.values[num])

  # 作製したファイルを保存
  with open(data_dir, 'r') as file:
    sheet_info = file.read()
  
  AdviceRecipe()
```

結果を表示する関数を宣言しています。

```python

def AdviceRecipe():
  # csvを読み込み
  rakuten_recipes_result = pd.read_csv(data_dir_result, encoding="UTF-8")

  # 列に追加する準備
  df_recipes_res = pd.DataFrame(rakuten_recipes_result)

  print(df_recipes_res)

  print("あなたにおススメの 「 " + food_name + " 」")
  print("Entry No.1: " + df_recipes_res['URL'][random.randint(0, len(df_recipes_res))])
  print("Entry No.2: " + df_recipes_res['URL'][random.randint(0, len(df_recipes_res))])
  print("Entry No.3: " + df_recipes_res['URL'][random.randint(0, len(df_recipes_res))])
```

最後に，作りたいレシピに得点をつけてオススメを表示します。

```python

'''

plug_in_score(repeat, easy, economy)を代入
  
  repeat : もう一度作りたくなるか？
  easy   : 作るのが簡単かどうか？
  economy: 節約して作れるか？

主観を1～5の5段評価し，整数を代入する。

1がネガティブ，5がポジション

'''

PlugInScore(1,1,1)
```

3種類の得点を
**1**: もう一度作りたくなるか？
**1**: 作るのが簡単かどうか？
**1**: 節約して作れるか？

としたときの実行結果は...
![Searching_of_Delicious_Food_ipynb_Colaboratory (3).png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/375869/d133ef62-4f7a-fd71-73f7-6d4b3e66b65d.png)

#4. 課題・問題点
・評価手法の検討
3種類の重み付けされた値が，最高値のレシピに引っ張られ極端な結果になってしまう。
そのため，1か5のみのどちらかに偏る。

・スタンプが押された記事が少ない
レシピにスタンプやレポートがついているのが約**10**%，オール0が約**90**%
そのため，レシピを点数で評価するのはナンセンスかも。オール0の中にも，素晴らしいレシピがあるはずなので。

#5. 考察
私はこのシステムを使い「豚キムチ」を検索して，作ってみました。
オススメのレシピだったので美味しかったです^^

埋もれていたレシピを発掘できるので，面白かったです。

ここまで読んでいただいた方，誠にありがとうございます。
ぜひ，コメントやアドバイスをいただけると嬉しいです^^