Skip to content

Pyodide: 瀏覽器運行 Python

傳統上,Python 作為一種後端語言,主要在伺服器上運行。若想在網頁前端執行 Python,通常需要一個後端伺服器來處理請求並返回結果。然而,隨著技術發展,現在可以直接在瀏覽器中運行 Python,而 Pyodide 正是實現這一目標的關鍵技術。

Pyodide 讓你能夠在前端環境中,無需任何伺服器,直接執行 Python 代碼,甚至支持第三方套件。本文探討 Pyodide 的基礎知識,並透過一系列範例,學習如何使用 Pyodide。

Image

上圖為在瀏覽器中運行 Python 的輸出

⚙️ Pyodide 如何運作?

要執行 Python 程式必要有一個 Python 直譯器,CPython 是主流的 Python 直譯器。

WebAssembly 是為瀏覽器設計的二進位指令格式,它是低階的組合語言,可以讓瀏覽器以接近原生的速度執行代碼。你可以把 C、C++ 等高效能語言編譯成 WebAssembly,然後在瀏覽器中運行。

Pyodide 是一個將 CPython 直譯器移植到 WebAssembly 的專案。CPython 直譯器已預先編譯為 WebAssembly,因此你是在瀏覽器執行 CPython 直譯器。


當你執行一段 Python 代碼時,實際流程是:

  1. JavaScript 將 Python 代碼字串傳遞給 Pyodide。
  2. 在 WebAssembly 環境中運行的 CPython 直譯器接收並解釋這段代碼。
  3. 執行結果可以返回給 JavaScript,或直接與瀏覽器環境互動。

🚀 如何在瀏覽器中加載 Pyodide

在網頁中使用 Pyodide 非常直接。最簡單的方法是透過 CDN 引入 Pyodide 的主腳本。

首先,創建一個基本的 HTML 檔案:

html
<!DOCTYPE html>
<html>
<head>
    <!-- 加載 Pyodide -->
    <script src="https://cdn.jsdelivr.net/pyodide/v0.26.1/full/pyodide.js"></script>
</head>
<body>
    <h1>Pyodide 測試</h1>
    <script type="text/javascript">
        // 異步函數來加載和初始化 Pyodide
        async function main() {
            console.log("正在載入 Pyodide...");
            let pyodide = await loadPyodide();
            console.log("成功載入 Pyodide!");
            // 這裡執行 Python 代碼 (未執行)
        }
        main();
    </script>

    <p>請按 F12 檢查瀏覽器 console 輸出</p>
</body>
</html>

loadPyodide() 是一個異步函數,它會返回一個 Promise。因此,我們需要使用 async/await 語法來等待 Pyodide 完全初始化。初始化完成後,pyodide object 就成為了 JavaScript 與 Python 環境溝通的橋樑。

執行實例 1

💻 執行你的第一個 Python 代碼

加載 Pyodide 後,你可以使用 pyodide.runPython() 方法來執行任意的 Python 代碼字串。

範例:基本的 print

預設情況下,Python 的 print 函數會將輸出發送到瀏覽器的開發者 Console。

html
<!DOCTYPE html>
<html>
<head>
    <script src="https://cdn.jsdelivr.net/pyodide/v0.26.1/full/pyodide.js"></script>
</head>
<body>
    <p>請按 F12 檢查瀏覽器 console 輸出</p>
    <script type="text/javascript">
        async function main() {
            let pyodide = await loadPyodide();
            // 執行 Python 代碼
            pyodide.runPython(`
                import sys
                # print 到 console
                print("Hello from Python!") 
                print(f"Python version: {sys.version}")
            `);
        }
        main();
    </script>
</body>
</html>

打開此 HTML 檔案並查看 Console 的 Python 輸出。

執行實例 2

📝 將輸出重定向到 DOM 元素

在 Console 中查看輸出很有用,但更多時候我們希望將結果直接顯示在網頁上。Pyodide 允許你在加載時配置標準輸出 (stdout) 和標準錯誤 (stderr) 的處理方式。

範例:將 print 輸出到 <pre> 標籤

html
<!DOCTYPE html>
<html>
<head>
    <script src="https://cdn.jsdelivr.net/pyodide/v0.26.1/full/pyodide.js"></script>
</head>
<body>
    <h2>Python 輸出:</h2>
    <pre id="output"></pre>
    <pre id="error"></pre>

    <script type="text/javascript">
        async function main() {
            const output = document.getElementById('output');
            const error = document.getElementById('error');
            
            // 在加載時傳入配置 object
            let pyodide = await loadPyodide({
                // 將 stdout 重定向到一個 JS 函數
                stdout: (text) => {
                    output.textContent += text + '\n';
                },
                // 將 stderr 也重定向
                stderr: (text) => {
                    error.textContent += text + '\n';
                }
            });

            // 執行 Python 代碼,輸出將顯示在頁面上
            pyodide.runPython(`
                print("直接把文字顯示在網頁的 pre 標籤中。")
                x = 10
                y = 20

                # print 到 DOM 上,而不是 console
                print(f"{x} + {y} = {x + y}") 

                # 模擬一個錯誤
                import sys
                sys.stderr.write("這是一個從 stderr 發出的錯誤訊息。\\n")
            `);
        }
        main();
    </script>
</body>
</html>

在這個範例中,我們將 stdoutstderr 指向一個 JavaScript 函數,該函數會將接收到的文字附加到指定的 DOM 元素上,而不是在 console 上。

執行實例 3

🌐 從 Python 操作網頁 DOM

Pyodide 提供了一個名為 js 的內置模組,它充當了 Python 和瀏覽器 JavaScript 環境之間的橋樑。透過 import js,你的 Python 代碼可以存取 windowdocument 等全局 JavaScript object,從而實現對 DOM 的直接操作。

範例:使用 Python 創建和修改 DOM 元素

html
<!DOCTYPE html>
<html>
<head>
    <script src="https://cdn.jsdelivr.net/pyodide/v0.26.1/full/pyodide.js"></script>
</head>
<body>
    <h2>使用 Pyodide 動態操作 DOM</h2>
    <p>輸入文字並點擊按鈕,Python 會將其新增到頁面中。</p>

    <input type="text" id="text-input" placeholder="在此輸入文字...">
    <button id="add-btn">用 Python 新增</button>

    <div id="container"></div>

    <script type="text/javascript">
        async function main() {
            // 載入 Pyodide 並設定控制台輸出
            let pyodide = await loadPyodide();
            pyodide.setStdout((text) => { console.log("Python:", text); });

            const pythonCode = `
                import js
                from pyodide.ffi import create_proxy

                # --- 核心邏輯在這個 Python 函式中 ---
                def add_element_from_input(event):
                    # 從輸入框獲取使用者輸入的文字
                    input_element = js.document.getElementById('text-input')
                    user_text = input_element.value

                    # 如果輸入為空,不新增內容
                    if not user_text:
                        return

                    # 建立新的 <p>
                    new_paragraph = js.document.createElement('p')
                    new_paragraph.textContent = "Python 新增內容:" + user_text

                    # 把新的 <p> 加到 container 中
                    container = js.document.getElementById('container')
                    container.appendChild(new_paragraph)

                    input_element.value = ""  # 清除輸入

                # --- 事件 ---
                button = js.document.getElementById('add-btn')

                # 為 Python 函式建立一個 proxy,並傳入一個 function
                add_element_proxy = create_proxy(add_element_from_input)

                # 當按鈕被點擊時,通過 proxy 執行 Python function
                button.addEventListener('click', add_element_proxy)
            `;
            
            // 5. 執行 Python 程式碼以設定事件監聽器
            await pyodide.runPythonAsync(pythonCode);
        }

        main();
    </script>
</body>
</html>

在這個範例中,Python 代碼使用 js.document 來操作 DOM。特別注意 create_proxy 的使用,當你需要將一個 Python 函數作為回調(如事件監聽器)傳遞給 JavaScript 時,必須使用它來創建一個代理。

執行實例 4

📦 套件管理:內置與 micropip

Pyodide 的強大之處在於其豐富的套件支援。它內置了大量經過優化的科學計算套件,這些套件也同樣被編譯成了 WebAssembly,以確保高效能。

對於那些未被內置的純 Python 套件,Pyodide 提供了 micropip 工具。micropip 可以在運行時從 Python 套件索引 (PyPI) 下載並安裝 wheel 格式的套件。

以下是一些常見套件的分類:

類型套件範例說明
內置套件numpy, pandas, matplotlib, scikit-learn, scipy已預先編譯為 WebAssembly,加載速度快,效能高。
其他套件,透過 micropip 安裝requests, beautifulsoup4, pytest, Pillow, lxml從 PyPI 下載純 Python wheel 檔案並安裝,無法安裝需要 C 編譯的套件。

📥 使用 micropip 安裝套件及下載數據

現在來看看如何安裝外部套件。首先需要用 pyodide.loadPackage() 加載 micropip 本身,然後就可以在 Python 代碼中用它來安裝其他套件了。

範例:安裝 requests 並從 API 獲取數據

requests 在 Pyodide 中會被自動轉換為使用瀏覽器內置的 fetch API,因此所有網絡請求都會受到同源策略(CORS)的限制。

html
<!DOCTYPE html>
<html>
<head>
    <script src="https://cdn.jsdelivr.net/pyodide/v0.26.1/full/pyodide.js"></script>
</head>
<body>
    <h2>使用 micropip 安裝 Requests 套件</h2>
    <pre id="output" style="white-space: pre-wrap; word-wrap: break-word;"></pre>

    <script>
        const output = document.getElementById('output');
        function logToPage(message) {
            output.textContent += message + '\n';
        }

        async function main() {
            try {
                logToPage("正在初始化 Pyodide...");
                let pyodide = await loadPyodide();
                logToPage("Pyodide 初始化完成。");

                pyodide.setStdout({ batched: logToPage });
                pyodide.setStderr({ batched: logToPage });

                logToPage("正在準備安裝套件 (micropip)...");
                await pyodide.loadPackage("micropip"); // 加載 micropip 套件
                logToPage("Micropip 準備完成。");
                
                const pythonCode = `
                    import micropip
                    import json

                    # 1. 用 micropip 安裝套件
                    print("--> 正在安裝 'requests'...")
                    await micropip.install('requests') # 安裝 requests
                    print("--> 'requests' 安裝成功!")

                    # 2. 導入套件
                    import requests

                    print("\\n--> 正在從 API 獲取數據...")
                    url = "https://jsonplaceholder.typicode.com/todos/1"

                    try:
                        response = requests.get(url)
                        response.raise_for_status()
                        
                        data = response.json()
                        pretty_data = json.dumps(data, indent=2, ensure_ascii=False)
                        
                        print("--> 成功獲取數據:")
                        print(pretty_data)

                    except Exception as e:
                        print(f"--> 獲取數據失敗: {e}")
                `;
                
                logToPage("\n--- 開始執行 Python 程式 ---");
                await pyodide.runPythonAsync(pythonCode);
                logToPage("--- Python 程式執行完畢 ---");

            } catch (error) {
                logToPage(`\n*** 發生嚴重錯誤: ${error} ***`);
            }
        }
        main();
    </script>
</body>
</html>

runPythonAsyncrunPython 的異步版本,當 Python 代碼中包含 await(例如 await micropip.install())時,必須使用它。

執行實例 5

📊 將數據加載到 Pandas DataFrame

結合前面的例子,現在將獲取到的數據加載到 Pandas DataFrame 中,並將結果以 HTML 表格的形式呈現在網頁上。

範例:獲取數據並用 Pandas 處理

html
<!DOCTYPE html>
<html>
<head>
    <script src="https://cdn.jsdelivr.net/pyodide/v0.26.1/full/pyodide.js"></script>
</head>
<body>
    <h2>使用 Pandas 處理 API 數據</h2>
    <div id="console-output"></div>
    <h3>Pandas DataFrame (HTML 格式):</h3>
    <div id="pandas-output"></div>

    <script>
        async function main() {
            const consoleOutput = document.getElementById('console-output');
            let pyodide = await loadPyodide();
            
            pyodide.setStdout((text) => { consoleOutput.textContent += text + '\n'; });

            consoleOutput.textContent = "正在加載 pandas 和 micropip...\n";
            await pyodide.loadPackage(['pandas', 'micropip']); // 加載套件
            consoleOutput.textContent += "套件加載完成。\n";
            
            const pythonCode = `
                import micropip
                await micropip.install('requests')
                import requests
                import pandas as pd
                import js

                print("正在從 API 獲取用戶數據...")
                url = "https://jsonplaceholder.typicode.com/users"
                response = requests.get(url)
                data = response.json()

                print("數據獲取成功,正在創建 DataFrame...")
                df = pd.DataFrame(data)

                # 僅選擇部分欄位
                df_subset = df[['id', 'name', 'username', 'email', 'phone']]

                print("DataFrame Head (顯示在 Console):")
                print(df_subset.head())

                # 將 DataFrame 轉換為 HTML 表格
                html_table = df_subset.to_html(classes='table', index=False)

                # 使用 js 模組將 HTML 插入到 DOM 中
                js.document.getElementById('pandas-output').innerHTML = html_table
            `;

            await pyodide.runPythonAsync(pythonCode);
        }
        main();
    </script>
</body>
</html>

在這個例子中,我們:

  1. 使用 pyodide.loadPackage() 一次性加載多個內置套件 (pandas, micropip)。
  2. 在 Python 中安裝 requests 並獲取數據。
  3. 使用 pd.DataFrame() 創建 DataFrame。
  4. 使用 df.to_html() 將 DataFrame 轉換成 HTML 字串。
  5. 透過 js.document.getElementById().innerHTML 將 HTML 表格渲染到網頁上。

執行實例 6

🧩 綜合應用:一個簡單的 Python REPL

最後,讓我們將前面學到的所有知識整合起來,創建一個簡單的、互動式的 Python REPL (Read-Eval-Print Loop) 網頁應用。

Image

功能:

  • 一個文本框用於輸入 Python 代碼。
  • 一個按鈕用於執行代碼。
  • 一個區域用於顯示輸出結果和錯誤。

執行 Pyodide REPL 實例

這個綜合範例展示了 Pyodide 的真正潛力。用戶可以在網頁上自由編寫和執行 Python 代碼,包括使用 pandasnumpy 等強大工具,而所有計算都在用戶的瀏覽器中完成,完全不需後端伺服器。

📚 參考資料

KF Software House