2012年11月29日木曜日

Clojure+Apache POI+Event API

Apache POIで普段使うのは'User API'というAPIのようですが、 このAPIを利用して大きなファイルを扱おうとすると大量のメモリを必要とし、OutOfMemoryが発生します。

この問題を回避するには、

  • ヒープ領域を増やして実行する
  • Event APIを使う
  • Streaming User APIを使う
  • xlsxファイルを解凍して中身のxmlファイルを処理する

といった解決方法があるようです。

ためしに、Clojure+Apache POIでEvent APIを利用したコードを書いてみます。
ぱっと見た限りではxlsxファイルを解凍して処理する手間の一部を省いてくれているだけのようです。

;; project.clj
(defproject poixml "0.1.0-SNAPSHOT"
  :description "dummy"
  :url "dummy"
  :license {:name "dummy"}
  :dependencies [[org.clojure/clojure "1.4.0"]
                 [org.apache.poi/poi "3.8"]
                 [org.apache.poi/poi-ooxml "3.8"]
                 [org.clojure/data.xml "0.0.6"]]
  :main poixml.core)
;; src/poixml/core.clj
(ns poixml.core
  (:gen-class))

;; @see http://poi.apache.org/spreadsheet/how-to.html

(require 'clojure.data.xml)

(import
 '(org.apache.poi.xssf.eventusermodel XSSFReader)
 '(org.apache.poi.xssf.model SharedStringsTable)
 '(org.apache.poi.xssf.usermodel XSSFSheet
                                 XSSFWorkbook
                                 XSSFRow
                                 XSSFCell
                                 XSSFRichTextString))

(defn call-with-cell-value [path f]
  (let [reader (XSSFReader. (org.apache.poi.openxml4j.opc.Package/open path))
        sst (.getSharedStringsTable reader)]
    (with-open [istream (.next (.getSheetsData reader))]
      (doseq [r (:content (first
                           (filter #(= :sheetData (:tag %))
                                   (:content (clojure.data.xml/parse istream)))))]
        (when (= :row (:tag r))
          (doseq [[col cell] (map vector (iterate inc 1) (:content r))]
                 (let [val (first (filter #(= :v (:tag %)) (:content cell)))]
                   (f (Integer/parseInt (:r (:attrs r))) ; 行番号
                      col ; 列番号
                      (if (= "s" (:t (:attrs cell)))
                          ;; 文字列はSharedStringsTableにある
                          (-> (.getEntryAt sst (bigint (first (:content val))))
                              XSSFRichTextString.
                              .toString)
                          (first (:content val)))))))))))

;; --------------------------------------------------
;; 適当にxlsxファイルを作成
(defn create-3x3 [path]
  (let [wb (XSSFWorkbook.)
        sh (.createSheet wb)]
    (dorun
     (for [y (range 3)]
       (.createRow sh y)))
    (dorun
     (for [x (range 3) y (range 3)]
       (-> (.createCell (.getRow sh y) x)
           (.setCellValue (str (* (inc x) (inc y)))))))
    (with-open [out (java.io.FileOutputStream. path)]
      (.write wb out))))

;; xlsxファイルの各セルを表示
(defn print-cells [path]
  (call-with-cell-value path
    (fn [row col val]
        (printf "[%d,%d] %s\n" row col val)
        (flush))))

;; 引数に作成するファイル名を指定
(defn -main
  [& args]
  (when (= (count args) 1)
    (let [path (first args)]
      (create-3x3 path)
      (print-cells path))))
> lein run test.xlsx 
Compiling poixml.core
[1,1] 1
[1,2] 2
[1,3] 3
[2,1] 2
[2,2] 4
[2,3] 6
[3,1] 3
[3,2] 6
[3,3] 9

0 件のコメント:

コメントを投稿