07_ファイル形式テンプレート__file_format_templates__

Chapter 7: ファイル形式テンプレート (File Format Templates)

こんにちは!第6章: 客先勤務表リーダー (ClientTimesheetReader)では、PDFやExcelといった様々な形式の客先勤務表ファイルを読み込むための「翻訳機」について学びましたね。リーダーがファイル形式を特定し、適切な専門家 (PDFReaderExcelReader) に処理を依頼するのでした。

しかし、ここで一つの疑問が残ります。その専門リーダーたちは、どうやって お客様ごと、あるいはファイルごとに微妙に異なるレイアウトや項目名 を理解し、正確に必要な情報(日付、時間、氏名など)を抜き出しているのでしょうか?

例えば、A社のPDFでは日付が表の左端にあるけれど、B社のExcelでは日付が表の真ん中あたりにあるかもしれません。休憩時間の書き方も、「1.0」時間だったり「60」分だったりするかもしれません。

これらの無数の「違い」に対応する魔法、それが今回学ぶ ファイル形式テンプレート (File Format Templates) なのです!

ファイル形式テンプレートとは? - ファイルごとの「読み方説明書」

ファイル形式テンプレートとは、客先ごとに異なるPDFやExcelのレイアウトや項目名を解析するための、個別の「読み方説明書」あるいは「レシピ」のようなものです。

それぞれのテンプレートが、特定の客先フォーマットについて、

といった情報を具体的に定義しています。

第6章: 客先勤務表リーダー (ClientTimesheetReader)で登場した PDFReaderExcelReader は、ファイルを受け取ると、まずそのファイルがどのテンプレート(読み方説明書)に合致するかを判断します。そして、見つけたテンプレートの指示に従って、ファイルの中から正確にデータを抽出していくのです。

鍵と鍵穴のアナロジーで考えてみましょう。

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種類のファイル形式に対応するためのテンプレート基盤があります。

  1. PDFテンプレート (PDFTemplate): PDFファイルからデータを抽出するためのテンプレートの基盤(インターフェース)。具体的な客先PDFレイアウトに対応するクラス(例: ClientKagaPDFTemplate, ClientNecPDFTemplate)は、これを元に作られます。
  2. Excelテンプレート (ExcelTemplate): Excelファイル(.xlsx, .xlsなど)からデータを抽出するためのテンプレートの基盤。同様に、具体的な客先Excelレイアウトに対応するクラス(例: ClientCWExcelTemplate, ClientC8ExcelTemplate)は、これを元に作られます。

これらのテンプレートは、src/pdf_handler/templates/src/excel_handler/templates/ といったフォルダの中に、客先ごとにファイルが分かれて置かれています。

テンプレートはどのように使われるのか?

第6章: 客先勤務表リーダー (ClientTimesheetReader) で見たように、PDFReaderExcelReader がファイルを受け取った後の流れを、もう少し詳しく見てみましょう。

  1. ファイル解析: リーダーはまず、受け取ったファイルを解析します。
  2. テンプレート検出: リーダーは、解析したファイルの特徴(特定の文字列の有無、表の形、シート名など)を元に、どのテンプレートを使うべきか を判断します。この判断は、PDFTemplateDetectorExcelTemplateDetector といった専門の「検出器」が行います。
  3. テンプレート選択: 検出器が「このファイルはA社のフォーマットだ」と判断したら、リーダーは対応するテンプレート(例: ClientAPDFTemplate)を選択します。
  4. データ抽出依頼: リーダーは、選択したテンプレートの extract_data メソッドを呼び出し、解析したファイルデータ(PDFのテーブル情報やExcelのDataFrame)を渡します。
  5. テンプレートによる抽出: テンプレートは、自身に定義された「読み方説明書」に従って、渡されたデータの中から必要な情報(社員名、期間、日付、時刻など)を正確に探し出し、抽出します。
  6. データ整形・変換: 抽出したデータ(多くは文字列)を、日付型 (date)、時刻型 (time)、時間差型 (timedelta) など、プログラムで扱いやすい形式に変換します。
  7. データモデル作成: 整形・変換したデータを使って、第2章: 勤務表データモデル (Timesheet Data Models) で定義された ClientDailyRecord を日ごと作成し、最後に ClientTimesheet オブジェクトにまとめます。
  8. 結果返却: 作成した 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

このように、リーダーは「どのテンプレートを使うか」を決め、実際のデータ抽出と変換は、選ばれたテンプレート自身が行う、という役割分担になっています。

テンプレートの中身を見てみよう (具体例)

では、実際のテンプレートがどのように書かれているのか、簡単な例を見てみましょう。

Excelテンプレートの例 (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

このコードから、テンプレートがいかに具体的かがわかりますね。

PDFテンプレート (ClientKagaPDFTemplate など) も基本的な考え方は同じですが、PDFから抽出されたテーブルデータ (List[PDFTable]) を入力として受け取り、テーブル内の特定の位置やキーワードを手がかりにデータを抽出する点が異なります。

新しいフォーマットへの対応

もし、新しいお客様からこれまでと違うフォーマットの勤務表が送られてきたらどうすればよいでしょうか?

  1. 新しいテンプレートを作成: そのフォーマットの「読み方説明書」となる新しいテンプレートクラスを作成します。PDFなら PDFTemplate を、Excelなら ExcelTemplate を参考に、extract_data メソッドなどを実装します。
  2. 検出器に登録: 作成したテンプレートを PDFTemplateDetector または ExcelTemplateDetector に認識させます。検出器が新しいフォーマットの特徴を捉え、適切なテンプレートID(例: "client_new") を返せるようにします。
  3. リーダーに登録: 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) について学びました。

テンプレートのおかげで、このツールは多様な入力ファイルに対応しつつ、プログラム内部では 勤務表データモデル (Timesheet Data Models) という統一された形式でデータを扱うことができるのです。これは、プログラム全体の保守性や拡張性を高める上で非常に重要な役割を果たしています。

さて、これでファイルの読み込みからデータモデルへの変換、そして比較処理までの流れがほぼ見えました。最後の章では、これらの処理の結果、つまり勤務表の比較結果が、どのようにユーザーに分かりやすく表示されるのかを見ていきます。

次の章: 第8章: 結果表示 (ResultViewer)


Generated by AI Codebase Knowledge Builder