Pythonで始めるCGIプログラム入門
このページでは、Python言語で作成したプログラムをWebプログラムとして動作させる方法を説明します。CGIとは、Webサーバと連携してプログラムを動作させる仕組みです。ブラウザからWebサイトにアクセスがあった場合、Webサーバはサーバ内にあるコンテンツを表示します。CGIプログラムでは、このコンテンツを動的に変化させて表示させることが出来ます。
Pythonには、主流となっているバージョンが2つ存在します。このページでは、Python3を基準に説明しています。
目次
PythonプログラムをCGIで動かす方法
Pythonは、スクリプト言語なので、プログラムソースは普通のテキストファイルになっています。これをプログラムとして動かすには、ソースコードを解析し、機械語に翻訳して実行するプログラムが必要になります。このプログラムをインタープリターと呼び、Python以外にもPerlやRubyがあります。
CGIでは、プログラムソースの一行目にインタープリターのパス(存在する場所)を記述するだけで、動作させることが出来ます。レンタルサーバなどでは、パスが公開されていますが、分からない場合は #!/usr/bin/env python3 とすることで、サーバ内のパス情報を元に起動させることが出来ます。
#!/usr/bin/python3
# 1行目にインタープリターのパスを記述
#!/usr/bin/env python3
# パスが分からない場合 1行目に
プログラムのコードは、テキストエディタで作成できます。CGIプログラムのファイル名は、name.cgiなど.cgiが付いた状態で保存します。ファイル名をindex.cgiにすると、通常http://example.com/のようなファイル名が不要なURLで表示することが出来ます。後はFTPソフトで、レンタルサーバなどへ設置します。またアップロードしたプログラムには、FTPソフトなどで実行権限(パーミッション)を与える必要があります。
PythonのCGIで文字を表示してみる
CGIプログラムでブラウザに文字を表示させてみます。CGIプログラムは、Webサーバにコンテンツを渡すことが出来ますが、テキストやHTML、画像などコンテンツには様々な種類があります。そのため、渡すコンテンツが何なのかを指定する必要があり、ヘッダー情報として渡します。
#!/usr/bin/python3
# ヘッダー情報、テキストファイルを指定
print("Content-Type: text/plain\n")
print('Hello, World!')
print()が表示させるための関数で、text/plainがコンテンツの内容になり、\nが改行になっています。python2では()は必要ありませんでしたが、python3では()がない場合、エラーとなります。
表示させるコンテンツデータは、Hello, World!になります。コンテンツデータ部の前は改行で空ける事になっているため、\nが必要です。Pythonのprint()では、改行付きで表示されるため、\nは一つで問題ありません。\n\nで表示させた場合、Webページの一番上の行が空行になっていることで確認できます。
また\nが有効となるためには、""で括る必要があります。''で括った場合は改行の意味にはならず、\nの文字列になります。
続いて日本語を表示させてみます。日本語の文字は、同じ文字でもエンコーディングの種類により異なった文字コードになるため、エンコーディングを指定する必要があります。多くのサイト表示や開発環境は utf-8 になるため、プログラムソースはutf-8で保存して下さい。
# .htaccess
SetEnv PYTHONIOENCODING utf-8
.htaccessファイルにて、環境変数PYTHONIOENCODINGで、標準入出力のエンコーディングをutf-8に設定しています。python2では、# coding: utf-8 でプログラムソースの文字コードも指定する必要がありました。python3では、指定する必要はありません。
#!/usr/bin/python3
# ヘッダー情報、テキストファイルを指定
print("Content-Type: text/plain; charset=utf-8\n)
print('こんにちは、せかい!')
PythonのCGIで計算をしてみる
今回は、例題を解くかたちで、Python言語で四則演算をする方法を覚えることにします。Pythonの加減乗除は、順番に+ - * /になります。剰余(割った余り)は%で求めることが出来ます。また除算(割り算)の / では、小数点以下まで結果が出ますが、切り捨てる場合は、// を使用します。
■例題
5人が集まって、1人が買い物に出かけました。購入金は5人で負担し、割り切れない金額は購入担当者が負担するものとします。所持金10,000円を持って、500円の物を7個、700円の物を4個、購入しました。1人当たりの購入金額と購入担当者の負担額を求めなさい。
#!/usr/bin/python3
print("Content-Type: text/plain; charset=utf-8\n")
# 消費税
tax = 1.08
# 所持金
wallet = 10000
# 人数
num = 5
# 商品価格A
priceA = 500
# 商品価格B
priceB = 700
# 購入金額
total = (priceA * 7 + priceB * 4) * tax
print("購入金額:{0:.0f}円".format(total));
print("残高:{0:.0f}円".format(wallet - total))
print("1人当たり:{0:.0f}円".format(total // num))
print("購入者負担:{0:.0f}円".format(total % num))
python2では、整数同士で割り算をすると、結果も整数になっていました。小数点以下を出す場合、例えば 1.0/5 のようにする必要がありましたが、python3では必要ありません。
今回は、変数を使用しています。Pythonでは、変数は英数字、アンダースコアで名前を付けることができます。ただし、先頭には数字は使えません。また数値を代入した変数を、決まった書式で文字列として表示する場合、format()が使用できます。他にstr()で文字列に変換する方法などがあります。
人数を変更したり、購入商品を増やしたり、ポイント還元率を作り、取得ポイントを計算してみて下さい。カスタマイズなどで実際に手を動かさないと身につきません。
PythonのCGIで日時を表示してみる
Pythonで日時を取得するには、datetimeモジュールを利用します。datetimeモジュールは標準ライブラリなので、インストール作業は必要なく、importするだけで利用できます。ライブラリの一部を利用する場合は、fromで限定できます。
またtimeモジュールで、Unixタイムスタンプを取得できます。Unixタイムスタンプとは、1970年1月1日0時0分0秒からの秒数になります。1秒毎の連番として利用できるほか、指定時刻を求める際にも利用できます。
現在日時だけを表示してもなので、今回も例題を解くかたちで覚えます。例題の1万時間とは、プロレベルのスキルを習得するのに必要となる時間とされています。「1万時間の法則」と呼ばれ、イギリスのマルコム・グラドウェル氏が提唱しています。
■例題
1日6時間を訓練に当てたとして、累計1万時間に達するのはいつになるのかを求めよ。
#!/usr/bin/python3
print("Content-Type: text/plain; charset=utf-8\n")
# 日時用モジュールをインポート
from datetime import datetime
# Unixタイムスタンプ用モジュールをインポート
import time
# 現在日時のオブジェクトを生成
now = datetime.now()
# 日時を文字列で表示
print(now.strftime('%Y年%m月%d日 %H時%M分%S秒'))
# Unixタイムスタンプを取得して秒単位に
# 1日6時間の訓練を1万時間続けた場合の秒数を足す
t = int(time.time()) + 10000 * 3600 * (24/6)
# Unixタイムスタンプから日時を文字列で表示
print(datetime.fromtimestamp(t).strftime('%Y年%m月%d日 %H時%M分%S秒'))
datetime.now()で現在日時のオブジェクトを取得できます。オブジェクトなのでメソッドを使用して、さらに取得や変換を行います。今回は strftime() メソッドで文字列にしています。%Y%m%dがそれぞれ年月日、%H%M%Sがそれぞれ時分秒に置き換わった文字列を取得できるので、その文字列を表示しています。
time.time()では、現在のUnixタイムスタンプが小数点以下も含めて取得できます。今回は、秒単位で必要なので、int()で整数にしています。また1万時間に3600を掛けて秒単位にします。1日6時間の場合、1日は24時間なので全体としては4倍の時間が必要になります。1日2時間の場合、1日10時間の場合などを求めてみて下さい。
PythonのCGIで条件分岐を覚える
プログラムは、ある変数の状態で真か偽かを判定して、条件に合わせた処理を行わせる流れになります。Pythonでは、if : else: でそれぞれの条件に合わせた処理をコーディングします。
また今回からCGIプログラムの処理として、ブラウザからの入力を受け取れるようにします。デコード用のモジュール cgi_decode を使うので、こちらからダウンロードして下さい。下記プログラムのURLへアクセスすると「入力がありません」が表示されると思います。続いてURLの末尾に、?word=文字列を入力後、アクセスしてみて下さい。
#!/usr/bin/python3
# モジュールのインポート
import cgi_decode
# 変数をセット
cgi_decode.Set()
# GET,POST,COOKIE,FILES が使えるように
print("Content-Type: text/plain; charset=utf-8\n")
# word=の入力があった場合
if 'word' in GET:
print(GET['word'])
else:
print('入力がありません')
今度は文字列が表示されると思います。inで変数(辞書)の中にキーがあるかを判定し、あった場合にif以下のブロック内の処理を実行しています。この時、else以下のブロックは処理されないため、表示されません。
PythonのCGIで繰り返し処理を覚える
プログラムで同じ処理を何度も行なう場合は、for in : が利用されます。Pythonでは、インクリメントやディクリメントが使えないため、ループする範囲を配列(リスト)で指定して繰り返し処理を行います。リストは、range()関数で作成できます。
他に繰り返し処理では、while 条件:が使用できます。下記のようなwhileの場合は、ブロック内で処理を停止する条件を記述し、breakで抜ける必要があります。
# 大きさを指定する
for i in range(10): #0-9
print(i)
# 範囲を指定する
for i in range(1,11): #1-10
print(i)
# 条件で停止する場合
i = 0
while True:
print(i)
i += 1
if i==10:
break
forでは、変数 i が増加している数値になります。for文の習得でよく使われる処理に、ピラミッド表示があります。ピラミッド表示とは、指定文字でのアスキーアートでピラミッド構造を構築する処理です。今回は*(アスタリスク)と「 」(スペース)で構築します。
■例題
*とスペースで、入力数に応じた段数のピラミッドを表示せよ。
初めから正解のかたちでコーディングは普通できないので、試行錯誤しながら作成していきます。まずは普通に段数ごとに*を増やす処理を実装します。
#!/usr/bin/python3
import cgi_decode
cgi_decode.Set()
# GET,POST,COOKIE,FILES が使えるように
print("Content-Type: text/plain; charset=utf-8\n")
# 入力があり、数値の場合
if 'n' in GET and GET['n'].isdecimal():
# 段数
for i in range(1,int(GET['n'])+1):
# 段の初めに初期化
t = ''
# その段の*を作成
for j in range(i):
t += '*'
print(t)
else:
print('入力がありません')
1つ目のfor文が段数を決定し、2つ目のfor文がその段での*の数を決定することになります。また1つ目のfor文の変数は i で、2つ目の変数は、j になっています。今回のケースでは同じ変数でも問題ありませんが、通常は下層ブロックになる場合は、別の変数にします。
確認時のURLは、?n=数値でアクセスしてみて下さい。GET送信からの入力 GET['n'] は文字列になっているため、int()で数値化しています。ここまでで、表示はピラミッドの半分の状態になっていると思います。
# ?n=5 とした場合
*
**
***
****
*****
この表示に、スペースを追加すればピラミッド構造を構築できますが、*の文字数から考えて、偶数の段と奇数の段ではうまく真ん中が揃いません。そのため奇数の段のみのピラミッドにするか、偶数の段をスペースで奇数にするかで解決する必要があります。今回は、奇数の段のみのピラミッドにしてみます。
# ?n=5 とした場合 スペースで調節
*
**
***
****
*****
次にスペースを入れる方法を考えます。一番下の段を0とした場合、1段目に1個、2段目に2個とスペースを増やせば良さそうです。ただし、現在の処理では一番上の段から処理しているため、上からのアルゴリズムにする必要があります。一番上の段のスペースの数は、4個で、全体の段数5からその段数1を引いた数になっています。
#!/usr/bin/python3
import cgi_decode
cgi_decode.Set()
# GET,POST,COOKIE,FILES が使えるように
print("Content-Type: text/plain; charset=utf-8\n")
# 入力があり、数値の場合
if 'n' in GET and GET['n'].isdecimal():
# 段数
for i in range(1,int(GET['n'])+1):
# その段に含まれる*の数を奇数個に
d = i*2 - 1
# 段の初めに初期化
t = ''
# 全体の段数からその段の段数を引いた数のスペースを追加
for j in range(int(GET['n']) - i):
t += ' '
# その段の*を作成
for j in range(d):
t += '*'
print(t)
else:
print('入力がありません')
実行結果は下記になります。
# ?n=5 とした場合
*
***
*****
*******
*********
偶数の段にスペースを入れて奇数にする方法も掲載しておきます。
#!/usr/bin/python3
import cgi_decode
cgi_decode.Set()
# GET,POST,COOKIE,FILES が使えるように
print("Content-Type: text/plain; charset=utf-8\n")
# 入力があり、数値の場合
if 'n' in GET and GET['n'].isdecimal():
# 段数
for i in range(1,int(GET['n'])+1):
# 段の初めに初期化
t = ''
# 全体の段数からその段の段数を引いた数のスペースを追加
for j in range(int(GET['n']) - i):
t += ' '
# その段の*を作成
for j in range(i):
# ここでスペースも追加
t += '* '
# 末尾のスペースを除去
t = t.rstrip()
print(t)
else:
print('入力がありません')
実行結果は下記になります。
# ?n=5 とした場合
*
* *
* * *
* * * *
* * * * *
PythonのCGIでセキュリティ問題を確認する
CGIプログラムでは、ユーザーからの入力を受け取って処理を行なうため、入力データの内容により想定外の結果を引き起こすことがあります。主にブラウザにHTMLタグを表示する際に起こる問題(クロスサイトスクリプティング)と、DBなどに保存する際に起こる問題(SQLインジェクション)があります。
今回は、不正なタグを受け取った場合の問題について確認します。今まではテキストデータとして表示を行っていたため、問題は起こりませんでしたが、HTMLとして表示を行なう場合は、エスケープを行わないと、サイトの表示が崩れたり、javascriptを実行されたりします。
下記では、エスケープを行わず、入力を直接表示しています。?word=<h1>などでアクセスすると、タグの機能によりWebサイトのデザインが壊れることになります。
#!/usr/bin/python3
import cgi_decode
cgi_decode.Set()
# GET,POST,COOKIE,FILES が使えるように
print("Content-Type: text/html; charset=utf-8\n")
# word=の入力があった場合
if 'word' in GET:
print(GET['word'])
else:
print('入力がありません')
print('<p>サイトのコンテンツ</p>')
Content-Typeの部分を、text/htmlにしています。以降の表示はHTMLとして扱われるため、タグで使われる文字はエスケープする必要があります。上記プログラムのご確認後は、サーバから削除して下さい。