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)) # あくまで動作確認用に日付を指定している