如何在浏览器中redirect/render Pyodide 输出?
How to redirect/render Pyodide output in browser?
我最近遇到了 Pyodide project。
我已经使用 Pyodide 构建了一个小演示,但是尽管我花了很多时间查看源代码,但我还不清楚如何重定向 print
的输出 python(除了修改 CPython 源代码),以及如何将输出从 matplotlib.pyplot 重定向到浏览器。
从源代码来看,FigureCanvasWasm 确实有一个 show()
方法和用于绘制到浏览器的适当后端 canvas - 但是,我不清楚如何实例化这个 class 并调用它的 show()
方法或者实际上,如果有另一种更明显的方法将绘图重定向到 canvas.
因此我的问题是:
- 如何重定向
print()
条消息
- 如何强制 pyodide 在浏览器中绘制 matplotlib 图形?
这是我的测试页:
<!doctype html>
<meta charset="utf-8">
<html lang="en">
<html>
<head>
<title>Demo</title>
<script src="../../pyodide/build/pyodide.js"></script>
</head>
<body>
</body>
<script type="text/javascript">
languagePluginLoader.then(() => {
pyodide.loadPackage(['matplotlib']).then(() => {
pyodide.runPython(`
import matplotlib.pyplot as plt
plt.plot([1, 2, 3, 4])
plt.ylabel('some numbers')
#fig = plt.gcf()
#fig.savefig(imgdata, format='png')
print('Done from python!')`
);
//var image = pyodide.pyimport('imgdata');
//console.log(image);
});});
</script>
<html>
首先让我们看看是否可以在浏览器中显示任何内容;例如一个普通的字符串。 Python 变量存储在 pyodide.globals
属性中。因此我们可以从那里获取 python 对象并将其放入页面上的 <div>
元素中。
<!doctype html>
<meta charset="utf-8">
<html>
<head>
<title>Demo</title>
<script src="../pyodide/pyodide.js"></script>
</head>
<body>
</body>
<script type="text/javascript">
languagePluginLoader.then(() => {
pyodide.runPython(`my_string = "This is a python string." `);
document.getElementById("textfield").innerText = pyodide.globals.my_string;
});
</script>
<div id="textfield"></div>
<html>
现在我想我们可以用 matplotlib 图做同样的事情。以下将显示文档中保存的 png 图像。
<!doctype html>
<meta charset="utf-8">
<html lang="en">
<html>
<head>
<title>Demo</title>
<script src="../pyodide/pyodide.js"></script>
</head>
<body>
</body>
<script type="text/javascript">
languagePluginLoader.then(() => {
pyodide.loadPackage(['matplotlib']).then(() => {
pyodide.runPython(`
import matplotlib.pyplot as plt
import io, base64
fig, ax = plt.subplots()
ax.plot([1,3,2])
buf = io.BytesIO()
fig.savefig(buf, format='png')
buf.seek(0)
img_str = 'data:image/png;base64,' + base64.b64encode(buf.read()).decode('UTF-8')`
);
document.getElementById("pyplotfigure").src=pyodide.globals.img_str
});});
</script>
<div id="textfield">A matplotlib figure:</div>
<div id="pyplotdiv"><img id="pyplotfigure"/></div>
<html>
我还没有研究过 backends.wasm_backend
,所以这可能允许以更自动化的方式执行上述操作。
当使用 wasm 后端时,图的 canvas 属性 是 FigureCanvasWasm 的实例。调用 canvas 的 show()
方法应该足以在浏览器中显示图形。不幸的是,canvas 的 create_root_element()
方法中的一个小错误阻止了图形的显示。此方法创建一个将包含图形的 div
元素。它首先尝试创建一个碘化物输出 div 元素。如果失败,将创建一个普通的 HTML div 元素。然而,此元素永远不会附加到文档中,因此保持不可见。
下面是 FigureCanvasWasm 中发生的代码行
def create_root_element(self):
# Designed to be overridden by subclasses for use in contexts other
# than iodide.
try:
from js import iodide
return iodide.output.element('div')
except ImportError:
return document.createElement('div')
评论表明 non-iodide 代码是一个存根,需要通过重写该方法进行扩展。这将需要 subclassing FigureCanvasWasm,将其安装为 pyodide 模块并配置 matplotlib 以使用该后端。
但是有一个捷径,因为 python 允许覆盖一个实例的方法,而不修改 class,根据问题 394770。将以下代码放入您的 HTML 文档中会在浏览器中显示一个数字
import numpy as np
from matplotlib import pyplot as plt
from js import document
x = np.linspace(0, 2*np.pi, 100)
y = np.sin(x)
f = plt.figure()
plt.plot(x,y)
# ordinary function to create a div
def create_root_element1(self):
div = document.createElement('div')
document.body.appendChild(div)
return div
#ordinary function to find an existing div
#you'll need to put a div with appropriate id somewhere in the document
def create_root_element2(self):
return document.getElementById('figure1')
#override create_root_element method of canvas by one of the functions above
f.canvas.create_root_element = create_root_element1.__get__(
create_root_element1, f.canvas.__class__)
f.canvas.show()
最初工具栏不显示图标。我必须下载、解压缩并安装 fontawesome 和 pyodide,并在 header 中包含以下行以获取那些
<link rel="stylesheet" href="font-awesome-4.7.0/css/font-awesome.min.css">
编辑:
关于你问题的第一部分,将输出流重定向到浏览器,你可以看看它是如何在 pyodide 的 console.html.
中完成的
它用 StringIO object
替换了 sys.stdout
pyodide.runPython(`
import sys
import io
sys.stdout = io.StringIO()
`);
然后 运行 python 代码(可以完全忽略它在 wasm 上下文中 运行ning 的事实)
pyodide.runPython(`
print("Hello, world!")
`);
最后,将标准输出缓冲区的内容发送到输出元素
var stdout = pyodide.runPython("sys.stdout.getvalue()")
var div = document.createElement('div');
div.innerText = stdout;
document.body.appendChild(div);
我为 Python 创建了一个简单的交互式 shell。如果您需要更多详细信息,请阅读我的 tutorial。
const output = document.getElementById("output")
const code = document.getElementById("code")
code.addEventListener("keydown", function (event) {
if (event.ctrlKey && event.key === "Enter") {
evaluatePython()
}
})
function addToOutput(s) {
output.value += `>>>${code.value}\n${s}\n`
output.scrollTop = output.scrollHeight
code.value=''
}
output.value = 'Initializing...\n'
// init pyodide
languagePluginLoader.then(() => { output.value += 'Ready!\n' })
function evaluatePython() {
pyodide.runPythonAsync(code.value)
.then(output => addToOutput(output))
.catch((err) => { addToOutput(err) })
}
<!DOCTYPE html>
<head>
<script type="text/javascript">
// this variable should be changed if you load pyodide from different source
window.languagePluginUrl = 'https://pyodide-cdn2.iodide.io/v0.15.0/full/';
</script>
<script src="https://pyodide-cdn2.iodide.io/v0.15.0/full/pyodide.js"></script>
</head>
<body>
Output:
</div>
<textarea id='output' style='width: 100%;' rows='10' disabled></textarea>
<textarea id='code' value='' rows='2'></textarea>
<button id='run' onclick='evaluatePython()'>Run</button>
<p>You can execute any Python code. Just enter something in the box above and click the button (or Ctrl+Enter).</p>
<div><a href='https://github.com/karray/truepyxel/demo.html'>Source code</a></div>
</body>
</html>
这里是 matplotlib
的例子。请注意,这将加载一堆依赖项,最多需要几分钟。
let python_code = `
from js import document
import numpy as np
import scipy.stats as stats
import matplotlib.pyplot as plt
import io, base64
def generate_plot_img():
# get values from inputs
mu = int(document.getElementById('mu').value)
sigma = int(document.getElementById('sigma').value)
# generate an interval
x = np.linspace(mu - 3*sigma, mu + 3*sigma, 100)
# calculate PDF for each value in the x given mu and sigma and plot a line
plt.plot(x, stats.norm.pdf(x, mu, sigma))
# create buffer for an image
buf = io.BytesIO()
# copy the plot into the buffer
plt.savefig(buf, format='png')
buf.seek(0)
# encode the image as Base64 string
img_str = 'data:image/png;base64,' + base64.b64encode(buf.read()).decode('UTF-8')
# show the image
img_tag = document.getElementById('fig')
img_tag.src = img_str
buf.close()
`
languagePluginLoader.then(()=>pyodide.runPythonAsync(python_code).then(()=>document.getElementById('status').innerHTML='Done!'))
<!DOCTYPE html>
<head>
<script type="text/javascript">
// this variable should be changed if you load pyodide from different source
window.languagePluginUrl = 'https://pyodide-cdn2.iodide.io/v0.15.0/full/';
</script>
<script src="https://pyodide-cdn2.iodide.io/v0.15.0/full/pyodide.js"></script>
</head>
<body>
Status: <strong id='status'>Initializing...</strong>
<br><br>
mu:
<input id='mu' value='1' type="number">
<br><br>
sigma:
<input id='sigma' value='1' type="number">
<br><br>
<button onclick='pyodide.globals.generate_plot_img()'>Plot</button>
<br>
<img id="fig" />
</body>
</html>
要从 pyodide 显示 print() 调用,您可以使用 loadPyodide 上的参数来重定向标准输出:
var paragraph = document.getElementById("p");
pyodide = await loadPyodide({
indexURL : "https://cdn.jsdelivr.net/pyodide/v0.18.1/full/",
stdin: window.prompt,
stdout: (text) => {paragraph.textContent += text;},
stderr: (text) => {paragraph.textContent += text;}
});
https://github.com/pyodide/pyodide/blob/main/src/js/pyodide.js
我最近遇到了 Pyodide project。
我已经使用 Pyodide 构建了一个小演示,但是尽管我花了很多时间查看源代码,但我还不清楚如何重定向 print
的输出 python(除了修改 CPython 源代码),以及如何将输出从 matplotlib.pyplot 重定向到浏览器。
从源代码来看,FigureCanvasWasm 确实有一个 show()
方法和用于绘制到浏览器的适当后端 canvas - 但是,我不清楚如何实例化这个 class 并调用它的 show()
方法或者实际上,如果有另一种更明显的方法将绘图重定向到 canvas.
因此我的问题是:
- 如何重定向
print()
条消息 - 如何强制 pyodide 在浏览器中绘制 matplotlib 图形?
这是我的测试页:
<!doctype html>
<meta charset="utf-8">
<html lang="en">
<html>
<head>
<title>Demo</title>
<script src="../../pyodide/build/pyodide.js"></script>
</head>
<body>
</body>
<script type="text/javascript">
languagePluginLoader.then(() => {
pyodide.loadPackage(['matplotlib']).then(() => {
pyodide.runPython(`
import matplotlib.pyplot as plt
plt.plot([1, 2, 3, 4])
plt.ylabel('some numbers')
#fig = plt.gcf()
#fig.savefig(imgdata, format='png')
print('Done from python!')`
);
//var image = pyodide.pyimport('imgdata');
//console.log(image);
});});
</script>
<html>
首先让我们看看是否可以在浏览器中显示任何内容;例如一个普通的字符串。 Python 变量存储在 pyodide.globals
属性中。因此我们可以从那里获取 python 对象并将其放入页面上的 <div>
元素中。
<!doctype html>
<meta charset="utf-8">
<html>
<head>
<title>Demo</title>
<script src="../pyodide/pyodide.js"></script>
</head>
<body>
</body>
<script type="text/javascript">
languagePluginLoader.then(() => {
pyodide.runPython(`my_string = "This is a python string." `);
document.getElementById("textfield").innerText = pyodide.globals.my_string;
});
</script>
<div id="textfield"></div>
<html>
现在我想我们可以用 matplotlib 图做同样的事情。以下将显示文档中保存的 png 图像。
<!doctype html>
<meta charset="utf-8">
<html lang="en">
<html>
<head>
<title>Demo</title>
<script src="../pyodide/pyodide.js"></script>
</head>
<body>
</body>
<script type="text/javascript">
languagePluginLoader.then(() => {
pyodide.loadPackage(['matplotlib']).then(() => {
pyodide.runPython(`
import matplotlib.pyplot as plt
import io, base64
fig, ax = plt.subplots()
ax.plot([1,3,2])
buf = io.BytesIO()
fig.savefig(buf, format='png')
buf.seek(0)
img_str = 'data:image/png;base64,' + base64.b64encode(buf.read()).decode('UTF-8')`
);
document.getElementById("pyplotfigure").src=pyodide.globals.img_str
});});
</script>
<div id="textfield">A matplotlib figure:</div>
<div id="pyplotdiv"><img id="pyplotfigure"/></div>
<html>
我还没有研究过 backends.wasm_backend
,所以这可能允许以更自动化的方式执行上述操作。
当使用 wasm 后端时,图的 canvas 属性 是 FigureCanvasWasm 的实例。调用 canvas 的 show()
方法应该足以在浏览器中显示图形。不幸的是,canvas 的 create_root_element()
方法中的一个小错误阻止了图形的显示。此方法创建一个将包含图形的 div
元素。它首先尝试创建一个碘化物输出 div 元素。如果失败,将创建一个普通的 HTML div 元素。然而,此元素永远不会附加到文档中,因此保持不可见。
下面是 FigureCanvasWasm 中发生的代码行
def create_root_element(self):
# Designed to be overridden by subclasses for use in contexts other
# than iodide.
try:
from js import iodide
return iodide.output.element('div')
except ImportError:
return document.createElement('div')
评论表明 non-iodide 代码是一个存根,需要通过重写该方法进行扩展。这将需要 subclassing FigureCanvasWasm,将其安装为 pyodide 模块并配置 matplotlib 以使用该后端。
但是有一个捷径,因为 python 允许覆盖一个实例的方法,而不修改 class,根据问题 394770。将以下代码放入您的 HTML 文档中会在浏览器中显示一个数字
import numpy as np
from matplotlib import pyplot as plt
from js import document
x = np.linspace(0, 2*np.pi, 100)
y = np.sin(x)
f = plt.figure()
plt.plot(x,y)
# ordinary function to create a div
def create_root_element1(self):
div = document.createElement('div')
document.body.appendChild(div)
return div
#ordinary function to find an existing div
#you'll need to put a div with appropriate id somewhere in the document
def create_root_element2(self):
return document.getElementById('figure1')
#override create_root_element method of canvas by one of the functions above
f.canvas.create_root_element = create_root_element1.__get__(
create_root_element1, f.canvas.__class__)
f.canvas.show()
最初工具栏不显示图标。我必须下载、解压缩并安装 fontawesome 和 pyodide,并在 header 中包含以下行以获取那些
<link rel="stylesheet" href="font-awesome-4.7.0/css/font-awesome.min.css">
编辑: 关于你问题的第一部分,将输出流重定向到浏览器,你可以看看它是如何在 pyodide 的 console.html.
中完成的它用 StringIO object
替换了 sys.stdoutpyodide.runPython(`
import sys
import io
sys.stdout = io.StringIO()
`);
然后 运行 python 代码(可以完全忽略它在 wasm 上下文中 运行ning 的事实)
pyodide.runPython(`
print("Hello, world!")
`);
最后,将标准输出缓冲区的内容发送到输出元素
var stdout = pyodide.runPython("sys.stdout.getvalue()")
var div = document.createElement('div');
div.innerText = stdout;
document.body.appendChild(div);
我为 Python 创建了一个简单的交互式 shell。如果您需要更多详细信息,请阅读我的 tutorial。
const output = document.getElementById("output")
const code = document.getElementById("code")
code.addEventListener("keydown", function (event) {
if (event.ctrlKey && event.key === "Enter") {
evaluatePython()
}
})
function addToOutput(s) {
output.value += `>>>${code.value}\n${s}\n`
output.scrollTop = output.scrollHeight
code.value=''
}
output.value = 'Initializing...\n'
// init pyodide
languagePluginLoader.then(() => { output.value += 'Ready!\n' })
function evaluatePython() {
pyodide.runPythonAsync(code.value)
.then(output => addToOutput(output))
.catch((err) => { addToOutput(err) })
}
<!DOCTYPE html>
<head>
<script type="text/javascript">
// this variable should be changed if you load pyodide from different source
window.languagePluginUrl = 'https://pyodide-cdn2.iodide.io/v0.15.0/full/';
</script>
<script src="https://pyodide-cdn2.iodide.io/v0.15.0/full/pyodide.js"></script>
</head>
<body>
Output:
</div>
<textarea id='output' style='width: 100%;' rows='10' disabled></textarea>
<textarea id='code' value='' rows='2'></textarea>
<button id='run' onclick='evaluatePython()'>Run</button>
<p>You can execute any Python code. Just enter something in the box above and click the button (or Ctrl+Enter).</p>
<div><a href='https://github.com/karray/truepyxel/demo.html'>Source code</a></div>
</body>
</html>
这里是 matplotlib
的例子。请注意,这将加载一堆依赖项,最多需要几分钟。
let python_code = `
from js import document
import numpy as np
import scipy.stats as stats
import matplotlib.pyplot as plt
import io, base64
def generate_plot_img():
# get values from inputs
mu = int(document.getElementById('mu').value)
sigma = int(document.getElementById('sigma').value)
# generate an interval
x = np.linspace(mu - 3*sigma, mu + 3*sigma, 100)
# calculate PDF for each value in the x given mu and sigma and plot a line
plt.plot(x, stats.norm.pdf(x, mu, sigma))
# create buffer for an image
buf = io.BytesIO()
# copy the plot into the buffer
plt.savefig(buf, format='png')
buf.seek(0)
# encode the image as Base64 string
img_str = 'data:image/png;base64,' + base64.b64encode(buf.read()).decode('UTF-8')
# show the image
img_tag = document.getElementById('fig')
img_tag.src = img_str
buf.close()
`
languagePluginLoader.then(()=>pyodide.runPythonAsync(python_code).then(()=>document.getElementById('status').innerHTML='Done!'))
<!DOCTYPE html>
<head>
<script type="text/javascript">
// this variable should be changed if you load pyodide from different source
window.languagePluginUrl = 'https://pyodide-cdn2.iodide.io/v0.15.0/full/';
</script>
<script src="https://pyodide-cdn2.iodide.io/v0.15.0/full/pyodide.js"></script>
</head>
<body>
Status: <strong id='status'>Initializing...</strong>
<br><br>
mu:
<input id='mu' value='1' type="number">
<br><br>
sigma:
<input id='sigma' value='1' type="number">
<br><br>
<button onclick='pyodide.globals.generate_plot_img()'>Plot</button>
<br>
<img id="fig" />
</body>
</html>
要从 pyodide 显示 print() 调用,您可以使用 loadPyodide 上的参数来重定向标准输出:
var paragraph = document.getElementById("p");
pyodide = await loadPyodide({
indexURL : "https://cdn.jsdelivr.net/pyodide/v0.18.1/full/",
stdin: window.prompt,
stdout: (text) => {paragraph.textContent += text;},
stderr: (text) => {paragraph.textContent += text;}
});
https://github.com/pyodide/pyodide/blob/main/src/js/pyodide.js