Pythonで HTML+PHPのローカルサーバを起動する

最終更新日

ファイルをチェックするとき、パスの関係でヘッダーやフッダー、画像など、うまく開けなかったりすることってありますよね。
そんなとき、ローカルサーバがあるとすごく便利です。URLでアクセスできるようになるので、
Excelマクロと組み合わせれば、複数ページのスクリーンショットをまとめて撮ることもできます。

ローカルサーバを立ち上げて、HTMLやPHPの動作を確認できるようにしておくと、
Webディレクターにとってはかなりメリットがあります。
自分で動きを確認できるので、デザインや仕様のズレにすぐ気づけるし、開発メンバーへの指示も的確に出せるようになります。

クライアントへの事前説明やデモも、ローカルで動かして見せられるので話が早く、公開前にしっかり確認できます。
ネットがない場所でも作業できるし、外部に公開しないからセキュリティ的にも安心。
スケジュール管理や品質チェックにも役立ちます。

XAMPPみたいなツールを使ってもOKですが、
Pythonが入っていれば、コマンド一発でサーバを立ち上げられるので、
わざわざ環境を整えなくてもすぐ確認できます
また複数のフォルダーから起動できるのでアプリと違って、どのフォルダーからでも立ち上げられるのでバージョン違いや必要なフォルダーを指定できるのがが嬉しいポイントです。

_run_server.py という名前で保存して実行
このPythonスクリプトは、index.php または index.html をローカルで簡単に確認できる ローカルサーバ起動ツール です。PHPが使える場合はPHPサーバを、使えない場合はPythonの簡易HTTPサーバを自動で選んで起動してくれます。


import subprocess
import sys
import importlib.util         # モジュール存在チェック用
import webbrowser
import time
import shutil
import os
import socket
import urllib.parse
from http.server import SimpleHTTPRequestHandler
from socketserver import TCPServer
import threading
import re

# ────────────────────────────
# スクリプト実行ディレクトリを基準とする
# ────────────────────────────
base_path = os.path.dirname(os.path.abspath(__file__))
# サーブするフォルダーをスクリプトのあるフォルダーに固定
os.chdir(base_path)

# ────────────────────────────
# ① 必要外部モジュールの確認と自動インストール
# ────────────────────────────
required_modules = {
    "bs4": "beautifulsoup4",
    "lxml": "lxml"
}
for module_name, package_name in required_modules.items():
    if importlib.util.find_spec(module_name) is None:
        resp = input(f"モジュール `{package_name}` が見つかりません。インストールしますか? (Y/n): ").strip().lower()
        if resp in ["", "y", "yes"]:
            print(f"`{package_name}` をインストール中...")
            try:
                subprocess.check_call([sys.executable, "-m", "pip", "install", package_name])
                print(f"`{package_name}` のインストールに成功しました。")
            except subprocess.CalledProcessError:
                print(f"❌ `{package_name}` のインストールに失敗しました。")
                sys.exit(1)
        else:
            print(f"❌ `{package_name}` が必要です。スクリプトを終了します。")
            sys.exit(1)
# bs4 のインポート(SSI風 include 用)
from bs4 import BeautifulSoup

# ────────────────────────────
# 利用可能なポート探し (8000~8008)
# ────────────────────────────
def find_available_port(start_port=8000, max_tries=9):
    for port in range(start_port, start_port + max_tries):
        with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
            try:
                s.bind(("localhost", port))
                return port
            except OSError:
                continue
    return None  # 全部使用中なら None

# ────────────────────────────────────
# カスタム HTTP ハンドラ (GET と POST 共通処理)
# ────────────────────────────────────
class CustomHTTPRequestHandler(SimpleHTTPRequestHandler):
    def do_GET(self):
        self.handle_request("GET")

    def do_POST(self):
        self.handle_request("POST")

    def handle_request(self, method="GET"):
        # クエリパラメータを除去してパス取得
        parsed = urllib.parse.urlparse(self.path)
        raw_path = parsed.path.lstrip("/")
        # POST のルートは index.html にフォールバック
        target = raw_path or ("index.html" if method == "POST" else "")
        file_path = os.path.join(base_path, target)

        if method == "POST":
            # POST データ読み込み
            length = int(self.headers.get('Content-Length', 0))
            raw = self.rfile.read(length).decode('utf-8')
            post_fields = urllib.parse.parse_qs(raw)
            # コンソールへログ出力
            print("📥 POSTされた内容:")
            for k, v in post_fields.items():
                print(f" - {k} = {v[0]}")
            # テンプレート or 一覧生成
            if os.path.isfile(file_path):
                with open(file_path, 'r', encoding='utf-8') as f:
                    content = f.read()
                content = self.process_includes(content)
                for k, v in post_fields.items():
                    content = content.replace(f"{{{{ {k} }}}}", v[0])
            else:
                items = "".join([f"<li><strong>{k}</strong>: {v[0]}</li>" for k, v in post_fields.items()])
                content = f"<html><body><h2>POSTデータを受信しました</h2><ul>{items}</ul></body></html>"
            self.send_response(200)
            self.send_header("Content-Type", "text/html; charset=utf-8")
            self.end_headers()
            self.wfile.write(content.encode('utf-8'))
            return

        # GET処理
        # ルートアクセスはディレクトリ一覧
        if parsed.path in ["/", ""]:
            self.path = "/"
            return super().do_GET()
        # ディレクトリアクセスは一覧表示
        if os.path.isdir(file_path):
            self.path = "/" + raw_path
            return super().do_GET()
        # ファイルアクセス
        if os.path.isfile(file_path):
            ext = os.path.splitext(file_path)[1].lower()
            if ext in ['.html', '.htm']:
                with open(file_path, 'r', encoding='utf-8') as f:
                    content = f.read()
                content = self.process_includes(content)
                self.send_response(200)
                self.send_header("Content-Type", "text/html; charset=utf-8")
                self.end_headers()
                self.wfile.write(content.encode('utf-8'))
            else:
                try:
                    with open(file_path, 'rb') as f:
                        data = f.read()
                    self.send_response(200)
                    self.send_header("Content-Type", self.guess_type(file_path))
                    self.send_header("Content-Length", str(len(data)))
                    self.end_headers()
                    self.wfile.write(data)
                except Exception as e:
                    self.send_error(500, f"バイナリ読み込み失敗: {e}")
        else:
            self.send_error(404, f"{raw_path or '/'} not found")

    def process_includes(self, content):
        # SSI風 include 処理
        pattern = r'<!--\s*#include\s+file="([^"]+)"\s*-->'
        def repl(m):
            inc = m.group(1)
            try:
                with open(os.path.join(base_path, inc), 'r', encoding='utf-8') as f:
                    return f.read()
            except Exception as e:
                return f"<!-- Include error: {e} -->"
        return re.sub(pattern, repl, content)

# ───────────────────────────
def start_python_server(port):
    handler = CustomHTTPRequestHandler
    try:
        httpd = TCPServer(("", port), handler)
    except OSError:
        port = find_available_port(start_port=port+1)
        if port is None:
            return None, None
        httpd = TCPServer(("", port), handler)
    print(f"✅ Python HTTPサーバを起動: http://localhost:{port}")
    thread = threading.Thread(target=httpd.serve_forever, daemon=True)
    thread.start()
    return httpd, port

def start_php_server(port):
    try:
        proc = subprocess.Popen(["php", "-S", f"localhost:{port}"])
    except OSError:
        port = find_available_port(start_port=port+1)
        if port is None:
            return None, None
        proc = subprocess.Popen(["php", "-S", f"localhost:{port}"])
    print(f"✅ PHPサーバを起動: http://localhost:{port}")
    return proc, port

# PHP利用可否の通知
php_available = shutil.which("php") is not None
has_index_php = os.path.isfile(os.path.join(base_path, "index.php"))
if php_available:
    print("✅ PHP が利用可能です。PHPサーバを起動します。" )
else:
    print("ℹ PHP が見つかりません。Pythonサーバを使用します。")

# サーバ起動判定
initial_port = 8000
if php_available and has_index_php:
    server_proc, PORT = start_php_server(initial_port)
    if server_proc is None:
        print("❌ 8000~8008 がすべて使用中です。終了します。")
        sys.exit(1)
    use_sub = True
else:
    httpd, PORT = start_python_server(initial_port)
    if httpd is None:
        print("❌ 8000~8008 がすべて使用中です。終了します。")
        sys.exit(1)
    use_sub = False

# 自動ブラウザ起動
time.sleep(1)
webbrowser.open(f"http://localhost:{PORT}")

# 終了待機ループ
try:
    while True:
        k = input("終了:大文字Q を押してください > ")
        if k.strip().upper() == "Q":
            print("🛑 サーバ停止中...")
            if use_sub:
                server_proc.terminate()
            else:
                httpd.shutdown()
            break
except KeyboardInterrupt:
    print("\n🛑 キーボード割り込みでサーバ停止中...")
    if use_sub:
        server_proc.terminate()
    else:
        httpd.shutdown()

使い方

このスクリプトの使い方

このスクリプトを使えば、ローカル環境でHTMLやPHPファイルを簡単に表示・確認できます。Webディレクターやデザイナーでも扱いやすく、面倒な設定は不要です。

事前準備

  1. 表示させたい index.html または index.php などのファイルを、スクリプト(例:run_server.py)と同じフォルダに置きます。
  2. PHPファイルを動かしたい場合は、PCにPHPをインストールしておきましょう(HTMLだけなら不要です)。

起動方法

  • スクリプトファイル(例:_run_server.py)を ダブルクリック するか、ターミナル(コマンドプロンプト)から以下を実行します:
python .\_run_server.py
  • 実行すると、フォルダ内の index.php が存在し、PHPが使える場合は PHPのローカルサーバ が起動します。
  • もしPHPが使えない場合や index.php がない場合は、Pythonの簡易HTTPサーバ が自動で使われます。

自動でブラウザが開く!

1秒ほど待つと、ブラウザが自動で開いて http://localhost:8000 を表示します。 これで、対象のHTMLやPHPページをローカルで確認できます。

⏹ サーバの停止方法

  • サーバを終了したいときは、Pythonのコンソール画面で 「q」 を入力してください。
  • もしくは、Ctrl + C を押して強制終了も可能です。

PHPも使いたいときは?

PHPを使いたい場合は、あらかじめPCにPHPをインストールしておけばOK!
このスクリプトは自動でPHPサーバを起動してくれるので、特別な設定は不要です。
HTMLだけならそのままでも使えるので、まずは気軽に試してみてください。

PHPのインストール方法(Windows向け)

✅ 1. PHP をダウンロードする

  1. 公式サイト(https://www.php.net/downloads)にアクセス
  2. Windows downloads」をクリック
  3. ZIP版(Non Thread Safe / x64 など)」(Zip [xx.xxMB]を選んでダウンロード
  4. ダウンロードしたZIPファイルを解凍し、
     例: C:\data\php フォルダに置いておく

✅ 2. 環境変数に PHP のパスを追加する

PHPをコマンドで使えるようにするための設定です。

  1. スタートメニューを開いて
     「環境変数」や「システムの環境変数を編集」と入力して、出てきた項目をクリック
  2. システムのプロパティ」というウィンドウが開きます
     → 右下の「環境変数(N)…」をクリック
  3. 環境変数」という画面が開きます
     → 下の「システム環境変数」または上の「ユーザー環境変数」の中から
       「Path」 を探して選び、「編集(E)…」をクリック
  4. 「Pathの編集」画面で
     → 「新規(N)」をクリックして、PHPを置いたフォルダのパスを入力
      例: C:\data\php
  5. OK を押して、すべてのウィンドウを閉じる

✅ 3. コマンドプロンプトで動作確認

次のコマンドを入力して Enter:
スタートメニューから「cmd」または「コマンドプロンプト」を起動

php -v

ちょっとした画面チェックや、クライアントに見せる簡易デモにもぴったり!
ネットがない環境でも使えるから、外出先や制限のある社内ネットでも安心です。
HTMLだけでも、PHPがあればさらに柔軟に対応できるので、ディレクター業務の強い味方になりますよ。
サンプルコードは、下記から取れます

2025.0623更新