大纲

本文关心代码在jupyter notebook里从前端传往后端的过程,并试图获取到钩子,因为我最近项目的缘故(可以参考我之前的文章),分析完通信过程,我将演示如何扩展和hack jupyter notebook

在接下来动手实践部分将演示如何将其以iframe嵌入到外部网页网页中(在概念上是嵌入一种资源)

实验环境

  • ubuntu 14.04
  • python 2.7.6
  • jupyter 4.3.0

安装jupyter(notebook)

1
2
3
virtualenv env
. env/bin/activate
pip install jupyter #  安装完后是4.3

运行

jupyter notebook --no-browser --port 5000 --ip=0.0.0.0

我将其跑在5000端口,并接收所有ip的请求

关于启动参数,不熟悉的同学可以参考我之前文章

值得注意的是第一次手动登陆,需要输入token,如果你没加入–no-brower,会自动打开http://127.0.0.1:5000/?token=d311b2834ac7337157c54aaba8d9a524ce48f7597c91xxxx,验证一次之后,浏览器就有cookie了,之后只需要127.0.0.1:5000就可访问

分析

从websocket入手

打开一个新的notebook: http://127.0.0.1:5000/notebooks/Untitled1.ipynb

从chrome调试面板的Network可以看到,有个websocket:ws://127.0.0.1:5000/api/kernels/d13a50b0-6baa-4d5e-8564-95f224daxxxx/channels?session_id=F552491A7C0448A2B5567DE1A71Cxxxx,代码经由它往后端发送,也经由它接收后台返回的信息

当我们运行上头的print(“hello world”)时,往后台发送如下数据

1
{"header":{"msg_id":"29A5EFC0B11848BE97B66D6E947AEB71","username":"username","session":"7C49DDC342FD43AA8F5624B08CD7BDAB","msg_type":"execute_request","version":"5.0"},"metadata":{},"content":{"code":"print(\"hello world\")","silent":false,"store_history":true,"user_expressions":{},"allow_stdin":true,"stop_on_error":true},"buffers":[],"parent_header":{},"channel":"shell"}

接下来有5个frames

其中包含了代码执行的结果

那么我们只要模拟建立这样的websocket,就拿到所要的钩子了,可以自如地运行代码

从页面入手

除了建立websocket,我们也可以找找js中钩子

这个问题在stack overflow里找到解答,方法如下

1
2
3
4
5
6
7
8
var handle_output = function (data) {console.log(data);}

var callbacks = {
            iopub : {output : handle_output,}
}

var kernel = IPython.notebook.kernel;
kernel.execute("print('hello')",callbacks)

如此一来我们找到了第二种钩子

消息是用websocket传输的,你可以试试实时性:kernel.execute("for i in range(5):import time;time.sleep(1);print('hello')",callbacks)

嵌入到外部页面中

接下来的部分,演示如何将jupyter notebook嵌入到外部页面里,如此一来可以利用jupyter强大的特性做很多好玩的东西,诸如各种语言的线上IDE

首先建立一个前端页面(my_test.html),然后以iframe的方式引入jupyter notebook(关于iframe的属性可以参考这里),接下来在外部网页中与其互操作

从建立网页开始

1
2
3
4
5
6
7
8
9
<html>
<title>wwj</title>
<body>
<iframe width="600" height="400" id="notebook" src="http://127.0.0.1:5000/notebooks/Untitled1.ipynb?token=c81b15a38f4fdfebe67cab0400b9feb0e60325f35bafxxxx" ></iframe>
    <script>
    console.log("wwjtest")
    </script>
</body>
</html>

噢报错了,这个问题这里有描述:How do I embed an Ipython Notebook in an iframe,解决方案这个比较靠谱:Can’t use Notebook inside an iframe

解决方案是生成配置文件:jupyter notebook --generate-config,做些配置。往~/.jupyter/jupyter_notebook_config.py加入c.NotebookApp.tornado_settings = { 'headers': { 'Content-Security-Policy': "frame-ancestors 'self' *" } }

现在没有问题啦 (注意不要直接打开my_test.html,使用网络访问它(python -m SimpleHTTPServer))

采用postMessage来传递消息

因为jupyter与外部页面可能存在跨域问题(别怕,最复杂的情况,也就是跨域了),我打算采用HTML5 postMessage来处理这个问题

postMessage()方法允许来自不同源的脚本采用异步方式进行有限的通信,可以实现跨文本档、多窗口、跨域消息传递。

往iframe里发送消息

1
2
3
var notebook = document.getElementById('notebook');
code="print('hello')";
notebook.contentWindow.postMessage(code,"*") ;

扩展jupyter notebook

为了在jupyter notebook中监听到外部网页发过来的消息,并给予响应,我们需要为其写js扩展,哈哈别紧张,jupyter设计得很漂亮,扩展它很简单(可能需要安装:pip install widgetsnbextension)

进入~/.ipython/nbextensions,

创建我们的扩展(iframe_extension.js):

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
define(function(){
      function _on_load(){
            console.info('iframe extension running!')
            window.addEventListener('message', function(event){
            console.log("the iframe get:"+event.data);
            window.parent.postMessage("data from  iframe extension",'*');
      })
      }
    
    return {load_ipython_extension: _on_load };
})

在外部网页等待iframe的消息

1
2
3
4
window.addEventListener('message',function(e){
    var data=e.data;
    console.log("response from notebook:"+data);
},false);

大功告成!