【ネタ】PythonがインストールできないWindowsマシンでPythonを動かすたった一つの冴えない方法

## 概要

Pythonをインストールしたくてもできない環境でもPythonを使いたい！ということでWindows(10または11)以外何もインストールされていない環境でもPythonを無理やり使う方法を考えました。


## 方法

PowerShellでHTTPサーバーを立て、CDNからPyScriptランタイムをロードしてEdgeブラウザ上でPythonを実行すればいけるのでは？ということでやりました。


```powershell:pyscript.bat
@if not "%~0"=="%~dp0.\%~nx0" (start /min cmd /c,"%~dp0.\%~nx0" %* & goto :eof)  else  (setlocal enableextensions enabledelayedexpansion & set "SELFPATH=%~dp0" & set "SELF=%~f0" & PowerShell.exe -ExecutionPolicy Bypass -Command "& (Invoke-Expression -Command ('{#' + ((Get-Content '!SELF:'=''!') -join \"`n\") + '}'))" %* & exit /b !errorlevel!)

# 設定
# ポート番号
$port = 18000
# ファイル書き込みリクエストを許可するかどうか
$useFileWrite = $true
# PyScriptのメインスクリプト
$pysScript = "main.py"
# PyScriptのスクリプトタイプ(py, mpy, py-game)
$pysType = "py"
# PyScriptの設定JSON
$pysConfigJson = @"
{
}
"@ -replace "`r`n|\n", ""
# PyScriptのバージョン
$pysVersion = "2025.5.1"
# 削除を禁止する拡張子
$protectedExtensions = @(".py", ".exe", ".bat", ".com", ".ps1")


# 実行ディレクトリの取得(このファイルがあるディレクトリ)
$curdir = $env:SELFPATH

# PyScriptを実行するためのHTML
$htmlContent = @"
<!DOCTYPE html>
<html lang="ja">

<head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>PyScript</title>
    <script type="module" src="https://pyscript.net/releases/$pysVersion/core.js"></script>
    <link rel="stylesheet" href="https://pyscript.net/releases/$pysVersion/core.css" />
    <style>
        :root {
            --spinner-size: 80px;
            --stroke-width: 8;
            --main-color: #3498db;
            --bg-color: #ffffff;
        }

        @keyframes spin {
            0% {
                transform: rotate(0deg);
            }
            100% {
                transform: rotate(360deg);
            }
        }

        body {
            background: var(--bg-color);
            margin: 0;
            padding: 0;
            overflow: hidden;
        }

        #loading {
            position: fixed;
            top: 0;
            left: 0;
            width: 100vw;
            height: 100vh;
            background: var(--bg-color);
            display: flex;
            flex-direction: column;
            justify-content: center;
            align-items: center;
            z-index: 1000;
            font-family: sans-serif;
        }

        .spinner {
            width: var(--spinner-size);
            height: var(--spinner-size);
            animation: spin 1s linear infinite;
            transform-origin: center;
            margin: 0 auto;
        }

        .loading-text {
            margin-top: 16px;
            font-size: 1.2em;
            color: var(--bg-color);
            max-width: 90vw;
            filter: invert(100%) grayscale(100%) contrast(100);
            text-align: center;
        }
    </style>
</head>

<body>
    <div id="loading">
        <svg class="spinner" viewBox="0 0 100 100">
            <defs>
                <linearGradient id="grad" x1="0%" y1="0%" x2="100%" y2="0%">
                    <stop offset="0%" stop-color="var(--main-color)" stop-opacity="1" />
                    <stop offset="100%" stop-color="var(--main-color)" stop-opacity="0" />
                </linearGradient>
            </defs>
            <circle cx="50" cy="50" r="40" fill="none" stroke="url(#grad)" stroke-width="var(--stroke-width)" stroke-linecap="round" />
        </svg>
        <div class="loading-text">
            Loading PyScript...
        </div>
    </div>
    <script type="$pysType" src="$pysScript" config='$pysConfigJson'></script>
    <script type="$pysType">
import js
js.document.getElementById("loading").style.display = "none"
    </script>
</body>

</html>
"@


# ポート使用中チェック
try {
    write-host "ポート $port の使用状況を確認中..."
    $listenerCheck = [System.Net.Sockets.TcpListener]::new([System.Net.IPAddress]::Loopback, $port)
    $listenerCheck.Start()
    $listenerCheck.Stop()
    $portAvailable = $true
} catch {
    $portAvailable = $false
}

# ポートが使用中の場合、/stop にリクエストを送信して停止を試みる
if (-not $portAvailable) {
    Write-Host "ポート $port は使用中です。/stop にリクエストを送信します..."
    try {
        $ProgressPreference = 'SilentlyContinue'
        $response = Invoke-WebRequest -Uri "http://localhost:$port/stop" -Method GET | Out-Null
    } catch {
    }
    Start-Sleep -Seconds 1
    try{
        $listenerCheck = [System.Net.Sockets.TcpListener]::new([System.Net.IPAddress]::Loopback, $port)
        $listenerCheck.Start()
        $listenerCheck.Stop()
        $portAvailable = $true
    } catch {
        $portAvailable = $false
    }
}

# それでもポートが使用中の場合は終了
if (-not $portAvailable) {
    Write-Host "ポート $port は既に使用中です。"
    exit
}

# Edge起動
Start-Process "msedge.exe" "http://localhost:$port/"

# HTTPサーバー起動
# Add-Type -AssemblyName System.Net.HttpListener
$listener = New-Object System.Net.HttpListener
$listener.Prefixes.Add("http://localhost:$port/")
$listener.Start()
Write-Host "サーバー起動中: http://localhost:$port/"

# HTTPリクエストの処理
while ($true) {
    # リクエストの待機
    try {
        $context = $listener.GetContext()
    } catch {
        break
    }

    # リクエスト情報の取得
    $request = $context.Request
    $response = $context.Response
    $httpMethod = $request.HttpMethod.ToUpper()
    $path = $request.Url.AbsolutePath.TrimStart('/')

    # stopリクエストを受け付けるためのフラグ
    $stoprequest = $false

    # キャッシュを無効にするヘッダの追加
    $response.AddHeader("Cache-Control", "no-cache, no-store, must-revalidate")
    $response.AddHeader("Pragma", "no-cache")
    $response.AddHeader("Expires", 0)

    # レスポンスの初期化
    $msg = ""
    $now = (Get-Date).ToString("yyyy-MM-dd HH:mm:ss")

    # パスチェック
    if ($path -match "[\\:*?\""<>|]") {
        $bytes = [System.Text.Encoding]::UTF8.GetBytes("不正なパスです: $path")
        $response.StatusCode = 400
        $response.ContentType = "text/plain; charset=UTF-8"
        $response.OutputStream.Write($bytes, 0, $bytes.Length)
        $response.Close()
        Write-Host "[$now] $httpMethod $path => 400 (不正なパス)"
        continue
    }

    # パスの正規化
    $targetPath = Join-Path $curdir $path

    # HTTPメソッドによる処理
    switch ($httpMethod) {
        "POST" {
            # POSTリクエストはファイルの上書き書き込みとして処理
            if ($useFileWrite) {
                $fs = [System.IO.File]::Create($targetPath)
                $request.InputStream.CopyTo($fs)
                $fs.Close()
                $msg = "保存完了: $path"
                $response.StatusCode = 200
            } else {
                $response.StatusCode = 403
                $msg = "ファイル書き込みは無効化されています。"
            }
        }
        "PUT" {
            # PUTリクエストはファイルの追記書き込みとして処理
            if ($useFileWrite) {
                $fs = [System.IO.File]::Open($targetPath, [System.IO.FileMode]::Append, [System.IO.FileAccess]::Write)
                $request.InputStream.CopyTo($fs)
                $fs.Close()
                $msg = "追記完了: $path"
                $response.StatusCode = 200
            } else {
                $response.StatusCode = 403
                $msg = "ファイル書き込みは無効化されています。"
            }
        }
        "DELETE" {
            # DELETEリクエストはファイルの削除として処理(ディレクトリの削除や特定の拡張子の削除は無効)
            if ($useFileWrite) {
                $ext = [System.IO.Path]::GetExtension($targetPath)
                if ($protectedExtensions -contains $ext) {
                    $response.StatusCode = 403
                    $msg = "この拡張子のファイルは削除できません: $ext"
                } elseif (Test-Path $targetPath) {
                    $item = Get-Item $targetPath
                    if ($item.PSIsContainer) {
                        $msg = "ディレクトリの削除はできません: $path"
                        $response.StatusCode = 403
                    } else {
                        Remove-Item $targetPath -Force
                        $msg = "削除完了: $path"
                        $response.StatusCode = 200
                    }
                } else {
                    $msg = "ファイルが存在しません: $path"
                    $response.StatusCode = 404
                }
            } else {
                $response.StatusCode = 403
                $msg = "ファイル書き込みは無効化されています。"
            }
        }
        "GET" {
            # GETリクエストは通常のHTTPと同じくファイルの取得として処理
            if ($path -eq "") {
                # ルートパスへのリクエストはPyScriptのHTMLを返す
                $bytes = [System.Text.Encoding]::UTF8.GetBytes($htmlContent)
                $response.ContentType = "text/html; charset=UTF-8"
                $response.OutputStream.Write($bytes, 0, $bytes.Length)
            } elseif ($path -eq "stop") {
                # /stop リクエストはサーバーの停止要求として処理
                $msg = "サーバーを停止します。"
                $stoprequest = $true
            } elseif (Test-Path $targetPath) {
                # ファイルが存在する場合はその内容を返す(ディレクトリ自体へのアクセスは無効)
                $item = Get-Item $targetPath
                if ($item.PSIsContainer) {
                    $msg = "ディレクトリへのアクセスはできません: $path"
                    $response.StatusCode = 403
                } else {
                    $bytes = [System.IO.File]::ReadAllBytes($targetPath)
                    switch ([System.IO.Path]::GetExtension($targetPath)) {
                        ".py" { $response.ContentType = "text/plain; charset=UTF-8" }
                        ".txt" { $response.ContentType = "text/plain; charset=UTF-8" }
                        ".css" { $response.ContentType = "text/css; charset=UTF-8" }
                        ".js" { $response.ContentType = "application/javascript; charset=UTF-8" }
                        ".html" { $response.ContentType = "text/html; charset=UTF-8" }
                        ".json" { $response.ContentType = "application/json; charset=UTF-8" }
                        ".xml" { $response.ContentType = "application/xml; charset=UTF-8" }
                        ".jpg" { $response.ContentType = "image/jpeg" }
                        ".png" { $response.ContentType = "image/png" }
                        ".gif" { $response.ContentType = "image/gif" }
                        ".svg" { $response.ContentType = "image/svg+xml" }
                        ".ogg" { $response.ContentType = "audio/ogg" }
                        ".mp3" { $response.ContentType = "audio/mpeg" }
                        ".mp4" { $response.ContentType = "video/mp4" }
                        ".webm" { $response.ContentType = "video/webm" }
                        ".pdf" { $response.ContentType = "application/pdf" }
                        default { $response.ContentType = "application/octet-stream" }
                    }
                    $response.OutputStream.Write($bytes, 0, $bytes.Length)
                }
            } else {
                # ファイルが存在しない場合は404エラーを返す
                $response.StatusCode = 404
                $msg = "ファイルが存在しません。"
            }
        }
        default {
            # 未対応のHTTPメソッドは405エラーを返す
            $response.StatusCode = 405
            $msg = "未対応のメソッドです: $httpMethod"
        }
    }

    # メッセージがある場合は出力
    if ($msg -ne "") {
        $bytes = [System.Text.Encoding]::UTF8.GetBytes($msg)
        $response.ContentType = "text/plain; charset=UTF-8"
        $response.ContentEncoding = [System.Text.Encoding]::UTF8
        $response.OutputStream.Write($bytes, 0, $bytes.Length)
    } else {
        $msg = "OK"
    }

    # ログの出力
    Write-Host "[$now] $httpMethod $path => $($response.StatusCode) ($msg)"

    # レスポンスの終了
    $response.Close()

    # stopリクエストの場合はサーバーを停止
    if ($stoprequest) {
        Write-Host "サーバーを停止します..."
        $listener.Stop()
        break
    }
}
```

ファイル関係の処理はChromeだとAPIでやらなくても[ブラウザからローカルのファイルを操作する機能](https://docs.pyscript.net/2025.5.1/user-guide/filesystem/#the-devices-local-filesystem)があったりするのですが、実際使ってみたら思いのほか使いにくかったのでAPIで実装することになりました。

## 感想

ここでは実装していませんが、キーコードやクリックイベントに該当する情報やPowerShellコードを受けつけるAPIを実装したりなんかすればRPAもできそうな気がしています、職場なんかでやったら多分速攻で禁止されそうな気がしますが。