Contents
- 1 ファイル削除はos.remove, フォルダ削除はshutil.rmtree
- 2 rarfile
- 3 pyaudio
- 4 ログをとる
- 5 文字コード
- 6 クラスもインスタンスもオブジェクトとして管理されている
- 7 すべてオブジェクトで管理している
- 8 モジュール・パッケージのインポートについて
- 9 標準出力に上書き・追記する
- 10 pythonスクリプトをプロンプトを開かずにダブルクリックで実行する
- 11 エラーログをとる
- 12 ZIPファイルを展開し中身をいじってまた固める
- 13 ファイル操作するときは
- 14 windows環境ではファイル読み込み・書き出し時のデフォルト文字コードがSJIS
- 15 後方参照
- 16 ファイル名の一括置換
- 17 ビルトインサーバー
- 18 ビルトインサーバーを止めるには
ファイル削除はos.remove, フォルダ削除はshutil.rmtree
os.removeでフォルダを削除しようとすると
1 |
PermissionError: [WinError 5] アクセスが拒否されました。 |
という例外が投げられる。これを額面通りに受け取ってos.chmodを使ってパーミッションをゆるめても問題解決しないので注意。
os.removeで削除できるのはファイルであり、フォルダは削除できない。フォルダ削除にはshutil.rmtreeを使うのが便利。フォルダが空でなくても削除することができる。
rarfile
rarfileモジュールはpip installしただけでは動作しない。unrar.exeをダウンロードしてPATHの通ったフォルダに置いておくことが必要。
unrar.exeが最新でないと、エラーが発生することがある。
pyaudio
まず大前提としてふたつのことをしなくてはならない。
スタート→設定→システム→サウンドに行き、
- サウンドコントロールパネル→録音タブでステレオミキサーを有効にする
- マイクのプライバシー設定で「アプリがマイクにアクセスできるようにする」をオンにする
以上を怠るとpyaudio.PyAudio().open()すると
1 |
OSError: [Errno -9999] Unanticipated host error |
というエラーが出る。
Windowsをクリーンインストールしたあとなど、いままで動いていたプログラムがいきなり動かなくなってはまる。かすかな記憶をたどってステレオミキサーを有効にするところまでは自分でたどり着いたが、マイクへのアクセス権の問題は思い出せなかった。もうこんな無駄なことはしたくないのでメモを残しておく。
ログをとる
いままでwith open~してf.write(datetime.datetime.now().strftime(・・・) + ‘エラーメッセージ’)という感じでログをとっていたが、loggingモジュールというのを使えばdatetimeとか使わなくても日時付きでかんたんにログをとることができた模様。
いままではこんな書きかたをしていた。
1 2 3 4 5 6 7 8 9 |
import traceback import datetime try: raise Exception('エラーだよ') except Exception as e: with open('log.txt', 'a', encoding='utf-8') as f: f.write('---------------------\n') f.write(datetime.datetime.now().strftime('%Y/%m/%d %H:%M:%S') + '\n' + traceback.format_exc() + '\n') |
datetimeモジュールとtracebackモジュールを使いつつファイルをオープンして・・・とやっているが、loggingモジュールを使うとすっきり書ける。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
import logging logging.basicConfig(format='---------------------\n%(asctime)s - %(levelname)s - %(message)s', datefmt='%Y-%m-%d %H:%M:%S', # ミリ秒まで出力すると邪魔なので handlers=[logging.FileHandler('log.txt', encoding='utf-8')]) # 文字化けしないように try: raise Exception('エラーだよ') except Exception as e: logging.exception('%s', e) # log.txtへの出力内容 # --------------------- # 2020-04-22 23:54:08 - ERROR - エラーだよ # Traceback (most recent call last): # File "E:/program/PycharmProjects/untitled/test/test.py", line 12, in <module> # raise Exception('エラーだよ') # Exception: エラーだよ |
logging.basicConfigメソッドでフォーマットを設定したのち、例外をキャッチしたところでlogging.exceptionで例外の内容を書きだす。logging.exceptionの場合、エラーレベルはlogging.ERRORなので、logging.basicConfigでlevelを設定する必要はない。
logging.basicConfigでhandlers引数を指定することでPycharmからUnexpected argumentだと注意されるのがうざいのだが。
どうしてもPycharmからの注意が目障りな場合
かなり行数が長くなるのだが…。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
import logging logger = logging.getLogger(__name__) handler = logging.FileHandler('log.txt', encoding='utf-8') fmt = logging.Formatter('---------------------\n%(asctime)s - %(levelname)s - %(message)s', datefmt='%Y-%m-%d %H:%M:%S') handler.setFormatter(fmt) logger.addHandler(handler) try: raise Exception('エラーだよ') except Exception as e: logger.exception(e) |
loggingでググっても信頼できそうで、かつわかりやすくまとまった記事はなかったのでけっきょく公式ドキュメントを拾い読みするしかなかった(Logging HOWTO — Python 3.8.2 ドキュメント)。
設定の行数がすげー長い。使っているクラスやメソッドも5つもあって覚えられない。毎回ググるのがつらい。
冒頭にいちど長い設定を書けばそれ以降はすっきり書けるので、多くの箇所でログを出したい場合はloggingを使ったほうがいい。しかし、ログを出したい箇所が1個や2個と少ない場合はwith openしてdatetimeとtraceback使いつつ書きだすほうが手間がかからないかも。
文字コード
pythonプログラム内では文字はUnicodeとして扱っている。
たとえばファイルを読み書きするときや、requests.postなんかするときに文字コードというものが出てくる。つまり、文字コードとはプログラムの出入り口で使われるもの。
プログラムの外に文字データを出すとき。たとえばファイルに書き込むときにutf-8やshift-jisなんかの文字コードにエンコードする。
あるいは、ファイルを読み込むときに文字コード表を使ってUnicodeにデコードする。間違った文字コードを使うと文字化けしたり例外となる。
requests.postするとプログラムの外=通信路にデータが出されるのでエンコードされる。ただこの場合、日本語や特殊な記号などはプログラム内ですべてパーセントエンコードされてASCIIコードの範囲に変換されてからエンコードされる。utf-8でもshift-jisでもASCIIコードの範囲内の文字は共通の文字コード。どの文字コードでエンコードしてもおなじになる。
ASCIIコード
ASCIIコードを書きだしてみる。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
for i in range(128): print("[" + f"{i}:" + chr(i) + "]", end='') if (i + 1) % 15 == 0: print() #[0: ][1:][2:][3:][4:][5:][6:][7:][8][9: ][10: ][14:] #[15:][16:][17:][18:][19:][20:][21:][22:][23:][24:][25:][26:][27:][28:][29:] #[30:][31:][32: ][33:!][34:"][35:#][36:$][37:%][38:&][39:'][40:(][41:)][42:*][43:+][44:,] #[45:-][46:.][47:/][48:0][49:1][50:2][51:3][52:4][53:5][54:6][55:7][56:8][57:9][58::][59:;] #[60:<][61:=][62:>][63:?][64:@][65:A][66:B][67:C][68:D][69:E][70:F][71:G][72:H][73:I][74:J] #[75:K][76:L][77:M][78:N][79:O][80:P][81:Q][82:R][83:S][84:T][85:U][86:V][87:W][88:X][89:Y] #[90:Z][91:[][92:\][93:]][94:^][95:_][96:`][97:a][98:b][99:c][100:d][101:e][102:f][103:g][104:h] #[105:i][106:j][107:k][108:l][109:m][110:n][111:o][112:p][113:q][114:r][115:s][116:t][117:u][118:v][119:w] #[120:x][121:y][122:z][123:{][124:|][125:}][126:~][127:] |
要は、キーボードに割り当てられている文字や制御文字の文字コードがASCIIコード。こいつらの文字コードはUTF-8でもShift-JISでもASCIIコードと一致する。
0はNULLなので空白。8はBS(Back Space)なので:が削除されている。10はLF(Line Feed)なので改行されている。32は半角スペース。何も表示されていないのは制御文字。11~13が表示されない理由はなんかよくわからん。
UnicodeコードポイントはASCIIコード(を含む文字コードぜんぶ)とは直接は関係ないのだが、結果として0~127の範囲はASCIIコードと一致する。
~について
Unicodeには「波型」と「全角ティルダ」という二つの異なる「〜」が存在する(参考1, 参考2)。
なんかよくわからんが、いちどcp932でエンコードしてからShift-JISでデコードすればいいようだ。
クラスもインスタンスもオブジェクトとして管理されている
test2.pyというモジュール。
1 2 3 4 5 6 7 8 9 10 |
class Person: def __init__(self, name): self.name = name def hello(self): print('Hello, I am {}.'.format(self.name)) def test(): jon = Person('Jon') return jon |
これをtest.pyというメインモジュールからインポートして使う。
1 2 3 4 5 |
from test2 import test person = test() person.hello() # Hello, I am Jon. |
メインモジュールではPersonクラスを知らないはずなのに、helloメソッドを呼ぶことができる。これはメインモジュールが受け取ったPersonクラスのインスタンスにhelloメソッドが含まれているからだ。
test2.pyをすこし変更する。奇妙な書き方だが、インスタンスではなくPersonクラスそのものをtest関数で返すようにした。
1 2 3 4 5 6 7 8 9 |
class Person: def __init__(self, name): self.name = name def hello(self): print('Hello, I am {}.'.format(self.name)) def test(): return Person |
これをtest.pyというメインモジュールからインポートして使う。
1 2 3 4 5 6 7 |
from test2 import test Person = test() ken = Person('Ken') ken.hello() # Hello, I am Ken. |
Personクラスをインポートしていないのだが、Personクラスでインスタンスを生成することができる。
インスタンスだけでなくクラスもオブジェクトであり、変数に格納できるし、importしなくてもモジュール間でやりとりすることができる。関数もそう。
もっともクラスや関数をこんなかたちでやりとりすることってないと思うけど。
クラスも関数もインスタンスとおなじくオブジェクトなのでは?とすれば上記のようなことができるのでは?と思い、実際にやってみたらやっぱりできたというだけのお話。
すべてオブジェクトで管理している
pythonプログラムはオブジェクトにアクセスすることで処理をしている。自分で定義した関数やクラスだけでなく、インポートしたモジュール(ファイル)やパッケージ(ディレクトリ)も全部オブジェクトとして管理している。
メインモジュール(実行ファイル)が操作することができるオブジェクトはdir関数で一覧を取得することができる。たとえばkerasというパッケージをインポートするとpprint(dir())したときそのなかにkerasが含まれる。
dir(keras)とするとkeras.につなげてアクセスできるオブジェクト一覧を取得できる。それらはkerasディレクトリ直下の__init__.pyで定義・インポートされたオブジェクト群である。
また、from keras import models としてmodelsというモジュールをインポートするとやはりdir()の一覧にmodelsが含まれる。dir(models)とするとmodels.につなげてアクセスできるオブジェクト一覧を取得できる。それらはmodels.pyというファイルで定義・インポートされたオブジェクト群である。
モジュール・パッケージのインポートについて
モジュール=ファイル、パッケージ=ディレクトリ
モジュールの実体はファイルであり、パッケージの実体はディレクトリ(に含まれる__init__.py)。
相対パス
相対パスでインポートする際、pythonコマンドで実行するファイルと同階層以上には昇れない。同階層より上のファイル(モジュール)やディレクトリ(パッケージ)を指定するとつぎのようなエラーとなる。
1 |
ValueError: attempted relative import beyond top-level package |
存在しないファイルやディレクトリを指定してもおなじエラーになるので、同階層で..(親ディレクトリ)を指定した段階で例外を投げていると思われる。
相対パスはパッケージ内で使うことを想定した機能といえる。すなわち、.(現在のディレクトリ)や..(親ディレクトリ)はパッケージ内でのみ使うことができる。
__all__
__init__.pyに定義する__all__は、from package import * と書いた際にインポートされるファイル(モジュール)を指定する。__all__を定義しないとfrom package import * としてもなにもインポートされない。ただし、ファイル名を明示してインポート(from package import module)すればインポートできる。
__all__を定義することでインポートされるファイルを*から隠すことはできるが、直接指定してインポートすればインポートできてしまう。完全に隠すことはできないと思われる。
__init__.py
パッケージ(ディレクトリ)packをインポートする( import pack)と、pack直下の__init__.pyが実行される。そのなかで定義・インポートされるオブジェクト(関数やクラスや変数やモジュールやパッケージ)をたとえばpack.funcなどとpack.につなげて書くことで使うことができる。__init__.pyに定義・インポートされていないオブジェクトにはアクセスできない。
まぎらわしいのは、たとえば__init__.pyのなかで
1 |
from .mod import func |
としたとき、pack.funcにアクセスできるのは当然だが、pack.mod.funcとしてもアクセスできる。つまり、「from .mod import 〇〇〇」としたとき、〇〇〇だけではなくmod全体もインポートされる。だからpack.mod.funcとしてもアクセスできるのだ。
mod全体がインポートされているので、modに含まれているfunc以外にもアクセスできる。pack.mod.func2などとするとアクセスできる。ただし、pack.func2としてはアクセスできない。
pack直下の__init__.pyで「from .mod import func」としたとき、packにぶら下がるオブジェクトはfuncとmodだけである。だからpack.func2はエラーとなる。
以上のことはパッケージ内に限った話である。たとえば
1 |
from pack import mod |
とするとmodにアクセスできるが、packにはアクセスできない。pack全体もインポートされるということはなく、modのみがインポートされる。パッケージ内のルールでパッケージ外のルールを類推する、あるいはその逆をすると混乱してしまう。完全に分けて考えるべきだ。
__init__.py その2
前節のとおり、__init__.pyに定義・インポートされていないオブジェクトにはアクセスできない。ただし__init__.pyでインポートされたモジュールの中でインポートされる__init__.pyと同階層のモジュールにもアクセスできる。
requestsというパッケージで確認する。requests直下にはhelp.pyというファイル(モジュール)があるが、__init__.pyでインポートされていないのでこうなる。
1 2 3 |
import requests print('help' in dir(requests)) # False |
ところが、_internal_utils.pyというファイルも同様に__init__.pyでインポートされていないにもかかわらずこうなる。
1 2 3 |
import requests print('_internal_utils' in dir(requests)) # True |
これにはかなり悩んだが、__init__.pyでインポートされているほかのモジュールで_internal_utilsがインポートされているからなのだ。findstrで調べたところ、models.pyとかsessions.pyなどからインポートされている。
ほんとうはこんなことやらないほうがいいんだろうけど、requestsの下にディレクトリsubをつくり、そのなかにtest.pyをつくった。そしてmodels.pyに「from .sub import test」を追記したうえでどうなるか。
1 2 3 |
import requests print('sub' in dir(requests)) # True print('test' in dir(requests)) # False |
ここからわかることは、__init__.pyでインポートされたモジュールからインポートされたモジュール(とパッケージ)のうち、__init__.pyと同階層にあるモジュール(とパッケージ)のみがアクセス可能になるということ。
傍証として、models.pyではdatetimeがインポートされているがこうなる。
1 2 3 |
import requests print('datetime' in dir(requests)) # False |
結論。たとえばrequestsというパッケージをimport requests としてインポートしたとき、requests.につなげてアクセスできるオブジェクトは以下。
- __init__.pyで定義・インポートされているオブジェクト
- __init__.pyでインポートされたモジュール内でインポートされている、__init__.pyと同階層のモジュール
うーん
1 |
import pack.subpack |
とすると、pack.subpackだけでなくpackにもアクセスできる。奇妙な感じがするが、そんなインポートの仕方はしないので考えないことにしよう。
ふと思いついて
1 2 |
import matplotlib.pyplot as plt print(matplotlib) |
としてみたらエラーになった。
1 2 |
import matplotlib.pyplot print(matplotlib) |
だとやはりmatplotlibにアクセスできる。
from ~ import ~
import ~でインポートできるのはパッケージ(ディレクトリ)やモジュール(ファイル)だけ。関数やクラスはインポートできない(import pack.mod.funcはできない)。from ~ import ~ならパッケージ・モジュールに加えて関数やクラスもインポートできる。ああ、まぎらわしい。
要するに
なにがなんだかわからなくなってきた。パッケージ外からインポートする場合について考える。
パッケージpackがインポートされると直下の__init__.pyが実行され、そこで定義されたオブジェクトがパッケージにぶら下がる。そこで定義されていないモジュールはファイルが存在してもぶら下がらない。
モジュールをインポートすると、そのモジュール内で定義されたオブジェクトがモジュールにぶら下がる。
関数やクラスをインポートすると、グローバルなスコープ(?)にぶら下がる。
結局
モジュールやパッケージをテキトーにインポートしてみて、dir(pack)やdir(mod)として目的の関数やクラスが使えるかどうか確認すればおk。
from mod import * はしないほうがいい
たとえばmod.py
1 2 3 4 |
import requests def get(): res = requests.get('https://google.com') print(res.status_code) |
がメインモジュールと同階層にあったとして、メインモジュール
1 2 3 |
from mod import * from pprint import pprint pprint(dir()) |
を実行するとこうなる。
1 2 3 4 5 6 7 8 9 10 11 12 |
['__annotations__', '__builtins__', '__cached__', '__doc__', '__file__', '__loader__', '__name__', '__package__', '__spec__', 'get', 'pprint', 'requests'] |
requestsまでインポートされてしまっている。これは気持ち悪い。
*でインポートするとモジュール内でインポートされているライブラリなんかもぜんぶインポートされてしまうので単純に無駄だし、名前が衝突する恐れも高まる。このように
1 2 3 4 5 |
import requests def get(): res = requests.get('https://google.com') print(res.status_code) __all__ = ['get'] |
__all__を定義すれば限定できるようだが、公式ドキュメントも*の使用を推奨していないし、使うものだけインポートすればいいと思う。
参考
6. モジュール — Python 3.8.2 ドキュメント
標準出力に上書き・追記する
print関数で大量にログを出力すると、行数が際限なくふえて見づらくなることがある。sys.stdout.writeを使うと標準出力の上書きや追記ができる(後述するが、べつにprint関数でもできる)。
まずは上書き。\r(カーソルを文頭へ戻すことを表す制御文字(Carriage Return))を入れる必要がある。sys.stdout.flush()はあってもなくてもうまくいくが、処理が重たくなると必要なのかもしれない(未確認)。
1 2 3 4 5 6 7 8 |
import sys import time for i in range(10): sys.stdout.write('\r{}'.format(i)) sys.stdout.flush() # なくてもいい time.sleep(0.5) # 9 |
つぎに追記。追記の場合はsys.stdout.flush()が必要なようだ。
1 2 3 4 5 |
for i in range(10): sys.stdout.write('{}'.format(i)) sys.stdout.flush() time.sleep(0.5) # 0123456789 |
print関数で上書き・追記する
ググって出てくる方法がsys.stdout.writeを使った方法だったので、print関数ではできないと思い込んでいたがそうではなかった。\rを調べるとカーソルを文頭に戻す制御文字とのこと。これを使えばprint関数でもできるんじゃね?と思って試したらふつうにできた。
1 2 3 4 5 6 |
import time for i in range(10): print('\r{}'.format(i), end='') time.sleep(0.5) # 9 |
print関数で改行をしないようにendオプションを指定しつつ、\rを使えばできる。
追記は以下。sys.stdout.flush()が必要。
1 2 3 4 5 6 7 8 |
import sys import time for i in range(10): print('{}'.format(i), end='') sys.stdout.flush() time.sleep(0.5) # 0123456789 |
pythonスクリプトをプロンプトを開かずにダブルクリックで実行する
以下のようなvbsファイルをつくるとできる(参考)。
1 |
CreateObject("WScript.Shell").Run "python.exe script.py",0 |
ショートカットのアイコンを変更するには、プロパティ→ショートカット→アイコンの変更→参照で画像ファイルを選択する。
エラーログをとる
以前はこんな感じにしていたが、これだとどこで例外発生したかわからない。行数の情報がほしい。
1 2 3 4 5 6 7 8 9 10 |
import datetime try: print(1/0) except Exception as e: print('例外発生:', e) with open('log_exception.txt', 'a', encoding='utf-8') as f: f.write(datetime.datetime.now().strftime('%Y/%m/%d %H:%M') + '\t' + str(e) + '\n') # 2019/10/09 14:39 division by zero |
ググるとtracebackモジュールのformat_excメソッドで例外の情報がとれるらしい。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
import datetime, traceback try: print(1/0) except Exception as e: print('例外発生:', e) with open('log_exception.txt', 'a', encoding='utf-8') as f: f.write('------------------------------------------------------------------------\n') f.write(datetime.datetime.now().strftime('%Y/%m/%d %H:%M') + '\n' + traceback.format_exc() + '\n') # ------------------------------------------------------------------------ # 2019/10/09 14:40 # Traceback (most recent call last): # File "E:/program/PycharmProjects/untitled/test/test.py", line 4, in <module> # print(1/0) # ZeroDivisionError: division by zero |
ZIPファイルを展開し中身をいじってまた固める
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
import os, glob, zipfile, shutil zip_file = 'ファイル名' temp_dir = 'temp_dir' with zipfile.ZipFile(zip_file, 'r') as z: z.extractall(temp_dir) os.remove(zip_file) paths = [path for path in glob.glob(temp_dir + '/**', recursive=True) if os.path.isfile(path)] for path in paths: # 処理 with zipfile.ZipFile(zip_file, 'w', compression=zipfile.ZIP_DEFLATED) as z: for path in paths: z.write(path, arcname=os.path.basename(path)) shutil.rmtree(temp_dir) |
ファイル操作するときは
毎回なにからはじめるか混乱するが、まずはパス(のリスト)を取得すべし。なぜならファイルを削除するにもリネームするにもつくるにも(ZIPで固めるとか)、まずはパスが必要。
インポートするモジュールはos, glob, shutil, zipfile。sysモジュールはなぜかosモジュールとセットでインポートしがちだけど、ファイル操作ではけっきょく使わない。
- glob.globで絶対パスのリストを取得
- os.path.exitsでファイル/ディレクトリの存在確認
- shutil.rmtreeでディレクトリごと削除(からのos.mkdirで同名のディレクトリ生成)
- zipfile.ZipFileで固める
windows環境ではファイル読み込み・書き出し時のデフォルト文字コードがSJIS
なので、以下のようにencodingオプションを指定してUTF-8で書き込む。
1 2 |
with open("log.txt", "w", encoding="utf-8") as f: f.write("日本語\n") |
読み込むときもencodingを指定する。意味的にはdecodingかなと思うが、そんなオプションはない。
1 2 |
with open("log.txt", "r", encoding="utf-8") as f: print(f.read()) |
後方参照
1 2 |
string = "BASENAME: 464233788.html" #.htmlを削除したい string = re.sub("BASENAME: (\d+).html", "BASENAME: \\1", string) |
\\1ではなく\1で後方参照したい場合はraw文字列にする。
1 2 |
string = "BASENAME: 464233788.html" #.htmlを削除したい string = re.sub("BASENAME: (\d+).html", r"BASENAME: \1", string) |
ファイル名の一括置換
ファイル名の一括置換は折にふれてやるのでメモ。はじめos.walkを使っていたが、globモジュールのほうが便利。
1 2 3 4 5 6 7 |
import os import re import glob dirname = "フォルダ名" for file in glob.glob(dirname + "/**", recursive=True): if os.path.isfile(file): os.rename(file, re.sub("正規表現", "\\1など後方参照使いつつ置換", file)) |
ビルトインサーバー
コマンドプロンプトから以下を実行。
1 |
python -m http.server --cgi 8000 |
pythonのcgiプログラムでは以下を記載する。
1 2 3 4 5 6 |
#-*- coding: utf-8 -*- import cgi import sys import io sys.stdout = io.TextIOWrapper(sys.stdout.buffer, encoding="utf-8") #日本語使う場合 form = cgi.FieldStorage() |
UNIXの場合は1行目に以下を追加。windowsでは不要。
1 |
#!/usr/bin/python |
POSTを受け取るには以下。
1 |
s = form.getvalue("name", "") |
変数sにname属性がnameの要素のvalueの値が入る。
1 2 3 4 5 6 7 8 9 10 11 12 13 |
html = """ <!DOCTYPE html> <html lang="ja"> <head> <meta charset="utf-8"> <title>python test</title> </head> <body> <h1>%s</h1> </body> </html> """ print(html % s) |
みたいな感じでHTMLを返す。
CGIプログラムの実行権限に注意。
これでいちおうpythonでクライアントにHTMLを返せるが、とはいってもHTMLはPHPで生成するのが楽だと思う。慣れてるし。
PHPでPOST受け取ってexecでpythonに流して処理の結果をPHPに戻し、最終的にクライアントに返す画面はPHPでつくる。
データ処理はpythonで、画面づくりはPHPで。
ビルトインサーバーを止めるには
Ctrl+Cでは止まらないので、Ctrl+Breakで止める。Breakキーはキーボードの右上のあたりにある。