こんにちは!第6章:
客先勤務表リーダー
(ClientTimesheetReader)では、PDFやExcelといった様々な形式の客先勤務表ファイルを読み込むための「翻訳機」について学びましたね。リーダーがファイル形式を特定し、適切な専門家
(PDFReader や ExcelReader)
に処理を依頼するのでした。
しかし、ここで一つの疑問が残ります。その専門リーダーたちは、どうやって お客様ごと、あるいはファイルごとに微妙に異なるレイアウトや項目名 を理解し、正確に必要な情報(日付、時間、氏名など)を抜き出しているのでしょうか?
例えば、A社のPDFでは日付が表の左端にあるけれど、B社のExcelでは日付が表の真ん中あたりにあるかもしれません。休憩時間の書き方も、「1.0」時間だったり「60」分だったりするかもしれません。
これらの無数の「違い」に対応する魔法、それが今回学ぶ ファイル形式テンプレート (File Format Templates) なのです!
ファイル形式テンプレートとは、客先ごとに異なるPDFやExcelのレイアウトや項目名を解析するための、個別の「読み方説明書」あるいは「レシピ」のようなものです。
それぞれのテンプレートが、特定の客先フォーマットについて、
といった情報を具体的に定義しています。
第6章:
客先勤務表リーダー (ClientTimesheetReader)で登場した
PDFReader や ExcelReader
は、ファイルを受け取ると、まずそのファイルがどのテンプレート(読み方説明書)に合致するかを判断します。そして、見つけたテンプレートの指示に従って、ファイルの中から正確にデータを抽出していくのです。
鍵と鍵穴のアナロジーで考えてみましょう。
graph LR
subgraph Files ["様々な鍵 (ファイル)"]
F1[A社.pdf]
F2[B社.xlsx]
F3[C社_revised.pdf]
end
subgraph Templates ["鍵穴セット (テンプレート集)"]
T1(A社PDFテンプレート)
T2(B社Excelテンプレート)
T3(C社PDFテンプレート_改訂版)
T4(...)
end
subgraph Reader ["鍵師 (Reader)"]
R((Reader))
end
F1 -- 鍵 --> R;
F2 -- 鍵 --> R;
F3 -- 鍵 --> R;
R -- 鍵に合う鍵穴を探す --> Templates;
Templates -- 正しい鍵穴(T1)が見つかる --> R;
R -- データ抽出 --> D1{"ClientTimesheet (A社データ)"};
Templates -- 正しい鍵穴(T2)が見つかる --> R;
R -- データ抽出 --> D2{"ClientTimesheet (B社データ)"};
Templates -- 正しい鍵穴(T3)が見つかる --> R;
R -- データ抽出 --> D3{"ClientTimesheet (C社データ)"};
style D1 fill:#f9f,stroke:#333,stroke-width:2px
style D2 fill:#f9f,stroke:#333,stroke-width:2px
style D3 fill:#f9f,stroke:#333,stroke-width:2px
このように、テンプレートがあるおかげで、Reader
は多種多様なファイル形式とレイアウトに対応できるのです。
このプロジェクトでは、主に2種類のファイル形式に対応するためのテンプレート基盤があります。
PDFTemplate):
PDFファイルからデータを抽出するためのテンプレートの基盤(インターフェース)。具体的な客先PDFレイアウトに対応するクラス(例:
ClientKagaPDFTemplate,
ClientNecPDFTemplate)は、これを元に作られます。ExcelTemplate):
Excelファイル(.xlsx,
.xlsなど)からデータを抽出するためのテンプレートの基盤。同様に、具体的な客先Excelレイアウトに対応するクラス(例:
ClientCWExcelTemplate,
ClientC8ExcelTemplate)は、これを元に作られます。これらのテンプレートは、src/pdf_handler/templates/ や
src/excel_handler/templates/
といったフォルダの中に、客先ごとにファイルが分かれて置かれています。
第6章:
客先勤務表リーダー (ClientTimesheetReader)
で見たように、PDFReader や ExcelReader
がファイルを受け取った後の流れを、もう少し詳しく見てみましょう。
PDFReader は camelot
などのライブラリを使って、PDF内のテーブル構造などを読み取ります。ExcelReader は pandas
などのライブラリを使って、Excelシートの内容を DataFrame
(表形式データ) として読み込みます。PDFTemplateDetector や
ExcelTemplateDetector
といった専門の「検出器」が行います。ClientAPDFTemplate)を選択します。extract_data
メソッドを呼び出し、解析したファイルデータ(PDFのテーブル情報やExcelのDataFrame)を渡します。date)、時刻型
(time)、時間差型 (timedelta)
など、プログラムで扱いやすい形式に変換します。ClientDailyRecord を日ごと作成し、最後に
ClientTimesheet オブジェクトにまとめます。ClientTimesheet
オブジェクト(またはそのリスト)をリーダーに返し、リーダーはそれを呼び出し元(通常は
FileSelector)に返します。この流れをシーケンス図で見てみましょう(PDFReader
の例)。
sequenceDiagram
participant Reader as PDFReader
participant Detector as PDFTemplateDetector
participant PDFData as PDF解析データ (例: テーブル)
participant KagaTmpl as ClientKagaPDFTemplate (具体的テンプレート)
participant NecTmpl as ClientNecPDFTemplate (具体的テンプレート)
participant CTS as ClientTimesheet
Reader->>Detector: detect(PDF解析データ) でテンプレートを判定
Detector->>PDFData: 特徴を分析 (例: 特定の文字列を探す)
PDFData-->>Detector: 分析結果
alt KAGA形式の場合
Detector-->>Reader: "client_kaga" を返す
Reader->>KagaTmpl: extract_data(PDF解析データ) で抽出依頼
KagaTmpl->>PDFData: 定義に従ってデータ抽出
PDFData-->>KagaTmpl: 抽出された生データ
KagaTmpl->>KagaTmpl: データを整形・変換
KagaTmpl->>CTS: ClientTimesheet オブジェクトを作成
CTS-->>KagaTmpl: 作成された ClientTimesheet
KagaTmpl-->>Reader: ClientTimesheet (リスト) を返す
else NEC形式の場合
Detector-->>Reader: "client_nec" を返す
Reader->>NecTmpl: extract_data(PDF解析データ) で抽出依頼
NecTmpl->>PDFData: 定義に従ってデータ抽出
PDFData-->>NecTmpl: 抽出された生データ
NecTmpl->>NecTmpl: データを整形・変換
NecTmpl->>CTS: ClientTimesheet オブジェクトを作成
CTS-->>NecTmpl: 作成された ClientTimesheet
NecTmpl-->>Reader: ClientTimesheet (リスト) を返す
else 他の形式...
Detector-->>Reader: エラーまたは未対応
end
このように、リーダーは「どのテンプレートを使うか」を決め、実際のデータ抽出と変換は、選ばれたテンプレート自身が行う、という役割分担になっています。
では、実際のテンプレートがどのように書かれているのか、簡単な例を見てみましょう。
ClientCWExcelTemplate)これは、特定のクライアント (CW社)
のExcelフォーマットに対応するテンプレートの一部です
(src/excel_handler/templates/client_cw_template.py
より簡略化)。
# src/excel_handler/templates/client_cw_template.py より (簡略化)
import pandas as pd
from datetime import datetime, timedelta, date, time
from ...models.client_timesheet import ClientTimesheet
from ...models.client_daily_record import ClientDailyRecord
class ClientCWExcelTemplate: # ExcelTemplate インターフェースを実装
"""クライアントCW社用 Excelテンプレート"""
def __init__(self):
"""テンプレートで使う設定(情報の場所など)を定義"""
# 社員名はどこにあるか? -> C列の5行目 (インデックスは0から始まるので 2, 4)
self.employee_name_column = 2
self.employee_name_row = 4
# 日々の記録はどの列にあるか?
self.date_column = 1 # 日付はB列 (インデックス 1)
self.start_time_hour_column = 3 # 開始時刻(時)はD列
self.start_time_minutes_column = 4 # 開始時刻(分)はE列
self.end_time_hour_column = 5 # 終了時刻(時)はF列
self.end_time_minutes_column = 6 # 終了時刻(分)はG列
self.break_time_column = 7 # 休憩時間はH列 (例: 1.0)
# ... 他の列定義 ...
# 日々の記録が始まる行は? -> 14行目 (インデックス 13)
self.table_start_row = 13
def extract_data(self, df: pd.DataFrame): # <- Excelデータ(DataFrame)を受け取る
"""Excelデータから ClientTimesheet を作成して返す"""
try:
# 1. 社員名の抽出
# iloc[行, 列] で指定したセルの値を取得
employee_name = df.iloc[self.employee_name_row, self.employee_name_column]
# 2. 日次データの抽出
daily_records = []
# table_start_row から最終行までループ
for i in range(self.table_start_row, df.shape[0]):
# 日付列が空でなければ、その行のデータを処理
if pd.notna(df.iloc[i, self.date_column]):
record = self._extract_daily_record(df, i) # <- 1行分の処理は別メソッドへ
if record: # 抽出に成功したらリストに追加
daily_records.append(record)
if not daily_records: return [] # データがなければ空リストを返す
# 3. ClientTimesheet オブジェクトを作成
start_day = daily_records[0].date # 最初の記録の日付
end_day = daily_records[-1].date # 最後の記録の日付
target_period = f"{start_day.year}年{start_day.month}月~{end_day.year}年{end_day.month}月"
return [ClientTimesheet( # 結果はリストで返す
employee_name=employee_name,
target_period=target_period,
daily_records=daily_records
)]
except Exception as e:
# エラー処理 (ログ出力など)
return [] # エラー時は空リスト
def _extract_daily_record(self, df: pd.DataFrame, row: int):
"""指定された行から ClientDailyRecord を作成する"""
try:
# 各列から値を取得
record_date = df.iloc[row, self.date_column].date() # 日付を取得し date 型に
# 時刻の組み立て (時間と分が別々の列にある場合)
start_hour = int(df.iloc[row, self.start_time_hour_column])
start_minute = int(df.iloc[row, self.start_time_minutes_column])
start_time = time(start_hour, start_minute) # time 型に変換
end_hour = int(df.iloc[row, self.end_time_hour_column])
end_minute = int(df.iloc[row, self.end_time_minutes_column])
end_time = time(end_hour, end_minute)
# 休憩時間の変換 (例: 1.0 -> 1時間0分)
break_hours, break_minutes = self._parse_decimal_time(df.iloc[row, self.break_time_column])
break_timedelta = timedelta(hours=break_hours, minutes=break_minutes)
# ClientDailyRecord を作成して返す
return ClientDailyRecord(
date=record_date,
start_time=start_time,
end_time=end_time,
break_time=break_timedelta,
# work_time も同様に取得・変換 ...
)
except Exception as e:
# エラー処理 (行のスキップなど)
return None # エラー時は None を返す
def _parse_decimal_time(self, value):
"""小数を時間と分に変換するヘルパー関数"""
if pd.isna(value): return 0, 0
hours = int(value)
minutes = int((value - hours) * 60)
return hours, minutesこのコードから、テンプレートがいかに具体的かがわかりますね。
__init__
で、どの情報がExcelシートの「何行目の何列目」にあるかを細かく定義しています。extract_data メソッドが、渡された
DataFrame (Excelデータ) に対して、__init__
で定義された場所 (iloc[行, 列])
から情報をピンポイントで取得しています。_extract_daily_record
のような補助的なメソッドを使って、1行分のデータを
ClientDailyRecord
オブジェクトに変換しています。この中で、時刻の組み立てや時間形式の変換など、そのフォーマット固有の処理を行っています。PDFテンプレート (ClientKagaPDFTemplate など)
も基本的な考え方は同じですが、PDFから抽出されたテーブルデータ
(List[PDFTable])
を入力として受け取り、テーブル内の特定の位置やキーワードを手がかりにデータを抽出する点が異なります。
もし、新しいお客様からこれまでと違うフォーマットの勤務表が送られてきたらどうすればよいでしょうか?
PDFTemplate を、Excelなら ExcelTemplate
を参考に、extract_data メソッドなどを実装します。PDFTemplateDetector または
ExcelTemplateDetector
に認識させます。検出器が新しいフォーマットの特徴を捉え、適切なテンプレートID(例:
"client_new") を返せるようにします。PDFReader または
ExcelReader の __init__
内にあるテンプレート辞書に、新しいテンプレートIDとクラスのインスタンスを追加します。# 例: ExcelReader に新しいテンプレートを追加する場合
# src/excel_handler/excel_reader.py の __init__ を修正
from .templates.client_new_template import ClientNewExcelTemplate # 新しいテンプレートをインポート
class ExcelReader:
def __init__(self):
self.template_detector = ExcelTemplateDetector()
self.templates: Dict[str, ExcelTemplate] = {
"client_cw": ClientCWExcelTemplate(),
"client_c8": ClientC8ExcelTemplate(),
"client_ntt": ClientNttExcelTemplate(),
# --- ここに追加 ---
"client_new": ClientNewExcelTemplate(), # 新しいテンプレートを登録
# -----------------
}これで、新しいフォーマットのファイルが来ても、リーダーが自動的に新しいテンプレートを使って読み込めるようになります。
この章では、客先ごとに異なる勤務表ファイルのレイアウトや形式の違いを吸収するための重要な仕組み、ファイル形式テンプレート (File Format Templates) について学びました。
PDFTemplate) と Excel用
(ExcelTemplate)
の基盤があり、具体的な客先フォーマットごとにクラスが作成されます。PDFReader や ExcelReader
は、まずファイルに合ったテンプレートを検出し、そのテンプレートに実際のデータ抽出と変換を依頼します。テンプレートのおかげで、このツールは多様な入力ファイルに対応しつつ、プログラム内部では 勤務表データモデル (Timesheet Data Models) という統一された形式でデータを扱うことができるのです。これは、プログラム全体の保守性や拡張性を高める上で非常に重要な役割を果たしています。
さて、これでファイルの読み込みからデータモデルへの変換、そして比較処理までの流れがほぼ見えました。最後の章では、これらの処理の結果、つまり勤務表の比較結果が、どのようにユーザーに分かりやすく表示されるのかを見ていきます。
Generated by AI Codebase Knowledge Builder