ホーム

pythonとOpenDocumentで帳票ツールを作ってみる

帳票を作るツールにExcelを使うのは合理的でいいんだけど、(多くの場合は)Excelを買わなくちゃいけない。OpenOfficeやLibreOfficeでも扱えるOpen Documentのスプレッドシート形式なら、こういうところを気にしないでいいじゃない、と思うので、例えばこんな方法があるよ、という話としてまとめておきます。Open DocumentはExcelでもある程度使えるから、そこらへんも都合いいですしね。

ここで使っているpythonは、2系です。

スプレッドシートをあらかじめ「テンプレート」として使えるように作っておいて、それに値を適当に放り込むプログラムを作ればいいんじゃないかな、という発想です。

「テンプレート」と言っても、別に難しいことをしようってのではないです。たとえばこんな感じのものを作っておいて…

この、{ナンチャラ} って感じのところに値を入れるようにしようってわけ。特に、明細の部分は、データの数だけコピーするつもり。

Open Documentの文書ファイルは、内部的には、実は単なるzipファイルです。その中には色々なファイルが入っているんだけど、今回のような用事では、いじるべきファイルは、content.xml というやつだけのようです。他は変更しません。ZIPファイルとXMLファイルを操作するだけのプログラムなんだから、pythonの標準配布モジュールだけでこと足ります。つまり、python以外に追加インストールするものがないってこと。

スクリプトを組み始める前に、もうちょっとだけ、「テンプレート」の仕様について考えておきましょう。

テンプレ用スプレッドシートの、行をいくつかまとめた単位を、「帯」って呼ぶことにします。実際の帳票は、空のテーブル上に、テンプレの「帯」をいくつか上から重ねて作っていくというイメージ。世の中の多くの帳票ツールと変わりません。

「帯」の名前は、シートの一番左のセルに書いておくという決まりにしましょう。実際にはここは印刷されてほしくない場所ですが、この列だけ広さをゼロにしちゃっておけば、実際には支障ないですし、これでいいでしょう。

この例では、ページヘッダである「h」と、カテゴリ見出しである「h2」と、詳細レコードである「d」と、あとは改ページ用の「break」っていう帯を設定しました。改ページをさせるには、このテンプレを作っている最中に、OpenOfficeなんかで普通にここを改ページ位置にしておくだけです。普段のノウハウを使うだけ。ページの向きだの、マージンだの、フォントだの、罫線だの、詰め方向だの、そんなのも好きに付け足しておけばいいです。こんな程度のものなら、プログラマじゃない人にも作ってもらえます。たぶんココ重要。

実際のファイルのダウンロード

で、これを使って帳票をつくるときのフレームワークにあたるスクリプトは、あんまり説明なしでここに置いてしまいます。zipfileモジュールとか、DOMの扱いとかを説明しはじめると、長くなってしまいますので。保存するときのスクリプト名はなんでもいいですけど、エンコーディングはUTF-8で。

# coding:utf-8

from zipfile import ZipFile, ZIP_DEFLATED
from xml.dom.minidom import parseString

# ノード内のテキストだけ再帰的に結合して取り出す
def get_text(n):
  if n.nodeName == '#text':
    return n.nodeValue
  elif n.hasChildNodes():
    return "".join([get_text(i) for i in n.childNodes])
  else:
    return ""

#再帰的にテキストノードをフォーマット
def dom_format(n, data):
  for i in n.childNodes:
    if i.nodeName == '#text':
      i.nodeValue = i.nodeValue.format(**data)
    else:
      if i.hasChildNodes():
        dom_format(i, data)

# OpenDocumentスプレッドシートをテンプレートとして使うクラス
class ODSpreadsheetTemplate(object):

  def __init__(self, templatefile):
    self.templatefile = templatefile
    #テンプレファイルから、content.xmlをパースしておく
    z = ZipFile(self.templatefile)
    a = z.open('content.xml').read()
    z.close()
    self.domdata = parseString(a)
    #最初のシートだけがテンプレ扱いの範囲
    self.table = self.domdata.getElementsByTagName("table:table")[0]
    self.extract_template()

  #最初のセルの中身を区分記号とみなしながら、元ドキュメントから切り離す
  def extract_template(self):
    n = self.table
    self.template = {}
    for i in n.getElementsByTagName("table:table-row"):
      c = i.getElementsByTagName("table:table-cell")
      if len(c) > 0:
        lbl = get_text(c[0])
        if lbl != "":
          self.template.setdefault(lbl, [])
          self.template[lbl].append(i)
      i.parentNode.removeChild(i)

  #区分記号に合致するレポート帯を追加
  def add_band(self, band, data):
    for i in self.template[band]:
      i2 = i.cloneNode(True)
      dom_format(i2, data)
      self.table.appendChild(i2)

  #元zipの全構成ファイルをコピーする。content.xmlだけは細工する
  def save(self, outfile):
    z = ZipFile(self.templatefile)
    z2 = ZipFile(outfile, "w", compression=ZIP_DEFLATED)
    for i in z.infolist():
      if i.filename == 'content.xml':
        z2.writestr(i.filename, self.domdata.toxml().encode('utf-8'))
      else:
        z2.writestr(i.filename, z.open(i.filename).read())


def main():
  t = ODSpreadsheetTemplate("template.ods")
  t.save("report.ods")

main()

このままだと、template.ods っていうファイルを読み込んで、それをもとに report.ods に書き出すだけです。元のスプレッドシートの中身は全部テンプレートとして一旦切り離されますから、この例では単にカラッポの帳票ができてしまいます。main関数の中身をもうちょっとちゃんと書けば、帳票らしいものになるはずです。

たとえば、main関数をこんな風に書きかえれば、ちょっとマトモな結果が得られます。値を決め打ちなのがかっこ悪いですけど。

def main():
  t = ODSpreadsheetTemplate("template.ods")
  t.add_band('h', {})
  t.add_band('h2', {'area': u'東海地方'})
  t.add_band('d', {'ken': u'愛知県', 'bird': u'コノハズク'})
  t.add_band('d', {'ken': u'三重県', 'bird': u'シロチドリ'})
  t.add_band('d', {'ken': u'岐阜県', 'bird': u'ライチョウ'})
  t.save("report.ods")

それぞれの「帯」で {ナンチャラ} として空けられている部分(プレースホルダともいいます)にあてはまるキーと値をもった辞書をつくって、add_band というメソッドを呼ぶと、テンプレの中身で「補完」された内容がレポートに追加されるという仕掛けです。値は、uをつけて、明示的にユニコード文字にしておかないといけないみたい。キーの部分にも日本語などを使うことはできるんだけど、テンプレート側で{ナンチャラ}というテキストが複数のエレメントにバラけてうまくいかないこともあるので、やらないほうが無難かも。「{」とか「}」も半角文字で書いておくこと。

ちゃんとしたデータを使いながらこのフレームワークを使うと、もっといい感じです。あらかじめ、ココから「県の鳥」のデータを持ってきておいて、スクリプトの横に birds.txt として置いてから、下のようにmain関数を書き換えましょう。「県の鳥」のデータが、わざとソート順を狂わせているので、ちょっと余計な感じのコードになっていますけど。

def main():
  import io
  brds = []
  # データ読んで、並べ替え
  for line in io.open('birds.txt', encoding='cp932'):
    brds.append(line[:-1].split("\t"))
  brds.sort(lambda a,b: cmp(a[0], b[0]))
  t = ODSpreadsheetTemplate("template.ods")
  bf = ''
  for i in brds:
    #キーブレイク
    if i[0] != bf:
      if bf != '':
        # 改ページ
        t.add_band('break', {})
      t.add_band('h', {'area':i[0]})
      t.add_band('h2', {'area':i[0]})
      bf = i[0]
    t.add_band('d', {'ken':i[1], 'bird':i[2] })
  t.save("report.ods")
実際の成果物サンプルをダウンロード

印刷プレビューなんかで確認すると、ちゃんとカテゴリーが変わったところで改ページもできているはずです。

やりようによって、いろいろ発展させられるアイデアじゃないかなー

2012.11.30

クリエイティブ・コモンズ・ライセンス
この 作品 は クリエイティブ・コモンズ 表示 3.0 非移植 ライセンスの下に提供されています。