FIELD NOTES: 書を持って街へ出よう

合同会社フィールドワークス プログラマ兼代表のブログ

jarファイルとjniライブラリのインストール方法

オフィスシーン(コーヒーブレイク) - 写真素材
(c) YsPhoto写真素材 PIXTA

Javaの外部ライブラリ(*.jar)をJavaの処理系が参照できるようにするためには,環境変数CLASSPATHに追加したり,コマンド実行時に -classpath オプションを追加するなどの方法がありますが,jarファイルの数が多くなってくるといちいち指定するのは面倒です。

Java 2 (JDK 1.2)からの機能拡張により,システム標準のjarファイル置き場が設けられているので,OS別に情報をまとめました。
同時に,jniライブラリ(*.so, *.dll, *.jnilib)の置き場所についても記しています。

なお以降の説明では,Javaの開発環境(JDK)のインストールディレクトリを環境変数JAVA_HOMEに,実行環境(JRE)のインストールディレクトリを環境変数JRE_HOMEに設定してあるものとします。

Linux

jarファイルの置き場所は,以下のとおりです。

$JAVA_HOME/jre/lib/ext
$JRE_HOME/lib/ext

jniライブラリ(*.so)の置き場所は,以下のとおりです。

$JAVA_HOME/jre/lib/<アーキテクチャ>
$JRE_HOME/lib/<アーキテクチャ>

<アーキテクチャ>の部分は,32ビット環境では“i386”に,64ビット環境では“amd64”になるようです(JDK 1.6で確認)。

複数のJDKJREをインストールしている場合は,それぞれのインストール先にjarファイルをコピーする必要があります。

Windows

jarファイルの置き場所は,以下のとおりです。

%JAVA_HOME%\jre\lib\ext
%JRE_HOME%\lib\ext

Windowsの場合,jniライブラリ(*.dll)の置き場所として特別な場所が用意されているわけではなく,PATHが通っていればどこでも良いようです。

2011-08-29 訂正

8/25の版では,Windows用jniライブラリ(*.dll)の置き場所を「%JAVA_HOME%\jre\lib\<アーキテクチャ>」または「 %JRE_HOME%\lib\<アーキテクチャ>」と書きましたが,JDK 1.6.0で確認したところ誤りでした。32ビット版JDKをインストールするとi386というフォルダができますが,ここにDLLをコピーしてもjava起動時に検索できずエラーとなります。

PATHの設定が空の状態で以下のプログラムを実行して,実際の検索パスを調べたところ,

public class LibraryTest {
    public static void main(String[] args) {
        System.out.println(System.getProperty("java.library.path"));
    }
}

次のようになりました。

C:\Program Files\Java\jdk1.6.0_24\bin;.;C:\WINDOWS\Sun\Java\bin;
C:\WINDOWS\system32;C:\WINDOWS

「上記の検索パス」+「PATHの設定」が,java実行時のjniライブラリの検索パスとなると思われます。

実用上は「%JAVA_HOME%\bin」に置くのが良さそうです。

Mac OS X

Appleが独自の改造を行っているため,LinuxともWindowsとも異なります。

jarファイルもjniライブラリ(*.jnilib)も同じ場所に置きます。

/Library/Java/Extensions/
~/Library/Java/Extensions/

Windows用PHP拡張モジュールを最短でビルドする方法

現在,「LL言語用PDF帳票ツール Field Reports」をWindowsへ移植しているのですが,Windows環境でPHP拡張モジュールをビルドする方法として断片的な情報しか見つからなくて苦労しました。
試行錯誤してなんとかビルド手順が確立できたので,わかったことをまとめておきます。

基礎知識

コンパイラ

基本的に,拡張モジュールをビルドする時は,PHPの実行モジュールをビルドした時と同じ環境でビルドする必要があるようです。
PHP起動時にチェックをしているようで,異なったビルド環境で作られた拡張モジュールを使おうとするとエラーとなりロードできません。
以下の条件をそろえる必要があるようです。

ちなみに,PHP公式サイトで配布されているWindows用バイナリモジュールは,PHP5.2がVisual Studio 6.0で,PHP5.3がVisual Studio 2008でビルドされていました。
PHP拡張モジュールをバイナリ配布するには,Visual Studio 6.0とVisual Studio 2008でビルドする必要がありそうです。

Windows版にはphpizeコマンドがない

Linux等のUnix系OSでの作成手順は,【PHP/C】PHP extension で Hello World! - 不動産に興味を持ち始めたWebエンジニアのメモ帳などを参照していただきたいのですが,標準的なビルド手順で必ず出てくる“phpize”コマンドがWindows版では用意されていません。
代わりに,ソースツリーのトップにある“buildconf.bat”コマンドを使えばよいと思われますが,任意のディレクトリに拡張モジュールのソースを展開して開発することは考慮されていないようです。

拡張モジュールのプログラミング

この記事では,Windowsで空の拡張モジュールを作成してビルドするまでの手順を扱っています。
拡張モジュールの中身のプログラミングについては,実例で学ぶPHP拡張モジュールの作り方|gihyo.jp … 技術評論社などを参照してください。

準備

Visual Studio

Visual Studio 6.0は中古で入手するしかないので,まずはVisual Studio 2008でPHP5.3用のモジュールを作成しました。

Visual Studio 2008 Express Editionはまだ入手できるようです。下記のマイクロソフトのサイトからダウンロードしてインストールしました。

http://www.microsoft.com/japan/msdn/vstudio/2008/product/express/

PHPの実行モジュール

PHP公式サイトからWindows用バイナリモジュールをダウンロードして展開します。

http://windows.php.net/download/

ここでは,展開したファイルを“C:\php”に置いたものとします。

PHPのソース

PHP公式サイトからPHPのソース一式をダウンロードします。

http://www.php.net/downloads.php

ここでは,展開したソースを“C:\php-src”に置いたものとします。

ビルド手順

コマンドプロンプトを開く

「スタート」メニューから「Visual Studio Tools/Visual Studio 2008 コマンドプロンプト」を選択し,コマンドプロンプト・ウインドウを開きます。
以後の作業は,このコマンドプロンプト上で行います。

スケルトンの作成

拡張モジュールのソースが展開されているディレクトリに移動し,PHPで記述されたスケルトン作成コマンドを実行します。

     > cd C:\php-src\ext
     > php ext_skel_win32.php --extname=test

ここでは,モジュール名を“test”としました。

config.w32ファイルの編集

作成されたディレクトリの中にある“config.w32”ファイルをテキストエディタで編集します。

// $Id$
// vim:ft=javascript

// If your extension references something external, use ARG_WITH
// ARG_WITH("test", "for test support", "no");

// Otherwise, use ARG_ENABLE
// ARG_ENABLE("test", "enable test support", "no");

if (PHP_TEST != "no") {
     EXTENSION("test", "test");
}

コメント部分を編集してARG_WITHの行かARG_ENABLEのどちらかの行を有効にします。
この行を有効にすると,後で実行するconfigureコマンドで“--enable-test”や“--with-test”オプションが選択できるようになります。今回は拡張モジュールのビルドだけが目的なので,一見無駄な作業ですが,編集しないとconfigureコマンドが動かないのでやはり編集する必要があります。
なお,ARG_ENABLEの行の先頭のコメントだけ削除して済ませようとするとconfigureコマンドが解析に失敗するので,以下の例のように余分なコメントはバッサリ削除します。

// $Id$
// vim:ft=javascript

ARG_ENABLE("test", "enable test support", "no");

if (PHP_TEST != "no") {
     EXTENSION("test", "test");
}
configureコマンドの実行

ソースツリーのトップへ移って,以下のコマンドを実行します。

> cd C:\php-src
> buildconf
> configure

これで,コンパイルに必要なヘッダファイル“main/config.w32.h”が作成されます。

コンパイル

“C:\php-src\ext\test\test.dsp”ファイルをVisual Studio 2008で開きます。
.dspファイルはVisual Studio 6.0形式のプロジェクトファイルなので,2008形式のプロジェクトに変換する必要があります。

ビルド構成メニューで「Release_TS」を選択し,プロジェクトをビルドします。

おそらく以下のようなエラーが発生すると思いますので,プロジェクトのプロパティダイアログを開いて「構成プロパティ/リンカ/全般」の「追加のライブラリ ディレクトリ」に「C:\php\dev」を追加します(もしくは,php5ts.libを参照できる場所にコピーします)。

LINK : fatal error LNK1181: 入力ファイル 'php5ts.lib' を開けません。

こんどは,ビルドが成功するはずです。
「C:\php-src\Release_TS\php_test.dll」が作成されました。

ノンスレッドセーフ モジュールのビルド

デフォルトで作成されるのは,スレッドセーフの拡張モジュールになります。

ノンスレッドセーフのモジュールをビルドする場合は,

  • プリプロセッサの定義」プロパティから“ZTS=1”の定義を削除します。
  • 「リンカ/入力/追加依存ファイル」で“php5ts.lib”を“php5.lib”に変更します。

LL言語向けPDF帳票ツールField ReportsがJavaに対応しました

弊社製品LL言語向けPDF帳票ツール Field ReportsJavaに対応させました。

当初の構想としては,Java対応の手段としてOCaml-Javaを使ってみようかと思っていたのですが,今回お手軽にJNIを使ってJava用I/F (Java Bridge) を作成してみました。

作成したJava Bridgeは,Field Reports 1.2.1に同梱して,本日(2011.07.12)よりリリースしています。
ただ,Java対応の手段としては,OCaml-Java を使った方法ももう少し検討してみたいと考えています。
したがって,今回のJava Bridgeのリリースは「暫定版」の扱いといたします(将来的には,サポート対象外の扱いにする可能性があります)。

Javaプログラムの作成

nativeメソッド宣言を含むJavaプログラムを以下のように作成しました。
帳票の構成やデータなどの構造を持った情報をJSONで与えているので,非常にシンプルなAPIとなっています。

package jp.co.field_works;

import sun.misc.HexDumpEncoder;

/**
 * Reports.java
 *
 * Java bridge for Field Reports
 *
 * @author <a href="http://www.field-works.co.jp/">Field Works, LLC.</a>
 */
public class Reports {
    /**
     * 共有ライブラリのロードとモジュールの初期化
     */
    static {
        System.loadLibrary("Reports");
        init();
    }

    /**
     * Reportsモジュールの初期化を行う。
     */
    private native static void init();
   
    /**
     * バージョン番号を取得します。
     */
    public native static String version()
        throws ReportsException;
   
    /**
     * ログ出力のレベルを設定します。
     * 有効な値の範囲は0〜4です:
     *  0: ログを出力しない
     *  1: ERRORログを出力する
     *  2: WARNログを出力する
     *  3: INFOログを出力する
     *  4: DEBUGログを出力する
     * 1以上の値を設定した場合,標準エラー出力にログを出力します(初期値:0)。
     */
    public native static void setLogLevel(int n)
        throws ReportsException;

    /**
     * レンダリング・パラメータのデフォルト値を設定します。
     * レンダリング・パラメータは,JSON形式の文字列で与えます。
     */
    public native static void setDefaults(String param)
        throws ReportsException;

    /**
     * レンダリング・パラメータparamを元にレンダリングを実行し,
     * 結果をバイト文字列として返します。
     * レンダリング・パラメータは,JSON形式の文字列で与えます。
     */
    public native static byte[] renders(String param)
        throws ReportsException;

    /**
     * レンダリング・パラメータparamを元にレンダリングを実行します。
     * 処理結果は,filenameで指定したファイルに出力されます。
     * レンダリング・パラメータは,JSON形式の文字列で与えます。
     */
    public native static void render(String param, String filename)
        throws ReportsException;

    public static void main(String[] args) throws ReportsException {
        // test
        Reports.setLogLevel(3);
        System.out.println("version: " + Reports.version());
     HexDumpEncoder hexdumpencoder = new HexDumpEncoder();
     System.out.println(hexdumpencoder.encodeBuffer(Reports.renders("{}")));
    }
}

Cプログラムの作成

javahプログラムで生成したヘッダファイルでの宣言にしたがって,Cプログラムを記述しました。

今までのPython, Ruby, Perl, PHPの言語Bridgeでは,LL言語ネイティブの辞書/ハッシュ等のデータ構造をCプログラム内でJSON相当のデータ構造に変換していたのですが,以下のような理由からJava BridgeではJSON文字列形式のみの対応としました。

  • JSONICなどのJava用JSONライブラリを見てみると,Map, Array/Listだけではく,Object, Iterable, Iteratorなど多彩なオブジェクトからの変換に対応しており,これと同レベルの変換プログラムをCで記述するのは非常に困難。
  • Clojure, Scala, GroovyなどのJVM上で動作するJava以外の言語では,独自のMap, Listのデータ構造を採用していることが多く,それらすべてに対応することは事実上不可能と考えた。
  • JSON変換とPDF生成の処理時間を比べると,PDF生成にかかる時間が圧倒的に大きく,JSON変換をCで記述してもパフォーマンス向上にあまり貢献しないことがわかった。

ビルドには,OCamlのインクルードファイルが必要です。

2011.11.28 追記

Field Reports 1.3.0からは,OCamlのインクルードファイルが不要になりました。

#include <jni.h>
#include <stdio.h>
#include <string.h>
#include "jni_reports.h"

extern "C" {
#include <caml/mlvalues.h>
#include <caml/alloc.h>
#include <caml/callback.h>
#include <caml/fail.h>
#include <caml/memory.h>
#include <caml/threads.h>
#include "reports.h"
}

static void throw_exn(JNIEnv* env, value exn)
{
    jclass clazz = env->FindClass("jp/co/field_works/Reports/ReportsException");
    env->ThrowNew(clazz, String_val(string_of_exn(Extract_exception(exn))));
}

static value encode_string(JNIEnv* env, jstring jstr)
{
    CAMLparam0();
    CAMLlocal1(result);

    const char* s = env->GetStringUTFChars(jstr, NULL);
    result = caml_copy_string(s);
    env->ReleaseStringUTFChars(jstr, s);

    CAMLreturn(result);
}

/*
 * Class:     jp_co_field_works_Reports
 * Method:    init
 * Signature: ()V
 */
JNIEXPORT void JNICALL
Java_jp_co_field_1works_Reports_init(JNIEnv* env, jclass clazz)
{
    init_reports();
}

/*
 * Class:     jp_co_field_works_Reports
 * Method:    version
 * Signature: ()Ljava/lang/String;
 */
JNIEXPORT jstring JNICALL
Java_jp_co_field_1works_Reports_version(JNIEnv* env, jclass clazz)
{
    jstring jresult = NULL;
    CAMLparam0();
    CAMLlocal1(caml_result);
    caml_leave_blocking_section();

    caml_result = reports_version();
    if (Is_exception_result(caml_result)) {
        // Error
        throw_exn(env, caml_result);
        goto exit;
    }
    jresult = env->NewStringUTF(String_val(caml_result));

exit:
    caml_enter_blocking_section();
    CAMLreturnT(jstring, jresult);
}

/*
 * Class:     jp_co_field_works_Reports
 * Method:    setLogLevel
 * Signature: (I)V
 */
JNIEXPORT void JNICALL
Java_jp_co_field_1works_Reports_setLogLevel(JNIEnv* env, jclass clazz, jint n)
{
    CAMLparam0();
    CAMLlocal2(caml_level, caml_result);
    caml_leave_blocking_section();

    caml_level = Val_int(n);
    caml_result = set_log_level(caml_level);
    if (Is_exception_result(caml_result)) {
        // Error
        throw_exn(env, caml_result);
        goto exit;
    }

exit:
    caml_enter_blocking_section();
    CAMLreturn0;
}

/*
 * Class:     jp_co_field_works_Reports
 * Method:    setDefaults
 * Signature: (Ljava/lang/String;)V
 */
JNIEXPORT void JNICALL
Java_jp_co_field_1works_Reports_setDefaults(JNIEnv* env, jclass clazz, jstring param)
{
    CAMLparam0();
    CAMLlocal2(caml_param, caml_result);
    caml_leave_blocking_section();

    caml_param = json_of_string(encode_string(env, param));
    if (Is_exception_result(caml_param)) {
        // Error
        throw_exn(env, caml_param);
        goto exit;
    }
    caml_result = set_defaults(caml_param);
    if (Is_exception_result(caml_result)) {
        // Error
        throw_exn(env, caml_result);
        goto exit;
    }

exit:
    caml_enter_blocking_section();
    CAMLreturn0;
}

/*
 * Class:     jp_co_field_works_Reports
 * Method:    renders
 * Signature: (Ljava/lang/String;)[B
 */
JNIEXPORT jbyteArray JNICALL
Java_jp_co_field_1works_Reports_renders(JNIEnv* env, jclass clazz, jstring param)
{
    jbyteArray jresult = NULL;
    int len = 0;
    CAMLparam0();
    CAMLlocal2(caml_param, caml_result);
    caml_leave_blocking_section();

    // convert parameter
    caml_param = json_of_string(encode_string(env, param));
    if (Is_exception_result(caml_param)) {
        // Error
        throw_exn(env, caml_param);
        goto exit;
    }
    // call OCaml function
    caml_result = pdf_of_json(caml_param);
    if (Is_exception_result(caml_result)) {
        // Error
        throw_exn(env, caml_result);
        goto exit;
    }
    // set return value
    len = string_length(caml_result);
    jresult = env->NewByteArray(len);
    env->SetByteArrayRegion(jresult, 0, len, (jbyte*)String_val(caml_result));

exit:
    caml_enter_blocking_section();
    CAMLreturnT(jbyteArray, jresult);
}

/*
 * Class:     jp_co_field_works_Reports
 * Method:    render
 * Signature: (Ljava/lang/String;Ljava/lang/String;)V
 */
JNIEXPORT void JNICALL
Java_jp_co_field_1works_Reports_render(JNIEnv* env, jclass clazz, jstring param, jstring filename)
{
    CAMLparam0();
    CAMLlocal3(caml_fname, caml_param, caml_result);
    caml_leave_blocking_section();

    // convert parameter
    caml_param = json_of_string(encode_string(env, param));
    if (Is_exception_result(caml_param)) {
        // Error
        throw_exn(env, caml_param);
        goto exit;
    }
    caml_fname = encode_string(env, filename);
    // call OCaml function
    caml_result = write_pdf_from_json(caml_param, caml_fname);
    if (Is_exception_result(caml_result)) {
        // Error
        throw_exn(env, caml_result);
        goto exit;
    }

exit:
    caml_enter_blocking_section();
    CAMLreturn0;
}

Javaからの利用

完成したJava Bridgeを使って,JavaからField Reportsを利用するサンプルプログラムを以下に示します。
JSONライブラリにはJSONICを使用しました。

Map型のリテラルがないため煩雑なプログラムになってしまいましたが,実際のプログラムではデータベースなどから取得したデータを元に帳票を生成することになると思うので,問題ないでしょう。

import java.util.ArrayList;
import java.util.HashMap;   
import net.arnx.jsonic.JSON; 
import jp.co.field_works.Reports;
import jp.co.field_works.ReportsException;

public class mitumori { 
    public static void main(String[] args) throws ReportsException {
        // レンダリング・パラメータの作成
        HashMap param = new HashMap(); 
        param.put("template", "./mitumori.pdf");
        HashMap context = new HashMap();
        context.put("date", "平成23年7月11日");
        context.put("number", "10R0001");
        context.put("to", "△△△惣菜株式会社");
        context.put("title", "肉じゃがの材料");
        context.put("delivery_date", "平成23年7月31日");
        context.put("delivery_place", "貴社指定場所");
        context.put("payment_terms", "銀行振込");
        context.put("expiration_date", "発行から3ヶ月以内");
        HashMap icon = new HashMap();
        icon.put("icon", "./stamp.png");
        context.put("stamp1", icon);
        ArrayList table = new ArrayList();
        table.add(new String[]{"1", "N001", "牛肉(切り落とし)", "200g", "250円", "500円"});
        table.add(new String[]{"2", "Y001", "じゃがいも(乱切り)", "3個", "30円", "90円"});
        table.add(new String[]{"3", "Y002", "にんじん(乱切り)", "1本", "40円", "40円"});
        table.add(new String[]{"4", "Y003", "たまねぎ(くし切り)", "1個", "50円", "50円"});
        table.add(new String[]{"5", "Y004", "しらたき", "1袋", "80円", "80円"});
        table.add(new String[]{"6", "Y005", "いんげん", "1袋", "40円", "40円"});
        context.put("table", table);
        context.put("sub_total", "800円");
        context.put("tax", "40円");
        context.put("total", "840円");
        param.put("context", context);
        final String json = JSON.encode(param);
        System.out.println(json);
        // レンダリング実行
        Reports.setLogLevel(3);
        Reports.render(json, "out.pdf");

    } 
} 

Clojureからの利用

試しにClojureでもサンプルプログラムを作ってみました。
非常にすっきりと書けますね。

(use 'clojure.contrib.json)
(import '(jp.co.field_works Reports))

(def param {
    :template "./mitumori.pdf",
    :context {
        :date "平成23年7月11日",
        :number "10R0001",
        :to "△△△惣菜株式会社",
        :title "肉じゃがの材料",
        :delivery_date "平成23年7月31日",
        :delivery_place "貴社指定場所",
        :payment_terms "銀行振込",
        :expiration_date "発行から3ヶ月以内",
        :stamp1 {:icon "./stamp.png"},
        :table [
            ["1" "N001" "牛肉(切り落とし)" "200g" "250円" "500円"]
            ["2" "Y001" "じゃがいも(乱切り)" "3個" "30円" "90円"]
            ["3" "Y002" "にんじん(乱切り)" "1本" "40円" "40円"]
            ["4" "Y003" "たまねぎ(くし切り)" "1個" "50円" "50円"]
            ["5" "Y004" "しらたき" "1袋" "80円" "80円"]
            ["6" "Y005" "いんげん" "1袋" "40円" "40円"]
        ]
        :sub_total "800円",
        :tax "40円",
        :total "840円"}})

(Reports/setLogLevel 3)
(Reports/render (json-str param) "./out.pdf")

まとめ

Field ReportsのJava用I/F (Java Bridge) を作成しました。

JNIを使えば,LL言語用の拡張モジュールを作成するのと同程度の手間で,Cモジュールを呼び出すI/Fが作成できることがわかりました。
Groovy, ScalaなどJVM上で動作しJSON出力が可能なプログラミング言語であれば,Field Reportsをご利用頂けると思われます。

Java用に限らず,Python, Ruby, Perl, PHP用の言語Bridge部分のソースは,BSDライセンスオープンソースとしています。
よろしければ,さまざまなプログラミング言語からOCamlで作成したプログラムを呼び出すサンプルとしてご参照ください。
また,もし弊社で対応しきれないプログラミング言語用の拡張モジュールを作って頂ける方がいらっしゃれば歓迎します。

オープンソースのExcel出力ライブラリ

Excelドキュメントと同様に,帳票出力の手段としてExcelを利用する方法も相変わらず人気があるようです。

Excel帳票を使用する際にはいくつか気をつけなければいけない事項があると思いますが,社内システムなどで既にMS Officeがインストールしてあり,かつ使用するプリンタが特定できる(もしくは印刷品質にはあまりこだわらない)のであれば,悪くない選択だと思われます。

【Excel帳票を選択する際の検討事項】

  • クライアントPCにExcelがインストールされているか?
  • プリンタ環境の違いによって改ページ位置などがずれることがある。
  • 特殊なフォント(外字フォント,OCRフォントなど)を必要としないか?
  • ユーザ側でのExcelの二次加工を許可するのか(禁止するのなら,パスワードで保護するなどの対策が必要)?

調べてみると,オープンソースのライブラリもいくつか存在するようなので,できるだけ集めてみました。
サーバサイドでOLEオートメーションを利用する方法は技術的にもライセンス的にも問題が多そうなので(Office のサーバーサイド オートメーションについて),OLEオートメーションを使わずにExcelファイルを直接読み書きできるものに限定しています。

Apache POI

http://poi.apache.org/

  • JavaからMS Officeのドキュメント(Excel, Word等)を操作するためのAPIを提供。
  • Apache License 2.0 ライセンス

ExCella

http://excella-core.sourceforge.jp/
http://sourceforge.jp/projects/excella-core/

  • 株式会社ビーブレイクシステムズがOSSとして公開しているExcel帳票ライブラリ
  • 内部的にApacheのPOIを利用し,高レベルAPIを提供している。
  • LGPLライセンス

Java Excel API

http://jexcelapi.sourceforge.net/

JODReports

http://jodreports.sourceforge.net/

PHPExcel

http://phpexcel.codeplex.com/

  • Excel 2007形式のファイルを操作
  • LGPLライセンス

pyExcelerator

http://pypi.python.org/pypi/pyExcelerator/

python-excel

http://www.python-excel.org/

Spreadsheet

http://rubyforge.org/projects/spreadsheet/

  • RubyからExecelファイルを操作するライブラリ
  • GPLv3 ライセンス

Spreadsheet::WriteExcelおよびSpreadsheet::ParseExcel

http://search.cpan.org/dist/Spreadsheet-WriteExcel/
http://search.cpan.org/dist/Spreadsheet-ParseExcel/

  • Perlで書かれたExcelライブラリ
  • The Perl 5 License (Artistic 1 & GPL 1)

RAA - parseexcel

http://raa.ruby-lang.org/project/parseexcel/

  • Spreadsheet::ParseExcelのRubyポート