在瀏覽器運行可交互Python代碼

本文同步在我的博客shymean.com上,歡迎關注前端

最近研究了一些在線運行代碼應用,感受頗爲有趣,在此稍做總結,並嘗試實現一種在瀏覽器運行可交互Python代碼的方案。node

所謂「可交互Python代碼」,指的是python中input等接受標準輸入數據的APIpython

下面列舉了一些在線編輯器,能夠體驗一番git

將Python轉換成JavaScript代碼

因爲Python也是解釋型代碼,所以能夠經過解析AST的方式,經過JavaScript運行Python代碼,常見的庫有github

  • brython,是一個Python在瀏覽器中運行的實現
  • skulpt,同上

相關API使用在文檔中均有說明,本文再也不贅述。web

因爲瀏覽器的限制,上面的這些庫會缺乏一些功能如文件操做等;此外如input方法,會經過window.prompt進行mock。shell

所以,直接在瀏覽器運行python存在一些問題npm

  • 須要引入額外的py to js庫文件,且這些庫或多或少缺乏部分API的支持,不能100%還原python代碼運行
  • 因爲最後是運行JavaScript代碼,代碼運行錯誤須要轉換才行

服務端沙盒運行python

另外的一種方案是:在服務端啓動一個代碼執行環境,經過網絡提交python代碼,而後將結果返回給前端。segmentfault

基礎方案

經過shelljs,咱們能夠在NodeJS中運行腳本命令瀏覽器

let shell = require('shelljs')
// 若是code是經過http傳輸的,就能夠直接在服務端環境運行python代碼
let code = `print('hello world')`
let res = shell.exec(`python3 -c "${code}"`)
// 經過res.stdout將輸出返回給瀏覽器
複製代碼

這種方式看起來比較簡單,甚至不須要引入額外的庫文件,只需一個提供python運行環境的服務器便可。在實現中碰見的一個問題是:如何解決python中input的問題?

爲了解決這個問題,咱們先來了解一下標準輸入和標準輸出的知識

標準輸入

下面是nodejs標準輸入示例代碼,須要瞭解process.stdinprocess.stdout模塊

process.stdin.resume();
process.stdin.setEncoding('utf-8');

var arr = [];
process.stdin.on('data', function (data) {
    var number = data.slice(0, -1);
    if (number == 'end') {
        process.stdin.emit('end');
    } else {
        arr.push(number);
    }
});
process.stdin.on('end', function () {
    console.log(arr);
});

// process.stdin.emit('data', '1 ') // 向標準輸入寫入
// process.stdin.emit('data', 'end ')

複製代碼

操做標準輸入的例子

讀取外界輸入

在nodejs中能夠直接使用process.argv獲取命令行參數。在線刷題時,須要從控制檯讀取輸入,可使用readline模塊,參考Nodejs 按行讀取控制檯輸入(stdin)的幾種方法

腳本自動登陸

參考:nodejs中如何知道子進程正在等待輸入,並向其輸入數據

假設一個命令行工具login須要經過交互的方式依次輸入username、password,如何編寫一個自動化腳本auto-login將參數直接傳遞給login呢?

本來的登陸工具login

// login.js
const readline = require('readline')

function createInput(msg) {
  const rl = readline.createInterface({
    input: process.stdin,
    output: process.stdout,
  })
  return new Promise(function(resolve, reject) {
    rl.question(`請輸入${msg}: `, data => {
      rl.close()
      resolve(data)
    })
  })
}

Promise.resolve()
  .then(() => {
    return createInput('用戶名').then(username => ({ username }))
  })
  .then(userInfo => {
    return createInput('密碼').then(password => ({
      ...userInfo,
      password,
    }))
  })
  .then(userInfo => {
    return createInput('郵箱').then(email => ({
      ...userInfo,
      email,
    }))
  })
  .then(userInfo => {
    console.log(userInfo)
    process.exit(0)
  })
複製代碼

自動登陸auto-login,主要實現是獲取子進程subProcess的實例,而後經過subProcess.stdin.write的方式傳回數據

const fs = require('fs');

const { spawn } = require('child_process');

var subProcess = spawn('node', ['login.js'], { cmd: __dirname });
subProcess.on('error', function() {
    console.log('error');
    console.log(arguments);
});
subProcess.on('close', code => {
    if (code != 0) {
        console.log(`子進程退出碼:${code}`);
    } else {
        console.log('登陸成功');
    }
    process.stdin.end();
});
subProcess.stdin.on('end', () => {
    process.stdout.write('end');
});

let getNextInput = (()=>{
    let cursor = 0
    var config = {username:'txm',password:'123',email:'xx@123.com'}
    let keys = Object.keys(config)

    return ()=>{
        return config[keys[cursor++]]
    }
})()

subProcess.stdout.on('data', onData);
subProcess.stderr.on('data', onData);
function onData(data) {
    process.stdout.write('# ' + data);
    let answer = getNextInput()
    subProcess.stdin.write(answer + '\n');
    
    // 若是須要手動輸入,則能夠將父進程的輸入重定向到子進程
    // process.stdin.on('data', input => {
    // input = input.toString().trim();
    // subProcess.stdin.write(input + '\n');
    // });
}
複製代碼

解決input的問題

上面login的node腳本可使用python編寫,大體以下

# 一個展現命令行交互的代碼
username = input('input username:')
password = input('input password:')
email = input('input email:')

print('username:%s, password: %s, email:%s' % (username, password, email))

# exit(0)
# 忽然想起了「人生苦短,我用python」這句話
複製代碼

片頭的問題能夠修改成:如何經過其餘程序,向一個等待標準輸入的python程序寫入數據?

想象一下整個流程

  • 在瀏覽器編寫python代碼
  • 經過http協議發送到服務端,服務端運行python腳本,執行到input時,等到輸入
  • 服務端通知瀏覽器,提示用戶輸入,並將輸入傳回服務端
  • 服務端將用戶輸入透傳給給正在等待輸入的python程序,程序繼續執行,如此往復
  • python程序執行完畢,服務端將輸出響應返回瀏覽器,用戶看見執行結果,運行完畢

能夠對於整個過程,存在瀏覽器和服務端的屢次通訊,能夠想到使用websocket來進行實現,在父子進程通訊的各個時機進行socket消息的發送。

服務端代碼實現

// server
socket.on("disconnect", function() {
    console.log("user disconnected");
});

// 運行傳回的代碼
let subProcess;
socket.on("run code", function(msg) {
    subProcess = runPython(socket, msg);
});

socket.on("code input", function(msg) {
    subProcess.stdin.write(msg + "\n");
});

function runPython(socket, code) {
    let fileName = "tmp.py"; // 能夠換成隨機文件名避免重複
    fs.writeFileSync(fileName, code, "utf8");

    let subProcess = spawn("python3", [fileName], { cmd: __dirname });

    let isClose = false;
    // 監聽子進程是否運行完畢
    subProcess.on("close", code => {
        isClose = true;
        console.log(code === 0 ? "登陸成功" : `子進程退出碼:${code}`);
        subProcess.stdout.off("data", onData);
        subProcess.stderr.off("data", onData);
    });

    subProcess.stdout.on("data", onData);
    subProcess.stderr.on("data", onData);

    process.stdin.on("data", input => {
        input = input.toString().trim();
        if (!isClose) {
            subProcess.stdin.write(input + "\n");
        }
    });

    function onData(data) {
        setTimeout(() => {
            if (isClose) {
                socket.emit("code response", data.toString());
            } else {
                socket.emit("stdout", data.toString());
            }
        }, 20);
    }
    return subProcess;
}
複製代碼

客戶端代碼實現

let socket = io();

function createStdout(msg) {
    let li = document.createElement("li");
    li.innerHTML = `<li> >>> ${msg}:<input class="stdin"/></li>`;
    list.appendChild(li);
}
function createResponse(msg) {
    let li = document.createElement("li");
    li.innerHTML = `<li> >>> ${msg}`;
    list.appendChild(li);
}
// 註冊響應
socket.on("stdout", function(msg) {
    createStdout(msg);
});
socket.on("code response", function(msg) {
    createResponse(msg);
});

// 註冊事件
btn.onclick = function send() {
    let code = content.value;
    socket.emit("run code", code);
};

function codeInput(e) {
    let target = e.target;
    let val = target.value;
    val && socket.emit("code input", val);
}

list.onclick = function(e) {
    let target = e.target;
    if (target.classList.contains("stdin")) {
        target.removeEventListener("blur", codeInput);
        target.addEventListener("blur", codeInput);
    }
};
複製代碼

至此,就實現了一個可交互的在線python運行工具,完整代碼已放在github上了。

小結

本文主要實現了一種在瀏覽器運行可交互python代碼的方案,主要原理是藉助服務器環境運行代碼,並經過websocket傳遞標準輸入與標準輸入。

此外還存在一些未解決的問題

  • 代碼注入帶來的安全問題,因爲代碼實際是在真實服務環境下運行,咱們必須考慮相關的權限和安全問題
  • 進程新建、切換帶來的性能問題,以及用戶長時間不輸入時致使進程一直沒法退出等場景

接下來會研究使用Docker構建運行沙盒來解決上述問題,後會有期。

相關文章
相關標籤/搜索