ワークブックの要素を識別する
大抵の人は、Excelファイルがワークシートで構成され、その中にセルが行列の形式で配置されていることは承知しています。Excelファイルがワークブックであることを知っている人も、それを気にすることはめったにないようです。そのため、本稿のサンプル実装では、ワークブックの概念をクライアントから見えなくしています。HSSFライブラリにはJavaでワークブックをExcelと同じように扱うためのメソッドがすべて用意されていますが、ここではファイルからデータを読み込んで利用することにしか関心がありません。つまり、Javaクラスを使って、ユーザーがExcelでするのとまったく同じことを、同じ手順で、なおかつExcelよりもずっと高速でできればよいわけです。必要な手順は次のようになります。
- ワークシートを開く
- 見出し行を特定する
- 行単位でデータを読み込む
- 列単位でデータを読み込む
値を読み込む
ワークシートは、HSSFWorkbookオブジェクトから名前またはインデックスで抽出できます。一般に、クライアントアプリケーションでワークシートの名前を重視するのは複数のワークシートがある場合で、インデックスを重視するのはデータを含むワークシートがExcelファイル内に1つだけあると考えられる場合です。今回のクライアントアプリケーションで取り出すのはワークシート内の値だけなので、これから作成するリーダークラスでは、HSSFSheetオブジェクトを取得するためのメソッドを公開する必要すらありません。使いやすい方法で値を返すメソッドがあれば十分なのです。
汎用性を考えると、データソースとして提供されるスプレッドシートの1行目には、フィールドの名前が含まれているものと仮定するのが安全です。実用的な見地から、入力として受け取るデータでは一定のフォーマットを定義すべきです。ユーザーの入力する任意のレイアウトからフォーマットを判別するようなロジックを工夫することも可能ですが、それは極めて複雑なものとなるでしょう。そこで、最初のユーティリティメソッドでは、与えられたワークシートの1行目の値を返すようにします。
public ArrayList<String> getColNames(int sheetIndex)
{
ArrayList<String> colNames = new ArrayList<String>();
HSSFSheet sheet = this.workbook.getSheetAt(sheetIndex);
HSSFRow row = sheet.getRow(0);
HSSFCell cell = null;
int cols = 0;
if (row != null)
{
cols = row.getPhysicalNumberOfCells();
for (int i = 0; i < cols; i++)
{
cell = row.getCell(i);
if (cell != null && cell.getCellType() == stringCell)
{
colNames.add(cell.getRichStringCellValue().getString());
}
}
}
return colNames;
}
複数のワークシートを含む入力にも対応できるよう、このメソッドをオーバーロードして、ワークシートの名前を使用するバージョンも用意します。
public ArrayList<String> getColNames(String sheetName)
{
return getColNames(this.workbook.getSheetIndex(sheetName));
}
スプレッドシート内の値を表現するには、行単位のデータに対してArrayListをマップするのが簡単なようです。列の名前をキーとして使い、これらのマップをArrayListに格納するわけです。これで、さまざまな目的の値を簡単なコードで読み込むことができ、新しいオブジェクトを生成したり、クライアントアプリケーションにHSSF APIを公開したりしなくて済みます。無論、これが唯一の方法ではありません。私がHSSF APIを初めて実装したとき、これが特に便利だと感じただけです(「Open Source-Based Portal-Lite」のUser-Friendly Updatesを参照)。
/**
* @param sheetIndex
* @return ArrayList<Map> where the key is the field names.
* Assumes first row contains field names
*/
public ArrayList<Map> getMappedValues(int sheetIndex)
{
ArrayList<String> colNames = null;
ArrayList<Map> mapArray = null;
HSSFRow row = null;
HSSFSheet sheet = null;
int sheetRows = 0;
int rowCols = 0;
Map<String, Object> rowMap = null;
sheet = this.workbook.getSheetAt(sheetIndex);
sheetRows = sheet.getPhysicalNumberOfRows();
mapArray = new ArrayList<Map>(sheetRows - 1);
colNames = getColNames(sheetIndex);
colNames.trimToSize();
rowCols = colNames.size();
for (int i = 1; i < sheetRows; i++)
{
row = sheet.getRow(i);
rowMap = new HashMap<String, Object>(rowCols);
for (int c = 0; c < rowCols; c++)
{
rowMap.put(colNames.get(c), getCellValue(row.getCell(c)));
}
mapArray.add(rowMap);
}
return mapArray;
}
ここでも、次のようにオーバーロードすれば、インデックスの代わりにシート名を使えるようになります。
public ArrayList<Map> getMappedValues(String sheetName)
{
return getMappedValues(this.workbook.getSheetIndex(sheetName));
}
