読者です 読者をやめる 読者になる 読者になる

IronPython を中継して、C#からPythonライブラリを使ってみる

.NET IronPython Python

IronPython から Python ライブラリを使うことは難しくないということなのでやってみた。
それだけじゃ面白くないので、さらにC#を連携させてみたらなんとか動いたのでメモ。

  • サンプル一式
    一応 EXCELのない環境でも .NET2.0 があれば動くはず・・・(そんな環境あるのか怪しいけど)


ただし以下のような問題があって、解決方法が思い浮かばない。

  • 日本語を含むと「System.Text.EncoderFallbackException」が発生する。ファイル操作自体は問題なさげ。
  • pyExcelerator で __slots__ が定義されているクラスをIronPythonに読み込めない。 __slots__ を削除すると読み込める。(サンプルは削除済み)
  • セルに書き込めるのは文字列のみ。
  • セルに日付が入力されていても判定できない。(pyExcelerator で判定する方法が分からない)
  • 初回起動が遅い。


手順としてはこんな感じで出来た。

1. C# でインターフェイスを定義

これはC#で作る普通のインターフェイス。

using System;
using System.Collections.Generic;
using System.Text;
namespace IronXls {
    public interface IXls {
        int GetMaxCol(string sheetname);
        int GetMaxRow(string sheetname);
        IList<string> GetSheetnames();//string[] や List<string>だと駄目だった
        void Open(string path);
        string Read(int row, int col);
        string Read(int row, int col, string sheetname);
        void Save(string path);
        void Write(int row, int col, string value);
        void Write(int row, int col, string value, string sheetname);
    }
}

2. IronPython で上記のインターフェイスを実装して Python ライブラリを使用

Python のクラス継承と同じ要領で class Xls(C#のインターフェイス) とすればいい。
あとは普通のPythonと同じようにする。

# -*- coding: utf-8 -*-

import sys
from pyExcelerator import *
import IronXls

class Xls(IronXls.IXls):
    def __init__(self, path=None):
        self._reset()
        if path:
            self.Open(path)

    def _reset(self):
        self.book = Workbook()
        self.sheets = {}
        self.matrix = {}
        self.maxnum = {}
        self.lastsheet = None

    def GetMaxCol(self, sheetname):
        if not self.maxnum.has_key(sheetname):return -1
        row, col = self.maxnum[sheetname]
        return col

    def GetMaxRow(self, sheetname):
        if not self.maxnum.has_key(sheetname):return -1
        row, col = self.maxnum[sheetname]
        return row

    def GetSheetnames(self):
        return self.sheets.keys()

    def Open(self, path):
        self._reset()
        for sheetname , values in parse_xls(path):
            w = self.book.add_sheet(sheetname)
            self.sheets[sheetname] = w
            for row, col in values.keys():
                self.Write(row, col, values[(row, col)], sheetname)

    def Read(self, row, col, sheetname=None):
        sheetname = self._getAccessSheet(sheetname)
        if not self.sheets.has_key(sheetname):
            raise Exception, u'sheet %s not found' % sheetname
        mrow, mcol = self.maxnum[sheetname]
        if row > mrow or col > mcol:
            raise Exception, u'(%d, %d) is out of range : sheet %s' % (row,col,sheetname)
        elif not self.matrix[sheetname].has_key((row, col)):
            return ""

        v = self.matrix[sheetname][(row, col)]
        if isinstance(v, str):#IronPython では Unicode 指定しても str になる
            return v
        else:
            return str(v)

    def Save(self, path):
        self.book.save(path)

    def Write(self, row, col, value, sheetname=None):
        sheetname = self._getAccessSheet(sheetname)
        if self.sheets.has_key(sheetname):
            w = self.sheets[sheetname]
        else:
            w = self.book.add_sheet(sheetname)
            self.sheets[sheetname] = w
        w.write(row, col, value)
        self.matrix.setdefault(sheetname, {})[(row, col)] = value
        mrow, mcol = self.maxnum.setdefault(sheetname,(-1,-1))
        if mrow < row or mcol < col:
            self.maxnum[sheetname] = (max(mrow, row), max(mcol, col))

    def _getAccessSheet(self, sheetname):
        if sheetname:
            self.lastsheet = sheetname
            return sheetname

        if not self.lastsheet:
            sheetname = "sheet1"
        elif self.lastsheet:
            sheetname = self.lastsheet
        self.lastsheet = sheetname
        return sheetname

4. C#IronPython エンジンを生成して上記のスクリプトを読み込む

肝心なのはこんなところかな。
Pythonライブラリのパスを指定
②中継用のIronPythonスクリプトの読み込み
IronPythonスクリプトで定義したクラスの生成

using System;
using System.IO;
using System.Reflection;
using IronPython.Hosting;
namespace IronXls {
    public class XlsFactory {
        private static XlsFactory __instance = new XlsFactory();
        private PythonEngine _engine;

        private XlsFactory() { }

        private PythonEngine Engine {
            get {
                if (_engine == null) {
                    _engine = new PythonEngine();
                    Assembly a = this.GetType().Assembly;
                    _engine.LoadAssembly(a);
                    string path = Path.Combine(Path.GetDirectoryName(a.Location), "Scripts");
                    _engine.Sys.path.Append(path);//①
                    _engine.ExecuteFile(Path.Combine(path, "bridge.py"));//②
                }
                return _engine;
            }
        }

        ~XlsFactory() {
            if (_engine != null) {
                _engine.Dispose();
            }
        }

        public static IXls Create() {
            return __instance.Engine.EvaluateAs<IXls>("Xls()");//③
        }

        public static IXls Create(string path) {
            return __instance.Engine.EvaluateAs<IXls>(string.Format("Xls(r'{0}')", path));
        }
    }
}