ひさふぃの日記

DjangoとPythonとLaravelが好き。大阪でフリーランスエンジニアやってます。

Pythonのコードをオブジェクト指向的にリファクタリングした

以前、書いたコードが読みづらいという指摘をしてもらっていたので、オブジェクト指向的に書くための知識を学習しました。

師匠に教えてもらった知識+javaの初心者サイトを活用。

shikouno.hatenablog.com

なんとなく把握できたため、Pythonでベタ書きしていたレシピサイトをスクレイピングするコードを書き直しました。

書き直す前のコードは過去記事を参照してください。

shikouno.hatenablog.com

よりオブジェクト指向的に書くためのアドバイスを師匠からもらっているのですが、読みやすくはなっているとのことなのでとりあえず一区切り。

現時点でどのように考えているかの備忘録を兼ねて書いていきます。

おまじない + メインクラス

メインクラスにインスタンスとして渡すクラスは3つです。

クラスの説明は後述。

各サイト個別のメソッドを記述し、それをまとめるメインのメソッド(get_and_make_content)を実行します。

サイト個別のメソッドをまとめた方がいいように見えたのですが、どうすればいいのかわからず断念。

師匠に聞いたところ、クラスを作ってしまって隠蔽するのが良いとのこと。なるほど、スッキリする。

# !/bin/usr/env python
# -*- coding: utf-8 -*-

from bs4 import BeautifulSoup
import requests
import re

class Main_Content:
    def __init__(self):
        self.url_list_collector = UrlListCollector()
        self.recipefilter = RecipeFilter()
        self.tablegenerator = TableGenerator()

    def get_and_make_content(self, word):
        self.make_orange_content(word)
        self.make_recipeblog_content(word)
        self.make_rakuten_content(word)
        print('complete method')

    def make_orange_content(self, word):
        orange_url_list = self.url_list_collector.get_orange(word)
        for url in orange_url_list:
            content = self.recipefilter.get_orange(url)
            content_table = self.tablegenerator.extract_content(content)
            print(content_table)

    def make_recipeblog_content(self, word):
        recipeblog_url_list = self.url_list_collector.get_recipeblog(word)
        for url in recipeblog_url_list:
            content = self.recipefilter.get_recipeblog(url)
            content_table = self.tablegenerator.extract_content(content)
            print(content_table)

    def make_rakuten_content(self, word):
        rakuten_url_list = self.url_list_collector.get_rakuten(word)
        for url in rakuten_url_list:
            content = self.recipefilter.get_rakuten(url)
            content_table = self.tablegenerator.extract_content(content)
            print(content_table)

メインクラスに渡すクラス

UrlListCollector:検索ワードを受取り、個別のレシピベージのURLのリストを取得
RecipeFileter:レシピのURLを受取り、表示に使う部分のタグを取得
TableGenerator:タグが含まれた項目を受取り、タグの中身を抽出したものを取得

RequestsAndBS4Gateway:BeautifulSoupをいい感じに処理してくれるクラス

ここでも各サイトの処理をまとめて抽象化クラスを作成するのがいいのですが、どうすればいいのか思いつかずに断念。

師匠に聞いたところ、クラスの戻り値を変えるのがいいとのこと。

今回のクラスですと戻り値が各サイトごとに異なるため、渡すクラスで戻り値が異なる点を考慮する必要があります。

サイトに依らない戻り値に統一すると、共通の処理を当てはめることができる。なるほど、抽象化しやすい。

class TableGenerator:
    def extract_content(self, content):
        title, image = content[0]['alt'], content[0]['src']
        if content[1] != None:
            numbers = content[1].text
        else:
            numbers = "人数表記なし"
        ingredient = [i.text + ":" + j.text for i, j in zip(content[2], content[3])]
        return title, image, numbers, ingredient

class RecipeFilter:
    def __init__(self):
        self.getting_gateway = RequestsAndBS4Gateway()

    def get_orange(self, url):
        recieved_html = self.getting_gateway.get_and_parse(url)
        img = recieved_html.find("img", {"itemprop":"photo"})
        numbers = recieved_html.find("h2")
        name = recieved_html.find_all("span" , {"itemprop":"name"})
        amount = recieved_html.find_all("span" , {"itemprop":"amount"})
        return img, numbers, name, amount

    def get_recipeblog(self, url):
        recieved_html = self.getting_gateway.get_and_parse(url)
        img = recieved_html.find("img", {"class":"photo"})
        numbers = recieved_html.find("span", {"class":"yield"})
        if numbers == None:
            numbers = recieved_html.find("h2", {"class":"mainTitle"})
        name = recieved_html.find_all("span" , {"class":"name"})
        amount = recieved_html.find_all("span" , {"class":"amount"})
        return img, numbers, name, amount

    def get_rakuten(self, url):
        recieved_html = self.getting_gateway.get_and_parse(url)
        img = recieved_html.find("img", {"itemprop":"image"})
        numbers = recieved_html.find("span", {"itemprop":"recipeYield"})
        name = recieved_html.find_all("a" , {"class":"name"})
        amount = recieved_html.find_all("p" , {"class":"amount"})
        return img, numbers, name, amount

class UrlListCollector:
    def __init__(self):
        self.getting_gateway = RequestsAndBS4Gateway()

    def get_orange(self, word):
        orange_search_url ="http://www.orangepage.net/recipes/search?utf8=✓\
&search_recipe%5Bkeyword%5D={}&searchBtn1=".format(word)
        soup = self.getting_gateway.get_and_parse(orange_search_url)
        tag = soup.find("div", {"class":"horizontal3Col"})
        tag2 = tag.find_all('a',href=re.compile("/recipes/detail"))
        url_list = {'http://www.orangepage.net{}'.format(i["href"]) for i in tag2}
        return url_list

    def get_recipeblog(self, word):
        search_url = ("http://www.recipe-blog.jp/search/recipe?keyword={}\
&type=recipe&sort_order=score".format(word))
        soup = self.getting_gateway.get_and_parse(search_url)
        tag = soup.find("div", {"class":"searchListArea"})
        tag2 = tag.find_all('a',href=re.compile("profile"))
        url_list = {i.get('href') for i in tag2}
        return url_list

    def get_rakuten(self, word):
        search_url = ("https://recipe.rakuten.co.jp/search/{}/".format(word))
        soup = self.getting_gateway.get_and_parse(search_url)
        tag = soup.find_all('a',href=re.compile("/recipe/"))
        url_list = {'http://recipe.rakuten.co.jp{}'.format(i["href"]) for i in tag}
        return url_list

class RequestsAndBS4Gateway:
    def get_and_parse(self, url):
        soup_content = []
        req = requests.get(url)
        req.encoding = "utf-8"
        soup_content = BeautifulSoup(req.text, "lxml")
        return soup_content

まとめ

まだ最終形ではありませんが見通しはよくなりました。次の目標は師匠のアドバイスを参考にさらに抽象化度をあげること。

しかし、リファクタリング以外にもやりたいことが山積みですので、そちらに軸足を移そうかと考えています。

その際に、今回の学びを生かして最初から見通しの良いものを書いていきたいですね。

最後までお読みいただきありがとうございました。