個人的Excel読み取りの実装パターン
ソフトウェアエンジニアとして業務システムを開発していると、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