🤯

個人的Excel読み取りの実装パターン

2023/12/23に公開

ソフトウェアエンジニアとして業務システムを開発していると、Excelファイルを読み取って何かしないといけないケースが稀によくあります。

しかし、エンドユーザーが編集するExcelファイルは、ソースコードのライフサイクルとは異なるタイミングでフォーマットが変わったり、そもそもフォーマットを破壊して入力されたりすることがあり、読み取り機能をメンテナブルに実装するのはなかなか難しいです。

今回は、自分が何度かExcel読み取りを実装した経験から、実装パターンを整理してみます。
※銀の弾丸的な解決策は思いついておらず、トレードオフで実装する感じと考えています。

サンプルでTypeScriptの擬似コードを書きますが、あくまでイメージで、特定のプラットフォーム(Excel、Googleスプレッドシート、OpenOfficeなど)でそのまま使えるものではないです

パターン1. セルの位置を定数で管理する

実装的に最もシンプルで簡単。
セルの位置が変わった場合、定数を修正する必要がある。ので、行・列の変更が激しい場合には向かない。

const NAME_CELL = 'A1';
const name = sheet.get(NAME_CELL);

// 数値で管理するケースも。
const NAME_CELL = {
    row: 1,
    column: 1,
};
const name = sheet.get(NAME_CELL.row, NAME_CELL.column);

[派生] セルの位置を書いてあるセルの位置を定数で管理する

(ぱっと見何を言ってるんだろう・・・ですが。)
このパターンだと、データセルの位置が変わった場合に、シートを修正するだけで済むので、エンジニアを介さずにフォーマットをメンテナンスできる。「シートフォーマットの安定度は低いが、読み取るべき情報は安定している」場合に使える。

// このセルには、Name のセルの位置 ("B10" など)が書かれている
const NAME_REFERENCE_CELL = 'A1'; 

const nameRef = sheet.get(NAME_REFERENCE_CELL);
const name = sheet.get(nameRef);

パターン2. 項目名を定数で管理し、ヘッダー行を検索して項目ごとの列番号を取得・保持する

フォーマットが(ある程度)変わっても、項目名が変わらなければ読み取れる。
単純なデータテーブルのような、ヘッダー + データ行のフォーマットに向いている。
ヘッダー行のフォーマットが複雑(複数行に跨ったり、結合されたり)していると、実装も複雑になる。

const NAME_COLUMN_NAME = '名前';
const AGE_COLUMN_NAME = '年齢';
const ADDRESS_COLUMN_NAME = '住所';

type Columns = {
    name: number;
    age: number;
    address: number;
};

const getColumns = (headerRow: string[]): Columns => {
    const columns: Columns = {
        name: -1,
        age: -1,
        address: -1,
    };
    headerRow.forEach((cell, index) => {
        switch (cell) {
            case NAME_COLUMN_NAME:
                columns.name = index;
                break;
            case AGE_COLUMN_NAME:
                columns.age = index;
                break;
            case ADDRESS_COLUMN_NAME:
                columns.address = index;
                break;
        }
    });
    return columns;
};

const rows = sheet.getRows();
const columns = getColumns(rows[0]);
for (let i = 1; i < rows.length; i++) {
    const row = rows[i];
    const name = row[columns.name];
    const age = row[columns.age];
    const address = row[columns.address];
    console.log(name, age, address);
}

パターン3. 項目名と、データセルの相対位置を定数で管理する

先ほどと同じく、シートのフォーマットが変更されても、項目名が変わらなければある程度読み取れる。
ただし、相対位置を定義するので、他のパターンに比べてフォーマット変更に弱い。

データテーブルのような定型フォーマットではない、シート全体からデータセルを探す必要がある場合に向いている。

また配列の参照エラーを考慮したり、効率よく何度も読み取りたかったりする場合、実装ムズない・・・?というデメリットもある

type DataCellRef = {
    // 項目名
    indicator: string;
    // データセルの行方向の相対位置
    rowOffset: number;
    // データセルの列方向の相対位置
    columnOffset: number;
};

const nameDataRef: DataCellRef = {
    indicator: '名前',
    rowOffset: 1,
    columnOffset: 0,
};

function matchDataRef(cellValue: string, dataRef: DataCellRef): boolean {
    return cellValue === dataRef.indicator;
}


const rows = sheet.getRows();

for (let i = 0; i < rows.length; i++) {
    for (let j = 0; j < rows[i].length; j++) {
        const cell = rows[i][j]; // 本来はout of rangeの考慮とかが必要
        if (matchDataRef(cell, nameDataRef)) {
            const name = rows[i + nameDataRef.rowOffset][j + nameDataRef.columnOffset];
            console.log(name);
        }
    }
}

派生として、これをパターン(2.)と組み合わせたりする。(シートのどこにデータテーブルがあるのかわからない場合など。実装ムズない・・・?)

おわりに

自分の経験値だと以上となります。
みなさんのExcel読み取り実装パターン苦労話も知りたいです。(これが一番書きたかったこと)

Discussion