3
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

【JS×Python】サーバー不要!ブラウザだけでPythonとpipを動かす「Pyodide」が凄すぎた

3
Last updated at Posted at 2026-03-08

はじめに

WebアプリでPythonを使おうとすると、通常は以下のような構成が必要になりますよね。
・バックエンドサーバーの構築(FlaskやFastAPI、Djangoなど)
・API経由でのフロントとの通信処理
しかし、「Pyodide(パイオダイド)」という技術を使えば、これらは一切不要になります!

💡 Pyodideとは?

サーバーを一切立てずに、ユーザーのブラウザ上(フロントエンド)だけでPythonを動かすことができる画期的な技術(WebAssemblyポート)です。

今回、実際に触ってみて「これはフロントエンド開発の幅が広がる!」と感動した3つの知見と、すぐに試せる最小構成のサンプルコードを紹介します。

💡 Pyodideから得られた3つの知見

1. ブラウザだけでPythonが動く (WebAssembly)

特別な環境構築やDockerなどは一切不要です。CDNからJavaScriptのライブラリを1つ読み込むだけで、ブラウザのメモリ上にPythonの実行エンジンが立ち上がります。

2. ブラウザ上で「pip」が使える (micropip)

これが個人的に一番の驚きでした。Pythonの強みは膨大な外部ライブラリですが、ブラウザ上であっても micropip を使えば、PyPI(公式パッケージリポジトリ)から動的にライブラリをダウンロードしてインストールできます。

3. JSとPythonで変数を直接やり取りできる

JavaScriptの変数をPython側に渡し、Pythonで処理した結果(戻り値)を再びJavaScriptで受け取ることができます。JSONへの変換(シリアライズ)などを意識せず、シームレスに連携できるのが非常に強力です。

🛠️ 最小構成サンプル:JSからPythonにテキストを渡してアスキーアート化する

上記の3つの知見をすべて盛り込んだ、コピペで動くHTML1枚のサンプルコードです。
Pythonの pyfiglet というライブラリをブラウザ上でインストールし、JSから渡した文字列をアスキーアートに変換します。


<html lang="ja">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Pyodide 技術実証デモ (Vanilla JS)</title>
    
    <!-- Tailwind CSSの読み込み -->
    <script src="https://cdn.tailwindcss.com"></script>
</head>
<body class="min-h-screen bg-slate-100 flex flex-col items-center py-10 px-4 font-sans text-slate-800">

    <div class="max-w-2xl w-full bg-white rounded-xl shadow-lg overflow-hidden border border-slate-200">
        
        <!-- ヘッダー -->
        <div class="bg-slate-800 p-6 text-white text-center">
            <h1 class="text-2xl font-bold flex items-center justify-center gap-2">
                <!-- ターミナルアイコン (SVG) -->
                <svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
                    <polyline points="4 17 10 11 4 5"></polyline>
                    <line x1="12" y1="19" x2="20" y2="19"></line>
                </svg>
                Pyodide 技術実証デモ
            </h1>
            <p class="text-slate-400 text-sm mt-2">
                ブラウザでのPython実行・PIPインストール・JS連携の最小サンプル (Vanilla JS版)
            </p>
        </div>

        <div class="p-6 space-y-4">
            <!-- コントロール -->
            <div class="flex justify-between items-center">
                <p class="text-sm font-medium text-slate-600">
                    ステータス: <span id="status-text" class="text-amber-500">初期化中...</span>
                </p>
                <button
                    id="run-btn"
                    disabled
                    class="px-6 py-2 rounded-lg font-bold flex items-center gap-2 transition-colors bg-slate-200 text-slate-400 cursor-not-allowed"
                >
                    <span id="btn-icon">
                        <!-- プレイアイコン (SVG) -->
                        <svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
                            <polygon points="5 3 19 12 5 21 5 3"></polygon>
                        </svg>
                    </span>
                    <span id="btn-text">デモを実行する</span>
                </button>
            </div>

            <!-- ターミナル風のログ表示エリア -->
            <div id="log-container" class="bg-slate-900 rounded-lg p-4 h-96 overflow-y-auto font-mono text-sm shadow-inner text-slate-500 italic">
                「デモを実行する」をクリックすると、ここに処理ログが表示されます。
            </div>
        </div>
    </div>

    <!-- JavaScript ロジック -->
    <script>
        // DOM要素の取得
        const statusText = document.getElementById('status-text');
        const runBtn = document.getElementById('run-btn');
        const btnText = document.getElementById('btn-text');
        const btnIcon = document.getElementById('btn-icon');
        const logContainer = document.getElementById('log-container');

        let isReady = false;
        let isRunning = false;

        // Pyodideのスクリプトを動的に読み込む(読み込み順序によるReferenceErrorを回避)
        const pyodideScript = document.createElement('script');
        pyodideScript.src = 'https://cdn.jsdelivr.net/pyodide/v0.25.0/full/pyodide.js';
        pyodideScript.onload = () => {
            isReady = true;
            statusText.textContent = "準備完了";
            statusText.className = "text-green-600 font-bold";
            updateButtonState();
        };
        document.body.appendChild(pyodideScript);

        // ボタンの見た目と状態を更新する関数
        function updateButtonState() {
            if (!isReady || isRunning) {
                runBtn.disabled = true;
                runBtn.className = "px-6 py-2 rounded-lg font-bold flex items-center gap-2 transition-colors bg-slate-200 text-slate-400 cursor-not-allowed";
            } else {
                runBtn.disabled = false;
                runBtn.className = "px-6 py-2 rounded-lg font-bold flex items-center gap-2 transition-colors bg-indigo-600 hover:bg-indigo-700 text-white shadow-md cursor-pointer";
            }

            if (isRunning) {
                btnIcon.style.display = 'none';
                btnText.textContent = "処理中...";
            } else {
                btnIcon.style.display = 'inline-block';
                btnText.textContent = "デモを実行する";
            }
        }

        // ログ画面にテキストを追加する関数
        function addLog(msg) {
            // 初回のプレースホルダーテキストを削除
            if (logContainer.classList.contains('text-slate-500')) {
                logContainer.innerHTML = '';
                logContainer.className = "bg-slate-900 rounded-lg p-4 h-96 overflow-y-auto font-mono text-sm shadow-inner";
            }

            const pre = document.createElement('pre');
            const isError = msg.includes('❌');
            pre.className = `whitespace-pre-wrap leading-relaxed ${isError ? 'text-red-400' : 'text-green-400'}`;
            pre.textContent = msg;
            
            logContainer.appendChild(pre);
            // 自動で一番下までスクロール
            logContainer.scrollTop = logContainer.scrollHeight;
        }

        // デモのメイン処理
        runBtn.addEventListener('click', async () => {
            if (!isReady || isRunning) return;

            isRunning = true;
            updateButtonState();
            
            // ログをクリアして開始
            logContainer.innerHTML = '';
            logContainer.className = "bg-slate-900 rounded-lg p-4 h-96 overflow-y-auto font-mono text-sm shadow-inner";
            
            addLog("▶ デモ開始...");

            try {
                // --------------------------------------------------------
                // 知見①: ブラウザだけでPythonを起動する
                // --------------------------------------------------------
                addLog("⏳ [1/4] Python環境をブラウザ上に構築しています...");
                const pyodide = await window.loadPyodide();
                addLog("✅ Python環境の起動完了!");

                // --------------------------------------------------------
                // 知見②: PIPを使って外部パッケージをインストールする
                // --------------------------------------------------------
                addLog("📦 [2/4] micropip(ブラウザ版pip)で 'pyfiglet' をインストール中...");
                await pyodide.loadPackage("micropip");
                const micropip = pyodide.pyimport("micropip");
                // ここで実際のPyPIからパッケージを取得しています!
                await micropip.install("pyfiglet"); 
                addLog("✅ インストール完了!");

                // --------------------------------------------------------
                // 知見③: JavaScriptからPythonへのデータ受け渡し
                // --------------------------------------------------------
                const jsMessage = "Hello Pyodide!";
                addLog(`📤 [3/4] JSからPythonへ変数を渡します: "${jsMessage}"`);
                
                pyodide.globals.set('js_message', jsMessage); 

                // --------------------------------------------------------
                // 知見④: Pythonコードの実行と、結果のJSへの返却
                // --------------------------------------------------------
                addLog("⚙️ [4/4] Pythonスクリプトを実行します...");
                const pythonCode = `
import pyfiglet

# JSから受け取った変数(js_message)を使ってアスキーアートを生成
art = pyfiglet.figlet_format(js_message)

art # 最後に評価された値が、JavaScriptに返却されます
                `;
                
                const result = await pyodide.runPythonAsync(pythonCode);
                addLog("📥 Pythonからの返却値を受け取りました!👇\n");
                
                addLog(result);

            } catch (error) {
                addLog(`❌ エラーが発生しました: ${error.message}`);
            } finally {
                isRunning = false;
                updateButtonState();
                addLog("\n⏹ デモ終了");
            }
        });
    </script>
</body>
</html>

HTMLコードから、Pythonとして実行されている部分のみを再掲

import pyfiglet

# JSから受け取った変数(js_message)を使ってアスキーアートを生成
art = pyfiglet.figlet_format(js_message)
art # 最後に評価された値が、JavaScriptに返却されます

最後に art とだけ書かれている理由は、Pyodide(パイオダイド)特有の「戻り値の返し方のルール」にあります。

Pyodideの runPythonAsync() という機能でPythonのコードを実行した場合、「一番最後に評価された値(式)」が、自動的にJavaScript側への戻り値として渡されるという仕様になっています。

これには以下のようないくつかの理由と背景があります。

  1. return は使えない
    このPythonコードは関数(def ...:)の中ではないため、return art と書くとPythonの文法エラーになってしまいます。

  2. print() では値が渡せない
    print(art) と書いた場合、文字通り「出力(表示)」されてしまうだけで、JavaScript側の変数(今回のコードでいう result)には何も値が返ってきません。

💻 実行結果のイメージ

ボタンを押すと、バックエンドと通信することなく、以下のようなアスキーアートがブラウザ上で生成されます。

スクリーンショット 2026-03-08 11.33.55.png

🚀 どんな時に役立つか?(ユースケース)

このアプローチは、「サーバーにデータを送りたくない」「バックエンドのインフラ管理をしたくない」というケースで絶大な威力を発揮します。

セキュアなファイル処理ツール
セキュアなファイル処理ツール等、PDFのパスワード設定や画像のリサイズなど。データがサーバーに送信されないため、情報漏洩のリスクがありません。

データサイエンス系のダッシュボード
PandasやNumPyなども動くため、ユーザーのブラウザの計算リソースを使ってデータ分析やグラフ描画が可能です。

教育用ツール・サンドボックス
ユーザーが書いたPythonコードをその場で安全に実行・評価する環境が簡単に作れます。

🎯 まとめ

JavaScriptだけでは実装が難しい複雑な処理も、Pyodideを使えば「フロントエンドの手軽さ」と「Pythonの強力なエコシステム」をいいとこ取りできます。
ちょっとしたツール作りに非常に便利なので、ぜひ皆さんもブラウザ上でPythonを動かしてみてください!

3
2
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
3
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?