【问题标题】:How to redirect/render Pyodide output in browser?如何在浏览器中重定向/渲染 Pyodide 输出?
【发布时间】:2019-10-28 05:40:57
【问题描述】:

我最近遇到了Pyodide project

我已经使用 Pyodide 构建了一个小演示,但是虽然我花了很多时间查看源代码,但我还不清楚如何从 python 重定向 print 输出(除了修改CPython 源代码),以及如何将输出从 matplotlib.pyplot 重定向到浏览器。

从源代码来看,FigureCanvasWasm 确实有一个 show() 方法和适当的后端用于绘制到浏览器画布 - 但是,我不清楚如何实例化这个类并调用它的 show() 方法或事实上,如果有另一种更明显的方式将绘图重定向到画布。

因此我的问题是:

  1. 如何重定向print() 消息
  2. 如何强制 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 matplotlib emscripten webassembly pyodide


    【解决方案1】:

    首先让我们看看是否可以在浏览器中显示任何内容;例如一个普通的字符串。 Python 变量存储在pyodide.globals 属性中。因此,我们可以从那里获取 python 对象并将其放入页面上的 &lt;div&gt; 元素中。

    <!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,所以这可能会允许上述更自动化的方式。

    【讨论】:

    • +1 这是一个奇妙的开始。这是一些非常好的洞穴探险! ... :) 但是,我正在寻找一个稍微低一些的级别(在幕后 - 就像它一样),这样用户就可以运行“普通”python脚本而不必求助于你的机制受雇。理想情况下,我想捕获 print() 语句和发送到控制台的所有消息。
    • 以下是 codepen 上的每个示例:Example 1, Example 2
    【解决方案2】:

    使用 wasm 后端时,图形的 canvas 属性是 FigureCanvasWasm 的实例。调用画布的show() 方法应该足以在浏览器中显示图形。不幸的是,画布的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')
    

    评论表明非碘代码是一个需要通过覆盖方法来扩展的存根。这需要子类化FigureCanvasWasm,将其安装为pyodide 模块并配置matplotlib 以使用该后端。

    但是有一个快捷方式,因为根据问题394770,python 允许在不修改类的情况下覆盖实例的方法。将以下代码放入您的 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()
    

    最初工具栏没有显示图标。我必须在 pyodide 旁边下载、解压缩和安装fontawesome,并在标题中包含以下行来获取这些

    <link rel="stylesheet" href="font-awesome-4.7.0/css/font-awesome.min.css">
    

    编辑: 关于你问题的第一部分,将输出流重定向到浏览器,你可以看看它是如何在 pyodide 的console.html 中完成的。

    它将 sys.stdout 替换为 StringIO 对象

    pyodide.runPython(`
        import sys
        import io
        sys.stdout = io.StringIO()
    `);
    

    然后运行 ​​python 代码(可以完全忽略它在 wasm 上下文中运行的事实)

    pyodide.runPython(`
        print("Hello, world!")
    `);
    

    最后,将标准输出缓冲区的内容发送到输出元素

    var stdout = pyodide.runPython("sys.stdout.getvalue()")
    var div = document.createElement('div');
    div.innerText = stdout;
    document.body.appendChild(div);
    

    【讨论】:

    • 效果很好,谢谢! pyodide 控制台的链接已损坏
    【解决方案3】:

    我为 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>

    【讨论】:

      【解决方案4】:

      要显示来自 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

      【讨论】:

        猜你喜欢
        • 1970-01-01
        • 1970-01-01
        • 2014-07-15
        • 2013-10-11
        • 2023-03-13
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        相关资源
        最近更新 更多