首页 > 解决方案 > 无法让 Jupyter 笔记本访问 javascript 变量

问题描述

我有一个做文件格式转换的网站,这个服务可以通过window.postMessage系统被其他网站使用。如果一个人也可以通过 Jupyter 笔记本做到这一点,那就太好了。然而,我遇到了一个严重的问题。我可以让 python 创建一个 Javascript 命令,将格式 A 的文件发送到网站,我可以在延迟一段时间后访问 Javascript 中的响应(格式 B 的文件),但我无法将 Javascript 的响应返回到 Python 中进一步处理。我制作了一个尽可能简单的笔记本来演示这个问题。原因似乎是 Python 首先执行所有单元格(在 Run all 场景中),然后才查看任何新的执行请求。

stackoverflow 上有类似的问题,例如: IPython Notebook Javascript: retrieve content from JavaScript variables。从我目前发现的情况来看,这个问题没有解决办法。我觉得这非常令人失望,因为创建可从 Jupyter 笔记本直接访问的 Web 服务会非常酷,而不必走 node.js 路线。

import asyncio, time
from IPython.display import display, Javascript

下面的代码将 Javascript 插入页面: 'sendRequest' 函数只是延迟 5 秒,但它旨在通过 window.postMessage 与另一个网页通信。

futureResponse 是一个 asyncio Future (python)。当 sendRequest (Javascript) 完成并使用 kernel.execute (Javascript) 调用 responseHandler (python) 时,它会得到一个结果。

global futureResponse

def responseHandler(response):
  futureResponse.set_result(response)

display(Javascript("""
  window.responseHandler = (response) => {
    var kernel = IPython.notebook.kernel;
    var pyCommand = 'responseHandler(\\''+response+'\\')';
    kernel.execute(pyCommand);
    console.log('responseHandler called')
  };
  window.sendRequest = () => {
    return new Promise( (resolve,reject) => { setTimeout(()=>resolve('RESPONSE'),5000) } )
  }
"""))

下面的代码插入了调用 sendRequest 的 Javascript。这将在 setTimeout 延迟(5 秒)之后调用 responseHandler(Javascript),它调用 kernel.execute 调用 responseHandler(python),它设置 futureResponse 的结果。但是,如果我使用“等待 futureResponse”,它永远不会实现。

# the value of futureResponse will be set by the responseHandler
futureResponse = asyncio.Future()

display(Javascript("""
window.sendRequest().then(
  (response) => window.responseHandler(response)
)
"""))

# This does not work (futureResponse unresolved):
time.sleep(6)

# With await, the notebook keeps waiting forever.
#await futureResponse

print(futureResponse)

如果在前一个单元格之后立即评估下面的单元格(如在 Run all 场景中),则 futureResponse 仍然未解决。但如果在 5 秒后评估,futureResponse 的值为“RESPONSE”。

futureResponse

标签: javascriptpythonjupyter-notebookpython-asyncio

解决方案


我对此进行了进一步研究并找到了一个解决方案,但它确实是一个可能会在 Jupyter notebook 的未来版本中破坏的 hack。我希望有人有一个更面向未来的答案。

这个想法是为了避免代码示例使用 Jupyter 消息系统的 stdin 通道创建的 catch-22 ( https://jupyter-client.readthedocs.io/en/stable/messaging.html )。

代码的工作方式如下:当代码单元包含input('prompt-text')命令时,将通过专用的标准输入通道将其发送到笔记本客户端,该通道独立于正常的单元执行流。因此,我“破解”了笔记本客户端的代码,以捕获内核发送的输入请求,并在不提示用户的情况下提供答案。

display(Javascript("""
  const CodeCell = window.IPython.CodeCell;

  CodeCell.prototype.native_handle_input_request = CodeCell.prototype.native_handle_input_request || CodeCell.prototype._handle_input_request
  CodeCell.prototype._handle_input_request = function(msg) {
    try {
      // only apply the hack if the command is valid JSON
      const command = JSON.parse(msg.content.prompt);
      const kernel = IPython.notebook.kernel;
      // in the future: send the command to a server and wait for a response.
      // for now: specify a 5 second delay and return 'RESPONSE'
      setTimeout(() => { kernel.send_input_reply('RESPONSE') },5000)
    } catch(err) {
      console.log('Not a command',msg,err);
      this.native_handle_input_request(msg);
    }
  }
"""))
response = input(json.dumps({"do":"something"}))
print(response)

这会阻止执行 5 秒,然后打印“响应”并继续。


推荐阅读