Excelのシート名一覧をJavaで取得する方法（POI vs SAX）

# はじめに
Excel ファイルをプログラムで扱うとき、シートの内容を解析する前段階としてシート名の一覧を取得したいケースがしばしばあります。
[以前の投稿](https://qiita.com/nmby/items/ca7227a1048528b1bf31)で紹介したExcelファイル比較ツール「方眼Diff」を作成する際にもそのような場面に遭遇しました。

<img src="https://qiita-image-store.s3.amazonaws.com/0/84939/f31c6363-2a8d-51ae-ea52-96c52994a602.png" width=384px>

本稿では、Excel ブックに含まれるシート名の一覧を Java で取得するための方法として、次の3つを紹介します。

- Apache POI の usermodel API を利用する方法（.xls/.xlsx/.xlsm）
- Apache POI の eventmodel API を利用する方法（.xls）
- SAX（Simple API for XML）を利用する方法（.xlsx/.xlsm）

# Apache POI の usermodel API を使う方法
## Apache POI とは
Apache POI は Microsoft Office 形式のファイルを読み書きできる Java ライブラリ（およびそのプロジェクト）で、Apache Software Foundation により Apache License 2.0 に基づいて公開されています。当該ライブラリをビルドパスに追加することにより利用することができます。（本稿末尾でその方法を紹介しています。）

POI には usermodel API と後で紹介する eventmodel API が用意されています。usermodel API は Java で Excel を扱う際の第一選択肢となるでしょう。

## 実装例[^1]
[^1]: 本稿で紹介するコード例では、紙幅の関係で入力チェックなどの本筋とは関係のない処理を省略しています。

```java:実装例1：SheetListerWithPOIUsermodelAPI.java
package mypackage;

import java.io.File;
import java.io.IOException;
import java.util.List;
import java.util.stream.Collectors;
import java.util.stream.IntStream;

import org.apache.poi.EncryptedDocumentException;
import org.apache.poi.openxml4j.exceptions.InvalidFormatException;
import org.apache.poi.ss.usermodel.Sheet;
import org.apache.poi.ss.usermodel.Workbook;
import org.apache.poi.ss.usermodel.WorkbookFactory;

public class SheetListerWithPOIUsermodelAPI {
    
    /**
     * 指定されたExcelファイルに含まれるシート名の一覧を返します。
     * 
     * @param file Excelファイル
     * @return シート名の一覧
     */
    public static List<String> getSheetNames(File file) throws Exception {
        
        try (Workbook book = WorkbookFactory.create(file)) {
            
            return IntStream.range(0, book.getNumberOfSheets())
                    .mapToObj(book::getSheetAt)
                    .map(Sheet::getSheetName)
                    .collect(Collectors.toList());
        }
    }
    
    public static void main(String[] args) throws Exception {
        List<String> sheetNames = getSheetNames(
                new File("C:\\Users\\user01\\Desktop\\ゲーム機売上一覧.xls"));
        
        sheetNames.forEach(System.out::println);
    }
}
```

```console:実行結果
売上一覧_2016年
グラフ_2016年
売上一覧_2017年
グラフ_2017年
売上一覧_2018年
グラフ_2018年
```

## 解説
POI の usermodel API では、初めに Excel ファイル全体を読み込んで解析する必要があります。`Workbook book = WorkbookFactory.create(file)` の部分がそれにあたります。その後、`Workbook#getNumberOfSheets()`, `Workbook#getSheetAt(int)`, `Sheet#getSheetName()` などを利用してシート名の一覧を取得しています。[^2]

上記のコード例では .xls 形式のファイルを処理していますが、.xlsx/.xlsm 形式のファイルも処理することができます。

[^2]: Apache POIのAPIドキュメントはこちらで公開されています：http://poi.apache.org/apidocs/index.html

## 長所
### 透過的な処理が可能である
ファイル形式（.xls形式 or .xlsx/.xlsm形式）を意識することなく透過的に処理を行うための各種 API が用意されています。

### オブジェクト指向による理解しやすいモデル化
ブック → シート → 行 → セルなどがExcelの外観に近い形でモデル化されており、直観的に理解して扱うことができます。

## 短所
### 多量のメモリと処理時間を必要とする
usermodel API では初めに Excel ブック全体をロードして解析する必要があるため、単にシート名を取得したいだけの場合も、Excel ファイル全体の内容に応じたメモリ量と処理時間を必要とします。これが実用上のネックになる場合があります。

### 痒いところに手が届かない
現在の usermodel API は、機能が充実しているとは言い難いです。

例えば上記のコード例ではワークシートに加えてグラフシートの名前も出力されています。ワークシートの名前だけに絞りたい場合、これが難しいのです。`Sheet#isChartSheet` というような API があっても良さそうなものですが、ないのです。

Excel シートを扱うためのインタフェース／クラスは次のような階層構造になっています。

```
Sheet ： 全形式のシートを透過的に扱うためのインタフェース
  ├─ XSSFSheet ： .xlsx/.xlsm形式のシート
  │    └─ XSSFChartSheet ： .xlsx/.xlsm形式のグラフシート
  │
  └─ HSSFSheet ： .xls形式のシート
```

従って、処理対象の Excel ファイルが .xlsx/.xlsm 形式の場合は、ソースコードを次のように改変することでグラフシートを除外することができます。

```java:実装例1-改
（前略）
    
    return IntStream.range(0, book.getNumberOfSheets())
            .mapToObj(book::getSheetAt)
            .filter(s -> !(s instanceof XSSFChartSheet))  //これを追加
            .map(Sheet::getSheetName)
            .collect(Collectors.toList());
    
（後略）
```

しかし、.xls 形式のグラフシートを除外する方法はありません[^3]。
POI で少し凝ったことをしようとすると、次から次へとこのような問題に遭遇してハマることとなるのです。

これらの短所が問題にならない場合は POI usermodel API が第一選択肢となるでしょう。その他の場合は、ほかの方法に頼ることとなります。

[^3]: もしかすると私が知らないだけかもしれません。ご存知の方はぜひコメントいただければと思います。

# Apache POI の eventmodel API を使う方法
## POI の eventmodel API とは
eventmodel API は Excel ファイルを高速かつ少量のメモリで読み取るために提供されている機能であり、ファイルへの書き込みはサポートされていません。

eventmodel API は Excel ファイルを先頭から末尾まで一方向に読み進め、ファイルの内容をイベントという形でアプリケーションに通知します。アプリケーションはイベント内容に応じて独自の処理を行います。

「[方眼Diff](https://qiita.com/nmby/items/ca7227a1048528b1bf31)」では、.xls 形式の Excel ブックからシート名一覧を読み取る際にこの方法を採用しています。
ここでは .xls 形式の Excel ブックを扱う場合の実装例を紹介します。

## 実装例
### ワークシート名およびグラフシート名を取得する場合

```java:実装例2-a：HSSFSheetListerWithPOIEventAPI.java
package mypackage;

import java.io.File;
import java.io.FileInputStream;
import java.util.ArrayList;
import java.util.List;

import org.apache.poi.hssf.eventusermodel.HSSFEventFactory;
import org.apache.poi.hssf.eventusermodel.HSSFListener;
import org.apache.poi.hssf.eventusermodel.HSSFRequest;
import org.apache.poi.hssf.record.BoundSheetRecord;
import org.apache.poi.hssf.record.Record;
import org.apache.poi.poifs.filesystem.POIFSFileSystem;

public class HSSFSheetListerWithPOIEventAPI {
    
    private static class HSSFSheetListingListener implements HSSFListener {
        
        private final List<String> sheetNames = new ArrayList<>();
        
        @Override
        public void processRecord(Record record) {
            if (record.getSid() == BoundSheetRecord.sid) {
                BoundSheetRecord bSheetRecord = (BoundSheetRecord) record;
                sheetNames.add(bSheetRecord.getSheetname());
            }
        }
    }
    
    /**
     * 指定されたExcelファイルに含まれるシート名の一覧を返します。
     * 
     * @param file Excelファイル
     * @return シート名の一覧
     */
    public static List<String> getSheetNames(File file) throws Exception {
        
        try (FileInputStream fis = new FileInputStream(file);
                POIFSFileSystem poifs = new POIFSFileSystem(fis)) {
            
            HSSFRequest req = new HSSFRequest();
            HSSFSheetListingListener listener = new HSSFSheetListingListener();
            req.addListenerForAllRecords(listener);
            HSSFEventFactory factory = new HSSFEventFactory();
            factory.abortableProcessWorkbookEvents(req, poifs);
            
            return listener.sheetNames;
        }
    }
    
    public static void main(String[] args) throws Exception {
        List<String> sheetNames = getSheetNames(
                new File("C:\\Users\\user01\\Desktop\\ゲーム機売上一覧.xls"));
        
        sheetNames.forEach(System.out::println);
    }
}
```

```console:実行結果
売上一覧_2016年
グラフ_2016年
売上一覧_2017年
グラフ_2017年
売上一覧_2018年
グラフ_2018年
```

### ワークシート名のみを取得する場合

```java:実装例2-b：HSSFSheetListerWithPOIEventAPI.java
package mypackage;

import java.io.File;
import java.io.FileInputStream;
import java.util.ArrayDeque;
import java.util.ArrayList;
import java.util.List;
import java.util.Queue;

import org.apache.poi.hssf.eventusermodel.HSSFEventFactory;
import org.apache.poi.hssf.eventusermodel.HSSFListener;
import org.apache.poi.hssf.eventusermodel.HSSFRequest;
import org.apache.poi.hssf.record.BOFRecord;
import org.apache.poi.hssf.record.BoundSheetRecord;
import org.apache.poi.hssf.record.Record;
import org.apache.poi.poifs.filesystem.POIFSFileSystem;

public class HSSFSheetListerWithPOIEventAPI {
    
    private static class HSSFSheetListingListener implements HSSFListener {
        
        private final Queue<String> queue = new ArrayDeque<>();
        private final List<String> sheetNames = new ArrayList<>();
        
        @Override
        public void processRecord(Record record) {
            switch (record.getSid()) {
            case BoundSheetRecord.sid:
                BoundSheetRecord bSheetRecord = (BoundSheetRecord) record;
                queue.add(bSheetRecord.getSheetname());
                break;
            
            case BOFRecord.sid:
                BOFRecord bofRecord = (BOFRecord) record;
                
                switch (bofRecord.getType()) {
                case BOFRecord.TYPE_WORKSHEET:
                    sheetNames.add(queue.remove());
                    break;
                
                case BOFRecord.TYPE_CHART:
                    queue.remove();
                    break;
                }
                
                break;
            }
        }
    }
    
    // 以降は実装例2-aと同じ
    
    /**
     * 指定されたExcelファイルに含まれるシート名の一覧を返します。
     * 
     * @param file Excelファイル
     * @return シート名の一覧
     */
    public static List<String> getSheetNames(File file) throws Exception {
        
        try (FileInputStream fis = new FileInputStream(file);
                POIFSFileSystem poifs = new POIFSFileSystem(fis)) {
            
            HSSFRequest req = new HSSFRequest();
            HSSFSheetListingListener listener = new HSSFSheetListingListener();
            req.addListenerForAllRecords(listener);
            HSSFEventFactory factory = new HSSFEventFactory();
            factory.abortableProcessWorkbookEvents(req, poifs);
            
            return listener.sheetNames;
        }
    }
    
    public static void main(String[] args) throws Exception {
        List<String> sheetNames = getSheetNames(
                new File("C:\\Users\\user01\\Desktop\\ゲーム機売上一覧.xls"));
        
        sheetNames.forEach(System.out::println);
    }
}
```

```console:実行結果
売上一覧_2016年
売上一覧_2017年
売上一覧_2018年
```

## 解説
先述の通り、POI の eventmodel API ではファイルの内容をイベントとして受け取ります。イベントを受け取るためには `HSSFListener` インタフェースを実装し、`processRecord(Record)` メソッドをオーバーライドする必要があります。

ファイルを読み進めるごとに `processRecord(Record)` メソッドが呼び出されるので、アプリケーションではイベントの内容（具体的には引数として渡される `Record` オブジェクトの内容）に応じて処理を行います。
実装例2-aでは `record.getSid() == BoundSheetRecord.sid` であるイベントを、実装例2-bでは `record.getSid() == BoundSheetRecord.sid` または `record.getSid() == BOFRecord.sid` であるイベントを処理しています。

どのようなイベントがどのような順番で発生するのかを理解するのはなかなか困難な作業です。私は [Record インタフェース](http://poi.apache.org/apidocs/org/apache/poi/hssf/record/Record.html)とそのサブインタフェース・サブクラスの API ドキュメントや、次のコードで確認できる実際のイベント内容を見比べながら徐々に理解を深めていきました。

```java:実際のイベント内容を出力させて確認する
（前略）
    
    @Override
    public void processRecord(Record record) {
        System.out.println(record);
    }
    
（後略）
```

なお、実装例2-a, 2-bで処理できるのは .xls 形式の Excel ブックだけです。.xlsx/.xlsm 形式の Excel ブックを処理することはできません。

## 長所
### 少量のメモリで高速に動作する
eventmodel API はファイルの先頭から末尾までをストリームで読み取る方式であることから、ファイル内容全体を解析してメモリ上に保持する方式である usermodel API と比較して、少量のメモリで高速に動作します。

### きめ細かな処理が可能である
ファイル内容をほぼ直に読み取ることができることから、usermodel API では実現できない処理も実現することができます。

## 短所
### ファイルの内部構造に関する理解が必要となる
eventmodel API で処理を行うためには、どのようなイベントがどのような順番で発生するのかという、Excel ファイルの内部構造に対する理解が欠かせません。

リファレンスブックがあるにはあります。『Microsoft Excel 97 Developers Kit』。洋書、絶版。本稿執筆時点で Amazon に[中古本が出品されていました](https://www.amazon.co.jp/Microsoft-Excel-Developers-Kit-Customize/dp/1572314982/ref=sr_1_1?ie=UTF8&qid=1530997082&sr=8-1&keywords=1572314982)。

`Record` 実装クラスの API ドキュメントには、この本の参照箇所が記載されています。例えば [`BoundSheetRecord` クラスの API ドキュメント](http://poi.apache.org/apidocs/org/apache/poi/hssf/record/BoundSheetRecord.html)には、「REFERENCE: PG 291 Microsoft Excel 97 Developer's Kit (ISBN: 1-57231-498-2)」というように記載されています。本格的なアプリケーションを作成する際には、リファレンスブック片手に取り組む必要があるでしょう。

私は API ドキュメントと実際のレコード内容を確認しながら実装を進めました。
いずれにせよ、トライアンドエラー的な実装を強いられることになるでしょう。

### ファイル内容に対するランダムアクセスができない
eventmodel API ではファイルを先頭から末尾に一直線に読み取るだけであることから、ファイル後方の内容に応じてファイル前方の内容を評価する必要があるような場合には、自身でファイル内容を管理・記録する必要があります。

# SAXを使う方法
## SAXとは
SAX は Simple API for XML の略で、Java8 においては `org.xml.sax` パッケージで標準 API として提供されています。
POI の eventmodel API と似たように、XML ファイルを先頭から末尾まで一方向に読み進め、XML ファイルの内容がイベントとしてアプリケーションに通知されます。

よく知られているように、.xlsx/.xlsm 形式の Excel ファイルの実体はいくつかの XML ファイルなどから成る ZIP ファイルですので、SAX を利用して Excel ブックの内容を読み取ることができます。

「[方眼Diff](https://qiita.com/nmby/items/ca7227a1048528b1bf31)」では、.xlsx/.xlsm 形式の Excel ブックからシート名一覧を読み取る際にこの方法を採用しています。

## 実装例
### ワークシート名およびグラフシート名を取得する場合

```java:実装例3-a：XSSFSheetListerWithSAX.java
package mypackage;

import java.io.File;
import java.io.InputStream;
import java.nio.file.FileSystem;
import java.nio.file.FileSystems;
import java.nio.file.Files;
import java.util.ArrayList;
import java.util.List;

import org.xml.sax.Attributes;
import org.xml.sax.InputSource;
import org.xml.sax.XMLReader;
import org.xml.sax.helpers.DefaultHandler;
import org.xml.sax.helpers.XMLReaderFactory;

public class XSSFSheetListerWithSAX {
    
    /**
     * zipファイルとしての .xlsx/.xlsm ファイルから次のエントリを読み込み、
     * シート名の一覧を抽出します。
     * 
     * *.xlsx
     *   +- xl
     *     +- workbook.xml
     */
    private static class Handler1 extends DefaultHandler {
        
        private final List<String> names = new ArrayList<>();
        
        @Override
        public void startElement(
                String uri,
                String localName,
                String qName,
                Attributes attributes) {
            
            if ("sheet".equals(qName)) {
                names.add(attributes.getValue("name"));
            }
        }
    }
    
    /**
     * 指定されたExcelファイルに含まれるシート名の一覧を返します。
     * 
     * @param file Excelファイル
     * @return シート名の一覧
     */
    public static List<String> getSheetNames(File file) throws Exception {
        try (FileSystem fs = FileSystems.newFileSystem(file.toPath(), null)) {
            Handler1 handler1 = new Handler1();
            
            try (InputStream is = Files.newInputStream(
                    fs.getPath("xl/workbook.xml"))) {
                
                InputSource source = new InputSource(is);
                XMLReader parser = XMLReaderFactory.createXMLReader();
                parser.setContentHandler(handler1);
                parser.parse(source);
            }
            
            return handler1.names;
        }
    }
    
    public static void main(String[] args) throws Exception {
        List<String> sheetNames = getSheetNames(
                new File("C:\\Users\\user01\\Desktop\\ゲーム機売上一覧.xlsx"));
        
        sheetNames.forEach(System.out::println);
    }
}
```

```console:実行結果
売上一覧_2016年
グラフ_2016年
売上一覧_2017年
グラフ_2017年
売上一覧_2018年
グラフ_2018年
```

### ワークシート名のみを取得する場合

```java:実装例3-b：XSSFSheetListerWithSAX.java
package mypackage;

import java.io.File;
import java.io.InputStream;
import java.nio.file.FileSystem;
import java.nio.file.FileSystems;
import java.nio.file.Files;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;

import org.xml.sax.Attributes;
import org.xml.sax.InputSource;
import org.xml.sax.XMLReader;
import org.xml.sax.helpers.DefaultHandler;
import org.xml.sax.helpers.XMLReaderFactory;

public class XSSFSheetListerWithSAX {
    
    /**
     * zipファイルとしての .xlsx/.xlsm ファイルから次のエントリを読み込み、
     * シート名の一覧、および、シート名とシートId（relId）のマップを抽出します。
     * 
     * *.xlsx
     *   +- xl
     *     +- workbook.xml
     */
    private static class Handler1 extends DefaultHandler {
        
        private final List<String> names = new ArrayList<>();
        private final Map<String, String> nameToId = new HashMap<>();
        
        @Override
        public void startElement(
                String uri,
                String localName,
                String qName,
                Attributes attributes) {
            
            if ("sheet".equals(qName)) {
                names.add(attributes.getValue("name"));
                nameToId.put(
                        attributes.getValue("name"),
                        attributes.getValue("r:id"));
            }
        }
    }
    
    /**
     * zipファイルとしての .xlsx/.xlsm ファイルから次のエントリを読み込み、
     * シートId（relId）とエントリパスのマップを抽出します。
     * 
     * *.xlsx
     *   +- xl
     *     +- _rels
     *       +- workbook.xml.rels
     */
    private static class Handler2 extends DefaultHandler {
        
        private final Map<String, String> idToSource = new HashMap<>();
        
        @Override
        public void startElement(
                String uri,
                String localName,
                String qName,
                Attributes attributes) {
            
            if ("Relationship".equals(qName)) {
                idToSource.put(
                        attributes.getValue("Id"),
                        attributes.getValue("Target"));
            }
        }
    }
    
    /**
     * 指定されたExcelファイルに含まれるシート名の一覧を返します。
     * 
     * @param file Excelファイル
     * @return シート名の一覧
     */
    public static List<String> getSheetNames(File file) throws Exception {
        try (FileSystem fs = FileSystems.newFileSystem(file.toPath(), null)) {
            
            Handler1 handler1 = new Handler1();
            try (InputStream is = Files.newInputStream(
                    fs.getPath("xl/workbook.xml"))) {
                
                InputSource source = new InputSource(is);
                XMLReader parser = XMLReaderFactory.createXMLReader();
                parser.setContentHandler(handler1);
                parser.parse(source);
            }
            
            Handler2 handler2 = new Handler2();
            try (InputStream is = Files.newInputStream(
                    fs.getPath("xl/_rels/workbook.xml.rels"))) {
                
                InputSource source = new InputSource(is);
                XMLReader parser = XMLReaderFactory.createXMLReader();
                parser.setContentHandler(handler2);
                parser.parse(source);
            }
            
            return handler1.names.stream()
                    .filter(name -> {
                        String id = handler1.nameToId.get(name);
                        String source = handler2.idToSource.get(id);
                        return source.startsWith("worksheets/");
                    })
                    .collect(Collectors.toList());
        }
    }
    
    public static void main(String[] args) throws Exception {
        List<String> sheetNames = getSheetNames(
                new File("C:\\Users\\user01\\Desktop\\ゲーム機売上一覧.xlsx"));
        
        sheetNames.forEach(System.out::println);
    }
}
```

```console:実行結果
売上一覧_2016年
売上一覧_2017年
売上一覧_2018年
```

## 解説
XML ファイルの内容をイベントとして受け取るためには、`org.xml.sax.helpers.DefaultHandler` を継承するのが便利です。`DefaultHandler` クラスが用意する様々なコールバックメソッドのうち、アプリケーションで必要とするイベントに対応するコールバックメソッドをオーバーライドして利用します。

上記のコード例では XML ファイルの開始タグが現れるたびに呼び出される `startElement(String, String, String, Attributes)` メソッドを利用しています。

実装例3-a では、Excel ファイルを zip ファイルとして解凍したときの `xl/workbook.xml` というエントリを、実装例3-bではそれに加えて `xl/_rels/workbook.xml.rels` というエントリを読み取っています。

それらをどのように解析しているかの解説は、長くなるので省きます。.xlsx/.xlsm 形式の Excel ファイルを ZIP 解凍ツールで展開することで、Excel ファイルを構成する XML ファイルの内容を容易に確認することができます。お手持ちの Excel ファイルで確認しながら、上記のコード例を読んでみてください。

## 長所
### 少量のメモリで高速に動作する
POI の eventmodel API と同様に、高速かつ省メモリで動作します。
加えて、Excel ファイルを構成する複数の XML ファイルのうち必要なものだけを読み取ることができるため、より一層高速に動作させることが可能です。

### きめ細かな処理が可能である
ファイル内容を直に読み取ることができることから、POI の usermodel API では実現できない処理も実現することができます。

### 外部ライブラリを必要としない
`org.xml.sax` およびそのサブパッケージは、Java8 において標準 API として提供されています。外部ライブラリへの依存が必要ありません。

## 短所
### ファイルの内部構造に関する理解が必要となる
POI の eventmodel API と同様に、Excel ファイルの内部構造に対する理解が不可欠となります。
ただし、.xlsx/.xlsm 形式の Excel ブックの構造は Office Open XML と呼ばれ、ISO/IEC 29500 として標準化されています。従って、仕様情報を入手するのが比較的容易です。また、ZIP 解凍ツールで容易に内容を確認することができることから、.xls 形式の Excel ブックを POI の eventmodel API で扱うよりは取っ付き易い面があると言えます。

### ファイル内容に対するランダムアクセスができない
POI の eventmodel API と同様に、SAX も XML ファイルを先頭から末尾に一直線に読み取るだけであることから、ファイル後方の内容に応じてファイル前方の内容を評価する必要があるような場合には、自身でファイル内容を管理・記録する必要があります。

# おわりに
本稿では、Excel ファイルに含まれるシート名の一覧を Java で取得する方法として、次の3点を紹介しました。

- Apache POI の usermodel API を利用する方法（.xls/.xlsx/.xlsm）
- Apache POI の eventmodel API を利用する方法（.xls）
- SAX（Simple API for XML）を利用する方法（.xlsx/.xlsm）

どの方法にも一長一短があり、私たちプログラマが目的に応じて正しく使い分ける必要があります。

私自身は、上記3つの方法ともに、「[方眼Diff](https://qiita.com/nmby/items/ca7227a1048528b1bf31)」を作成する際の必要性に迫られて初めてチャレンジしたものでした。
特に eventmodel API や SAX を使う方法にはハードルの高さを感じましたが、意外や意外、始めてしまえばあまり難しくなく、慣れてしまえば便利に感じるものです。皆さんも初めの抵抗を乗り越えてチャレンジしてみると、実装力の幅が広がるかもしれません。


「方眼Diff」では Excel ファイルの更新にも複数の方法を駆使しています。これについては稿を改めて紹介したいと思います。

# APPENDIX
## Apache POI のライブラリをビルドパスに加えて利用する方法
Apache POI の提供するいくつかの jar ファイルをビルドパスに追加することで、その機能を利用できるようになります。
ここでは手動でダウンロードした jar ファイルを Eclipse でビルドパスに追加する方法を紹介します。（Maven などを利用する方法は他を当たってください。）

### 1. ライブラリ（zip ファイル）をダウンロードする
[Apache POI サイト](https://poi.apache.org/) の[ダウンロードページ](https://poi.apache.org/download.html)から zip ファイル（本稿執筆時点では poi-bin-3.17-20170915.zip）をダウンロードすることができます。

### 2. zip ファイルを解凍する
ローカルPCの任意の場所で zip ファイルを解凍します。
次のように解凍されると思います。

```
poi-3.17
  ├─ docs
  │   └─ 多数のhtmlファイル群
  │
  ├─ lib
  │   ├─ commons-codec-1.10.jar
  │   ├─ commons-collections4-4.1.jar
  │   ├─ commons-logging-1.2.jar
  │   ├─ junit-4.12.jar
  │   └─ log4j-1.2.17.jar
  │
  ├─ ooxml-lib
  │   ├─ curvesapi-1.04.jar
  │   └─ xmlbeans-2.6.0.jar
  │
  ├─ LICENSE
  ├─ NOTICE
  ├─ poi-3.17.jar
  ├─ poi-examples-3.17.jar
  ├─ poi-excelant-3.17.jar
  ├─ poi-ooxml-3.17.jar
  ├─ poi-ooxml-schemas-3.17.jar
  └─ poi-scratchpad-3.17.jar
```

### 3. ユーザーライブラリを作成する
A) Eclipse の設定にて [Java] > [ビルド・パス] > [ユーザー・ライブラリー] > [新規...] を選択します。

<img src="https://qiita-image-store.s3.amazonaws.com/0/84939/91e59743-7603-ec96-b529-d7c9ca104883.png" width=851px>

B) お好きな名前、たとえば「poi-3.17」とでも入力してOKを押下します。

<img src="https://qiita-image-store.s3.amazonaws.com/0/84939/244f1d3c-97e8-cb85-0343-8d46a3d6e4bb.png" width=233px>

C) 作成されたユーザーライブラリを選択したうえで、[外部 JAR の追加...] を押下します。

<img src="https://qiita-image-store.s3.amazonaws.com/0/84939/415bacff-627d-6831-483f-6231067facb4.png" width=851px>

D) 上記2.の解凍物に含まれる jar ファイルを指定します。lib フォルダや ooxml-lib フォルダに含まれるものも指定します。

中には Excel だけでなく Word や PowerPoint などを操作するための jar ファイルやサンプルファイルなども含まれており、Excel ファイルを操作するだけならそれらの jar ファイルは不要なのですが、ま、全部入れちゃえば良いんじゃないでしょうか。

<img src="https://qiita-image-store.s3.amazonaws.com/0/84939/eb7f4a42-90a3-e645-3f81-1cfa1f49c5df.png" width=1135px>

E) [適用して閉じる] を押下します。

### 4. プロジェクトのビルドパスにユーザーライブラリを追加する
A) プロジェクトを選択し、コンテキストメニューから [ビルド・パス] > [ライブラリーの追加...] を選択します。

<img src="https://qiita-image-store.s3.amazonaws.com/0/84939/8ba7fb5d-e325-fe0c-1c8b-e9a54eddf4ba.png" width=866px>

B) ライブラリー・タイプとして [ユーザー・ライブラリー] を選択し、その対象として先ほど作成したユーザーライブラリを指定します。

<img src="https://qiita-image-store.s3.amazonaws.com/0/84939/d4529cc6-599b-dc4e-4778-4b2d5e66c25a.png" width=579px>

<img src="https://qiita-image-store.s3.amazonaws.com/0/84939/2a2c76a3-d957-0134-994c-1ee917579193.png" width=579px>

以上で完了です。