料理レシピ動画サービスのクラシルをスクレイピングする【検索機能に要望あり】

料理レシピ動画サービスのクラシルをスクレイピングする【検索機能に要望あり】 プログラミング

料理レシピサイトを利用していますか?
私は、主にクラシルを利用しています。

一時期は、クラシルプレミアムという有料プランに加入していました。
月480円(税込)ですね。
そこそこ、利用する価値はありました。

でも、今はもう有料プランには加入していません。
特に不満はなかったのですが、いつの間にか解約したという感じです。

もしクラシルに求める機能があれば、解約はしていなかったでしょう。
その求める機能とは、検索と材料計算に関するモノです。

それらがないから、自分で作ろうと言うことです。
そのための手始めとして、メイン画面をスクレイピングします。

本記事の内容

  • クラシルに求める機能
  • クラシルのデータ確認
  • クラシルのスクレイピング

それでは、上記に沿って解説していきます。

クラシルに求める機能

まずは、クラシルに求める機能を定義しておきます。
仕様を明確にしておかないと、どのデータが必要かどうかわかりません。

必要なデータを明確にしておかないと、スクレイピングしても無駄に終わる可能性があります。
そのため、求める機能の仕様をまとめます。

クラシルに求める機能は以下の2つ。

  • 材料の容量によるレシピ検索機能
  • 人数指定による材料計算機能

それぞれの説明を行います。

材料の容量によるレシピ検索機能

材料による検索は、現時点でも可能です。
基本的には、これでレシピを検索しています。

検索ワードに「キャベツ」と入力して検索します。
そうするとキャベツを材料に用いたレシピが表示されます。

ただし、キャベツの容量に関しては何もわかりません。
例えば、キャベツ400gを消費したいというケースがあるとします。
野菜室に期限ギリギリのキャベツが、400gあったというケースです。

「キャベツ 400g」で検索します。
そうすると、何も検索結果に出てきません。

多すぎるのかと思い、「キャベツ 250g」で検索し直します。
それでも、何も検索結果に出てきません。

なお、「キャベツが沢山食べれる 豚バラポン酢ガーリック炒め レシピ・作り方」のレシピは以下。
https://www.kurashiru.com/recipes/e104b537-76a7-4e7e-8d20-ce14d3a9cf3b

「キャベツ 250g」で検索にヒットしてもいいはずなのですけどね。
「キャベツ 250」でもヒットしません。

この材料と容量を組み合わせた検索機能が、どうしても欲しいのです。
250gジャストは無理でも、300gで検索した場合に300g以下のレシピが出ればOK。

ただ、それはそれで問題があります。
何人分の容量を要するのか?という問題です。

例えば、4人前の料理を400gのキャベツで作りたいケースがあるとします。
「キャベツ 400g」で検索した場合、上記メニューはヒットします。

でも、2人前で250gのキャベツなのです。
ということは、4人前だと合計で500gのキャベツが必要となります。

その場合、「キャベツ 400g」で上記メニューはヒットして欲しくありません。
よって、「キャベツ 400g」に「4人前」という条件を付加する必要が出てきます。

話がややこしくなってきましたので、この辺りで終わります。
とにかく、検索機能をもっと機能アップして欲しいということです。

人数指定による材料計算機能

これは、上記の2人前や4人前の話と共通する部分があります。
上記レシピで4人前の料理をする場合、それぞれの容量を4人前用に計算する手間があります。

2人前から4人前の場合は、単純に倍にするので簡単です。
でも、これが5人前となると、2.5倍なので一気に複雑になります。

この手間をカバーして欲しいというのが希望です。
実際、そのようなサイトはあります。

1人前、2人前、3人前・・・というように容量を記載してくれるサイトも。
でも、ほとんどありませんし、あっても有料会員登録が必要です。

どのレシピサイトも標準で実装すべき機能だと思うのですけどね。。。
技術的には、それほど大変なことでもありません。

ただし、最初の設計がダメだとかなり大変にはなります。
どういうことかと言うと、すべて数値で管理しておけばなんとかなります。

ダメな例で説明します。
クックパッドは、典型的なダメな設計です。
あくまで、検索する場合においてですけどね。

上記の値に関して、システムで扱うために妥協した値と理想の値を記載します。

材料名現実の値妥協した値理想の値
牛すじ肉400gくらい400g400g
こんにゃく一枚1枚300g
材料が隠れるくらい800ml800ml
味噌大さじ2~大さじ3大さじ2.522.5g

妥協した値では、半角数値に変換しています。
理想の値では、測定可能な数値に落とし込んでいます。

クックパッドは、ユーザーに自由に入力させています。
だから、基本的にシステムで扱うことは困難です。

そもそも、材料自体も呼称がバラバラですからね。
おそらく、材料マスタというものが無いのでしょう。
この点からも、検索で扱うのは不可と言えます。

その点、クラシルは運営側が管理しているため、なんとかなりそうです。
材料マスタのようなモノの存在も確認できています。

クラシルのデータ確認

「薄切り肉で梅しそチーズとんかつ レシピ・作り方」
https://www.kurashiru.com/recipes/ec28c79c-07ff-4e3e-b011-be628982eec3

このレシピの材料は以下。

バカ正直にこの部分をスクレイピングすれば、「アウト」だったでしょう。
「豚ロース (薄切り、8枚)」という材料なんて、マスタ化できないですからね。

そして、ページを分析すると使える情報を見つけました。

<script type="application/json" id="hydration_initial_state">{"path":"/recipes/ec28c79c-07ff-4e3e-b011-be628982eec3","userAgent":"Ruby, Mozilla/5.0 (Windows NT 10.0; WOW64; Trident/7.0; rv:11.0) like Gecko","state":{"fetchVideo":{"data":{"data":{"id":"ec28c79c-07ff-4e3e-b011-be628982eec3","type":"videos","attributes":{"title":"薄切り肉で梅しそチーズとんかつ","detail-content":"【材料】 2人前\r\n豚ロース(薄切り、8枚)   300g\r\n塩こしょう          小さじ1/4\r\n大葉             2枚\r\n梅肉             小さじ1\r\nスライスチーズ        2枚\r\n\r\n----- バッター液 -----\r\n薄力粉            大さじ3\r\n卵(Mサイズ)        1個\r\n水              大さじ1.5\r\n\r\nパン粉            大さじ3\r\nウスターソース        適量\r\nキャベツ(千切り)      20g\r\nミニトマト          4個\r\n揚げ油            適量\r\n\r\n【手順】\r\n1. 大葉は軸を切り落とし半分に切ります。\r\n2. スライスチーズは半分に切ります。\r\n3. 豚ロースを2枚重ね梅肉、1、2をのせたら豚ロースをさらに2枚重ねます。これをもう1組作り両面に塩こしょうをふります。\r\n4. バッター液の材料をバットに入れ、泡立て器で混ぜ合わせます。\r\n5. 3の全体につけ、パン粉をまぶします。\r\n6. フライパンの底から3cmの高さまで揚げ油を注ぎ、170℃に熱して5を入れてきつね色になり火が通るまで3分ほど揚げ、油切りをします。\r\n7. 皿に盛り付けウスターソースをかけ、キャベツ、ミニトマトを添えたら完成です。\r\n\n【コツ・ポイント】\nバッター液を作る際は、菜箸ではなく泡立て器でしっかりと混ぜ合わせることで薄力粉がダマになるのを防ぎます。","publish-status":"published","mp4-url":"https://video.kurashiru.com/production/videos/ec28c79c-07ff-4e3e-b011-be628982eec3/original.mp4","hls-url":"https://video.kurashiru.com/production/videos/ec28c79c-07ff-4e3e-b011-be628982eec3/hlsv2/master.m3u8","webm-url":"https://video.kurashiru.com/production/videos/ec28c79c-07ff-4e3e-b011-be628982eec3/webm.webm","thumbnail-square-small-url":"https://video.kurashiru.com/production/videos/ec28c79c-07ff-4e3e-b011-be628982eec3/compressed_thumbnail_square_small.jpg?1597820343","thumbnail-square-normal-url":"https://video.kurashiru.com/production/videos/ec28c79c-07ff-4e3e-b011-be628982eec3/compressed_thumbnail_square_normal.jpg?1597820343","thumbnail-square-large-url":"https://video.kurashiru.com/production/videos/ec28c79c-07ff-4e3e-b011-be628982eec3/compressed_thumbnail_square_large.jpg?1597820343","cooking-time":20,"content-type":"normal","sponsored":"","published-at":"2020-08-20T14:01:10.000+09:00","introduction":"豚ロースに大葉、梅肉チーズをサンドしたとんかつのご紹介です。バッター液を衣に使用することで、しっかりとパン粉が付きサクッとした食感に仕上りますよ。とろりと溶け出すチーズと梅肉の酸味、大葉の風味がさっぱりとしておいしいです。ぜひお試しください。","ingredients":[{"id":910,"type":"ingredients","group-id":null,"name":"豚ロース (薄切り、8枚)","actual-name":"豚ロース","quantity-amount":"300g"},{"id":1725,"type":"ingredients","group-id":null,"name":"塩こしょう","actual-name":"塩こしょう","quantity-amount":"小さじ1/4"},{"id":99,"type":"ingredients","group-id":null,"name":"大葉","actual-name":"大葉","quantity-amount":"2枚"},{"id":955,"type":"ingredients","group-id":null,"name":"梅肉","actual-name":"梅肉","quantity-amount":"小さじ1"},{"id":1102,"type":"ingredients","group-id":null,"name":"スライスチーズ","actual-name":"スライスチーズ","quantity-amount":"2枚"},{"type":"heading","id":25239,"title":"バッター液"},{"id":342,"type":"ingredients","group-id":25239,"name":"薄力粉","actual-name":"薄力粉","quantity-amount":"大さじ3"},{"id":76,"type":"ingredients","group-id":25239,"name":"卵 (Mサイズ)","actual-name":"卵","quantity-amount":"1個"},{"id":179,"type":"ingredients","group-id":25239,"name":"水","actual-name":"水","quantity-amount":"大さじ1.5"},{"id":71,"type":"ingredients","group-id":null,"name":"パン粉","actual-name":"パン粉","quantity-amount":"大さじ3"},{"id":135,"type":"ingredients","group-id":null,"name":"ウスターソース","actual-name":"ウスターソース","quantity-amount":"適量"},{"id":3348,"type":"ingredients","group-id":null,"name":"キャベツ (千切り)","actual-name":"キャベツ","quantity-amount":"20g"},{"id":412,"type":"ingredients","group-id":null,"name":"ミニトマト","actual-name":"ミニトマト","quantity-amount":"4個"},{"id":1746,"type":"ingredients","group-id":null,"name":"揚げ油","actual-name":"揚げ油","quantity-amount":"適量"}],"ingredients-inline":"豚ロース、大葉、梅肉、スライスチーズ、塩こしょう、キャベツ、ミニトマト、薄力粉、卵、水、パン粉、揚げ油、ウスターソース","expense":500,"cooking-time-supplement":"","memo":"バッター液を作る際は、菜箸ではなく泡立て器でしっかりと混ぜ合わせることで薄力粉がダマになるのを防ぎます。","servings":"2人前","instructions":[{"type":"instructions","sort-order":1,"body":"大葉は軸を切り落とし半分に切ります。"},{"type":"instructions","sort-order":2,"body":"スライスチーズは半分に切ります。"},{"type":"instructions","sort-order":3,"body":"豚ロースを2枚重ね梅肉、1、2をのせたら豚ロースをさらに2枚重ねます。これをもう1組作り両面に塩こしょうをふります。"},{"type":"instructions","sort-order":4,"body":"バッター液の材料をバットに入れ、泡立て器で混ぜ合わせます。"},{"type":"instructions","sort-order":5,"body":"3の全体につけ、パン粉をまぶします。"},{"type":"instructions","sort-order":6,"body":"フライパンの底から3cmの高さまで揚げ油を注ぎ、170℃に熱して5を入れてきつね色になり火が通るまで3分ほど揚げ、油切りをします。"},{"type":"instructions","sort-order":7,"body":"皿に盛り付けウスターソースをかけ、キャベツ、ミニトマトを添えたら完成です。"}],"breadcrumbs":[["薄切り肉で梅しそチーズとんかつ","ec28c79c-07ff-4e3e-b011-be628982eec3","videos"]],"related-categories":[],"favorite-count":null,"view-count":null,"calorie":null,"protein":null,"fat":null,"carbohydrate":null,"salt":null},"relationships":{"video-tags":{"data":[{"id":"976","type":"video-tags"}]},"article":{"data":null}}},"included":[{"id":"976","type":"video-tags","attributes":{"name":"トンカツ","slug":"トンカツ","count":10,"description":""}}],"meta":{"favorited":false}},"status":200},"fetchRelatedVideos":null,"fetchVideoQuestions":null,"fetchPopularWords":{"data":{"data":[{"id":"201","type":"suggest-words","attributes":{"word":"オクラ","jump-path":"/search?query=%E3%82%AA%E3%82%AF%E3%83%A9"},"relationships":{"videos":{"data":[]}}},{"id":"67","type":"suggest-words","attributes":{"word":"ゴーヤ","jump-path":"/search?query=%E3%82%B4%E3%83%BC%E3%83%A4"},"relationships":{"videos":{"data":[]}}},{"id":"36","type":"suggest-words","attributes":{"word":"ピーマン","jump-path":"/search?query=%E3%83%94%E3%83%BC%E3%83%9E%E3%83%B3"},"relationships":{"videos":{"data":[]}}},{"id":"106","type":"suggest-words","attributes":{"word":"ホットケーキミックス","jump-path":"/search?query=%E3%83%9B%E3%83%83%E3%83%88%E3%82%B1%E3%83%BC%E3%82%AD%E3%83%9F%E3%83%83%E3%82%AF%E3%82%B9"},"relationships":{"videos":{"data":[]}}},{"id":"137","type":"suggest-words","attributes":{"word":"そうめん","jump-path":"/search?query=%E3%81%9D%E3%81%86%E3%82%81%E3%82%93"},"relationships":{"videos":{"data":[]}}}]},"status":200},"fetchFooterVideoSearchCategories":null}}</script><script>

htmlソースに上記が記述されているのです。
正体は、レシピの情報のjsonデータです。

おそらく、JavaScriptで利用するためにjsonとして吐き出しているのでしょう。
それをそのまま取得しましょう。

必要な情報は、すべてこのjsonに存在しています。
さらに、「豚ロース (薄切り、8枚)」の件に関してもクリアです。

{"id":910,"type":"ingredients","group-id":null,"name":"豚ロース (薄切り、8枚)","actual-name":"豚ロース","quantity-amount":"300g"}

actual-nameである「豚ロース」がマスタ上の名前なのでしょう。
そして、idが910です。
nameである「豚ロース (薄切り、8枚)」は、レシピごとで表記を変更すると思われます。

また、容量に関してもある程度の規則性があるようです。
1000件ぐらいレシピデータを集めて、規則を見つけ出す必要はあるでしょうけどね。

actual-namequantity-amount
豚ロース300g
塩こしょう小さじ1/4
大葉2枚
梅肉小さじ1
スライスチーズ2枚
薄力粉大さじ3
1個
大さじ1.5
パン粉大さじ3
ウスターソース適量
キャベツ20g
ミニトマト4個
揚げ油適量

クラシルのデータを収集すれば、材料マスタを作成できそうです。
そして、容量を数値(測定可能な値)として保存できそうです。

これができれば、「材料の容量によるレシピ検索機能」は実現可能です。

また、何人前かという情報も取れます。

"servings":"2人前"

今までに挙げてきたデータに加えて、「servings」を加えれば1人前あたりのレシピが作成可能です。
そうなれば、「人数指定による材料計算機能」も実現可能となります。

クラシルのスクレイピング

今回、スクレイピングはそれほどしていません。
jsonデータが丸ごと取得できましたので。

他のサイトもjsonデータを用意してくれれば、もっと簡単にスクレイピングできるのですけどね。
どんなふうに対策しても、スクレイピングは可能ですから。

以下が、レシピページをスクレイピングするサンプルコードです。
2020年08月21日時点では動いています。

import requests
import re
from bs4 import BeautifulSoup
import json

# User-Agent
user_agent = "Mozilla/5.0 (Windows NT 6.1; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/Chrome/84.0.4147.135 Safari/537.36"

def get_html(url, params=None, headers=None):

    try:
        # データ取得
        resp = requests.get(url, params=params, headers=headers)
        resp.encoding = 'utf8'  # 文字コード
        
        # 要素の抽出
        soup = BeautifulSoup(resp.text, "html.parser")
        return soup
    except Exception as e:
        return None

def get_page_info(url):
    
    # 結果
    result = {}
        
    # データ取得
    params = {}
    headers = {"User-Agent": user_agent}
    soup = get_html(url, params, headers)

    if soup != None:
        
        try:
            json_str_elem = soup.find(id="hydration_initial_state")
            json_str = get_text_by_elem(json_str_elem)
            json_load = json.loads(json_str)
            data_dic = json_load["state"]["fetchVideo"]["data"]["data"]
            ingredients_inline = data_dic["attributes"]
            
            if data_dic:
                recipe_id = data_dic["id"]
                recipe_name = data_dic["attributes"]["title"]
                detail = data_dic["attributes"]["detail-content"]
                video_url = data_dic["attributes"]["mp4-url"]
                img_small_url = data_dic["attributes"]["thumbnail-square-small-url"]
                img_normal_url = data_dic["attributes"]["thumbnail-square-normal-url"]
                img_large_url = data_dic["attributes"]["thumbnail-square-large-url"]
                cooking_time = data_dic["attributes"]["cooking-time"]
                published_datetime = data_dic["attributes"]["published-at"]
                introduction = data_dic["attributes"]["introduction"]
                ingredients = data_dic["attributes"]["ingredients"]
                ingredients_inline = data_dic["attributes"]["ingredients-inline"]
                expense = data_dic["attributes"]["expense"]
                memo = data_dic["attributes"]["memo"]
                servings_str = data_dic["attributes"]["servings"]
                
                servings_val = 0
                if servings_str:
                    servings_dic = re.findall(r'\d+', servings_str)
                    servings_val = servings_dic[0]
                
                result["recipe_id"] = recipe_id
                result["recipe_name"] = recipe_name
                result["detail"] = detail
                result["video_url"] = video_url
                result["img_small_url"] = img_small_url
                result["img_normal_url"] = img_normal_url
                result["img_large_url"] = img_large_url
                result["cooking_time"] = cooking_time
                result["published_datetime"] = published_datetime
                result["introduction"] = introduction
                result["ingredients_inline"] = ingredients_inline
                result["introduction"] = introduction
                result["expense"] = expense
                result["memo"] = memo
                result["servings_str"] = servings_str
                result["servings_val"] = servings_val
                    
                ingredient_list = []
                
                for ingredient in ingredients:
                    
                    ingredient_dic = {}
                    
                    ingredient_id = ingredient["id"]
                    ingredient_type = ingredient["type"]
                    
                    if ingredient_type=="ingredients":
                        ingredient_group_id = ingredient["group-id"]
                        ingredient_title = ingredient["name"]
                        ingredient_name = ingredient["actual-name"]
                        ingredient_amount = ingredient["quantity-amount"]
                        
                    else:
                        ingredient_title = ingredient["title"]
                        ingredient_group_id = 0
                        ingredient_name = ""
                        ingredient_amount = ""
                    
                    ingredient_dic["ingredient_id"] = ingredient_id
                    ingredient_dic["ingredient_type"] = ingredient_type
                    ingredient_dic["ingredient_title"] = ingredient_title
                    ingredient_dic["ingredient_name"] = ingredient_name
                    ingredient_dic["ingredient_amount"] = ingredient_amount
                    
                    ingredient_list.append(ingredient_dic)
                    
                result["ingredient_list"] = ingredient_list
                    
        except Exception as e:
            return None
    else:
        print("エラー")
    
    return result

def get_text_by_elem(elem):
    
    try:
        text = elem.text
        text = text.strip()  
        return text
    except Exception as e:
        return None
    
if __name__ == '__main__':
    
    url = "https://www.kurashiru.com/recipes/ec28c79c-07ff-4e3e-b011-be628982eec3"
    
    result = get_page_info(url)
    print(result)

別のページをスクレイピングする場合は、下記のURLを変更します。

url = "https://www.kurashiru.com/recipes/ec28c79c-07ff-4e3e-b011-be628982eec3"
タイトルとURLをコピーしました