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

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

nginx+SCGI+web2pyの構成をお試し中

nginx+uWSGI+web2pyの構成で3ヶ月ぐらい運用していたのですが,あるトラブルがありuWSGIをSCGIに置き換えて様子を見ています。

「あるトラブル」とは?

運用しているサイトは帳票開発ツールのデモサイトなので,応答として動的に生成したPDFを返すページがいくつかあります。 最初のうちは問題ないのですが,しばらく使っているとPDFを生成するページで応答が返らなくなり, Gateway Errorとなってしまうというものです。

プロセスの状態を確認すると,uwsgiのプロセスがCPU使用率100%の状態になっていて,いつまで経っても使用率は下がりません。 こうなると,uwsgiサービスを再起動するしかありませんでした。

色々調べたのですが原因が分からず,uWSGIを他の物に置き換えて様子を見ることにしました。 切り替えて2日ぐらい経過しましたが,今のところ問題は発生していません。

SCGIとは?

FastCGIのプロトコルを更に単純にした規格のようです。 nginx,web2py共にSCGIに対応していたので,使ってみることにしました。

nginxの設定を修正

ゲートウェイとしてSCGIを使用するように,設定ファイルを修正します。

$ sudo vim /etc/nginx/conf.d/web2py.conf

server {
        listen          80;
        server_name     $hostname;
        location ~* /(\w+)/static/ {
           root /var/www/web2py/applications/;
        }
        location / {
                include         /etc/nginx/scgi_params;
                scgi_pass       127.0.0.1:4000;
        }
}

server {
        listen          443;
        server_name     $hostname;
        ssl                     on;
        ssl_certificate         /etc/nginx/ssl/rapid.crt;
        ssl_certificate_key     /etc/nginx/ssl/rapid.key;
        keepalive_timeout      70;
        location / {
                include         /etc/nginx/scgi_params;
                scgi_param     HTTPS yes;
                scgi_pass       127.0.0.1:4000;
        }
}

SCGIの設定

web2py付属のscgihandler.pyを動かすにはwsgitoolsが必要なので, まずはwsgitoolsをインストールします。 CentOSでは,pipコマンドがpip-pythonになっています。

$ sudo pip-python install wsgitools

次にこのサイトに掲載されている起動スクリプトをコピーして,/etc/init.d/scgihandler に置きました。

ただ,これはDebian用なので,CentOSでは動きません。 この記事の手順で,start-stop-daemonをビルドしました。

最後に起動スクリプトをサービスとして登録します。

$ sudo chmod +x /etc/init.d/scgihandler
$ sudo chkconfig —add scgihandler

参照サイト

書類送付状生成デモの改造

先日作成した書類送付状生成デモを少し改造してみました。

改造点は以下のとおりです。

  • すべてをcontrollerで実装していたのを改め,controllerとviewに分離した。
  • viewを切り替えて,PDFとHTML両方の出力形式に対応できるようにした。

Controller部

フォームに出力形式(PDFとHTML)を選択するラジオボタンを追加しました。 どちらを選択したかにより,リダイレクト先のURLを変えています。

web2pyでは,拡張子付きのURLが呼ばれると,最初に拡張子を抜いた名前のcontroller関数が呼ばれます。 次に拡張子付きの名前のviewファイルが探され,あればそのviewを用いてレンダリングが実行されます。

# coding: utf8

from gluon.contrib import simplejson
try:
    from cStringIO import StringIO
except:
    from StringIO import StringIO
import tools

def index():
    def docinput(num):
       return TR(TD(INPUT(_name='_no'+str(num), _size=5)),
                 TD(INPUT(_name='_document'+str(num), _size=60)),
                 TD(INPUT(_name='_count'+str(num), _size=5)))
    form = FORM(
        UL(LI(LABEL(u'日付'), INPUT(_name="date", _value="")),
           FIELDSET(LEGEND(u'宛先'),
               LI(LABEL(u'郵便番号'), INPUT(_name='to_post', _size=8)),
               LI(LABEL(u'住所1'), INPUT(_name='to_address1', _size=30)),
               LI(LABEL(u'住所2'), INPUT(_name='to_address2', _size=30)),
               LI(LABEL(u'会社名'), INPUT(_name='to_company', _size=30)),
               LI(LABEL(u'氏名'), INPUT(_name='to_name', _size=30))),
           FIELDSET(LEGEND(u'差出人'),
               LI(LABEL(u'郵便番号'), INPUT(_name='from_post', _size=8)),
               LI(LABEL(u'住所1'), INPUT(_name='from_address1', _size=30)),
               LI(LABEL(u'住所2'), INPUT(_name='from_address2', _size=30)),
               LI(LABEL(u'会社名'), INPUT(_name='from_company', _size=30)),
               LI(LABEL(u'氏名'), INPUT(_name='from_name', _size=30)),
               LI(LABEL(u'連絡先1'), INPUT(_name='from_contact1', _size=30)),
               LI(LABEL(u'連絡先2'), INPUT(_name='from_contact2', _size=30))),
           FIELDSET(LEGEND(u'送付物'),
               TABLE(
                   TR(TH(u'No.'), TH(u'書類名'), TH(u'部数')),
                   docinput(0), docinput(1), docinput(2), docinput(3),
                   docinput(4), docinput(5), docinput(6), docinput(7),
                   docinput(8), docinput(9), docinput(10))),
           LI(LABEL(u'出力媒体'),
             INPUT(_type="radio", _name="_media", _value="pdf", _checked="checked"), u"PDF ",
             INPUT(_type="radio", _name="_media", _value="html"), u"HTML "),
           LI(INPUT(_type='submit', _value='帳票出力'),
              INPUT(_type='reset', _value='リセット'), _align='right'), _id="simple"))
    if form.accepts(request.vars):
        response.flash = u'入力を受け付けました'
        context = form.vars
        context['no'] = [form.vars['_no'+str(i)] for i in range(11)]
        context['document'] = [form.vars['_document'+str(i)] for i in range(11)]
        context['count'] = [form.vars['_count'+str(i)] for i in range(11)]
        if form.vars['_media'] == "pdf":
            redirect(URL('render.pdf', vars=context))
        else:
            redirect(URL('render.html', vars=context))
    elif form.errors:
        response.flash = u'入力に誤りがあります'
    else:
        response.flash = u'フォームに入力してください'
    return dict(form=form)

def render():
    return dict(request.vars)

View部

PDF用のviewファイルを views/cover/render.pdf に, HTML用のviewファイルを views/cover/render.htmlに置きます。

PDF用のviewでは,URLパラメータの内容を元にPDFレンダリング用のパラメータを作成し, Field Reports を呼び出しています。

URLの拡張子が'pdf'の場合は,Content-Typeの指定は特に必要ないようです。

Content-Dispositionでファイル名を指定すると,PDFファイルをダウンロードさせることができます。 ファイル名を指定しないと,ブラウザのウインドウにPDFを表示します。

 {{
 from field import reports
 import os
 import gluon.fileutils
 settings = {
      'template-root': os.path.join(gluon.fileutils.abspath('applications',
           request.application), 'resources'),
 }
 params = {
      'settings': settings,
      'template': 'templates/sofu.pdf',
      'context': response._vars,
 }
 reports.set_defaults({})
 reports.set_log_level(3)
 #response.headers['Content-Type'] = 'application/pdf'
 #response.headers['Content-Disposition'] = 'attachment; filename="cover.pdf"'
 =XML(reports.renders(params))
 }

HTML用のviewは手抜きですが,単にURLパラメータの内容をダンプします。

{{extend 'layout.html'}}
<h2>書類送付状</h2>
{{=BEAUTIFY(response._vars)}}

まとめ

書類送付状生成デモを改造して,出力形式をPDFとHTMLで選択できるようにしました。 出力形式の切り替えは,二つのviewをURLにより区別することで実現しています。

今回は,PDF出力を行うページにHTML出力を追加しましたが, 逆に既存のHTML出力を行うページにPDF出力を追加することも可能です。 PDF帳票開発ツール Field Reports の入力パラメータは, LL言語用のWebフレームワークで標準的に用いられる辞書形式を基本としているので, 既存のWebサイトに最小の労力でPDF出力機能を追加することができます。

書類送付状生成デモの作成

前回のはがき宛名面の作成デモに続いて,書類送付状を作成するデモを作成しました。

実用的にも使えると思いますので,よかったら使ってみてください。

テンプレートの作成

まずは,オフィスソフトを利用して,帳票のレイアウトをデザインします。

今回は,長形3号の窓付き封筒で使えるように宛先欄を左上に配置しました。 Mac OS X 用の表計算ソフト Numbers を使用して元原稿を作成し,PDF形式で書き出しました。

次に,Adobe Acrobat Professional で作成したPDFを開き,テキストを動的に埋め込む位置にフォーム・フィールドを配置していきます。

基本的には,フォント・フォントサイズ・表示色などの表示属性をこの時決めて設定しますが, 帳票生成時にプログラムから指定することもできます。

テーブル形式のフィールドを配置する際には,基準となるフィールドを配置してから,「複数のフィールドを配置...」で必要な数だけ複製します。

以上で,テンプレートが完成しました。

因みに Field Reports では,フォーム・フィールドをテキストの表示位置やフォント指定などの属性を取得するためのプレース・フォルダとして利用しているにすぎません。テキストを配置した後は削除され,最終的に作成される帳票には残りません。

プログラムの作成

今回もフレームワークとしてweb2pyを使用しました。 controller部のコードを以下に示します。

「if form.accepts(request.vars):」以降で Field Reports に渡すパラメータを組み立てています。 基本的には,Webのフォームにつけた名前とPDFテンプレートに配置したフィールドの名前を一致させているので, form.varsの値がそのまま使えますが,テーブル形式の部分はリスト形式にする必要があるので, この部分の変換を行なっています。

render関数の中では,テンプレートの場所などその他のパラメータを追加して実際に Field Reports のAPIを呼び出しています。

# coding: utf8

from gluon.contrib import simplejson
try:
    from cStringIO import StringIO
except:
    from StringIO import StringIO
import tools

def index():
    def docinput(num):
       return TR(TD(INPUT(_name='_no'+str(num), _size=5)),
                 TD(INPUT(_name='_document'+str(num), _size=60)),
                 TD(INPUT(_name='_count'+str(num), _size=5)))
    form = FORM(
        UL(LI(LABEL(u'日付'), INPUT(_name="date", _value="")),
           FIELDSET(LEGEND(u'宛先'),
               LI(LABEL(u'郵便番号'), INPUT(_name='to_post', _size=8)),
               LI(LABEL(u'住所1'), INPUT(_name='to_address1', _size=30)),
               LI(LABEL(u'住所2'), INPUT(_name='to_address2', _size=30)),
               LI(LABEL(u'会社名'), INPUT(_name='to_company', _size=30)),
               LI(LABEL(u'氏名'), INPUT(_name='to_name', _size=30))),
           FIELDSET(LEGEND(u'差出人'),
               LI(LABEL(u'郵便番号'), INPUT(_name='from_post', _size=8)),
               LI(LABEL(u'住所1'), INPUT(_name='from_address1', _size=30)),
               LI(LABEL(u'住所2'), INPUT(_name='from_address2', _size=30)),
               LI(LABEL(u'会社名'), INPUT(_name='from_company', _size=30)),
               LI(LABEL(u'氏名'), INPUT(_name='from_name', _size=30)),
               LI(LABEL(u'連絡先1'), INPUT(_name='from_contact1', _size=30)),
               LI(LABEL(u'連絡先2'), INPUT(_name='from_contact2', _size=30))),
           FIELDSET(LEGEND(u'送付物'),
               TABLE(
                   TR(TH(u'No.'), TH(u'書類名'), TH(u'部数')),
                   docinput(0), docinput(1), docinput(2), docinput(3),
                   docinput(4), docinput(5), docinput(6), docinput(7),
                   docinput(8), docinput(9), docinput(10))),
           LI(INPUT(_type='submit', _value='帳票出力'),
              INPUT(_type='reset', _value='リセット'), _align='right'), _id="simple"))

    if form.accepts(request.vars):
        response.flash = u'入力を受け付けました'
        context = form.vars
        context['no'] = [form.vars['_no'+str(i)] for i in range(11)]
        context['document'] = [form.vars['_document'+str(i)] for i in range(11)]
        context['count'] = [form.vars['_count'+str(i)] for i in range(11)]
        response.headers['Content-Type'] = 'application/pdf'
        response.headers['Content-Disposition'] = 'attachment; filename="cover.pdf"'
        return render(context)
    elif form.errors:
        response.flash = u'入力に誤りがあります'
    else:
        response.flash = u'フォームに入力してください'
    return dict(form=form)

def render(context):
    import os
    from field import reports
    import gluon.fileutils
    settings = {
        'template-root': os.path.join(gluon.fileutils.abspath('applications',
            request.application), 'resources'),
    }
    template = 'templates/sofu.pdf'
    params = {
        'settings': settings,
        'template': template,
        'context': context,
    }
    reports.set_defaults({})
    reports.set_log_level(3)
    return reports.renders(params)

実行例

以下に実際の実行例を示します。

参照

書類送付状生成デモの改造

Google App Engine からPDF帳票を生成する

Google App Engine (GAE) から Field Reports for Cloud β版で提供しているWeb APIサービスを用いて,帳票を生成する方法を解説します。

今回は,「Hello World!」を表示するデモを作成します。

プログラムの作成

Webアプリケーションフレームワークとしてweb2pyを使用しました。

web2pyで作成したアプリケーションの controllers/default.py に以下のコードを追加します。

<APIキー>の部分には,Field Reports for Cloud β版のサイトで取得したAPIキーを挿入します。

from gluon.contrib import simplejson
import urllib

def hello():
    params = {
        "settings": {
            "api-key": "<APIキー>"
        },
        "template": {"paper": "A4"},
        "context": {
            "hello_1": {
                "new": "Tx",
                "value": "Hello, World!",
                "rect": [100, 700, 400, 750],
                "font": "/Times-Roman"
            },
            "hello_2": {
                "new": "Tx",
                "value": "Hello, World!",
                "rect": [100, 600, 400, 650],
                "font": "/Helvetica-Oblique"
            },
            "hello_3": {
                "new": "Tx",
                "value": "Hello, World!",
                "rect": [100, 500, 400, 550],
                "font": "/Courier-Bold"
            },
            "hello_4": {
                "new": "Tx",
                "value": "ABCDEFGHIJKLMN",
                "rect": [100, 400, 400, 450],
                "font": "/ZapfDingbats"
            },
            "hello_5": {
                "new": "Tx",
                "value": "こんにちは世界",
                "rect": [100, 300, 400, 350],
                "font": "/KozGo-Medium"
            }
        }
    }
    url = "https://labs.field-works.co.jp/webapi/render"
    f = urllib.urlopen(url, simplejson.dumps(params))
    response.headers['Content-Type'] = 'application/pdf'
    return f.read()

アプリケーションのデプロイ

ローカル環境で動作確認を行ったら,作成したアプリケーションをGAEにデプロイします。 具体的なデプロイ手順については,参照サイト等を参照してください。

デモの実行

以下のURLにアクセスすると,生成したPDFを開きます。

http://field-reports.appspot.com/hello

参照サイト