Python中級 - Python中級のチャレンジ - そこそこ複雑な注文集計処理を作る演習問題 - 解説コメント

main.py の各処理にコメントを入れています。

  1from datetime import datetime, date
  2import os
  3import re
  4
  5# ファイルの定義(各ファイル名、ディレクトリ名を定数に代入)
  6ITEM_TSV_PATH = 'items.tsv'
  7ORDER_DIR = 'order/'
  8FAILURE_DIR = 'failure/'
  9DELIVERY_DIR = 'delivery/'
 10
 11
 12class Item:
 13    """ 1商品に対応するクラス
 14    """
 15    def __init__(self, item_id, name, price):
 16        # 各属性を初期化
 17        self.item_id = item_id  # 商品ID
 18        self.name = name  # 商品名
 19        self.price = price  # 価格
 20
 21
 22class Items:
 23    """ 商品の一覧に対応するクラス
 24    """
 25    def __init__(self, items):
 26        #  属性itemsにItemインスタンスをリストに持つ値を代入する
 27        self.items = items
 28
 29    def has_id(self, item_id):
 30        """ item_id をもつ商品が存在するかチェックする
 31        """
 32        # Itemsに含まれる商品一覧に引数で指定されたitem_idを持つ商品が存在するか確認
 33        for item in self.items:  # ここの変数itemは、Itemのインスタンス
 34            # 右辺のitem_idは注文ファイル側の商品ID
 35            if item.item_id == item_id:
 36                return True
 37        return False
 38
 39
 40class Order:
 41    """ 1つの注文を表すクラス
 42    """
 43    AMOUNT_RE = re.compile(r'^[0-9]+$')
 44    TEL_RE = re.compile(r'^[0-9]{2,4}-[0-9]{4}-[0-9]{4}$')
 45
 46    def __init__(self, item_id, amount, shipping_address, tel_number,
 47                 fullname, shipping_date_str, order_file):
 48        # 各属性を初期化(注文ファイルの1行分の内容を各属性に代入)
 49        self.item_id = item_id  # 商品ID
 50        self.amount = amount  # 個数(文字列)
 51        self.shipping_address = shipping_address  # 宅配住所
 52        self.tel_number = tel_number  # 電話番号
 53        self.fullname = fullname  # 氏名
 54        self.shipping_date_str = shipping_date_str  # 宅配日(文字列)
 55        self.order_file = order_file  # 元のファイル名
 56
 57        self.amount_int = None  # 個数(数値)
 58        self.shipping_date = None  # 宅配日(datetime)
 59
 60    def validate(self, items):
 61        """ 各注文の値が正しいかバリデーションチェックする。OKの場合True、NGの場合False
 62        """
 63        # 商品マスタ(引数のitems)に存在する商品ID(item_id)が存在するか確認
 64        if not items.has_id(self.item_id):
 65            return False
 66        # 個数amountが数値で指定されているか正規表現で確認
 67        if not self.AMOUNT_RE.search(self.amount):
 68            return False
 69        # ファイルから取り出して文字列なので、整数型に変更
 70        self.amount_int = int(self.amount)
 71        # 値がマイナスでないか確認
 72        if self.amount_int <= 0:
 73            return False
 74        # 宅配先住所に値が設定されているか確認
 75        if not self.shipping_address:
 76            return False
 77        # 電話番号が正しい形式で指定されてるか正規表現で確認
 78        if not self.TEL_RE.search(self.tel_number):
 79            return False
 80        # 名前が指定されているか確認
 81        if not self.fullname:
 82            return False
 83        # 宅配日が正しく日付として扱えるか、変換して確認
 84        # 変換の途中でエラーが起きたら、日付として正しくない
 85        try:
 86            self.shipping_date = datetime.strptime(self.shipping_date_str, '%Y-%m-%d')
 87        except ValueError:
 88            return False
 89        # 全て成功したらTrueを返す
 90        return True
 91
 92    def row_string(self):
 93        # 1注文の一覧をカンマ(,)区切りで結合して、返す
 94        return ','.join((
 95            self.item_id,
 96            self.amount,
 97            self.shipping_address,
 98            self.tel_number,
 99            self.fullname,
100            self.shipping_date_str,
101            self.order_file,
102        ))
103
104
105def load_items():
106    """ ITEM_TSV_PATHのTSVからItemsを作る
107    """
108    # 読み込んだItemを保存しておくリスト
109    items = []
110    # 商品マスター(items.tsv)を読み込む
111    with open(ITEM_TSV_PATH, encoding='utf-8') as f:
112        # 1行ずつ処理
113        for row in f:
114            # 1行の文字列をタブ文字(\t)で分割
115            # 商品ID(item_id)、商品名(name)、価格(price)の各変数に値を代入
116            item_id, name, price = row.split('\t')
117            # Item(1商品のデータ)作成
118            item = Item(item_id.strip(), name.strip(), price.strip())
119            # リストに追加
120            items.append(item)
121    # Items(全商品のデータ)に追加して、Itemsを作成し、返す
122    return Items(items)
123
124
125def load_orders(target_date):
126    """ ORDER_DIR のCSVからOrderのリストを作る
127
128    * 各値の前後から空白を除外する
129    """
130    date_str = f'{target_date:%Y%m%d}'
131    orders = []
132    # 注文受付ファイル(order/の下のcsvファイル)読み込み
133    for filename in os.listdir(ORDER_DIR):
134        if date_str not in filename:
135            # 対象日でないファイルは無視する
136            continue
137
138        filepath = os.path.join(ORDER_DIR, filename)
139        with open(filepath, encoding='utf-8') as f:
140            for row in f:
141                # 各行のデータからOrder(1注文のデータ)を作成
142                item_id, amount, address, tel, name, shipping_date = row.split(',')
143                order = Order(
144                    item_id.strip(),
145                    amount.strip(),
146                    address.strip(),
147                    tel.strip(),
148                    name.strip(),
149                    shipping_date.strip(),
150                    filename,
151                )
152                # リストordersに作成した注文データを追加
153                orders.append(order)
154    # 読み込んだ注文のリストを作成し、返す
155    return orders
156
157
158def write_deliver_orders(orders):
159    """ Orderのリストを受け取って日別注文ファイルに書き込み
160    """
161    # 宅配日毎に集計。ファイルをオープンする回数を減らすため事前にまとめる
162    date_orders = {}
163    for order in orders:
164        if order.shipping_date in date_orders:
165            date_orders[order.shipping_date].append(order)
166        else:
167            date_orders[order.shipping_date] = [order]
168
169    for d, day_orders in date_orders.items():
170        filename = 'delivery_{}.csv'.format(d.strftime('%Y%m%d'))
171        filepath = os.path.join(DELIVERY_DIR, filename)
172        with open(filepath, 'a', encoding='utf-8') as f:
173            for order in day_orders:
174                f.write(order.row_string() + '\n')
175
176
177def write_failure_orders(orders, order_date):
178    """ Orderのリストを受け取って注文受付失敗ファイルに書き込み
179    """
180    filename = 'failure_{}.csv'.format(order_date.strftime('%Y%m%d'))
181    filepath = os.path.join(FAILURE_DIR, filename)
182    with open(filepath, 'a', encoding='utf-8') as f:
183        for order in orders:
184            f.write(order.row_string() + '\n')
185
186
187def main(target_date=None):
188    """ 毎日の注文集計用スクリプト
189
190    1. 商品マスター読み込み
191    2. 当日分の注文受付ファイル読み込み
192    3. 注文をバリデーションチェック
193    4. 日別注文ファイル書き込み
194    5. 注文受付失敗ファイル書き込み
195    """
196    # 集計を行う日付を決め、変数target_dateに指定
197    # 引数としてtarget_dateが指定されていれば、
198    # 指定された日(今回の場合は、2016-12-14)
199    # 引数のtarget_dateがNoneの場合は、実行時の日付を代入
200    target_date = target_date or date.today()
201
202    # 1. 商品マスター読み込み(load_items関数を呼び出し)
203    # 結果を変数itemsに代入
204    items = load_items()
205
206    # 2. 当日分の注文受付ファイル読み込み(関数load_ordersを呼び出し)
207    # 結果を変数ordersに代入
208    orders = load_orders(target_date)
209
210    # 保存用に空のリストvalidated_ordersとfailed_ordersを作成
211    # 注文受け付けできる注文の場合は、リストvalidated_ordersに追加、
212    # 注文受付失敗の注文の場合は、リストfailed_ordersに追加していきます。
213    validated_orders = []
214    failed_orders = []
215    # 注文リストを1つずつ確認
216    for order in orders:
217        # 3. 注文をバリデーションチェック
218        # 注文が正しいかチェック
219        # Orderクラスに定義されているvalidateメソッドで確認
220        # このチェックですべての確認が成功したらTrueが返り、失敗したらFalseが返ります。
221        if order.validate(items):
222            # 戻り値がTrueの場合はリストvalidated_ordersに追加
223            validated_orders.append(order)
224        else:
225            # Falseの場合は、リストfailed_ordersに追加します。
226            failed_orders.append(order)
227
228    if validated_orders:
229        # 4. 日別注文ファイル書き込み
230        write_deliver_orders(validated_orders)
231    if failed_orders:
232        # 5. 注文受付失敗ファイル書き込み
233        write_failure_orders(failed_orders, target_date)
234
235# Start
236if __name__ == '__main__':
237    # 注文を確認する日付として動作確認用に2016-12-14を指定
238    # main関数を呼び出し
239    main(date(2016, 12, 14))  # あくまで動作確認用に日付を指定している
当コンテンツの知的財産権は株式会社ビープラウドに所属します。詳しくは利用規約をご確認ください。