06_客先勤務表リーダー__clienttimesheetreader__

Chapter 6: 客先勤務表リーダー (ClientTimesheetReader)

こんにちは!前の章、第5章: 社内勤務表リーダー (InternalTimesheetReader)では、社内用のCSVファイルを読み込んで、プログラムが理解できる共通のデータ形式 (InternalTimesheet) に変換する専門家について学びましたね。

しかし、比較のためにはもう一方、お客様から受け取る勤務表も読み込む必要があります。ここで少し困った問題があります。お客様によって、勤務表の形式がバラバラなのです!あるお客様はPDFで、別のお客様はExcelで、さらにそのExcelの中でもレイアウトが違ったり…と、多種多様です。

これら全ての形式に個別に対応するのは大変ですよね。そこで登場するのが、今回学ぶ客先勤務表リーダー (ClientTimesheetReader) です!

客先勤務表リーダー (ClientTimesheetReader) とは? - 多言語対応の「翻訳機」

ClientTimesheetReader は、一言でいうと、様々な形式(PDF、Excelなど)で送られてくる客先勤務表ファイルを読み込み、プログラムが統一的に扱える共通の形式 (ClientTimesheet データモデル) に変換するための機能群 のことです。

ちょうど、英語の文書もフランス語の文書も、日本語という共通言語に翻訳してくれる多言語対応の翻訳機のような役割を果たします。この「翻訳機」があるおかげで、プログラムの他の部分(特に第1章: 勤務表比較 (TimesheetComparator))は、元のファイルがPDFであろうとExcelであろうと、常に「日本語」(= ClientTimesheet 形式)で書かれた情報を扱えるようになります。

具体的には、ClientTimesheetReader は以下のような仕事を担当します:

  1. ファイル形式の特定: まず、渡されたファイルがPDFなのか、Excelなのか、それとも他の形式なのかを見分けます。
  2. 適切なリーダーの選択: ファイル形式に応じて、その形式を専門に扱う「翻訳担当者」を選びます。例えば、PDFファイルなら PDFReader を、Excelファイルなら ExcelReader を選びます。
  3. 読み込みと解析の委任: 選んだ専門リーダーに、実際のファイルの読み込みと内容の解析を依頼します。
  4. 共通形式への変換: 専門リーダーが解析した結果を、第2章: 勤務表データモデル (Timesheet Data Models) で学んだ ClientTimesheet という共通の形式にまとめます。

重要なのは、ClientTimesheetReader 自体が一つの具体的なクラスというよりは、「客先の様々な勤務表を読み込む」という役割全体を指す概念であり、その役割を果たすための窓口(インターフェース)と、実際の処理を行う専門家(PDFReader, ExcelReader など)の集まりである、という点です。

なぜ「翻訳機」が必要なの? (具体例)

例えば、A社からは下のようなレイアウトのPDF勤務表が、B社からは全く違うレイアウトのExcel勤務表が送られてくるとします。

A社 PDF (例)

+-------------------------------------+
| 勤務時間報告書                      |
| 氏名: 田中 太郎      期間: 2024/07 |
+-------------------------------------+
| 日付 | 開始 | 終了 | 休憩 | 実働 |
|------|------|------|------|------|
| 7/1  | 9:00 | 18:00| 1.0  | 8.0  |
| 7/2  | 9:05 | 18:00| 1.0  | 7.92 |
| ...  | ...  | ...  | ...  | ...  |
+-------------------------------------+

B社 Excel (例)

+------------------------------------------------------+
| 月次作業報告書 (2024年7月)                           |
| 作業者: 佐藤 次郎                                    |
+------------------------------------------------------+
| 日 | 曜日 | 業務開始 | 業務終了 | 休憩(分) | 稼働時間 |
|----|------|----------|----------|----------|----------|
| 1  | 月   | 09:00    | 17:30    | 60       | 7.5      |
| 2  | 火   | 09:00    | 18:00    | 60       | 8.0      |
| ...| ...  | ...      | ...      | ...      | ...      |
+------------------------------------------------------+

これらは見た目も形式も全く違いますが、どちらも「誰が」「いつ」「どれだけ働いたか」という情報を含んでいます。ClientTimesheetReader のおかげで、プログラムはこれらの違いを意識することなく、最終的に同じ形の ClientTimesheet データとして情報を取得できるのです。

graph LR
    A[A社.pdf] -- ClientTimesheetReader --> C{ClientTimesheet};
    B[B社.xlsx] -- ClientTimesheetReader --> C;

    subgraph ClientTimesheetReader [客先勤務表リーダー]
        direction LR
        D(ファイル形式判定) --> E{PDF?};
        E -- Yes --> F[PDFReader];
        E -- No --> G{Excel?};
        G -- Yes --> H[ExcelReader];
        F -- 解析 --> I(データ抽出);
        H -- 解析 --> I;
        I -- 変換 --> J([共通形式 ClientTimesheet]);
    end

    C -- データ利用 --> K([勤務表比較]);

    style C fill:#f9f,stroke:#333,stroke-width:2px

この図のように、ClientTimesheetReader が入り口となり、ファイルの種類に応じて適切な専門リーダー (PDFReaderExcelReader) に処理を振り分け、最終的に共通の ClientTimesheet 形式に「翻訳」している様子がわかります。

ClientTimesheetReader の使い方 (概念)

第4章: ファイル選択 (FileSelector) が客先勤務表のファイルパスを受け取った後、内部では ClientTimesheetReader を使ってデータを読み込みます。その際のイメージは以下のようになります。

# FileSelector の get_client_timesheet メソッド内で... (イメージ)

# ユーザーが選択したファイルのパスを取得
client_file_path = self._client_file_path # 例: "C:/path/to/client_timesheet.pdf" or "D:/data/customer_report.xlsx"

# 適切なリーダーを取得する (ファイル拡張子などから判断)
# ここでは ClientTimesheetReader が内部的に PDFReader か ExcelReader を選択するイメージ
reader: ClientTimesheetReader # 型ヒントで Reader であることを示す

file_extension = client_file_path.suffix.lower()
if file_extension == ".pdf":
    reader = PDFReader() # PDFリーダーを選択
elif file_extension in [".xlsx", ".xls", ".xlsm"]:
    reader = ExcelReader() # Excelリーダーを選択
else:
    raise ValueError(f"未対応のファイル形式です: {file_extension}")

# リーダーの read メソッドを呼び出してファイルを読み込む
try:
    # どのリーダーでも同じ read メソッドを呼べば良い!
    client_timesheet_list = reader.read(client_file_path)
    # 成功! client_timesheet_list には ClientTimesheet オブジェクトのリストが入る
    print("客先勤務表の読み込みに成功しました!")
    # この後、このデータが比較処理に使われる
except FileNotFoundError:
    print("エラー: ファイルが見つかりません。")
except ValueError as e:
    print(f"エラー: ファイル形式が正しくないか、未対応の可能性があります: {e}")
except Exception as e:
    print(f"エラー: 予期せぬ問題が発生しました: {e}")

このコードのポイントは、ファイルがPDFでもExcelでも、最終的に reader.read(client_file_path) という同じ形で読み込み処理を呼び出せている点です。これは、PDFReaderExcelReader も、ClientTimesheetReader という共通の「お約束」(インターフェース)に従って作られているためです。

内部の仕組み:どうやって翻訳しているの?

ClientTimesheetReader (具体的には PDFReaderExcelReader) が read メソッドで呼び出されたとき、内部では何が起こっているのでしょうか?

  1. ファイルを開く: まず、指定されたPDFファイルやExcelファイルを開きます。
  2. 中身を解析する:
  3. テンプレートの利用 (重要!): 多くの場合、同じお客様からでも、少しレイアウトが違うファイルが来ることがあります。また、お客様ごとに全くレイアウトが異なります。そこで、「このレイアウトのファイルなら、このセル(またはこの場所)に日付があるはず」といった情報を定義した「テンプレート」を使います。PDFReaderExcelReader は、読み込んだファイルのレイアウトがどのテンプレートに合致するかを判定し、そのテンプレートの指示に従って必要な情報(日付、開始時刻、終了時刻など)を正確に抽出します。このテンプレートの詳細は第7章: ファイル形式テンプレート (File Format Templates)で詳しく学びます。
  4. データ整形: 抽出した生のデータ(多くは文字列)を、プログラムで扱いやすい形式(日付型、時刻型、時間差型など)に変換します。
  5. データモデルへの変換: 整形したデータを元に、第2章: 勤務表データモデル (Timesheet Data Models) で定義された ClientDailyRecord オブジェクトを日ごと作成し、それらをまとめて ClientTimesheet オブジェクトを作成します。
  6. 結果を返す: 作成した ClientTimesheet オブジェクト(のリスト)を呼び出し元に返します。

大まかな流れをシーケンス図で見てみましょう。(PDFReader の例)

sequenceDiagram
    participant FS as FileSelector
    participant PR as PDFReader (ClientTimesheetReader の具体例)
    participant Lib as 外部ライブラリ (Camelot)
    participant Tmpl as テンプレート (例: KagaPDFTemplate)
    participant PDF as PDFファイル
    participant CTS as ClientTimesheet

    FS->>PR: read(ファイルパス) を呼び出す
    PR->>Lib: PDF解析を依頼 (camelot.read_pdf)
    Lib->>PDF: ファイル内容を読み込む
    PDF-->>Lib: PDFデータ
    Lib-->>PR: 解析結果 (テーブルデータなど)
    PR->>PR: どのテンプレートを使うか判定 (detect_format)
    Note right of PR: レイアウトを分析
    PR->>Tmpl: テンプレートの指示でデータ抽出を依頼 (extract_data)
    Tmpl->>PR: 解析結果から必要な情報を抽出
    PR-->>Tmpl: 抽出データ
    Tmpl->>Tmpl: データを整形
    Tmpl->>CTS: ClientTimesheet オブジェクトを作成
    CTS-->>Tmpl: 作成された ClientTimesheet
    Tmpl-->>PR: ClientTimesheet (リスト)
    PR-->>FS: ClientTimesheet (リスト) を返す

この図から、PDFReader が外部ライブラリやテンプレートと連携しながら、PDFファイルという「外国語」を ClientTimesheet という「共通言語」に翻訳している様子がわかりますね。ExcelReader も同様に、Excel用のライブラリやテンプレートを使って処理を行います。

コードの中身 (共通インターフェースと具体例)

ClientTimesheetReader の考え方を支える重要な要素が、共通のインターフェース(お約束)です。

ClientTimesheetReader インターフェース

src/models/client_timesheet_reader.py には、客先勤務表リーダーが最低限満たすべき「お約束」が定義されています。

# src/models/client_timesheet_reader.py より (抜粋)
from abc import abstractmethod
from pathlib import Path
from typing import Protocol # Protocolを使って「お約束」を定義

from .client_timesheet import ClientTimesheet

class ClientTimesheetReader(Protocol):
    """クライアント勤務表読み取りインターフェース"""

    @abstractmethod # このメソッドは必ず実装する必要があるという印
    def read(self, file_path: Path | str) -> ClientTimesheet:
        """
        勤務表ファイルを読み取り、ClientTimesheetオブジェクトを返します。
        """
        raise NotImplementedError # 中身はここでは定義しない

    # detect_format メソッドも同様に定義されている場合があります
    # @abstractmethod
    # def detect_format(self, ...) -> str:
    #     """ファイルの形式を検出します。"""
    #     raise NotImplementedError

これは Protocol という Python の機能を使って、「ClientTimesheetReader を名乗るなら、必ず read という名前のメソッドを持っていて、それはファイルパスを受け取って ClientTimesheet を返すものである」というルールを定めています。PDFReaderExcelReader も、このルールに従って作られています。

PDFReader の例

src/pdf_handler/pdf_reader.py にある PDFReader は、この ClientTimesheetReader のルールに従った具体例の一つです。

# src/pdf_handler/pdf_reader.py より (簡略化)
from pathlib import Path
import camelot # PDF解析ライブラリ
from ..models.client_timesheet import ClientTimesheet
# ClientTimesheetReader のルールに従うことを示す (暗黙的に)
from ..models.client_timesheet_reader import ClientTimesheetReader
from .pdf_template_detector import PDFTemplateDetector # テンプレート判定役
from .templates import PDFTemplate # 各テンプレートの基底クラス
from .templates.client_kaga_template import ClientKagaPDFTemplate # 具体的なテンプレート例

class PDFReader: # ClientTimesheetReader Protocol を実装
    """PDFリーダークラス"""

    def __init__(self):
        self.template_detector = PDFTemplateDetector()
        # 対応しているテンプレートを登録
        self.templates: Dict[str, PDFTemplate] = {
            "client_kaga": ClientKagaPDFTemplate(),
            # 他のテンプレートもここに追加 ...
        }

    def read(self, file_path: Path | str): # インターフェースで定義されたメソッド
        file_path = Path(file_path)
        if not file_path.exists():
            raise FileNotFoundError(f"ファイルが見つかりません: {file_path}")

        try:
            # 1. camelot で PDF からテーブルデータを抽出
            pdf_tables = camelot.read_pdf(str(file_path), pages="all", ...)

            # 2. どのテンプレートを使うか判定
            template_id = self.template_detector.detect(pdf_tables)
            if template_id not in self.templates:
                raise ValueError(f"未対応のテンプレート形式です: {template_id}")

            # 3. 対応するテンプレートを取得
            template = self.templates[template_id]

            # 4. テンプレートを使ってデータを抽出し、ClientTimesheet を作成
            client_timesheet_list = template.extract_data(pdf_tables)

            # 5. (オプション) データの妥当性チェック
            if not template.validate(client_timesheet_list):
                raise ValueError("抽出されたデータが不正です。")

            return client_timesheet_list # 結果を返す

        except Exception as e:
            # エラー処理
            raise IOError(f"PDFの読み取りまたは解析に失敗しました: {e}")

PDFReaderread メソッドは、ライブラリ (camelot) を使ってPDFを解析し、template_detector で適切なテンプレートを特定し、そのテンプレート (template) に実際のデータ抽出 (extract_data) と検証 (validate) を任せていることがわかります。

ExcelReader の例

同様に src/excel_handler/excel_reader.py にある ExcelReaderClientTimesheetReader のルールに従っています。

# src/excel_handler/excel_reader.py より (簡略化)
from pathlib import Path
import pandas as pd # Excel操作ライブラリ
from ..models.client_timesheet import ClientTimesheet
# ClientTimesheetReader のルールに従う
from ..models.client_timesheet_reader import ClientTimesheetReader
from .excel_template_detector import ExcelTemplateDetector
from .templates import ExcelTemplate
from .templates.client_cw_template import ClientCWExcelTemplate # 具体的なテンプレート例

class ExcelReader: # ClientTimesheetReader Protocol を実装
    """Excelリーダークラス"""

    def __init__(self):
        self.template_detector = ExcelTemplateDetector()
        # 対応しているテンプレートを登録
        self.templates: Dict[str, ExcelTemplate] = {
            "client_cw": ClientCWExcelTemplate(),
            # 他のテンプレートもここに追加 ...
        }

    def read(self, file_path: Path | str): # インターフェースで定義されたメソッド
        file_path = Path(file_path)
        if not file_path.exists():
            raise FileNotFoundError(f"ファイルが見つかりません: {file_path}")

        try:
            # 1. pandas で Excel ファイルを読み込む
            df = pd.read_excel(file_path, engine='openpyxl') # または 'xlrd'

            # 2. どのテンプレートを使うか判定
            template_id = self.template_detector.detect(df)
            if template_id not in self.templates:
                raise ValueError(f"未対応のテンプレート形式です: {template_id}")

            # 3. 対応するテンプレートを取得
            template = self.templates[template_id]

            # 4. テンプレートを使ってデータを抽出し、ClientTimesheet を作成
            client_timesheet = template.extract_data(df)

            # 5. (オプション) データの妥当性チェック
            # if not template.validate(client_timesheet):
            #     raise ValueError("抽出されたデータが不正です。")

            return client_timesheet # 結果を返す (Excelは通常1ファイル1シート)

        except Exception as e:
            # エラー処理
            raise IOError(f"Excelの読み取りまたは解析に失敗しました: {e}")

ExcelReaderPDFReader と非常によく似た構造で、Excel用ライブラリ (pandas) とExcel用テンプレートを使って、共通の ClientTimesheet 形式にデータを変換しています。

このように、共通のインターフェース (ClientTimesheetReader) を設けることで、ファイル形式ごとに特化したリーダー (PDFReader, ExcelReader) を用意しつつ、それらを呼び出す側はリーダーの種類を意識せずに済む、という柔軟な設計が実現されています。

まとめ

この章では、多種多様な形式の客先勤務表を読み込むための「翻訳機」である客先勤務表リーダー (ClientTimesheetReader) について学びました。

これで、形式の異なる客先勤務表が、どのようにしてプログラム内部で統一的に扱えるデータになるのか、その仕組みが理解できたはずです。

しかし、PDFReaderExcelReader が、どうやって様々なレイアウトのファイルから正確に情報を見つけ出しているのでしょうか?その鍵を握るのが「テンプレート」です。次の章では、このテンプレートの仕組みについて詳しく見ていきましょう。

次の章: 第7章: ファイル形式テンプレート (File Format Templates)


Generated by AI Codebase Knowledge Builder