今回はラズパイ(Raspberry Pi)4BでPythonのBottleというフレームワークを使って簡易Webサーバーを作ってみたいと思います。
外部HTMLファイルやCSSファイル、画像ファイルを使った表示、URLクエリパラメータを使った表示も可能で、しかもHTTPヘッダ情報も操作できて、個人用途のWeb表示ならば必要充分な機能がありました。しかも軽くて速く、趣味用途としては大満足でしたよ。
Pythonについては、このブログでこちらの過去記事のようにGoogle Colaboratory上でほんの少し扱った事はあって、いまいちPythonについてよくわからないところがありましたが、今回の実験でPythonの動作について少し理解が深まった気がしましたね。
そして、今までマイコンを使ったWeb表示はESP8266やESP32によるC/C++で扱った経験はあったのですが、ラズパイLinux上のPythonで表示させたことはとても新鮮でした。
これができれば、ディープラーニングと組み合わせた自作Webサーバーもできそうな気がしてワクワクしますね。
では、Bottleを使った実験を自分なりに紹介してみようと思います。
今回もあくまで自分用の備忘録です。
なお、ラズパイやLinux、Pythonについてはド素人ですので、何か誤り等ありましたらコメント投稿等でお知らせいただくと助かります。
使った環境
Raspberry Pi 4 model B
以前、こちらの記事で紹介したOKdoの全部入りセット Raspberry Pi 4 model B を使いました。
Ubuntu Server 22.04.01 LTS
ラズパイのOSは、メジャーなRasbianを使わず、Ubuntu Server 22.04.01 LTS を使っています。
インストールはこちらの記事で紹介しました。
Pythonパッケージ環境
今回、pip でインストールした各パッケージのバージョンは以下です。
Package Version ----------- ------- bottle 0.12.25 numpy 1.24.2 pip 23.0.1 setuptools 67.6.0 somepackage 1.2.3 wheel 0.40.0
Bottleについて
BottleとはPython環境のpipでインストールできるWeb用フレームワークだそうです。
詳細は公式の下記のページにあります。
https://bottlepy.org/docs/dev/index.html
【抜粋翻訳】
Bottle は、 Python用の高速でシンプルかつ軽量なWSGIマイクロ Web フレームワークで、単一のファイルモジュールとして配布され、 Python 標準ライブラリ以外の依存関係はありません。
簡単に言うと、軽くて高速で初心者向けのPythonライブラリっていう感じだと思います。
ArduinoなどのC/C++経験者は最初なかなか取っ付きにくいかも知れませんが、慣れると確かに扱いやすく、軽快に動いてくれました。それに、他のライブラリに依存関係が無いというのは嬉しいですね。
そして、個人の小規模単一Webページ程度ならば必要充分な機能があると思います。
初心者の自分にとっては好都合なフレームワークでした。
01.事前準備
各パッケージのアップデートや、Linuxファイアウォール設定でポートを開放しておきます。
01-01. LinuxおよびPython環境アップデート
まず、Linuxお決まりの
sudo apt update sudo apt upgrade -y
を実行しておきます。
そして、pyenvを使っている場合はアップデートしておきます。
そして、Pythonのpipによる各パッケージもアップデートしておきます。
私の使ったパッケージのバージョンは先に紹介した通りです。
requirements.txtを使ったアップデートはこちらの記事を参照してください。
01-02. 自作Webサーバー用のポートを開放しておく
以前のこちらの記事でUbuntu Serverのファイアウォール設定を紹介した時には、必要最低限のポートしか開放していませんでした。
今回は、ラズパイ4BをWebサーバーにしてHTMLファイルを表示させるため、それ用のTCPポートを開放しておきます。
例えば開放したいポート番号を8080とした場合、以下のufwコマンドを打ちます。
sudo ufw allow 8080/tcp
すると、こうなります。
~/mypy$ sudo ufw allow 8080/tcp ルールを追加しました ルールを追加しました (v6)
そして、ufwを再起動します。
sudo ufw reload
これで反映されているはずですが、念のためラズパイを再起動してもよいと思います。
02.Bottleを使う
PythonプログラミングでWebサーバーにするにはいろいろパッケージがありますが、先に紹介したように今回は比較的簡単で動作が軽いBottleというパッケージを使ってみます。
以下の公式サイトを参考にしました。
https://bottlepy.org/docs/dev/
まず、pipを使って、Bottleパッケージをインストールします。
pip install bottle
又は、requirements.txtを使う場合は、前回の記事で紹介したようにbottleを追加すれば良いです。
bottle numpy pip setuptools somepackage wheel
今回は全てアップデートしたいので、バージョンは記入しません。
この状態で、以下のコマンドを打ってアップデートします。
pip install -r requirements.txt -U
これでBottleパッケージがインストールできたと思います。
02-01. 最も簡単なBottleによるWeb表示コード
では、Bottle公式サイトを参考に最も簡単なソースコードを以下のように組んでいきます。
ファイル名はbottle_test1.py
としておきます。
#!/usr/bin/env python3.10 # bottle を使った超簡単なWeb表示 from bottle import route, run @route('/') def hello(): return "<h3>Hello World!</h3><br><h2>こんちは!</h2>" if __name__ == '__main__': run( host='localhost', port=8080, reloader=True, debug=True)
最初の1行目はPythonのシバン(Shebang)です。これを書いておくと、コードを実行する場合にわざわざバージョンを指定せずともここに記載されたバージョンで実行してくれます。
Bottleのroute
とrun
をインポートします。
そして、@
マークはPythonのデコレータという、いわゆる関数を装飾するものだそうです。これについては今までC/C++を使っていた自分にとっては意味不明でした。しばらく使っていると何となくわかってくると思いますが…。
関数名はhello
としていますが、名前は好きな名前でOKです。
return文のところでHTML文字列を返しています。
run
関数の引数のhostでは、ここではlocalhost
としていますが、IPアドレス(例えば、192.168.0.10
)でも良いです。でも、localhost
としておいた方が何かと便利かと思います。
ここで注意点ですが、port番号は以前のこちらの記事のSSH接続で設定したポート番号とは異なるものにします。同じポートだとブラウザ表示できません。
また、run関数の引数でreloader=True
というのは、コードを変更して上書き保存した場合、いちいちターミナルでPythonを再起動しなくても自動で反映してくれて便利です。
ただし、直ぐには反映されません。自分の環境では、30秒~1分くらいかかりました。
debug=True
はコードに誤りがあったり、通信が上手く出来なかったりした場合にブラウザ上にメッセージが表示されて便利です。
では、VSCodeのターミナルで、カレントディレクトリに移り(ここではmypyディレクトリに移動)、以下のようにVSCodeのターミナルに入力して走らせてみます。
シバンを利用して実行する場合は以下のようにします。
./bottle_test1.py
ただ、この場合、ファイルのパーミッションで実行を許可しないと実行できないので注意です。
シバン無しの場合は以下のように入力して実行します。私の場合はもっぱらこの方法です。
python bottle_test1.py
すると、VSCodeのターミナルには下図のように表示されます。
(図02-01-01)
このように、「ブラウザーで開く」ボタンを押すとブラウザが開きますが、ブラウザのURL入力欄に以下のように入力しても良いです。
http://localhost:8080/
これを途中で止めるにはそこのメッセージにあるように「Ctrl」+「C」キーを押せばコマンド入力状態に戻ります。
ところで、上図のVSCoceターミナルに警告メッセージが出ています。
<frozen importlib._bootstrap>:914: ImportWarning: _ImportRedirect.find_spec() not found; falling back to find_module()
find_spec()
が見つからないというメッセージです。
これについては以下のPython公式サイトに記載がありました。
https://docs.python.org/3.10/whatsnew/3.10.html#deprecated
【翻訳】
このリリースから、Python 2.7 との互換性のために維持されていた古い import セマンティックの整理を開始するための協調的な取り組みが行われます。具体的には、
find_loader()/find_module()
(find_spec()
に取って代わられました)、load_module()
(exec_module()
に取って代わられました)、module_repr()
(import システムがあなたのために処理します)、__package__ attribute
(__spec__. parent
に取って代わられました)。 parent)、__loader__
属性(__spec__.loader
)、__cached__
属性(__spec__.cached
)は徐々に削除されます(importlibの他のクラスやメソッドと同様に)。この移行期間中に更新が必要なコードを特定するために、必要に応じてImportWarning
やDeprecationWarning
が発生する予定です。www.DeepL.com/Translator(無料版)で翻訳しました。
これはたぶん、Python バージョン2.7からの互換性を保つためのfind_module()
モジュールが使われていて、Python 3.10 ではfind_spec()
に代わったということらしく、移行期間中にしばらく表示されるメッセージのようです。Bottle等のパッケージが更新されればこのメッセージは出なくなるかも知れません。なので放っておきます。
結果的にブラウザに下図の様に表示されればOKです。
(図02-01-02)
これでひとまずはラズパイ4BのPython 簡易Webサーバーが実現できました。
02-02. 外部HTMLファイルを表示させてみる
先ほどはHTMLをPythonコード内に記述して、それをブラウザに出力しましたが、今度はHTMLテキストを別ファイルにして表示させてみます。
まず、下図の様にVSCodeのエクスプローラーでカレントディレクトリ(ここではmypy)にviews
フォルダを作り、そこに例としてindex.html
ファイルを作ります。このviews
というフォルダ名は他の名前はダメみたいです。
(図02-02-01)
そうしたら、以下のようにHTMLコードを入力して保存します。
<h1 style="color: red;">ほげほげ</h1> <div>これは外部HTMLファイルだよ</div>
そして、Pythonコードを以下のようにします。ファイル名はbottle_test2.py
とします。
#!/usr/bin/env python3.10 # bottle を使って外部HTMLファイルをWeb表示 from bottle import route, template, run @route('/') def index(): return template('./index.html') if __name__ == '__main__': run( host='localhost', port=8080, reloader=True, debug=True)
4行目で新たにtemplate
をインポートします。
そして、return
文でtemplate
関数の引数として、先ほど作成したindex.html
のパスを入力します。
これを走らせると、ブラウザに下図の様に表示されると思います。
(図02-02-02)
02-03. 階層化した複数のHTMLファイルを表示させてみる
前節では外部HTMLファイルはトップページの1つでしたが、今度は階層化されたファイルやフォルダ構成でWebアクセスできるようにBottleでプログラミングしてみます。
下図の様にVSCodeのエクスプローラーでviews
フォルダ配下にtop.html
ファイルを作り、そしてcontent
フォルダ(名前は何でも良い)を新規に作成します。そして、content
フォルダ配下にpage1.html
ファイルとpage2.html
ファイルを新規に作成します。
(図02-03-01)
top.html
には例として以下のコードを入力します。
<h1 style="color: red;">Topページ</h1> <div>これはtop.htmlだよ</div> <br> <a href="http://localhost:8080/content/page1">1ページ目</a> <a href="http://localhost:8080/content/page2">2ページ目</a>
page1.html
には以下のコードを入力します。
<h2 style="background-color: aquamarine;">1ページ目</h2> <div>これは<code>/content/page1.html</code>を表示してま~す!</div> <br> <a href="http://localhost:8080/">Topページ</a> <a href="http://localhost:8080/content/page2">2ページ目</a>
page2.html
には以下のコードを入力します。
<h2 style="background-color:orange">2ページ目</h2> <div>これは<code>/content/page2.html</code>を表示してま~す!</div> <br> <a href="http://localhost:8080/">Topページ</a> <a href="http://localhost:8080/content/page1">1ページ目</a>
そして、カレントディレクトリのmypy
配下にはbottle_test3.py
というPythonファイルを作成します。
#!/usr/bin/env python3.10 # bottle を使って階層化されたHTMLファイルをWeb表示 from bottle import route, template, run @route('/') def top(): return template('./top.html') @route('/content/page1') def page1(): return template('./content/page1.html') @route('/content/page2') def page2(): return template('./content/page2.html') if __name__ == '__main__': run( host='localhost', port=8080, reloader=True, debug=True)
このように、@route
デコレータで階層化したファイルをアサインすればいい感じです。
では、これを走らせてみます。
すると、ブラウザのURL入力欄にhttp://localhost:8080/
と入力してアクセスすると、下図の様に表示されました。
(図02-03-02)
「1ページ目」のリンクをクリックすると下図の様に表示されます。
(図02-03-03)
「2ページ目」のリンクをクリックすると、下図の様に表示されます。
(図02-03-04)
これで階層化されたHTMLファイルを表示させることができました。意外と簡単でしたね。
02-04. input、form要素に入力してURLクエリパラメータを利用したWeb表示
次に、HTMLのinput
要素に数値や文字列を入力して画面に反映させたいと思います。
今回はform
要素でsubmit
した際にGETリクエストでURLクエリパラメータを送信して、それをページに反映させます。
まず、トップページのHTMLファイルを以下のようにします。
<h1 style="color: red;">クエリパラメータのテスト</h1> <div> <form action="" method="GET"> <input name='number' placeholder='何か英数値を入力してちょ'> <input name='message' placeholder='何かメッセージを入力してちょ'> <p> <button type='submit'>送信</button> </p> </form> </div> <p>【送信結果】</p> <p>数値={}</p> <p>メッセージ={}</p> <a href="http://localhost:8080/">Clear(Topページ)</a>
このポイントは、12行、13行目の波括弧です。
ここに以下のPythonコードでURLクエリパラメータから取得したstr1
とstr2
の値が反映されて表示されるわけです。
では、PythonコードについてはBottleからrequest
をインポートして、以下のようにします。
#!/usr/bin/env python3.10 # bottleのrequestでクエリパラメータを使う # 参考:https://bottlepy.org/docs/dev/tutorial.html#query-variables from bottle import route, run, template, request @route("/", method="GET") def top_query(): # idは半角英数字のみ str1 = request.query.get("number") # nameは日本語もOK str2 = request.query.getunicode("message") return template('./top_query.html').format(str1, str2) if __name__ == '__main__': run( host='localhost', port=8080, reloader=True, debug=True)
5行目でbottle
からrequest
をインポートします。
10行目のrequest.query.get("number")
でHTMLのform要素から送信されてきたnumber
というクエリパラメータを取得します。ただ、この場合は英数値のみで、日本語は非対応です。
12行目のrequest.query.getunicode("message")
でmessage
というクエリパラメータ値を取得します。
14行目のtemplate('./top_query.html').format(str1, str2)
で、str1
とstr2
の値を先ほど作成したHTML内の波括弧のところに返すというわけです。
では、これを実行し、ブラウザ表示させると初回は以下のように表示されます。
(図02-04-01)
これでそれぞれ英数値と日本語メッセージを入力し、送信ボタンを押すと以下のように表示されます。
(図02-04-02)
このように、ブラウザのURL入力欄を見て分かる通り、クエリパラメータを含めたURLは以下のように表示されています。
http://localhost:8080/?number=123456&message=ほげほげ
number
とmessage
に代入された値がしっかりWeb表示に反映されていますね。
ちなみに英数値のところに日本語を入力してしまうと文字化けします。
なので、Pythonコードでは、request.query.getunicode
を使えば両対応なので安心ですね。
ここまで出来ればBottleで大抵のWebサイトはできそうです。
02-05. CSSファイルを使う
次に、CSSファイルを使えるようにしてみます。
Web表示で少し凝ったデザインにしたい場合はスタイルシートを外部のCSSファイルに書き出した方が圧倒的に便利です。
Bottle公式の以下のドキュメントを参考に作っていきます。
https://bottlepy.org/docs/dev/tutorial.html
公式ドキュメントにはstatic
というフォルダ名に保存されている静的ファイルを使うように書かれていますが、フォルダ名は何でも良いです。
まず、下図の様にVSCodeのエクスプローラーでカレントディレクトリにスタイルシート用フォルダとしてcss
というフォルダ(名前は何でも良い)を新たに作成します。
そして、css
フォルダ配下にCSS(スタイルシート)ファイルを新たに作成します。ここではtest_style1.css
というファイル名にしました。
そして、viewsフォルダにトップページのHTMLファイルを作成します。ここではtop_css_test.html
というファイル名にします。
Pythonファイルはカレントディレクトリ(mypy
フォルダ)にbottle_css_test1.py
という名前で作っておきます。
(図02-05-01)
そして、test_style1.css
ファイルには以下のスタイルシートを入力してみます。
.my-h1 { background:linear-gradient(to bottom, #f87d02,#fbec84); color:black; border:2px solid; width:fit-content; padding: 5px; } .my-div { color:white; border-radius:25px; background:linear-gradient(to bottom, #9ff8dc, #4e3dbe); width:fit-content; padding:5px; }
そして、top_css_test.html
ファイルには以下のHTMLを入力してみます。
<!DOCTYPE html> <html lang="ja"> <head> <meta charset="UTF-8"> <link rel="stylesheet" href="/css/test_style1.css" type="text/css" /> </head> <body> <h1 class="my-h1">Topページ</h1> <div class="my-div">外部CSSファイルを使う</div> </body> </html>
そして、bottle_css_test1.py
ファイルには以下のPythonコードを入力します。
#!/usr/bin/env python3.10 # bottle を使ったWeb表示で、外部CSSファイルを使う from bottle import route, template, run, static_file @route('/') def index(): return template('./top_css_test.html') @route('/css/<filename>') def server_css(filename): return static_file(filename, root='./css') if __name__ == '__main__': run( host='localhost', port=8080, reloader=True, debug=True)
4行目ではbottleから static_file
をインポートします。
10~12行目でCSSファイルを読み込んでブラウザに送信しています。この場合はcssフォルダ内に限定したファイルを送信しています。
では、これを走らせてみます。
下図の様に表示されると思います。
(図02-05-02)
また、cssフォルダ内に新たにフォルダを作成して、その中のスタイルシートを反映させたい場合、
例えば、/css/test1/style.css
としてスタイルシートを反映させたい場合のPythonコードは以下のようにします。
#!/usr/bin/env python3.10 # bottle を使ったWeb表示で、外部CSSファイルを使う from bottle import route, template, run, static_file @route('/') def index(): return template('./top_css_test.html') @route('/css/<filepath:path>') def server_css(filepath): return static_file(filepath, root='./css') if __name__ == '__main__': run( host='localhost', port=8080, reloader=True, debug=True)
<filepath:path>
については公式ドキュメントを見てもイマイチよくわからなかったのですが、どうやらファイルパスにスラッシュが入っている場合はこれを使うようです。
なお、12行目のところのrootを root='./css/test'
としてしまうと、なぜか404エラーとなってしまうので、root='./css'
で良いようです。rootを設定した方がセキュリティ的にはおススメらしいです。
また、ファイルパスを正規表現にしたい場合は以下のコードだそうです。
#!/usr/bin/env python3.10 # bottle を使ったWeb表示で、外部CSSファイルを使う from bottle import route, template, run, static_file @route('/') def index(): return template('./top_css_path_test.html') @route('/css/<filepath:re:.*\.css>') def server_css(filepath): return static_file(filepath, root='./css') if __name__ == '__main__': run( host='localhost', port=8080, reloader=True, debug=True)
10行目のところで :re
を使えば正規表現が使えるそうです。
02-06. 画像を表示させる
次に、画像ファイルを準備して、それをWeb表示させてみたいと思います。
前節で説明した static_file
をインポートして使います。
まず、下図の様にVSCodeのエクスプローラーでカレントディレクトリにimages
フォルダ(フォルダ名は何でも良い)を作成し、そこに適当な画像ファイル(ここではJPEG)をコピペしておきます。
そして、views
フォルダに新たにHTMLファイルを作成(ここではtop_img_test.html
)し、カレントディレクトリに新たにPythonファイル(ここではbottle_img_test1.py
)を作成しておきます。
(図02-06-01)
そして、top_img_test.html
ファイルには以下のコードを入力します。
<!DOCTYPE html> <html lang="ja"> <head> <meta charset="UTF-8"> </head> <body> <h1>画像表示テストTopページ</h1> <img src="/images/img_01.jpg" width="200"> <img src="/images/img_02.jpg" width="200"> </body> </html>
そして、bottle_img_test1.py
には以下のPythonコードを入力します。
#!/usr/bin/env python3.10 # bottle を使ったWeb表示で、画像ファイルを使う from bottle import route, template, run, static_file @route('/') def index(): return template('./top_img_test.html') @route('/images/<filename:re:.*\.jpg>') def server_static(filename): return static_file(filename, root='./images') if __name__ == '__main__': run( host='localhost', port=8080, reloader=True, debug=True)
10行目のところは前節で説明した正規表現を使っています。
では、これを実行すると、下図の様に表示されると思います。
(図02-06-02)
因みに、対象の画像が無い場合はライブラリ側で自動的に404レスポンスを返すそうで、新たに404レスポンス用のコードを書かなくて良いようです。これは楽ですね。
02-07. HTTPヘッダを取得、設定する
今まではあえてHTTPヘッダを設定しなくても、Bottle側で自動的にレスポンスヘッダを設定してブラウザに送信していましたが、自分で自由のヘッダ情報を取得したり設定したりしてみたいと思います。
Pythonコードはbottleからrequestとresponseをインポートして、以下のように入力してみます。
#!/usr/bin/env python3.10 # bottleでHTTPヘッダ情報を使う # 参考:https://bottlepy.org/docs/dev/tutorial.html from bottle import route, run, request, response @route('/') def top(): head = request.headers.get('accept-language') print(head) response.set_header('Content-Length', '8') return "<h3>HTTPヘッダ情報テストTopページ</h3>" if __name__ == '__main__': run( host='localhost', port=8080, reloader=True, debug=True)
9行目の request.headers.get()
でブラウザからラズパイへ送信されてきたHTTPリクエストヘッダ情報を取得します。これはWSGI形式で、基本的にdict型(辞書型)だそうです。
よって、request.headers.get('accept-language')
で Accept-Language
のパラメータを取得できるというわけです。
12行目でラズパイからブラウザへ送信するHTTPレスポンスヘッダを設定しています。
Content-Length
を8としているので、送ったHTMLのうち8バイト分だけを使用せよとしています。
では、これを実行してみます。
まずはVSCodeのターミナルでは以下のように表示されました。
~/mypy$ python bottle_header_test1.py Bottle v0.12.25 server starting up (using WSGIRefServer())... Listening on http://localhost:8080/ Hit Ctrl-C to quit. <frozen importlib._bootstrap>:914: ImportWarning: _ImportRedirect.find_spec() not found; falling back to find_module() ja,en-US;q=0.9,en;q=0.8,da;q=0.7,af;q=0.6 127.0.0.1 - - [24/Mar/2023 22:17:13] "GET / HTTP/1.1" 200 49 127.0.0.1 - - [24/Mar/2023 22:17:14] "GET /favicon.ico HTTP/1.1" 404 742
このように、ブラウザから送信されてきたHTTPリクエストヘッダの Accept-Language は
ja,en-US;q=0.9,en;q=0.8,da;q=0.7,af;q=0.6
と表示されました。
これは、ブラウザのデベロッパーツールのネットワークで確認したリクエストヘッダと同じでした。
これができれば、外部端末からラズパイにHTTPリクエストが送られてきた場合にPythonプログラミングで事前にいろいろと選別できそうです。
では、ブラウザには以下のように表示されます。
(図02-07-01)
意図した結果になりました。
先のPythonコードの14行目で返しているHTML文
<h3>HTTPヘッダ情報テストTopページ</h3>
のうち、HTTPレスポンスヘッダの Content-Length で8バイト分だけ使うと設定したので、半角の英数字8文字分まで表示されているわけです。
つまり、<h3>HTTP
だけ表示したというわけです。
これで、確実にHTTPレスポンスヘッダも設定できました。
以上、ここまでできれば、殆どのWeb表示に対応できそうです。
まとめ
こんな感じでPythonのBottleを使えば意外と簡単に簡易的なWebサーバーが作れました。
Python初心者の自分としてはデコレータなどの取っ付きにくいものがありましたが、しばらく使って慣れてくると簡単にWeb表示ができて軽快で便利だなと思いました。
HTTPヘッダ情報の変更もできて、個人用途ではやりたいことはほぼ何でもできそうな気がしました。
次はもうちょっとツッコんだWeb表示に挑戦してみたいと思います。
今回はここまでです。
ではまた・・・。
コメント