0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

【Python初学者】楽天価格チェッカーを作ってみた ⑧(最終回) ― 全体統括(main_flow.py)

0
Posted at

【Python初学者】楽天価格チェッカーを作ってみた ⑧(最終回) ― 全体統括(main_flow.py)

はじめに🐰

Python初学者のわたしが覚えるために、学んだことを整理し、理解を深めるために記事を書いています。
私が実際にやってみて、悩んだ部分や過程などを残していきます🐥シリーズ最終回です!

今回は main_flow.py の解説です。これまでのファイルすべてを組み合わせて、プログラム全体の処理を順番に実行するクラスです。「指揮者」のような役割を担っています。


本日のゴール⚽️

  • クラスの __init__ で複数のオブジェクトを準備する方法を理解する
  • try / except で予期せぬエラーを安全に処理できるようになる
  • 処理をメソッドに分割する設計の考え方を理解する

完成したコード

import os
import sys

sys.path.append(os.path.abspath(os.path.join(os.path.dirname(__file__), "./")))

from api.rakuten_api import RakutenAPI
from price_tools.price_list_builder import PriceListBuilder
from price_tools.price_stats import PriceStats
from utils.csv_saver import CsvSaver
from utils.popup import PopupManager


class MainFlow:
    def __init__(self):
        # ① 各機能クラスをここで準備する(まとめて持っておく)
        self.api     = RakutenAPI()
        self.builder = PriceListBuilder()
        self.stats   = PriceStats()
        self.csv     = CsvSaver()

    # -----------------------
    # ① キーワード入力(ユーザー操作)
    # -----------------------
    def input_keywords(self) -> tuple[str, str]:
        raw_input = PopupManager.ask_keywords()

        # カンマ区切り → リスト化(空白は削除)
        keyword_list = [kw.strip() for kw in raw_input.split(",") if kw.strip()]

        # 表示用(見やすい形)
        display_keyword = ", ".join(keyword_list)

        # API用(スペース区切り)
        api_keyword = " ".join(keyword_list)

        return display_keyword, api_keyword

    # -----------------------
    # ② 楽天APIから商品取得(複数ページ対応)
    # -----------------------
    def fetch_all_products(self, api_keyword: str, max_pages: int = 5) -> list[dict]:
        all_products = []

        for page in range(1, max_pages + 1):
            api_data = self.api.search(api_keyword, page=page)

            if not api_data:
                break

            products = self.api.format_product_data(api_data)

            if not products:
                break

            all_products.extend(products)

        return all_products

    # -----------------------
    # ③ データ加工(フィルタ・並び替え・集計)
    # -----------------------
    def process_products(self, raw_list: list[dict]) -> tuple[list[dict], dict]:

        # 条件に合う商品のみ残す
        filtered = self.builder.filter_list(raw_list)

        if not filtered:
            return [], {}

        # 安い順に並び替え
        sorted_list = self.builder.sort_by_price(filtered)

        # 最小・最大・平均などを計算
        summary = self.stats.get_price_summary(sorted_list)

        return sorted_list, summary

    # -----------------------
    # ④ 結果表示(画面出力)
    # -----------------------
    def print_result(self, display_keyword: str, sorted_list: list[dict], summary: dict):

        print("\n" + "=" * 50)
        print(f"検索結果:{display_keyword}")
        print("=" * 50)

        if not sorted_list:
            print("該当商品が見つかりませんでした。")
            return

        print(f"件数:{len(sorted_list)}")
        print(f"最安:{summary.get('min_price', '-'):,}")
        print(f"平均:{summary.get('avr_price', '-'):,}")
        print(f"最高:{summary.get('max_price', '-'):,}")
        print("-" * 50)

        print("▼ 上位10件(安い順)")
        for i, p in enumerate(sorted_list[:10], start=1):
            print(f"{i:>2}. {p['price']:>8,}{p['name'][:40]}")

    # -----------------------
    # ⑤ CSV保存
    # -----------------------
    def save_csv(self, display_keyword: str, sorted_list: list[dict], summary: dict):
        filepath = self.csv.save(
            keyword=display_keyword,
            summary=summary,
            product_list=sorted_list,
            count=len(sorted_list)
        )

        PopupManager.show_complete(filepath)

    # -----------------------
    # ⑥ 全体の流れ(司令塔)
    # -----------------------
    def run(self):

        try:
            # ① キーワード入力
            display_keyword, api_keyword = self.input_keywords()

            # ② API取得
            raw_list = self.fetch_all_products(api_keyword)

            if not raw_list:
                print("商品が取得できませんでした")
                return

            # ③ 加工
            sorted_list, summary = self.process_products(raw_list)

            # ④ 表示
            self.print_result(display_keyword, sorted_list, summary)

            if not sorted_list:
                return

            # ⑤ CSV保存
            self.save_csv(display_keyword, sorted_list, summary)

        except ValueError as e:
            PopupManager.show_error(str(e))

        except Exception as e:
            PopupManager.show_error(f"予期せぬエラーが発生しました\n\n{e}")

コードの詳細解説

① main_flowは「司令塔」

def run(self):

ここは何も考えずに順番に呼んでいるだけです。
👉 料理でいうと「レシピ通りに工程を並べてるだけ」


② 処理をメソッドに分ける設計

def run(self) -> None:
    display_keyword, api_keyword = self.input_keywords()
    raw_list = self.fetch_all_products(api_keyword)
    sorted_list, summary = self.process_products(raw_list)
    self.print_result(display_keyword, sorted_list, summary)
    self.save_csv(display_keyword, sorted_list, summary)

run() メソッドは①〜⑤の処理を順番に呼び出すだけです。各処理の詳細は別メソッドに任せています。

なぜ分けるのか?

  • run() を見るだけで全体の流れが一目でわかる
  • 各メソッドが独立しているので、1つを修正しても他に影響しない
  • 部分的なテストが書きやすくなる

③ tuple で複数の値を返す

def input_keywords(self) -> tuple[str, str]:
    ...
    return display_keyword, api_keyword

# 呼び出し側
display_keyword, api_keyword = self.input_keywords()

Pythonはカンマで区切ることで複数の値をまとめて返せます(タプル)。受け取る側も同様にカンマで分けて受け取れます(アンパック)。

# タプルの基本
def get_name_age():
    return "えりこ", 25

name, age = get_name_age()
# name → "えりこ"
# age  → 25

④ split() と strip() でキーワードを整形する

keyword_list = [kw.strip() for kw in raw_input.split(",") if kw.strip()]

ユーザーが「青汁, 雑穀米 , 」と入力したとき(スペースや末尾のカンマが混じっている)、きれいに整形しています。

raw_input = "青汁, 雑穀米 , "

# split(",") でカンマ区切り
raw_input.split(",")
# → ["青汁", " 雑穀米 ", " ", ""]

# strip() で前後の空白を除去
# if kw.strip() で空文字を除外
keyword_list = [kw.strip() for kw in raw_input.split(",") if kw.strip()]
# → ["青汁", "雑穀米"]
メソッド 役割
split(",") カンマで区切りリストにする
strip() 文字列の前後の空白(スペース・改行)を除去する

⑤ for + range() でページ番号をループする

for page in range(1, max_pages + 1):

range(1, 6)1, 2, 3, 4, 5 を生成します(6は含まない)。max_pages = 5 のとき、range(1, 6) でページ1〜5をループします。

# range() の基本
range(5)        # → 0, 1, 2, 3, 4
range(1, 6)     # → 1, 2, 3, 4, 5
range(1, 10, 2) # → 1, 3, 5, 7, 9(2ずつ増える)

⑥ extend() でリストを結合する

all_products.extend(products)

append() は1つの要素を追加しますが、extend() はリストの中身を展開して追加します。

# append と extend の違い
a = [1, 2, 3]
b = [4, 5]

a.append(b)   # → [1, 2, 3, [4, 5]]  ← リストがそのまま入る
a.extend(b)   # → [1, 2, 3, 4, 5]   ← 中身が展開される

複数ページの商品をまとめて1つのリストにするため extend() を使っています。


⑦ f文字列の書式指定

print(f"最安価格:{summary.get('min_price', '-'):,}")
print(f"  {i:>2}. {product['price']:>8,}")

f文字列では {変数:書式} の形で表示形式を指定できます。

書式 意味
:, 3桁カンマ区切り 1980 → 1,980
:>8 右寄せ(幅8文字) 980 → 980
:<10 左寄せ(幅10文字) abc → abc

価格の列を揃えて表示するために使っています。


⑧ try / except で2種類のエラーを分けて処理する

try:
    ...
except ValueError as e:
    PopupManager.show_error(str(e))

except Exception as e:
    PopupManager.show_error(f"予期せぬエラーが発生しました。\n\n{e}")

ValueError はキャンセル時など「想定内のエラー」です。Exception はその他すべての「想定外のエラー」を受け取ります。

try
  ↓ エラー発生
except ValueError   ← まずこちらで確認(特定のエラー)
  ↓ 該当しない
except Exception    ← それ以外のすべてをここで受け取る

except は上から順番に評価されるため、特定のエラーを先に書き、汎用的な Exception は最後にするのがルールです。


このプロジェクトを通じて学んだこと

全9回を通じて、次のPython知識が使われていました。

ファイル 主な学び
rakuten_api.py requests、API連携、.envtry/except
price_list_builder.py リスト内包表記、any()sorted()lambda
price_stats.py min() max() sum()// 整数除算
csv_saver.py csvモジュール、datetime、ファイル操作
popup.py tkinter@staticmethod、イベント処理
path_helper.py pathlib.Path__file__getattr()
config.py 定数の分離管理
main_flow.py クラス設計、tuplesplit/stripextend()

まとめ

main_flow.py はこのプロジェクトの指揮者です。各クラスを __init__ で準備し、run() から順番に呼び出すだけのシンプルな設計にすることで、全体の流れが一目でわかるようになっています。

シリーズを通じて「実際に動くツールを作りながらPythonを学ぶ」ことを目標に書いてきました。間違いや改善点があれば、ぜひコメントで教えてください!


シリーズ一覧(完結)

  • ① プロジェクト全体像とフォルダ構成
  • ② 楽天APIリクエスト(rakuten_api.py)
  • ③ フィルタリング・並び替え(price_list_builder.py)
  • ④ 統計計算(price_stats.py)
  • ⑤ CSV保存(csv_saver.py)
  • ⑥ ポップアップUI(popup.py)
  • ⑦ パス管理・設定(path_helper.py & config.py)
  • ⑧ 全体統括(main_flow.py) ← 今回(最終回)
0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?