Create a gist now

Instantly share code, notes, and snippets.

What would you like to do?
Clojure から POI ってみる

Clojure から POI ってみる

この記事は、変態アドベントカレンダー 6日目の記事です。前日は はがねのつるぎさん でした。

このアドベントカレンダーは、意識もスキルも高い変態勇者がハイテクを駆使してかくもの、と思ってらっしゃる方にはちょっと申し訳ありません。若干変態度もハイテク度も低めで肩透かしを食らうことになりますが、しばしお付き合いください。

今日のネタは、私自身の日々の切実な課題である、Excel文書と戦うための準備、といったところです。

Excel ドキュメントにに対する不満

はじめに言っておきますが、私は「 Excel を dis るつもりはない」のです。

人を憎んでExcel憎まず です。 Excel 自体は軽快で素直でとても良いツールだとおもいます。現に、Excelライクなソフトはいくつかあれど、本家Excel程の使い勝手を実現できているところまでは来ていないと思っています。ただ、その 使い方 に問題が多いのです。

私の周りでは仕様書はもっぱら Excel 方眼紙に書くもの、となっています。 Word の使い勝手がいまいちなのと、やれ MarkdownReSTructured Text だの言ってみたところで、「じゃあ明日からコレで書け」といっても、書く人がついてこれなくて困ってしまいます。「だからExcel」という状態がすでに15年以上はたっているような気がします。

でも、Subversion (gitじゃないよ。ヤレヤレだぜorz)で管理された Excel 文書のバイナリが増えるにつれ、

  • 印刷がプリンタによってずれちまう、チョーめんどくせぇ
  • ファイル/ディレクトリ横断的にサクッと検索できねぇ
  • 差分が取れね〜(セルのテキストに限ってはら出来るのですが図形とか色とか...)
  • Emacs のバッファで見れねぇwww

といった、「オレ的ストレス」は増える一方。なんとかしないとまた倒れてしまいます(実は今年1月〜9月まで休職してました)。

愚痴を言っても始まらない

まわりの人たちを変える、とかすでに作成されてしまった文書を全て書き直す、とか、非現実的なことは考えるだけ無駄です。ここはひとつ、自分にできるストレス対策として、 Excel文書と向き合う 方向で考えます。

前置きが長くなってしまいましたが、とりあえずは Excel 文書を扱うプログラムを さくっと つくれるような下地作りをしておきたいと思っています。

自分は以前に Ruby で Win32 OLE 経由で Excel 文書をどーにかこーにかしたことはあったのですが、数年前仕事用に Mac を Get したので Mac で扱えることが前提です。で OLE はパス。POI は使ったことがないのですが、POI なら Clojure から簡単に使えそうです。

Getting Started

Clojure から POI を使うには、leiningen でプロジェクトを作って、:dependencies に

[org.apache.poi/poi "3.8"]
[org.apache.poi/poi-ooxml "3.8"]

とか追加すればよいです。

POI の Java のサンプルは ここらへん にあります。Clojure は「書きやすいJava」と 言えなくもない ので、最悪「直訳」的に書いてしまえます。サンプルにあるコードの Creating Cells というのを「直訳」してみます。

(defn add-two-sheets-and-save
  "シートとセルを追加する。"
  ([] (add-two-sheets-and-save "workbook-01.xls"))
  ([fname]
     (with-open [out (FileOutputStream. fname)]
       (let [wb (HSSFWorkbook.)
             helper (.getCreationHelper wb)
             sheet (.createSheet wb "new-sheet")]
         (let [row (.createRow sheet 0)]
           (-> row (.createCell 0) (.setCellValue 1.0)) ;; 1 だとコンパイルエラー
           (-> row (.createCell 1) (.setCellValue 1.2))
           (-> row (.createCell 2) (.setCellValue (.createRichTextString helper "This is a string.")))
           (-> row (.createCell 3) (.setCellValue true)))
         (.write wb out)))))

はい、全然イケてないですねwww。でもいいんです。今はサクッと書けることに重きをおきます。

実際のところ、一から book を作るというケースはあまり無いです。どちらかというと、読む方が多いでしょう。なので読む方のサンプルを作りましょう。

元ネタを何にしようか迷ったのですが、以前ちょっと流行った クリエイターかるた というやつにしました。ここ で公開されています。GoogleDrive 上のドキュメントですので、メニューから [ファイル] - [形式を指定してダウンロード] で手元にダウンロードして (.xlsx か .ods 形式) 、さらに Excel2003 形式 に LibreOffice とかで変換しておきます。(これまたオレオレ事情で、会社では 2003形式が標準なんですよ)。

せっかくなので REPL 上でインタラクティブに作ってみましょう。hentai_poi/core.clj には、まずは

(ns hentai-poi.core
  (:import
   (java.io FileOutputStream FileInputStream)
   (java.util Date Calendar)
   (org.apache.poi.hssf.usermodel HSSFWorkbook)
   (org.apache.poi.ss.usermodel Workbook Sheet Cell Row WorkbookFactory DateUtil
                                IndexedColors CellStyle Font CellValue)
   (org.apache.poi.ss.util CellReference AreaReference CellRangeAddress RegionUtil)))

とだけ書いておき、lein repl で REPL を起動、require しておきます。

; nREPL 0.1.6-preview
user> (require 'hentai-poi.core)
nil
user> (ns hentai-poi.core)
nil
hentai-poi.core> 

ファイルを読むので FileInputSream を作っておきます。

hentai-poi.core> (def infile "クリエイターかるた.xls")
#'hentai-poi.core/infile
hentai-poi.core> (def input (FileInputStream. infile))
#'hentai-poi.core/input
hentai-poi.core> 

ワークブックを作ります。

hentai-poi.core> (def wb (WorkbookFactory/create input))
#'hentai-poi.core/wb
hentai-poi.core> (class wb)
org.apache.poi.hssf.usermodel.HSSFWorkbook
hentai-poi.core> 

ちゃんと作れているようですね。セルを読むには、シート -> 行 -> セル という3階層に手繰っていく必要があります。順番に試してみましょう。シートは手を抜いて1シート目にしてみます。

hentai-poi.core> (def sheet (.getSheetAt wb 0))
#'hentai-poi.core/sheet
hentai-poi.core> (class sheet)
org.apache.poi.hssf.usermodel.HSSFSheet
hentai-poi.core> (bean sheet)
{:drawingPatriarch #<HSSFPatriarch org.apache.poi.hssf.usermodel.HSSFPatriarch@37734b10>, :displayZeros true, :autobreaks false, :displayFormulas false, :forceFormulaRecalculation false, :objectProtect false, :workbook #<HSSFWorkbook org.apache.poi.hssf.usermodel.HSSFWorkbook@724523e0>, :displayRowColHeadings true, :rightToLeft false, :leftCol 0, :class org.apache.poi.hssf.usermodel.HSSFSheet, :topRow 0, :gridsPrinted false, :alternateFormula true, :header #<HSSFHeader org.apache.poi.hssf.usermodel.HSSFHeader@e82751a>, :defaultRowHeight 330, :sheetName "本当クリエイティブ職は地獄だぜフゥーハハハーハァー!", :defaultColumnWidth 15, :horizontallyCenter false, :alternateExpression true, :sheetConditionalFormatting #<HSSFSheetConditionalFormatting org.apache.poi.hssf.usermodel.HSSFSheetConditionalFormatting@9e0a562>, :displayGridlines true, :lastRowNum 57, :defaultRowHeightInPoints 16.5, :firstRowNum 0, :active true, :dataValidationHelper #<HSSFDataValidationHelper org.apache.poi.hssf.usermodel.HSSFDataValidationHelper@1bf5d5bc>, :rowSumsRight false, :printSetup #<HSSFPrintSetup org.apache.poi.hssf.usermodel.HSSFPrintSetup@3d80fbe2>, :scenarioProtect false, :verticallyCenter false, :printGridlines false, :drawingEscherAggregate #<EscherAggregate [ESCHERAGGREGATE]null[/ESCHERAGGREGATE]null>, :numMergedRegions 1, :columnBreaks #<int[] [I@1006bed5>, :rowBreaks #<int[] [I@1006bed5>, :rowSumsBelow false, :displayGuts false, :protect false, :dialog false, :selected true, :footer #<HSSFFooter org.apache.poi.hssf.usermodel.HSSFFooter@744decf5>, :fitToPage true, :password 0, :physicalNumberOfRows 58, :paneInformation #<PaneInformation org.apache.poi.hssf.util.PaneInformation@1925fa1a>}
hentai-poi.core> 

bean 関数は、JavaBean を Clojure の HashMap に変換してくれる頼もしい味方です。REPL からオブジェクトの様子を時々覗くのに便利ですね。シートの次は行ですが、行は sheet.rowIterator() で取得できるのですが、Clojure 的には Java のイテレーターはあまり嬉しくないので iterator-seq でシーケンスに変換しちゃいます。

hentai-poi.core> (def rows (iterator-seq (.rowIterator sheet)))
#'hentai-poi.core/rows
hentai-poi.core> (count rows)
58
hentai-poi.core> 

count してみたら 58 と出ました。なんか知らんけど、「イケそうな気がする〜」(最近見ないなぁ、天津木村)。続いてセルも、と行きたいところですが、セルは1つの行に対して .cellIterator() と呼んでやる必要があるので、まずは最初の行で挙動を見ておきましょう。

hentai-poi.core> (def row (first rows))
#'hentai-poi.core/row
hentai-poi.core> (bean row)
{:heightInPoints 25.0, :class org.apache.poi.hssf.usermodel.HSSFRow, :sheet #<HSSFSheet org.apache.poi.hssf.usermodel.HSSFSheet@3e67f6a8>, :formatted false, :zeroHeight false, :rowNum 0, :rowStyle nil, :lastCellNum 8, :firstCellNum 0, :height 500, :physicalNumberOfCells 8}
hentai-poi.core> 

最初の行を row に取得できたので、.cellIterator と iterator-seq でシーケンスを取得します。

hentai-poi.core> (def cells (iterator-seq (.cellIterator row)))
#'hentai-poi.core/cells
hentai-poi.core> (count cells)
8
hentai-poi.core> 

最初の行には 8 個、セルがあるようです。最初のセルは何かな?

hentai-poi.core> (bean (first cells))
IllegalStateException Only formula cells have cached results  org.apache.poi.hssf.usermodel.HSSFCell.getCachedFormulaResultType (HSSFCell.java:1184)
hentai-poi.core> 

うわぁ。ま、大勢に影響しないのであまり気にしないことにします。ここまでの情報で、シート内のセルを取得できることがわかりました。あとは、今まで使った関数を適当に組み合わせて適当にコードを書きます(説明雑やな)。

ソース一式は GitHub にあげてあります。

出来上がったソースの解説は端折りましたが、REPL を使ったインタラクティブに開発するイメージが伝わりましたでしょうか。POI のような巨大なライブラリを相手にする場合でもなんとか戦える気がしてきました。Excel方眼紙は 巨大な敵 ですが、これで業務時間中に堂々と Clojure で...S式ラブ♡

変態度も難易度も低い目の記事になってしまいましたが、このへんにしておきます。明日はもじゃ変さんの記事です。よろしくお願いします。

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment