Jupyter5-学习指南-二-

68 阅读12分钟

Jupyter5 学习指南(二)

原文:annas-archive.org/md5/9a12d92ec3c259b9feace22b7716b78f

译者:飞龙

协议:CC BY-NC-SA 4.0

第六章:Jupyter JavaScript 编程

JavaScript 是一种高级、动态、无类型并且解释型的编程语言。许多基于 JavaScript 的衍生语言应运而生。对于 Jupyter 来说,底层的 JavaScript 实际上是 Node.js。Node.js 是一个基于事件的框架,使用 JavaScript,可以用于开发大型可扩展的应用程序。需要注意的是,这与本书前面介绍的主要用于数据分析的语言不同(虽然 Python 也是一种通用语言,但它有着明确的数据分析能力)。

在本章中,我们将涵盖以下主题:

  • 向 Jupyter 添加 JavaScript 包

  • JavaScript Jupyter Notebook

  • Jupyter 中的基本 JavaScript

  • Node.js d3

  • Node.js stats-analysis

  • Node.js JSON 处理

  • Node.js canvas

  • Node.js plotly

  • Node.js 异步线程

  • Node.js decision-tree

向你的安装中添加 JavaScript 脚本

在本节中,我们将安装 macOS 和 Windows 上的 JavaScript 脚本。对于每个环境,Jupyter 安装中启用 JavaScript 脚本的步骤是不同的。macOS 的安装非常简洁,而 Windows 的安装仍然在变化中,我预计以下说明将随着时间变化。

在 macOS 或 Windows 上向 Jupyter 添加 JavaScript 脚本

我按照 github.com/n-riesco/iJavaScript 中的说明加载了 Anaconda 的 JavaScript 引擎。步骤如下:

conda install nodejs 
npm install -g iJavaScript 
ijsinstall 

此时,启动 Jupyter 时会提供 JavaScript(Node.js)引擎作为选择,如下截图所示:

JavaScript Hello World Jupyter Notebook

安装完成后,我们可以通过点击“New”菜单并选择 JavaScript 来尝试第一个 JavaScript Notebook。我们将把 Notebook 命名为 Hello, World! 并在脚本中输入以下行:

var msg = "Hello, World!" 
console.log(msg) 

这个脚本设置了一个变量并显示该变量的内容。输入脚本并运行(单击“Cell | Run All”),我们将看到如下截图所示的 Notebook 界面:

我们应该指出这一页的一些亮点:

  • 我们在右上角看到熟悉的语言标识,表示正在使用的脚本类型

  • Notebook 中的每一行都有输出

  • 更重要的是,我们可以看到 Notebook 的真实输出(如第一个行所示),其中字符串被回显

  • 否则,Notebook 看起来和我们见过的其他类型一样熟悉

如果查看磁盘上的 Notebook 内容,我们也能看到类似的结果:

{ 
  "cells": [ 
    <<same format as seen earlier for the cells>> 
  ], 
  "metadata": { 
    "kernelspec": { 
      "display_name": "JavaScript (Node.js)", 
      "language": "JavaScript", 
      "name": "JavaScript" 
    }, 
    "language_info": { 
      "file_extension": ".js", 
      "mimetype": "application/JavaScript", 
      "name": "JavaScript", 
      "version": "8.9.3" 
    } 
  }, 
  "nbformat": 4, 
  "nbformat_minor": 2 
} 

因此,通过使用相同的 Notebook 和 JSON 文件格式,Jupyter 通过适当修改 metadatalanguage_info 值,提供了不同的语言供 Notebook 使用。

向 Jupyter 添加 JavaScript 包

JavaScript 语言通常不会安装额外的包,它通过程序中的运行时包含指令来引用其他包。其他包可以通过网络引用,也可以本地复制到你的环境中。假设通过 CDN 访问库是一种更高效、更快速的机制。

然而,Node.js 为 JavaScript 语法添加了所需的动词。在这个例子中,你的代码需要加载另一个模块,假设它已经安装在你当前的环境中。要安装另一个模块,可以使用 npm,例如,在以下命令中:

npm install name-of-module 

这将安装被引用的模块(包括所需的嵌入式包)到你的计算机上,以便 require 语句可以正常工作。

Jupyter 中的基础 JavaScript

JavaScript,甚至 Node.js,通常并不以数据处理著称,而是以应用(网站)开发为主。这一点与我们之前讨论的语言有所不同。然而,本章的例子将重点介绍如何使用 JavaScript 进行应用开发,并结合数据访问和分析功能。

Jupyter 中的 JavaScript 限制

JavaScript 最初是专门为了在 HTML 页面中进行脚本编写而设计的,通常是在客户端(浏览器中)。因此,它被构建为能够操作页面上的 HTML 元素。为进一步扩展这一功能,尤其是使用 Node.js 等扩展,已经开发了多个包,甚至可以用来创建 Web 服务器。

在 Jupyter 中使用任何 HTML 操作和生成特性时都会遇到障碍,因为 Jupyter 期望控制用户的呈现方式。

Node.js d3 包

d3 包具有数据访问功能。在这个例子中,我们将从一个制表符分隔的文件中读取数据并计算平均值。请注意使用了变量名以 _ 开头来表示 lodash。以下划线开头的变量名通常被认为是私有的。然而,在这个例子中,它仅仅是我们使用的包名的变体,即 lodash,或者叫做 underscore。lodash 也是一个广泛使用的 utility 包。

为了执行这个脚本,我做了如下操作:

  • 安装 d3

  • 安装 lodash

  • 安装 isomorphic-fetchnpm install --save isomorphic-fetch es6-promise

  • 导入 isomorphic-fetch

我们将使用的脚本如下:

var fs = require("fs");
var d3 = require("d3");
var _ = require("lodash");
var _ = require("isomorphic-fetch");

//read and parse the animals file
console.log("Animal\tWeight");
d3.csv("http://www.dantoomeysoftware.com/data/animals.csv", function(data) {
    console.log(data.name + '\t' + data.avg_weight);
});

这假设我们已经使用 npm 加载了 fsd3 包,如前面的脚本所述。

对于这个例子,我在我的网站上创建了一个 data 目录,因为我们输入的 URL 被期望是一个绝对 URL,并在该目录下创建了一个 CSV 文件(animal.csv):

Name,avg_weight 
Lion,400 
Tiger,400 
Human,150 
Elephant,2000 

如果我们将这个脚本加载到一个 Notebook 中并运行,它将输出以下内容,正如预期:

需要注意的是,d3 函数(实际上有很多)是异步操作的。在我们的例子中,我们只是打印文件的每一行。你可以想象更复杂的功能。

Node.js 数据分析包

stats-analysis 包包含了你可能需要对数据执行的许多常见统计操作。你需要使用 npm 安装这个包,正如之前所解释的那样。

如果我们有一小组人的体温数据可以使用,我们可以通过使用这个脚本快速获得数据的一些统计信息:

const stats = require("stats-analysis"); 

var arr = [98, 98.6, 98.4, 98.8, 200, 120, 98.5]; 

//standard deviation 
var my_stddev = stats.stdev(arr).toFixed(2); 

//mean 
var my_mean = stats.mean(arr).toFixed(2); 

//median 
var my_median = stats.median(arr); 

//median absolute deviation 
var my_mad = stats.MAD(arr); 

// Get the index locations of the outliers in the data set 
var my_outliers = stats.indexOfOutliers(arr); 

// Remove the outliers 
var my_without_outliers = stats.filterOutliers(arr); 

//display our stats 
console.log("Raw data is ", arr); 
console.log("Standard Deviation is ", my_stddev); 
console.log("Mean is ", my_mean); 
console.log("Median is ", my_median); 
console.log("Median Abs Deviation is " + my_mad); 
console.log("The outliers of the data set are ", my_outliers); 
console.log("The data set without outliers is ", my_without_outliers); 

当这个脚本输入到 Notebook 中时,我们会得到类似于下图所示的内容:

执行时,我们得到如下截图所示的结果:

有趣的是,98.5 被认为是一个异常值。我猜测命令中有一个可选参数可以改变使用的限制。否则,结果与预期一致。

异常值来自于将原始数据当作纯数学项处理。所以,技术上来说,从提供的数据中,我们已经识别出异常值。然而,我们可能会使用不同的方法来确定异常值,了解人类体温的领域平均值。

Node.js JSON 处理

在这个示例中,我们将加载一个 JSON 数据集,并对数据进行一些标准的操作。我引用了来自 www.carqueryapi.com/api/0.3/?callback=?&cmd=getModels&make=ford 的 FORD 模型列表。我不能直接引用这个,因为它不是一个扁平化文件,而是一个 API 调用。因此,我将数据下载到一个名为 fords.json 的本地文件中。此外,API 调用的输出会将 JSON 包裹成这样:?(json);。在解析之前,这部分需要被移除。

我们将使用的脚本如下。在脚本中,JSON 是 Node.js 的内建包,因此我们可以直接引用这个包。JSON 包提供了处理 JSON 文件和对象所需的许多标准工具。

这里值得关注的是 JSON 文件读取器,它构建了一个标准的 JavaScript 对象数组。每个对象的属性可以通过 name 进行引用,例如 model.model_name

//load the JSON dataset 
//http://www.carqueryapi.com/api/0.3/?callback=?&cmd=getModels&make=ford 
var fords = require('/Users/dtoomey/fords.json'); 

//display how many Ford models are in our data set 
console.log("There are " + fords.Models.length + " Ford models in the data set"); 

//loop over the set 
var index = 1 
for(var i=0; i<fords.Models.length; i++) { 

    //get this model 
    var model = fords.Models[i]; 

    //pull it's name 
    var name = model.model_name; 

    //if the model name does not have numerics in it 
    if(! name.match(/[0-9]/i)) { 
        //display the model name 
        console.log("Model " + index + " is a " + name); 
        index++; 
    } 

    //only display the first 5 
    if (index>5) break; 
} 

如果我们将这个脚本引入一个新的 Notebook 条目中,我们会得到以下截图:

当脚本执行时,我们会得到预期的结果,如下所示:

Node.js canvas 包

canvas 包用于在 Node.js 中生成图形。我们可以使用来自 canvas 包主页的示例 (www.npmjs.com/package/canvas)。

首先,我们需要安装 canvas 及其依赖项。不同操作系统的安装指南可以在主页上找到,但它与我们之前见过的工具非常相似(我们已经见过 macOS 的版本):

npm install canvas 
brew install pkg-config cairo libpng jpeg giflib 

这个示例在 Windows 中无法运行。Windows 安装要求安装 Microsoft Visual C++。我尝试了多个版本,但未能成功。

在机器上安装了 canvas 包后,我们可以使用一个小的 Node.js 脚本来创建图形:

// create a canvas 200 by 200 pixels 
var Canvas = require('canvas') 
  , Image = Canvas.Image 
  , canvas = new Canvas(200, 200) 
  , ctx = canvas.getContext('2d') 
  , string = "Jupyter!"; 

// place our string on the canvas 
ctx.font = '30px Impact'; 
ctx.rotate(.1); 
ctx.fillText(string, 50, 100); 

var te = ctx.measureText(string); 
ctx.strokeStyle = 'rgba(0,0,0,0.5)'; 
ctx.beginPath(); 
ctx.lineTo(50, 102); 
ctx.lineTo(50 + te.width, 102); 
ctx.stroke(); 

//create an html img tag, with embedded graphics 
console.log('<img src="img/' + canvas.toDataURL() + '" />'); 

这个脚本在 canvas 上写入字符串 Jupyter!,然后生成带有图形的 HTML img 标签。

在 Notebook 中运行脚本后,我们会得到 img 标签作为输出:

我们可以将 img 标签保存为 HTML 页面,效果如下:

<html>
 <body>
 <img src="img/png;base64,iVBORw0KGgo<the rest of the tag>CC" />
 </body>
 </head> 

然后,我们可以用浏览器打开 HTML 文件来显示我们的图形:

Node.js plotly 包

plotly 是一个与大多数包不同的工具。使用此软件时,您必须注册一个 username 账户,这样您就能获得一个 api_key(在 plot.ly/)。然后将 usernameapi_key 放入您的脚本中。此时,您可以使用 plotly 包的所有功能。

首先,像其他包一样,我们需要先安装它:

npm install plotly 

安装完成后,我们可以根据需要引用 plotly 包。使用一个简单的脚本,我们可以使用 plotly 生成一个 histogram(直方图):

//set random seed 
var seedrandom = require('seedrandom'); 
var rng = seedrandom('Jupyter'); 
//setup plotly 
var plotly = require('plotly')(username="<username>", api_key="<key>") 
var x = []; 
for (var i = 0; i < 500; i ++) { 
    x[i] = Math.random(); 
} 
require('plotly')(username, api_key); 
var data = [ 
  { 
    x: x, 
    type: "histogram" 
  } 
]; 
var graphOptions = {filename: "basic-histogram", fileopt: "overwrite"}; 
plotly.plot(data, graphOptions, function (err, msg) { 
    console.log(msg); 
}); 

一旦在 Jupyter 中加载并运行为 Notebook,我们将看到以下界面:

与创建本地文件或仅在屏幕上显示图形不同,任何创建的图形都会存储在 Plotly 上。plot 命令的输出是一组返回值,其中最重要的是您可以访问图形的 URL。

理想情况下,我应该能够通过提供的 URL 访问我的图形(直方图),URL 为 plot.ly/~dantoomey/1。插入 ~ 字符后,返回的 URL 按预期工作。然而,当我浏览 Plotly 网站时,我发现我的图形位于与预期略有不同的路径中。所有图形都在我的主页上,在我的情况下是 plot.ly/~dantoomey。现在,我可以访问所有的图形。直方图如下所示:

Node.js 异步线程

Node.js 内置了创建线程并使其异步执行的机制。借用 book.mixu.net/node/ch7.html 中的示例,我们可以得到以下代码:

//thread function - invoked for every number in items array 
function async(arg, callback) { 
  console.log('cube \''+arg+'\', and return 2 seconds later'); 
  setTimeout(function() { callback(arg * 3); }, 2000); 
} 

//function called once - after all threads complete 
function final() { console.log('Done', results); } 

//list of numbers to operate upon 
var items = [ 0, 1, 1, 2, 3, 5, 7, 11 ]; 

//results of each step 
var results = []; 

//loop the drives the whole process 
items.forEach(function(item) { 
  async(item, function(result){ 
    results.push(result); 
    if(results.length == items.length) { 
      final(); 
    } 
  }) 
}); 

该脚本创建了一个对数字进行操作的异步函数。对于每个数字(item),我们调用内联函数,将数字传递给该函数,函数将该数字应用于 results 列表。在这个例子中,它只是将数字乘以三并等待两秒钟。主循环(在脚本的底部)为列表中的每个数字创建一个线程,然后等待它们全部完成后再调用 final() 例程。

笔记本页面如下所示:

当我们运行脚本时,我们会得到类似于以下的输出:

很奇怪的是,看到最后一行输出(来自 final() 例程)显示时有延迟,尽管我们在之前编写 async 函数时明确指定了添加延迟。

此外,当我尝试其他函数时,例如对每个数字进行立方运算,results 列表的顺序发生了很大的变化。我本来没想到这么基础的数学运算会对性能产生影响。

Node.js 的 decision-tree 包

decision-tree 包是一个机器学习包的例子。它可以在 www.npmjs.com/package/decision-tree 获取。该包可以通过以下命令安装:

npm install decision-tree 

我们需要一个数据集来用于训练/开发我们的决策树。我使用了以下网页中的汽车 MPG 数据集:alliance.seas.upenn.edu/~cis520/wiki/index.php?n=Lectures.DecisionTrees。它似乎无法直接获得,所以我将其复制到 Excel 并保存为本地 CSV 文件。

机器学习的逻辑非常相似:

  • 加载我们的数据集

  • 将数据集划分为训练集和测试集

  • 使用训练集来开发我们的模型

  • 在测试集上测试模式

通常,您可能会将三分之二的数据用于训练,三分之一用于测试。

使用 decision-tree 包和 car-mpg 数据集,我们将拥有一个类似于以下的脚本:

//Import the modules 
var DecisionTree = require('decision-tree'); 
var fs = require("fs"); 
var d3 = require("d3"); 
var util = require('util'); 

//read in the car/mpg file 
fs.readFile("/Users/dtoomey/car-mpg.csv", "utf8", function(error, data) { 

    //parse out the csv into a dataset 
    var dataset = d3.tsvParse(data); 

    //display on screen - just for debugging 
    //console.log(JSON.stringify(dataset)); 

    var rows = dataset.length; 
    console.log("rows = " + rows); 
    var training_size = rows * 2 / 3; 
    console.log("training_size = " + training_size); 
    var test_size = rows - training_size; 
    console.log("test_size = " + test_size); 

    //Prepare training dataset 
    var training_data = dataset.slice(1, training_size); 

    //Prepare test dataset 
    var test_data = dataset.slice(training_size, rows); 

    //Setup Target Class used for prediction 
    var class_name = "mpg"; 

    //Setup Features to be used by decision tree 
    var features = ["cylinders","displacement","horsepower", "weight", "acceleration", "modelyear", "maker"]; 

    //Create decision tree and train model 
    var dt = new DecisionTree(training_data, class_name, features); 
    console.log("Decision Tree is " + util.inspect(dt, {showHidden: false, depth: null})); 

    //Predict class label for an instance 
    var predicted_class = dt.predict({ 
        cylinders: 8, 
        displacement: 400, 
        horsepower: 200, 
        weight: 4000, 
        acceleration: 12, 
        modelyear: 70, 
        maker: "US" 
    }); 
    console.log("Predicted Class is " + util.inspect(predicted_class, {showHidden: false, depth: null})); 

    //Evaluate model on a dataset 
    var accuracy = dt.evaluate(test_data); 
    console.log("Accuracy is " + accuracy); 

    //Export underlying model for visualization or inspection 
    var treeModel = dt.toJSON(); 
    console.log("Decision Tree JSON is " + util.inspect(treeModel, {showHidden: false, depth: null})); 
}); 

console.log 被广泛用于显示关于正在进行的处理过程的渐进信息。我进一步使用了 util() 函数,以便显示正在使用的对象成员。

这些包也必须使用 npm 进行安装。

如果我们在笔记本中运行它,我们会得到如下输出顶部显示的结果:

在这里,系统仅记录它在文件中找到的条目,并根据我们分配的不同变量呈现决策点。例如,当 cylinders8displacement400 时,mpgBad,依此类推。

我们得到了一个模型,用于确定车辆的 mpg 是否可接受,这取决于车辆的特征。在这种情况下,正如结果中所示,我们有一个不太好的预测器。

总结

在本章中,我们学习了如何将 JavaScript 添加到我们的 Jupyter Notebook 中。我们了解了在 Jupyter 中使用 JavaScript 的一些限制。我们还看了一些典型的 Node.js 编程包的示例,包括用于图形的d3,用于统计分析的stats-analysis,内置的 JSON 处理,canvas 用于创建图形文件,以及 plotly,它用于通过第三方工具生成图形。我们还看到了如何在 Jupyter 中使用 Node.js 开发多线程应用程序。最后,我们看到了如何进行机器学习以开发决策树。

在下一章中,我们将学习如何创建可以在你的 Notebook 中使用的交互式小部件。

第七章:Jupyter Scala

Scala 已经变得非常流行。它是建立在 Java 之上的(因此具有完全的互操作性,包括在 Scala 代码中嵌入 Java)。然而,它的语法更加简洁直观,改进了 Java 中的一些怪异之处。

在本章中,我们将涵盖以下主题:

  • 安装 Scala 用于 Jupyter

  • 使用 Scala 特性

安装 Scala 内核

macOS 的安装步骤如下(摘自 developer.ibm.com/hadoop/2016/05/04/install-jupyter-notebook-spark):

我无法在 Windows 10 机器上使用 Scala 内核的步骤。

  1. 使用以下命令安装 git
yum install git 
  1. scala 包复制到本地:
git clone https://github.com/alexarchambault/jupyter-scala.git 
  1. 通过运行以下命令安装 sbt 构建工具:
sudo yum install sbt 
  1. jupyter-scala 目录移动到 scala 包:
cd jupyter-scala 
  1. 构建包:
sbt cli/packArchive 
  1. 要启动 Scala shell,请使用以下命令:
./jupyter-scala 
  1. 通过运行此命令检查已安装的内核(现在应该可以在列表中看到 Scala):
 jupyter kernelspec list  
  1. 启动 Jupyter Notebook:
jupyter notebook 
  1. 现在您可以选择使用 Scala 2.11 的 shell。

此时,如果启动 Jupyter,您将看到 Scala 被列出:

如果我们创建一个 Scala Notebook,我们最终会看到熟悉的布局,图标显示我们正在运行 Scala,并且引擎类型字符串标识为 Scala。内核名称也在 Jupyter 的 URL 中指定:

所以,在将我们的 Notebook 命名为 Scala Notebook 并保存后,我们将在主页上看到熟悉的 Notebook 显示,其中新 Notebook 被命名为 Scala Notebook.ipynb

如果我们查看 .ipynb 文件,我们可以看到类似于其他 Notebook 类型的标记,带有 Scala 特有的标记:

{ 
 "cells": [ 
  { 
   "cell_type": "code", 
   "execution_count": null, 
   "metadata": {}, 
   "outputs": [], 
   "source": [] 
  } 
 ], 
 "metadata": { 
  "kernelspec": { 
   "display_name": "Scala", 
   "language": "scala", 
   "name": "scala" 
  }, 
  "language_info": { 
   "codemirror_mode": "text/x-scala", 
   "file_extension": ".scala", 
   "mimetype": "text/x-scala", 
   "name": "scala211", 
   "nbconvert_exporter": "script", 
   "pygments_lexer": "scala", 
   "version": "2.11.11" 
  } 
 }, 
 "nbformat": 4, 
 "nbformat_minor": 2 
} 

现在,我们可以在某些单元格中输入 Scala 代码。根据之前章节中的语言示例,我们可以输入以下内容:

val name = "Dan" 
val age = 37 
show(name + " is " + age) 

Scala 有可变变量(var)和不可变变量(val)。我们不打算更改字段,因此它们是 val。最后一条语句 show 是 Jupyter 为 Scala 提供的扩展,用于显示一个变量。

如果我们在 Jupyter 中运行这个脚本,我们将看到如下内容:

在单元格的输出区域,我们看到预期的 Dan is 37。有趣的是,Scala 还会在脚本的这一点显示每个变量的当前类型和值。

Jupyter 中的 Scala 数据访问

在加利福尼亚大学(Irvine)的网站上有一个 iris 数据集的副本,网址是 archive.ics.uci.edu/ml/machine-learning-databases/iris/iris.data。我们将访问此数据并进行几个统计操作:

Scala 代码如下:

import scala.io.Source;
//copied file locally https://archive.ics.uci.edu/ml/machine-learning-databases/iris/iris.data
val filename = "iris.data"
//println("SepalLength, SepalWidth, PetalLength, PetalWidth, Class");
val array = scala.collection.mutable.ArrayBuffer.empty[Float]
for (line <- Source.fromFile(filename).getLines) {
    var cols = line.split(",").map(_.trim);
//println(s"${cols(0)}|${cols(1)}|${cols(2)}|${cols(3)} |${cols(4)}");
   val i = cols(0).toFloat
   array += i;
}
val count = array.length;
var min:Double = 9999.0;
var max:Double = 0.0;
var total:Double = 0.0;
for ( x <- array ) {
    if (x < min) { min = x; }
    if (x > max) { max = x; }
    total += x;
}
val mean:Double = total / count;

似乎存在通过互联网访问 CSV 文件的问题。所以,我将文件复制到本地,放入与 Notebook 相同的目录中。

关于这个脚本,有一个值得注意的地方是,我们不必像通常那样将 Scala 代码包裹在一个对象中,因为 Jupyter 提供了wrapper类。

当我们运行脚本时,看到这些结果:

这是iris数据的不同版本,因此,我们看到的统计结果与之前有所不同。

Scala 数组操作

Scala 没有 pandas,但我们可以通过自己的编码来模拟其中的一些逻辑。我们将使用第二章中使用的Titanic数据集,Jupyter Python 脚本,该数据集来自titanic-gettingStarted/fdownload/train.csv,并已下载到我们的本地空间。

我们可以使用类似于第二章中Jupyter Python 脚本使用的代码来处理 pandas:

import scala.io.Source; 

val filename = "train.csv" 
//PassengerId,Survived,Pclass,Name,Sex,Age,SibSp,Parch,Ticket,Fare,Cabin,Embarked 
//1,0,3,"Braund, Mr. Owen Harris",male,22,1,0,A/5 21171,7.25,,S 

var males = 0 
var females = 0 
var males_survived = 0 
var females_survived = 0 
for (line <- Source.fromFile(filename).getLines) { 
    var cols = line.split(",").map(_.trim); 
    var sex = cols(5); 
    if (sex == "male") {  
        males = males + 1; 
        if (cols(1).toInt == 1) { 
            males_survived = males_survived + 1; 
        } 
    } 
    if (sex == "female") {  
        females = females + 1;  
        if (cols(1).toInt == 1) { 
            females_survived = females_survived + 1; 
        } 
    }     
} 
val mens_survival_rate = males_survived.toFloat/males.toFloat 
val womens_survival_rate = females_survived.toFloat/females.toFloat 

在代码中,我们逐行读取文件,解析出列(它是 CSV 格式),然后根据数据的sex列进行计算。有趣的是,Scala 的数组不是零索引的。

当我们运行这个脚本时,我们看到的结果与之前非常相似:

所以,我们看到女性的生存率显著更高。我认为关于“妇女和儿童优先”的故事是事实。

Scala 随机数在 Jupyter 中的使用

在这个例子中,我们模拟掷骰子并统计每种组合出现的次数。然后我们展示了一个简单的直方图来做说明。

脚本如下:

val r = new scala.util.Random 
r.setSeed(113L) 
val samples = 1000 
var dice = new ArrayInt 
for( i <- 1 to samples){ 
    var total = r.nextInt(6) + r.nextInt(6) 
    dice(total) = dice(total) + 1 
} 
val max = dice.reduceLeft(_ max _) 
for( i <- 0 to 11) { 
    var str = "" 
    for( j <- 1 to dice(i)/3) { 
        str = str + "X" 
    } 
    print(i+1, str, "\n") 
} 

我们首先引入了 Scala 的Random库。我们设置了种子(以便得到可重复的结果)。我们进行1000次掷骰。每次掷骰时,我们都会增加一个计数器,记录骰子一和骰子二上的点数之和出现的次数。然后我们展示结果的简化直方图。

Scala 有许多快捷方法,可以快速扫描列表/集合,如reduceLeft(_ max _)语句所示。我们也可以通过在reduceLeft语句中使用min代替max来找到最小值。

当我们运行脚本时,我们得到这些结果:

我们可以看到粗略的直方图,以及脚本中标量变量的当前值显示。请注意,我将结果除以三,以便让结果能适应一页显示。

Scala 闭包

闭包就是一个函数。这个函数的返回值依赖于函数外部声明的变量值。

我们可以通过以下小脚本进行说明:

var factor = 7
val multiplier = (i:Int) => i * factor
val a = multiplier(11)
val b = multiplier(12)

我们定义了一个名为multiplier的函数。该函数期望一个Int类型的参数。对于每个参数,我们将其与外部factor变量相乘。

我们看到以下结果:

闭包的使用感觉很好,它们做了你所期望的事情,而且非常简单直接。

Scala 高阶函数

高阶函数要么接受其他函数作为参数,要么将一个函数作为结果返回。

我们可以使用以下示例脚本:

def squared(x: Int): Int = x * x
def cubed(x: Int): Int = x * x * x

def process(a: Int, processor: Int => Int): Int = {processor(a) }

val fiveSquared = process(5, squared)
val sevenCubed = process(7, cubed)

我们定义了两个函数:一个对传入的数字进行平方,另一个对传入的数字进行立方。

接下来,我们定义了一个高阶函数,该函数接受一个数字进行处理并应用一个处理器。

最后,我们调用每个函数。例如,我们调用process()并传入5squared函数。process()函数将5传递给squared()函数并返回结果:

我们利用 Scala 的引擎自动打印变量值,以查看预期的结果。

这些函数并没有做太多的事情。当我运行它们时,结果显示花费了几秒钟。我认为在 Scala 中使用高阶函数会导致性能上的大幅下降。

Scala 模式匹配

Scala 具有非常有用的内置模式匹配。模式匹配可以用于测试整个值、对象的部分内容等的精确匹配和/或部分匹配。

我们可以使用这个示例脚本作为参考:

def matchTest(x: Any): Any = x match { 
  case 7 => "seven" 
  case "two" => 2 
  case _ => "something" 
} 
val isItTwo = matchTest("two") 
val isItTest = matchTest("test") 
val isItSeven = matchTest(7) 

我们定义了一个名为matchTest的函数。matchTest函数可以接受任何类型的参数,并返回任何类型的结果。(不确定这是否是实际编程中的情况)

关键字是match。这意味着函数会遍历选择列表,直到找到与传入的x值匹配的项,然后返回。

如你所见,我们有数字和字符串作为输入和输出。

最后的case语句是一个通配符,_catchall,这意味着如果代码执行到这一行,它将匹配任何参数。

我们可以看到以下输出:

Scala case

case类是一种简化类型,可以在不调用new Classname(..)的情况下使用。例如,我们可以有以下脚本,定义一个case类并使用它:

case class Car(brand: String, model: String) 
val buickLeSabre = Car("Buick", "LeSabre") 

所以,我们有一个名为Carcase类。我们创建了这个类的一个实例,称为buickLeSabre

case类最适用于模式匹配,因为我们可以轻松构造复杂对象并检查其内容。例如:

def carType(car: Car) = car match { 
  case Car("Honda", "Accord") => "sedan" 
  case Car("GM", "Denali") => "suv" 
  case Car("Mercedes", "300") => "luxury" 
  case Car("Buick", "LeSabre") => "sedan" 
  case _ => "Car: is of unknown type" 
} 
val typeOfBuick = carType(buickLeSabre) 

我们定义一个模式match块(如本章前一部分所述)。在match块中,我们查看一个Car对象,它的brandGMmodelDenali,等等。对于每个感兴趣的模型,我们对其类型进行分类。我们还在末尾使用了catchall _,这样我们就可以捕捉到意外的值。

我们可以在 Jupyter 中练习case类,如下截图所示:

我们定义并使用了case类作为Car。然后我们使用Car类进行了模式匹配。

Scala 不变性

不变性意味着你不能改变某些东西。在 Scala 中,除非特别标记,否则所有变量都是不可变的。这与像 Java 这样的语言相反,在这些语言中,所有变量默认都是可变的,除非特别标记为不可变。

在 Java 中,我们可以有如下的函数:

public void calculate(integer amount) { 
} 

我们可以在calculate函数内部修改amount的值。如果我们使用final关键字,我们可以告诉 Java 不允许更改该值,如下所示:

public void calculate(final integer amount) { 
} 

而在 Scala 中:

def calculate (amount: Int): Int = {  
        amount = amount + 1; 
        return amount; 
} 
var balance = 100
val result = calculate(balance)

一个类似的例程将amount变量的值保持在例程调用前的状态:

我们可以在显示中看到,即使balance是一个变量(标记为var),Scala 也不会允许你在函数内部更改它的值。传递给calculate函数的amount参数被认为是一个val,一旦初始化后就不能更改。

Scala 集合

在 Scala 中,集合根据你的使用方式自动是mutableimmutablescala.collections.immutable中的所有集合都是immutable。反之亦然,对于scala.collections.mutable。默认情况下,Scala 选择immutable集合,因此你的代码将自动使用mutable集合,如下所示:

var List mylist; 

或者,你可以在变量前加上immutable

var mylist immutable.List; 

我们可以在这个简短的示例中看到这一点:

var mutableList = List(1, 2, 3); 
var immutableList = scala.collection.immutable.List(4, 5, 6); 
mutableList.updated(1,400); 
immutableList.updated(1,700); 

正如我们在这个 Notebook 中看到的:

注意,Scala 在这里有一点小伎俩:当我们更新immutableList时,它创建了一个新的collection,正如你可以看到的变量名real_3所示。

命名参数

Scala 允许你通过名称而不是仅仅通过位置来指定参数赋值。例如,我们可以有如下代码:

def divide(dividend:Int, divisor:Int): Float =  
{ dividend.toFloat / divisor.toFloat } 
divide(40, 5) 
divide(divisor = 40, dividend = 5) 

如果我们在 Notebook 中运行这个代码,我们可以看到结果:

第一次调用通过位置划分传递的参数。第二次调用通过指定参数,而不是使用标准的序号位置分配。

Scala 特性

Scala 中的trait函数定义了一组可以被类实现的特性。trait接口类似于 Java 中的接口。

trait函数可以部分实现,强制trait的使用者(类)来实现具体的细节。

举个例子,我们可以有如下代码:

trait Color {
 def isRed(): Boolean
}
class Red extends Color {
 def isRed() = true
}
class Blue extends Color {
 def isRed() = false
}
var red = new Red();
var blue = new Blue();
red.isRed()
blue.isRed() 

该代码创建了一个名为Colortrait,并包含一个部分实现的函数isRed。因此,任何使用Color的类都必须实现isRed()

然后我们实现两个类,RedBlue,它们继承Color特性(这是使用trait的 Scala 语法)。由于isRed()函数是部分实现的,因此两个类都必须提供trait函数的实现。

我们可以在以下 Notebook 显示中看到这如何操作:

我们在输出部分(底部)看到,traitclass被创建,并且创建了两个实例,同时显示了调用这两个类的trait函数的结果。

总结

在本章中,我们为 Jupyter 安装了 Scala。我们使用 Scala 编写代码来访问更大的数据集。我们展示了 Scala 如何操作数组。我们在 Scala 中生成了随机数。还展示了高阶函数和模式匹配的例子。我们使用了 case 类。我们看到了 Scala 中不变性的例子。我们使用 Scala 包构建了集合。最后,我们了解了 Scala 特质。

在下一章中,我们将学习如何在 Jupyter 中使用大数据。

第八章:Jupyter 与大数据

大数据是每个人都在关注的话题。我认为在 Jupyter 中看看大数据能做什么会很有意思。一个处理大数据集的新兴语言是 Spark。Spark 是一个开源工具集,我们可以像使用其他语言一样在 Jupyter 中使用 Spark 代码。

在本章中,我们将涵盖以下主题:

  • 在 Jupyter 中安装 Spark

  • 使用 Spark 的功能

Apache Spark

我们将使用的工具之一是 Apache Spark。Spark 是一个用于集群计算的开源工具集。虽然我们不会使用集群,但 Spark 的典型用途是将多个机器或集群并行操作,用于分析大数据集。安装说明可以在www.dataquest.io/blog/pyspark-installation-guide找到。

在 macOS 上安装 Spark

安装 Spark 的最新说明可以在medium.freecodecamp.org/installing-scala-and-apache-spark-on-mac-os-837ae57d283f找到。主要步骤如下:

  1. brew.sh获取 Homebrew。如果你在 macOS 上进行软件开发,你很可能已经安装了 Homebrew。

  2. 安装xcode-selectxcode-select用于不同的编程语言。在 Spark 中,我们使用 Java、Scala,当然还有 Spark,具体如下:

xcode-select -install 

同样,很可能你已经为其他软件开发任务安装了此项工具。

  1. 使用 Homebrew 安装 Java:
brew cask install java 
  1. 使用 Homebrew 安装 Scala:
brew install scala 
  1. 使用 Homebrew 安装 Apache Spark:
brew install apache-spark 
  1. 你应该使用 Spark shell 测试此功能是否正常,像这样运行命令:
spark-shell 
  1. 这将显示熟悉的 Logo:
Welcome to 
 ____              __ 
 / __/__  ___ _____/ /__ 
 _\ \/ _ \/ _ `/ __/  '_/ 
 /__ / .__/\_,_/_/ /_/\_\   version 2.0.0 
 /_/ 

Using Python version 2.7.12 (default, Jul  2 2016 17:43:17) 
SparkSession available as 'spark'. 
>>> 

该网站继续讨论设置导出等,但我并没有需要做这些。

此时,我们可以启动一个 Python 3 笔记本并开始在 Jupyter 中使用 Spark。

你可以输入quit()退出。

现在,当我们在使用 Python 内核时运行笔记本时,我们可以访问 Spark。

Windows 安装

我以前在 Jupyter 中使用 Python 2 运行过 Spark。对于 Windows,我无法正确安装。

第一个 Spark 脚本

我们的第一个脚本读取一个文本文件,并查看每行的长度总和,如下所示。请注意,我们读取的是当前运行的笔记本文件;该笔记本名为Spark File Lengths,并存储在Spark File Lengths.ipynb文件中:

import pyspark
if not 'sc' in globals():
    sc = pyspark.SparkContext()
lines = sc.textFile("Spark File Line Lengths.ipynb")
lineLengths = lines.map(lambda s: len(s))
totalLengths = lineLengths.reduce(lambda a, b: a + b)
print(totalLengths)

print(totalLengths)脚本中,我们首先初始化 Spark,但仅当我们尚未初始化它时才会进行此操作。如果你尝试多次初始化 Spark,它会报错,因此所有 Spark 脚本都应该有这个if语句前缀。

该脚本读取一个文本文件(此脚本的源文件),处理每一行,计算其长度,然后将所有长度相加。

lambda函数是一个匿名(无名)函数,它接受参数并返回一个值。在第一个例子中,给定一个s字符串,返回其长度。

reduce函数将每个值作为参数,应用第二个参数,并用结果替换第一个值,然后继续处理剩余的列表。在我们的案例中,它遍历行的长度并将它们加总。

然后,在 Notebook 中运行时,我们看到如下截图。请注意,你的文件大小可能略有不同。

此外,当你第一次启动 Spark 引擎(使用sc = pyspark.SparkContext()这一行代码)时,可能需要一些时间,且脚本可能无法成功完成。如果发生这种情况,请再试一次:

Spark 单词计数

既然我们已经看到了一些功能,接下来让我们进一步探索。我们可以使用类似以下的脚本来统计文件中单词的出现次数:

import pyspark
if not 'sc' in globals():
    sc = pyspark.SparkContext()

#load in the file
text_file = sc.textFile("Spark File Words.ipynb")

#split file into distinct words
counts = text_file.flatMap(lambda line: line.split(" ")) \
    .map(lambda word: (word, 1)) \
    .reduceByKey(lambda a, b: a + b)

# print out words found
for x in counts.collect():
    print(x)

我们对编码有相同的引言。然后,我们将文本文件加载到内存中。

一旦文件加载完成,我们将每行拆分成单词,并使用lambda函数统计每个单词的出现次数。代码实际上为每个单词出现创建了一个新记录,比如"at"出现一次。这个过程可以分配到多个处理器,每个处理器生成这些低级别的信息位。我们并不关注优化这个过程。

一旦我们拥有了所有这些记录,我们就根据单词出现的次数来减少/总结记录集。

counts对象在 Spark 中称为弹性分布式数据集RDD)。它是弹性的,因为我们在处理数据集时会确保持久化。RDD 是分布式的,因为它可以被操作集群中的所有节点所处理。当然,它是由各种数据项组成的数据集。

最后的for循环对 RDD 执行collect()操作。如前所述,RDD 可能分布在多个节点上。collect()函数将 RDD 的所有副本拉取到一个位置。然后,我们遍历每条记录。

当我们在 Jupyter 中运行时,我们看到类似于以下的输出:

列表略有缩略,因为单词列表会继续一段时间。值得注意的是,Spark 中的单词拆分逻辑似乎并不十分有效;一些结果并不是单词,比如第一个条目是空字符串。

排序后的单词计数

使用相同的脚本,稍微修改一下,我们可以再进行一次调用并对结果进行排序。现在脚本如下所示:

import pyspark
if not 'sc' in globals():
    sc = pyspark.SparkContext()

#load in the file
text_file = sc.textFile("Spark Sort Words from File.ipynb")

#split file into sorted, distinct words
sorted_counts = text_file.flatMap(lambda line: line.split(" ")) \
    .map(lambda word: (word, 1)) \
    .reduceByKey(lambda a, b: a + b) \
    .sortByKey()

# print out words found (in sorted order)
for x in sorted_counts.collect():
    print(x)

在这里,我们添加了另一个 RDD 创建函数调用,sortByKey()。因此,在我们完成映射/减少操作并得到了单词和出现次数的列表之后,我们可以轻松地对结果进行排序。

结果输出如下所示:

估算π值

如果我们有如下代码,我们可以使用mapreduce来估算π值:

import pyspark 
import random 
if not 'sc' in globals(): 
    sc = pyspark.SparkContext() 

NUM_SAMPLES = 10000 
random.seed(113) 

def sample(p): 
    x, y = random.random(), random.random() 
    return 1 if x*x + y*y < 1 else 0 

count = sc.parallelize(range(0, NUM_SAMPLES)) \ 
    .map(sample) \ 
    .reduce(lambda a, b: a + b) 

print("Pi is roughly %f" % (4.0 * count / NUM_SAMPLES)) 

这段代码有相同的引言。我们正在使用 Python 的random包,并设定了样本的数量常量。

我们正在构建一个称为count的 RDD。我们调用parallelize函数来在可用的节点之间分配这个过程。代码只是映射sample函数调用的结果。最后,我们通过将生成的映射集合减少来得到总数。

sample函数获取两个随机数,并根据它们的大小返回 1 或 0。我们正在寻找一个小范围内的随机数,然后检查它们是否落在同一直径的圆内。足够大的样本,我们将得到PI(3.141...)

如果我们在 Jupyter 中运行,我们会看到以下内容:

当我使用NUM_SAMPLES = 100000运行时,最终得到了PI = 3.126400

日志文件检查

我从monitorware.com下载了一个access_log文件。和任何其他的网页访问日志一样,我们每条记录一行,就像这样:

64.242.88.10 - - [07/Mar/2004:16:05:49 -0800] "GET /twiki/bin/edit/Main/Double_bounce_sender?topicparent=Main.ConfigurationVariables HTTP/1.1" 401 12846 

第一部分是调用者的 IP 地址,然后是时间戳,HTTP 访问类型,引用的 URL,HTTP 类型,生成的 HTTP 响应代码,最后是响应中的字节数。

我们可以使用 Spark 加载和解析日志条目的一些统计信息,就像这个脚本一样:

import pyspark
if not 'sc' in globals():
    sc = pyspark.SparkContext()

textFile = sc.textFile("access_log")
print(textFile.count(), "access records")

gets = textFile.filter(lambda line: "GET" in line)
print(gets.count(), "GETs")

posts = textFile.filter(lambda line: "POST" in line)
print(posts.count(), "POSTs")

other = textFile.subtract(gets).subtract(posts)
print(other.count(), "Other")

#curious what Other requests may have been
for x in other.collect():
    print(x)

这个脚本与其他脚本有相同的序言。我们读取access_log文件。然后,我们打印count记录。

类似地,我们了解到有多少日志条目是GETPOST操作。GET被认为是最普遍的。

当我第一次这样做时,我真的不希望有任何其他情况发生,所以我从集合中移除了getsposts,并打印出离群值,看看它们是什么。

当我们在 Jupyter 中运行时,我们看到了预期的输出:

文本处理速度不是很快(特别是对于如此少量的记录)。

我喜欢能够以这样的方式处理数据框架。以程序化的方式进行基本代数运算,而不必担心边界情况,这确实令人愉悦。

顺便说一句,HEAD请求的工作方式与GET完全相同,但不返回HTTP正文。这允许调用者确定应该返回什么类型的响应,并做出相应的响应。

Spark 质数

我们可以运行一系列数字通过一个过滤器来确定每个数字是否是质数或不是。我们可以使用这个脚本:

import pyspark
if not 'sc' in globals():
    sc = pyspark.SparkContext()

def is_it_prime(number):

    #make sure n is a positive integer
    number = abs(number)

    #simple tests
    if number < 2:
        return False

    #2 is special case
    if number == 2:
        return True

    #all other even numbers are not prime
    if not number & 1:
        return False

    #divisible into it's square root
    for x in range(3, int(number**0.5)+1, 2):
        if number % x == 0:
            return False

    #must be a prime
    return True

# pick a good range
numbers = sc.parallelize(range(100000))

# see how many primes are in that range
print(numbers.filter(is_it_prime).count())

这个脚本生成了最多100000的数字。

然后,我们循环遍历每个数字,并将其传递给我们的过滤器。如果过滤器返回True,我们就得到一条记录。然后,我们只需计算找到的结果数量。

在 Jupyter 中运行时,我们看到以下内容:

这个速度非常快。我在等待中没有注意到它如此迅速。

Spark 文本文件分析

在这个示例中,我们将浏览一篇新闻文章,以确定文章的一些基本信息。

我们将会使用以下脚本对来自www.newsitem.com的 2600 条新闻文章进行处理:

import pyspark
if not 'sc' in globals():
    sc = pyspark.SparkContext()

#pull out sentences from article
sentences = sc.textFile('2600raid.txt') \
    .glom() \
    .map(lambda x: " ".join(x)) \
    .flatMap(lambda x: x.split("."))
print(sentences.count(),"sentences")

#find the bigrams in the sentences
bigrams = sentences.map(lambda x:x.split()) \
    .flatMap(lambda x: [((x[i],x[i+1]),1) for i in range(0, len(x)-1)])
print(bigrams.count(),"bigrams")

#find the (10) most common bigrams
frequent_bigrams = bigrams.reduceByKey(lambda x,y:x+y) \
    .map(lambda x:(x[1], x[0])) \
    .sortByKey(False)
frequent_bigrams.take(10)

代码读取文章并根据句点的出现将其拆分成sentences。然后,代码映射出出现的bigrams。bigram 是一对相邻的单词。接着,我们对列表进行排序,打印出前十个最常见的词对。

当我们在 Notebook 中运行这个时,看到以下结果:

我完全不知道从输出中该期待什么。很奇怪的是,你可以从文章中获取一些洞察力,因为themall出现了15次,而theguards出现了11次——应该是发生了一次袭击,地点是在商场,且某种程度上涉及了保安。

Spark 正在评估历史数据

在这个例子中,我们结合前面几个部分,查看一些历史数据并确定一些有用的属性。

我们使用的历史数据是 Jon Stewart 电视节目的嘉宾名单。数据中的典型记录如下所示:

1999,actor,1/11/99,Acting,Michael J. Fox 

这包含了年份、嘉宾的职业、出现日期、职业的逻辑分组和嘉宾的姓名。

对于我们的分析,我们将查看每年出现的次数、最常出现的职业以及最常出现的人物。

我们将使用这个脚本:

#Spark Daily Show Guests
import pyspark
import csv
import operator
import itertools
import collections

if not 'sc' in globals():
 sc = pyspark.SparkContext()

years = {}
occupations = {}
guests = {}

#file header contains column descriptors:
#YEAR, GoogleKnowledge_Occupation, Show, Group, Raw_Guest_List

with open('daily_show_guests.csv', 'rt', errors = 'ignore') as csvfile: 
 reader = csv.DictReader(csvfile)
 for row in reader:
 year = row['YEAR']
 if year in years:
 years[year] = years[year] + 1
 else:
 years[year] = 1

 occupation = row['GoogleKnowlege_Occupation']
 if occupation in occupations:
 occupations[occupation] = occupations[occupation] + 1
 else:
 occupations[occupation] = 1

 guest = row['Raw_Guest_List']
 if guest in guests:
 guests[guest] = guests[guest] + 1
 else:
 guests[guest] = 1

#sort for higher occurrence
syears = sorted(years.items(), key = operator.itemgetter(1), reverse = True)
soccupations = sorted(occupations.items(), key = operator.itemgetter(1), reverse = True)
sguests = sorted(guests.items(), key = operator.itemgetter(1), reverse = True)

#print out top 5's
print(syears[:5])
print(soccupations[:5])
print(sguests[:5]) 

该脚本有一些特点:

  • 我们使用了几个包。

  • 它有熟悉的上下文序言。

  • 我们为yearsoccupationsguests启动字典。字典包含key和值。对于此用例,键将是来自 CSV 的原始值,值将是数据集中该项出现的次数。

  • 我们打开文件并开始逐行读取,使用reader对象。由于文件中有一些空值,我们在读取时忽略错误。

  • 在每一行中,我们获取感兴趣的值(yearoccupationname)。

  • 我们查看该值是否存在于相应的字典中。

  • 如果存在,递增该值(计数器)。

  • 否则,初始化字典中的一个条目。

  • 然后我们按照每个项目出现次数的逆序对字典进行排序。

  • 最后,我们展示每个字典的前五个值。

如果我们在 Notebook 中运行这个,我们会得到以下输出:

我们展示脚本的尾部和之前的输出。

可能有更聪明的方法来做这些事情,但我并不清楚。

累加器的构建方式相当标准,无论你使用什么语言。我认为这里有机会使用map()函数。

我真的很喜欢只需要轻松去除列表/数组,而不需要调用函数。

每年嘉宾的数量非常一致。演员占据主导地位——他们大概是观众最感兴趣的群体。嘉宾名单有点令人惊讶。嘉宾大多数是演员,但我认为所有人都有强烈的政治倾向。

总结

在本章中,我们通过 Python 编程在 Jupyter 中使用了 Spark 功能。首先,我们安装了 Spark 插件到 Jupyter。我们编写了一个初始脚本,它只是从文本文件中读取行。我们进一步分析了该文件中的单词计数,并对结果进行了排序。我们编写了一个脚本来估算 pi 值。我们评估了网页日志文件中的异常。我们确定了一组素数,并对文本流进行了某些特征的评估。

第九章:交互式小部件

Jupyter 有一个机制可以在脚本运行时收集用户的输入。为了实现这一点,我们在脚本中使用小部件或用户界面控制进行编码。本章中我们将使用的小部件定义在ipywidgets.readthedocs.io/en/latest/

例如,有以下小部件:

  • 文本输入:Notebook 用户输入一个字符串,该字符串将在后续脚本中使用。

  • 按钮点击:这些通过按钮向用户展示多个选项。然后,依据选择的按钮(点击的按钮),你的脚本可以根据用户选择的按钮改变方向。

  • 滑块:你可以为用户提供一个滑块,让用户在你指定的范围内选择一个值,然后你的脚本可以根据这个值进行相应的操作。

  • 切换框和复选框:这些让用户选择他们有兴趣操作的脚本选项。

  • 进度条:进度条可以用来显示他们在多步骤过程中已完成的进度。

在某些情况下,这个机制可以完全开放,因为底层的从用户收集输入通常是可用的。因此,你可以制作出不符合标准用户输入控制范式的非常有趣的小部件。例如,有一个小部件允许用户点击地理地图以发现数据。

在本章中,我们将讨论以下主题:

  • 安装小部件

  • 小部件基础

  • Interact 小部件

  • 交互式小部件

  • 小部件包

安装小部件

  • 小部件包是对标准 Jupyter 安装的升级。你可以使用这个命令来更新小部件包:
pip install ipywidgets
  • 完成后,你必须使用这个命令来升级你的 Jupyter 安装:
jupyter nbextension enable --py widgetsnbextension
  • 然后你必须使用这个命令:
conda update jupyter_core jupyter_client
  • 我们整理了一个基本示例小部件笔记本,以确保一切正常工作:
 #import our libraries 
from ipywidgets import * 
from IPython.display import display 

#create a slider and message box 
slider = widgets.FloatSlider() 
message = widgets.Text(value = 'Hello World') 

#add them to the container 
container = widgets.Box(children = (slider, message)) 
container.layout.border = '1px black solid' 

display(container) 
  • 最终我们得到了以下截图,其中显示了包含滑块和消息框的容器小部件:

小部件基础

所有小部件通常以相同的方式工作:

  • 你可以创建或定义一个小部件实例。

  • 你可以预设小部件的属性,比如它的初始值或要显示的标签。

  • 小部件可以对用户的不同输入做出反应。这些输入是通过一个处理器或 Python 函数收集的,当用户对小部件执行某些操作时,函数就会被调用。例如,当用户点击按钮时,调用处理器。

  • 小部件的值可以像任何其他变量一样在脚本中使用。例如,你可以使用一个小部件来确定绘制多少个圆。

Interact 小部件

Interact 是一个基本的小部件,似乎用于衍生所有其他小部件。它具有可变参数,依赖于参数的不同,将呈现不同种类的用户输入控制。

Interact 小部件滑动条

我们可以使用 interact 通过传入一个范围来生成一个滑动条。例如,以下是我们的脚本:

#imports 
from ipywidgets import interact 

# define a function to work with (cubes the number) 
def myfunction(arg): 
    return arg+1 

#take values from slidebar and apply to function provided 
interact(myfunction, arg=9); 

请注意,interact函数调用后的分号是必须的。

我们有一个脚本,它会执行以下操作:

  • 引用我们要使用的包

  • 定义一个函数,每次用户输入一个值时都会调用此函数。

  • 调用interact,传递我们的处理函数和一系列值

当我们运行此脚本时,我们会得到一个用户可修改的滚动条:

用户可以通过滑动垂直条在值的范围内滑动。上限为 27,下限为-1(假设我们可以向interact传递额外的参数来设置可选择的值范围)。每次interact小部件中的值更改时,myfunction都会被调用,并打印结果。因此,我们看到选中的是 27,显示的是数字 28(经过myfunction处理后 - 27 + 1)。

交互式复选框控件

我们可以根据传递给interact的参数来更改生成的控件类型。如果我们有以下脚本:

from ipywidgets import interact
def myfunction(x):
    return x
# the second argument defines which of the interact widgets to use
interact(myfunction, x=False);

我们正在执行与之前相同的步骤;然而,传递的值是False(但它也可以是True)。interact函数检查传入的参数,判断它是布尔值,并为布尔值提供适当的控件:复选框。

如果我们在 Notebook 中运行上面的脚本,我们将得到如下显示:

交互式文本框控件

我们可以通过传递不同的参数给interact来生成文本输入控件。例如,参见以下脚本:

from ipywidgets import interact
def myfunction(x):
    return x
#since argument is a text string, interact generates a textbox control for it
interact(myfunction, x= "Hello World");

这会生成一个初始值为Hello World的文本输入控件:

交互式下拉菜单

我们还可以使用interact函数为用户生成一个下拉列表框,让他们选择。例如,在下面的脚本中,我们生成一个包含两个选项的下拉菜单:

from ipywidgets import interact 
def myfunction(x): 
    return x 
interact(myfunction, x=['red','green']); 

这个脚本将执行以下操作:

  • 引入interact引用,

  • 定义一个函数,每当用户更改控件值时都会调用该函数

  • 调用interact函数并传入一组值,interact将解释这些值,意味着为用户创建一个下拉菜单。

如果我们在 Notebook 中运行此脚本,我们将得到以下显示:

上面的截图显示,我们在底部打印的值将根据用户在下拉菜单中选择的内容而变化。

交互式小部件

还有一个交互式小部件。交互式小部件类似于interact小部件,但不会在脚本直接调用时显示用户输入控件。如果你有一些需要计算的参数,或者想在运行时决定是否需要显示控件,这会非常有用。

例如,我们可以有一个类似于之前脚本的脚本,如下所示:

from ipywidgets import interactive
from IPython.display import display
def myfunction(x):
return x
w = interactive(myfunction, x= "Hello World ");
display(w)

我们对脚本做了几个更改:

  • 我们正在引用交互式小部件

  • 交互式函数返回的是一个控件,而不是立即显示一个值。

  • 我们必须自己编写显示控件的脚本

如果我们运行以下脚本,用户看到的效果非常相似:

控件

还有另一个控件包,叫做widgets,它包含了你可能想要使用的所有标准控件,提供了许多可选参数,允许你自定义显示效果。

进度条控件

这个包中有一个控件可以向用户显示进度条。我们可以使用以下脚本:

import ipywidgets as widgets 
widgets.FloatProgress( 
    value=45, 
    min=0, 
    max=100, 
    step=5, 
    description='Percent:', 
) 

上面的脚本将会如下所示显示我们的进度条:

我们看到一个进度条,看起来大约是 45%。

列表框控件

我们也可以使用listbox控件,称为Dropdown,如下脚本所示:

import ipywidgets as widgets 
from IPython.display import display 
w = widgets.Dropdown( 
    options={'Pen': 7732, 'Pencil': 102, 'Pad': 33331}, 
    description='Item:', 
) 
display(w) 
w.value 

这个脚本会向用户显示一个包含PenPencilPad值的列表框。当用户选择其中一个值时,相关的值会返回并存储在w变量中,我们会像以下截图那样显示它:

因此,我们看到了与笔相关的库存值。

文本控件

text控件从用户那里收集一个文本字符串,供脚本中的其他地方重用。文本控件有一个描述(标签)和一个值(由用户输入或在脚本中预设)。

在这个示例中,我们只会收集一个文本字符串,并将其作为脚本输出的一部分显示在屏幕上。我们将使用以下脚本:

from ipywidgets import widgets
from IPython.display import display
#take the text from the box, define variable for handler
text = widgets.Text()
#display it
display(text)
def handle_submit(sender):
    print(text.value)
#when we hit return in the textbox call upon the handler
text.on_submit(handle_submit)

包含基本控件的 Python 包是ipywidgets,所以我们需要引用它。我们为文本字段定义了一个处理程序,当用户输入文本值并点击提交时,该处理程序会被调用。在这里,我们使用了text控件。

如果我们在 Notebook 中运行上面的脚本,显示效果如下:

我们应该指出本页的一些亮点:

  • 页面元素的排列顺序很重要。处理程序引用的文本字段必须在引用之前定义。

  • 当调用控件时,控件会自动寻找与脚本相关联的处理程序。在这种情况下,我们有一个submit处理程序。还有许多其他处理程序可以使用。text.on_submit将处理程序分配给控件。

  • 否则,我们将得到一个标准的 Python Notebook。

  • 如果我们运行脚本(单元格 | 运行全部),我们将得到上面的截图(等待我们在文本框中输入值):

  • 所以,我们的脚本已经设置了一个控件,用于收集用户的输入,之后对这个值进行处理。(我们这里只是显示它,但我们也可以使用这个输入进行进一步处理。)

按钮控件

类似地,我们也可以在脚本中使用一个Button控件,如下例所示:

from ipywidgets import widgets 
from IPython.display import display 

button = widgets.Button(description="Submit"); 
display(button) 

def on_button_clicked(widget): 
    print("Clicked Button:" + widget.description); 

button.on_click(on_button_clicked); 

这个脚本的功能如下:

  • 引用我们想要使用的widgets包中的功能。

  • 创建我们的按钮。

  • 定义了一个按钮点击事件的处理程序。该处理程序接收被点击的Button对象(小部件)。

  • 该处理程序显示了关于点击按钮的信息(你可以想象,如果我们有多个按钮在同一界面上,我们会希望确定是哪个按钮被点击了)。

  • 最后,我们将定义的处理程序分配给我们创建的Button对象。

请注意处理程序代码的缩进;这是标准的 Python 风格,必须遵循。

如果我们在 Notebook 中运行前面的脚本,我们会看到如下截图:

请注意以下图片底部的Submit按钮。你可以更改按钮的其他属性,例如位置、大小、颜色等。

如果我们点击Submit按钮,则会显示以下界面,显示我们关于按钮被点击的消息:

小部件属性

所有小部件控件都有一组可调整的显示属性。你可以通过获取一个控件的实例,并在 Notebook 中运行control.keys命令来查看属性列表,如下例所示:

from ipywidgets import * 
w = IntSlider() 
w.keys 

这个脚本引入了所有小部件控件的一个通用引用。接着,我们创建一个IntSlider实例,并显示可以调整的属性列表。最终,我们得到如下所示的界面:

如你所见,这个列表非常全面:

属性描述
orientation是否左对齐、右对齐或居中对齐
color字体颜色
height控件的高度
disabled控件是否被禁用
visible控件是否显示?
font_style字体样式,例如,斜体
min最小值(在范围列表中使用)
background_color控件的背景颜色
width控件的宽度
font_family控件中文本使用的字体系列
description描述字段用于文档说明
max最大值(范围内)
padding应用的内边距(控件边缘)
font_weight控件中使用的字体粗细,例如,粗体
font_size控件中文本的字体大小
value控件选定和输入的值
margin显示控件时使用的边距

调整小部件属性

我们可以在脚本中调整这些属性,例如使用以下脚本来禁用文本框(文本框仍然会显示,但用户不能输入任何值)。我们可以使用以下脚本:

from ipywidgets import * 
Text(value='You can not change this text!', disabled=True) 

这是前面代码的截图:

当一个字段被禁用时,文本框会变灰。当用户将光标悬停在该字段上时,会出现一个带有斜线的红色圆圈,表示该字段不能被更改。

调整属性

所有之前显示的属性都可以读写。我们可以通过一个小脚本来展示这个过渡,脚本如下:

from ipywidgets import * 
w = IntSlider() 
original = w.value 
w.value = 5 
original, w.value 

该脚本创建一个滑块,获取其当前值,将值更改为 5,然后显示控件的原始值和当前值。

如果我们在 Notebook 中运行前面的脚本,我们将看到以下预期的结果:

控件事件

所有控件都通过响应用户的操作来工作,无论是鼠标还是键盘。控件的默认操作是内置在软件中的,但你可以添加自己的事件处理(用户操作)。

我们之前见过这种事件处理(例如,在滑块的部分,每当用户改变滑块值时,都会调用一个函数)。但是,让我们更深入地探讨一下。

我们可以有以下脚本:

from ipywidgets import widgets
from IPython.display import display
button = widgets.Button(description="Click Me!")
display(button)
def on_button_clicked(b):
    print("Button clicked.")
button.on_click(on_button_clicked)

这个脚本执行了以下操作:

  • 创建一个按钮。

  • 向用户显示按钮。

  • 定义了一个事件处理程序的点击事件。它打印出你点击了屏幕上的消息。你可以在处理程序中写入任何你想要的 Python 语句。

  • 最后,我们将点击事件处理程序与我们创建的按钮关联。所以,现在当用户点击我们的按钮时,事件处理程序被调用,并且 Button clicked 消息显示在屏幕上(如下面的截图所示):

如果我们在 Notebook 中运行上面的脚本,并点击按钮几次,我们将得到以下显示:

控件容器

你也可以直接使用 Python 语法通过将子元素传递给构造函数来组合控件容器。例如,我们可以有以下脚本:

 #import our libraries 
from ipywidgets import * 
from IPython.display import display 

#create a slider and message box 
slider = widgets.FloatSlider() 
message = widgets.Text(value = 'Hello World') 

#add them to the container 
container = widgets.Box(children = (slider, message)) 
container.layout.border = '1px black solid' 

display(container) 

上面的脚本显示了我们正在创建一个容器(这是一个 Box 控件),在其中我们指定了子控件。调用显示容器的命令时,也会依次显示子元素。所以,我们最终会得到如下截图所示的显示:

你可以看到框周围的边框和框中的两个控件。

类似地,我们也可以在容器显示后,使用如下语法将子元素添加到容器中:

from ipywidgets import * 
from IPython.display import display 
container = widgets.Box() 
container.layout.border = '1px black solid' 
display(container) 
slider = widgets.FloatSlider() 
message = widgets.Text(value='Hello World') 
container.children=[slider, message] 

当我们将子元素添加到容器时,容器会重新绘制,这会导致所有子元素的重新绘制。

如果我们在另一个 Notebook 中运行这个脚本,我们将得到一个与之前示例非常相似的结果,显示如下截图所示:

总结

在本章中,我们将控件添加到了我们的 Jupyter 安装中,并使用了 interact 和 interactive 控件来产生各种用户输入控件。然后,我们深入研究了控件包,调查了可用的用户控件、容器中的可用属性、从控件发出的事件以及如何为控件构建容器。

在下一章中,我们将探讨共享笔记本并将其转换为不同格式。

第十章:分享与转换 Jupyter 笔记本

一旦你开发好了笔记本,你就可能希望与其他人分享。我们将在本章中介绍一个典型的分享机制——将你的笔记本放置在一个可访问的互联网上的服务器上。

当你将一个笔记本提供给另一个人时,他们可能需要以不同的格式接收笔记本,以满足他们的系统要求。我们还将介绍一些机制,帮助你将笔记本以不同格式提供给他人。

在本章中,我们将涵盖以下主题:

  • 分享笔记本

  • 转换笔记本

分享笔记本

分享笔记本的典型机制是将笔记本放置在一个网站上。网站运行在一个服务器或分配的机器空间上。服务器处理所有与运行网站相关的事务,例如跟踪多个用户并进行用户登录和注销。

然而,为了使笔记本能够被使用,网站必须安装笔记本逻辑。一个典型的网站知道如何根据一些源文件将内容作为 HTML 交付。最基本的形式是纯 HTML,其中你在网站上访问的每一页都与 Web 服务器上的一个 HTML 文件完全对应。其他语言也可以用来开发网站(例如 Java 或 PHP),因此服务器需要知道如何从这些源文件中访问所需的 HTML。在我们的上下文中,服务器需要知道如何访问你的笔记本,以便将 HTML 交付给用户。

即使笔记本仅在你的本地机器上运行,它们也是通过浏览器访问你的本地机器(服务器),而不是通过互联网——因此,Web、HTML 和互联网访问已经被提供。

如果笔记本位于一个真实的网站上,那么所有能够访问该网站的人都可以使用它——无论服务器是在你办公室环境中的本地网络上运行,还是可以通过互联网供所有用户访问。

你始终可以为网站增加安全措施,以确保只有那些你授权的用户才能使用你的笔记本。安全机制取决于所使用的 Web 服务器软件类型。

在笔记本服务器上分享笔记本

Jupyter 过程内置了将笔记本暴露为其自身 Web 服务器的功能。假设服务器是一个其他用户可以访问的机器,你可以配置 Jupyter 在该服务器上运行。你必须向 Jupyter 提供配置信息,以便它知道如何进行。生成 Jupyter 安装配置文件的命令如下所示:

请注意,由于我们是在 Anaconda 中运行,我正在从该目录中运行命令:

C:\Users\Dan\Anaconda3\Scripts>jupyter notebook --generate-config

默认的config文件被写入:C:\Users\Dan\.jupyter\jupyter_notebook_config.py

此命令将在你的~./jupyter目录中生成一个jupyter_notebook_config.py文件。对于 Microsoft 用户,该目录是你主目录下的一个子目录。

配置文件包含了您可以用来将笔记本暴露为服务器的设置:

c.NotebookApp.certfile = u'/path/to/your/cert/cert.pem'
c.NotebookApp.keyfile = u'/ path/to/your/cert/key.key'
c.NotebookApp.ip = '*'
c.NotebookApp.password = u'hashed-password'
c.NotebookApp.open_browser = False
c.NotebookApp.port = 8888

文件中的设置在下表中进行了说明:

设置描述
c.NotebookApp.certfile这是您网站证书的文件路径。如果您有 SSL 证书,您需要更改该设置以指向文件的位置。该文件可能不是 .PEM 扩展名的文件。SSL 证书有多种格式。
c.NotebookApp.keyfile这是访问您网站证书密钥位置的路径。与其直接指定证书密钥,您应该将密钥存储在一个文件中。所以,如果您想为您的笔记本应用 SSL 证书,您需要指定该文件的位置。密钥通常是一个非常大的十六进制数字,因此它被存储在自己的文件中。此外,将其存储在文件中提供了额外的保护,因为密钥存储所在的目录通常有有限的访问权限。
c.NotebookApp.ip机器的 IP 地址。使用通配符 * 来开放给所有人。这里,我们指定了访问笔记本网站的机器的 IP 地址。
c.NotebookApp.password哈希密码——用户访问您的笔记本时需要提供密码,以响应标准的登录验证。
c.NotebookApp.open_browserTrue/False——启动笔记本服务器时是否打开浏览器窗口?
c.NotebookApp.port用于访问您的服务器的端口;它应该对机器开放。

每个网站在较低层次上通过 IP 地址进行定位。IP 地址是一个四部分的数字,用于标识所涉及服务器的位置。IP 地址可能类似于 172.32.88.7

网络浏览器与互联网软件协同工作,知道如何使用 IP 地址来定位目标服务器。该软件套件也知道如何将您在浏览器中提到的 URL(例如 www.microsoft.com)转化为 IP 地址。

一旦您适当地更改了设置,您应该能够在浏览器中访问配置的 URL 来访问您的笔记本。该 URL 将是 HTTP 或 HTTPS(取决于是否应用了 SSL 证书)、IP 地址和端口的组合,例如 HTTPS://123.45.56.9:8888

在笔记本服务器上共享加密的笔记本

如果您有 SSL 证书可以应用,之前展示的两项设置可以使用。如果没有 SSL 证书,密码(如前所述)以及所有其他交互将在用户的浏览器和服务器之间以明文传输。如果您在笔记本中处理敏感信息,应该获取 SSL 证书并为您的服务器做出相应的设置更改。

如果你需要更高的安全性来保护笔记本的访问,下一步是提供一个 SSL 证书(将证书放在你的计算机上,并在配置文件中提供路径)。有许多公司提供 SSL 证书。截止目前,最便宜的是Let's encrypt,它会免费提供一个低级别的 SSL 证书。(SSL 证书有不同等级,有些是需要付费的。)

再次说明,一旦你设置了有关证书的前述设置,你就可以通过HTTPS://前缀访问你的笔记本服务器,并且可以确保用户浏览器和笔记本服务器之间的所有传输都是加密的,因此是安全的。

在 Web 服务器上共享笔记本

为了将你的笔记本添加到现有的 Web 服务器中,你需要执行前面的步骤,并在笔记本的配置文件中添加一些额外的信息,如下例所示:

c.NotebookApp.tornado_settings = {
'headers': {
'Content-Security-Policy': "frame-ancestors 'https://yourwebsite.com' 'self' "
}
}

在这里,你需要将yourwebsite.com替换为你网站的 URL。

完成后,你可以通过你的网站访问笔记本。

通过 Docker 共享笔记本

Docker 是一个开源的、轻量级的容器,用于分发软件。一个典型的 Docker 实例在机器的某个端口上运行着一个完整的 Web 服务器和一个特定的 Web 应用。Docker 实例中运行的软件的具体信息由 Dockerfile 控制。该文件向 Docker 环境提供命令,指示使用哪些组件来配置该实例。一个用于 Jupyter 实现的 Dockerfile 示例如下:

ENV TINI_VERSION v0.6.0
ADD https://github.com/krallin/tini/releases/download/${TINI_VERSION}/tini /usr/bin/tini
RUN chmod +x /usr/bin/tini
ENTRYPOINT ["/usr/bin/tini", "--"]
EXPOSE 8888
CMD ["jupyter", "Notebook", "--port=8888", "--no-browser", "--ip=0.0.0.0"]

以下是对每个 Dockerfile 命令的讨论:

  • ENV命令告诉 Docker 使用一个专用的操作系统。这是必要的,因为 Jupyter 存在一个缺陷,会不断地从你的计算机上获取和释放资源。

  • ADD命令告诉 Dockertini代码的位置。

  • RUN命令改变tini目录的访问权限。

  • ENTRYPOINT命令告诉 Docker 使用什么作为 Docker 实例的操作系统。

  • EXPOSE命令告诉 Docker 要在哪个端口上公开你的笔记本。

  • CMD命令告诉 Docker 在环境设置好之后运行哪些命令。CMD参数告诉你熟悉的jupyter Notebook命令,它用于在你的机器上启动 Jupyter。

一旦 Docker 实例部署到你的 Docker 机器上,你可以在指定的端口(8888)访问该 Docker 实例,例如,http://machinename.com:8888

在公共服务器上共享笔记本

当前,有一个托管公司允许你免费托管笔记本:GitHub。GitHub 是源代码管理系统(Git 源代码管理)的标准 Web 提供商。源代码管理用于保持文件的历史版本,帮助你回溯步骤。

GitHub 的实现包括了您在笔记本中使用所需的所有工具,这些工具已经安装在服务器上。例如,在前面的章节中,如果您想在笔记本中使用 R 编程,您需要在自己的计算机上安装 R 工具集。而 GitHub 已经完成了这些步骤。

访问 github.com/ 网站并注册一个免费的账户。

登录后,您将获得一个可以添加内容的网站。如果您有开发工具(git 推送命令是程序员用来将文件存储到 Git 服务器上的命令),您可以使用这些工具,或者直接将您的笔记本文件(ipynb)拖放到您的 GitHub 网站上。

我在那里创建了一个帐户,包含一个 notebooks 目录,并将其中一个 notebooks 文件放置到该网站上。我的 GitHub 网站如下所示:

您可以看到 Python Data Access.ipynb 文件位于屏幕的顶部。

如果您点击该 notebooks 文件,您将在浏览器中看到预期的笔记本运行效果。

注意:目前在 GitHub 上运行 notebooks 存在问题。之前是可以正常工作的。我预计会有修复措施来重新启用 Jupyter。

如果您回顾上一章,您会看到相同的显示(去掉了 GitHub 装饰元素)。

这个笔记本可以通过以下 URL 直接访问: github.com/danieltoomey/notebooks/blob/master/Python%20Data%20Access.ipynb。因此,您可以将您的笔记本提供给其他用户,只需将 URL 交给他们即可。

当您登录 GitHub 后,显示界面会略有不同,因为您将对 GitHub 内容有更多的控制权限。

转换笔记本

将笔记本转换为其他格式的标准工具是使用 nbconvert 工具。它已内置在您的 Jupyter 安装中。您可以直接在笔记本的用户界面中访问该工具。如果您打开一个笔记本并选择 Jupyter 文件菜单项,您将看到多个“另存为”选项:

选项包括:

格式类型文件扩展名
Notebook.ipynb
Scala211.scala
HTML.html
Markdown.md
reST.rst
LaTeX.tex
PDF via LaTeX.pdf

注意:由于我们使用的是 Scala 笔记本,因此第二个选项中提供的是该语言。如果我们有其他语言的笔记本,那么该语言将是选择项。

对于这些示例,如果我们从前一章节中取出一个笔记本,Jupyter 笔记本看起来是这样的:

笔记本格式

笔记本格式(.ipynb)是笔记本的原生格式。我们在前面的章节中已经查看过该文件,了解 Jupyter 在笔记本中存储了什么内容。

如果您想让其他用户完全访问您的 Notebook,可以使用 Notebook 格式,因为他们将从自己的系统运行您的 Notebook。

您也可能想这样做,以便将您的 Notebook 保存在其他介质中。

Scala 格式

Scala 格式(.scala)对应于您的 Notebook 的 Scala 实现。如果您使用 JavaScript 作为 Notebook 的语言,这将是 Notebook 页面的直接导出。

如果您使用了其他语言(例如 Python)编写 Notebook 脚本,那么“下载为”选项会相应变化,例如“下载为 | Python(.py)”。

使用我们的示例,如预期的那样,Scala 格式等同于 Jupyter 显示:

import scala.io.Source; 

//copied file locally https://archive.ics.uci.edu/ml/machine-learning-databases/iris/iris.data 
val filename = "iris.data" 
//println("SepalLength, SepalWidth, PetalLength, PetalWidth, Class"); 

//load iris data into an array 
val array = scala.collection.mutable.ArrayBuffer.empty[Float] 
for (line <- Source.fromFile(filename).getLines) { 
 var cols = line.split(",").map(_.trim); 
 //println(s"${cols(0)}|${cols(1)}|${cols(2)}|${cols(3)} |${cols(4)}"); 
 val i = cols(0).toFloat 
 array += i; 
} 

//get some minimal statistics 
val count = array.length; 
var min:Double = 9999.0; 
var max:Double = 0.0; 
var total:Double = 0.0; 
for ( x <- array ) { 
 if (x < min) { min = x; } 
 if (x > max) { max = x; } 
 total += x; 
} 
val mean:Double = total / count; 

使用 Scala 格式,您可以直接使用 Scala 解释器运行脚本。在 macOS 下,有 scala 命令。Windows 系统也有类似的工具。

此外,对于其他脚本语言,您应该能够在适当的解释器中运行脚本。

如果我们从命令行窗口运行此 Scala 文件,我们可以看到预期的结果:

HTML 格式

HTML(.html)格式对应于在网页浏览器中显示页面所需的 HTML,就像在 Notebook 中看到的那样。生成的 HTML 没有任何编码逻辑;它仅包含显示类似页面所需的 HTML。

HTML 格式对传递 Notebook 的结果给另一个用户也非常有用。如果您想将 Notebook 通过电子邮件发送给其他用户(此时原始 HTML 会通过电子邮件客户端应用程序传输并可查看),那么使用 HTML 格式非常合适。

如果您有可用的 web 服务,可以插入新页面,那么 HTML 格式也很有用。如果 web 服务器不支持 Jupyter 文件(请参考本章的第一节),那么 HTML 可能是您的唯一选择。同样,即使 web 服务器支持 Jupyter,您可能也不希望直接交给用户您的源 Jupyter Notebook(.ipynb)文件。

导出的 HTML 格式在浏览器中的显示效果如下:

请注意,Jupyter 的标题信息不会显示或提供。否则,它看起来与 Jupyter 显示完全相同。

Markdown 格式

markdown(.md)格式是 超文本标记语言HTML)的简化版本。.md 文件可由某些工具使用,通常作为软件分发的 README 文件格式(在这种情况下,客户端的显示能力可能非常有限)。

例如,同一 Notebook 的 markdown 格式如下所示:


```scala211

import scala.io.Source;

//从本地复制文件 https://archive.ics.uci.edu/ml/machine-learning-databases/iris/iris.data

val filename = "iris.data"

//println("花萼长度, 花萼宽度, 花瓣长度, 花瓣宽度, 分类");

//将 iris 数据加载到数组中

val array = scala.collection.mutable.ArrayBuffer.empty[Float]

for (line <- Source.fromFile(filename).getLines) {

var cols = line.split(",").map(_.trim);

//println(s"${cols(0)}|${cols(1)}|${cols(2)}|${cols(3)} |${cols(4)}");

val i = cols(0).toFloat

array += i;

}

//获取一些最小统计数据

val count = array.length;

var min:Double = 9999.0;

var max:Double = 0.0;

var total:Double = 0.0;

for ( x <- array ) {

if (x < min) { min = x; }

if (x > max) { max = x; }

total += x;

}

val mean:Double = total / count;

```py 

显然,Markdown 格式是一种非常基础的显示格式。它只有少量的文本标记,帮助读者区分不同的格式。我使用 Atom 编辑器查看在解析时的显示效果:

再次显示的是非常干净的界面,仍然类似于 Jupyter Notebook 的显示,文件底部有一些奇怪的编码和大量的转义字符。不确定为什么会这样。

重结构化文本格式

重结构化文本格式(.rst)是一种简单的纯文本标记语言,偶尔用于编程文档。它看起来与前面讨论的 .md 格式非常相似。

例如,示例页面的 RST 格式如下所示:


.. code:: scala 

 import scala.io.Source; 

 //copied file locally https://archive.ics.uci.edu/ml/machine-learning-databases/iris/iris.data 
 val filename = "iris.data" 
 //println("SepalLength, SepalWidth, PetalLength, PetalWidth, Class"); 

 //load iris data into an array 
 val array = scala.collection.mutable.ArrayBuffer.empty[Float] 
 for (line <- Source.fromFile(filename).getLines) { 
 var cols = line.split(",").map(_.trim); 
 //println(s"${cols(0)}|${cols(1)}|${cols(2)}|${cols(3)} |${cols(4)}"); 
 val i = cols(0).toFloat 
 array += i; 
 } 

 //get some minimal statistics 
 val count = array.length; 
 var min:Double = 9999.0; 
 var max:Double = 0.0; 
 var total:Double = 0.0; 
 for ( x <- array ) { 
 if (x < min) { min = x; } 
 if (x > max) { max = x; } 
 total += x; 
 } 
 val mean:Double = total / count; 

.. parsed-literal:: 
... 

如你所见,这与前面的 Markdown 示例类似;代码大致分为几个块。

使用 Atom 显示 .rst 文件会得到如下结果:

.rst 显示没有其他格式那样漂亮。

LaTeX 格式

LaTeX 是一种来自 1970 年代末的排版格式,至今仍在许多 Unix 衍生系统中用于生成手册等。

Jupyter 使用 LaTeX 包将笔记本的图像导出为 PDF 文件。为了使其正常工作,你需要在计算机上安装这个包。在 macOS 上,这涉及以下步骤:

  • 安装 LaTeX —— Windows 和 macOS 有不同的安装方式。

  • 在 macOS 上,我尝试使用 MacTeX。你必须使用 Safari 下载该包。它提示格式错误,我不得不多次重试。

  • 在 Windows 上,我尝试使用 TeXLive。它试图下载数百个字体。

  • 以下是(macOS)字体相关的命令:

    • sudo tlmgr install adjustbox

    • sudo tlmgr install collection-fontsrecommended

请注意,这个安装过程相当繁琐。我已经安装了完整的 LaTeX,但后来又有提示要安装一个小版本的 LaTeX,并且安装字体时非常麻烦。我对这些步骤在 Windows 机器上是否能正确执行信心不足。

如果你没有完整的包,尝试下载 PDF 文件时,Notebook 会打开一个新屏幕,并显示一条长长的错误信息,告诉你缺少哪个部分。

你可以下载一个适用于你使用的操作系统的文本文件阅读器。我为我的 macOS 下载了 MacTeX。

注意:你需要一个 Tex 解释器,才能进行下一步的 PDF 下载,因为它使用 Tex 作为开发 PDF 文件的基础。

PDF 格式

PDF(.pdf)格式是一种广泛使用的展示格式,适用于多种用途。PDF 是一个很好的格式,能够向其他用户传递不可修改的内容。其他用户将无法以任何方式修改结果,但他们能够查看并理解你的逻辑。

PDF 生成依赖于 LaTeX 正确安装。这次我没能成功运行它。在之前的版本中,我曾在 Windows 和 Mac 上成功安装过。

总结

在这一章中,我们在 Notebook 服务器上共享了 Notebooks。我们将 Notebook 添加到我们的 Web 服务器,并通过 GitHub 分发了一个 Notebook。我们还探讨了将 Notebooks 转换为不同格式的方式,如 HTML 和 PDF。

在下一章,我们将探讨如何允许多个用户同时与 Notebook 进行交互。

第十一章:多用户 Jupyter Notebooks

Jupyter Notebooks 本身具有可被用户修改的能力,当用户输入数据或做出选择时,Notebook 会做出相应变化。然而,标准的 Notebook 服务器软件存在一个问题,它没有考虑到同时有多个用户在同一个 Notebook 上工作。Notebook 服务器软件是底层的 Jupyter 软件,负责显示页面并与用户交互——它根据你的 Notebook 中的指令进行显示和交互。

一个 Notebook 服务器,实际上是一个专用的互联网 Web 服务器,通常会为每个用户创建一个新的执行路径或线程,以便支持多个用户。当一个较低级别的子例程用于所有实例时,如果没有正确考虑每个用户拥有自己一组数据的情况,就会出现问题。

请注意,本章中的一些代码/安装可能在 Windows 环境下无法运行。

在本章中,我们将探讨以下内容:

  • 给出一个例子,说明在标准 Jupyter 安装中,当多个用户同时访问同一个 Notebook 时会发生的问题。

  • 使用新版本的 Jupyter,JupyterHub,它是通过扩展 Jupyter 特别为解决多用户问题而开发的。

  • 另外,使用 Docker,一种允许多个软件实例同时运行的工具,以解决这个问题。

一个示例交互式 Notebook

在本章中,我们将使用一个简单的 Notebook,要求用户提供一些信息,并显示特定的信息。

例如,我们可以有一个像这样的脚本(取自前面章节 第九章,交互式小部件):

from ipywidgets import interact 
def myfunction(x): 
    return x 
interact(myfunction, x= "Hello World "); 

这个脚本向用户展示一个文本框,文本框内的原始值为 Hello World 字符串。当用户与输入字段互动并修改其值时,脚本中的 x 变量的值也会相应变化,并显示在屏幕上。例如,我将值更改为字母 A

我们可以看到多用户问题:如果我们在另一个浏览器窗口中打开相同的页面(复制 URL,打开新浏览器窗口,粘贴 URL,按下Enter键),我们会看到完全相同的显示。新窗口应该从一个新的脚本开始,只是提示你默认的 Hello World 信息。然而,由于 Jupyter 服务器软件只期望一个用户,因此只有一个 x 变量的副本,所以它显示了它的值。

JupyterHub

一旦 Jupyter Notebooks 被共享,显然需要解决多用户问题。于是开发了一个新的 Jupyter 软件版本,叫做JupyterHub。JupyterHub 专门设计用来处理多个用户,每个用户都有自己的一组变量来操作。实际上,系统会为每个用户提供一个全新的 Jupyter 软件实例——这是一种强力方法,但它有效。

当 JupyterHub 启动时,它会启动一个中心或控制代理。该中心将启动一个用于处理 Jupyter 请求的监听器或代理。当代理接收到 Jupyter 的请求时,它将它们交给中心处理。如果中心判断这是一个新用户,它将为他们生成一个新的 Jupyter 服务器实例,并将所有后续的用户与 Jupyter 的交互附加到他们自己的服务器版本上。

安装

JupyterHub 需要 Python 3.3 或更高版本,并且我们将使用 Python 工具 pip3 来安装 JupyterHub。您可以通过在命令行中输入 Python 来检查您正在运行的 Python 版本,前言将回显当前版本。例如,请参阅以下内容:

Python 
Python 3.6.0a4 (v3.6.0a4:017cf260936b, Aug 15 2016, 13:38:16)  
[GCC 4.2.1 (Apple Inc. build 5666) (dot 3)] on darwin 
Type "help", "copyright", "credits" or "license" for more information. 

如果您需要升级到新版本,请参考 python.org 上的说明,因为它们针对操作系统和 Python 版本有具体的要求。

JupyterHub 的安装方式与其他软件类似,使用以下命令:

npm install -g configurable-http-proxy
pip3 install jupyterhub

首先,安装代理。在代理安装中 -g 表示将该软件对所有用户可用:

npm install -g configurable-http-proxy 
/usr/local/bin/configurable-http-proxy -> /usr/local/lib/node_modules/configurable-http-proxy/bin/configurable-http-proxy 
/usr/local/lib 
└─┬ configurable-http-proxy@1.3.0  
 ├─┬ commander@2.9.0 
 │ └── graceful-readlink@1.0.1 
 ├─┬ http-proxy@1.13.3 
 │ ├── eventemitter3@1.2.0 
 │ └── requires-port@1.0.0 
 ├─┬ lynx@0.2.0 
 │ ├── mersenne@0.0.3 
 │ └── statsd-parser@0.0.4 
 ├── strftime@0.9.2 
 └─┬ winston@2.2.0 
 ├── async@1.0.0 
 ├── colors@1.0.3 
 ├── cycle@1.0.3 
 ├── eyes@0.1.8 
 ├── isstream@0.1.2 
 ├── pkginfo@0.3.1 
 └── stack-trace@0.0.9 

然后,我们安装 JupyterHub:

pip3.6 install jupyterhub 
Collecting jupyterhub 
 Downloading jupyterhub-0.6.1-py3-none-any.whl (1.3MB) 
 100% |████████████████████████████████| 1.4MB 789kB/s 
Collecting requests (from jupyterhub) 
 Downloading requests-2.11.1-py2.py3-none-any.whl (514kB) 
 100% |████████████████████████████████| 522kB 1.5MB/s 
Collecting traitlets>=4.1 (from jupyterhub) 
 Downloading traitlets-4.2.2-py2.py3-none-any.whl (68kB) 
 100% |████████████████████████████████| 71kB 4.3MB/s 
Collecting sqlalchemy>=1.0 (from jupyterhub) 
 Downloading SQLAlchemy-1.0.14.tar.gz (4.8MB) 
 100% |████████████████████████████████| 4.8MB 267kB/s 
Collecting jinja2 (from jupyterhub) 
 Downloading Jinja2-2.8-py2.py3-none-any.whl (263kB) 
 100% |████████████████████████████████| 266kB 838kB/s 
... 

操作

现在我们可以直接从命令行启动 JupyterHub:

jupyterhub 

这将导致在命令控制台窗口中显示以下内容:

[I 2016-08-28 14:30:57.895 JupyterHub app:643] Writing cookie_secret to /Users/dtoomey/jupyterhub_cookie_secret 
[W 2016-08-28 14:30:57.953 JupyterHub app:304]  
 Generating CONFIGPROXY_AUTH_TOKEN. Restarting the Hub will require restarting the proxy. 
 Set CONFIGPROXY_AUTH_TOKEN env or JupyterHub.proxy_auth_token config to avoid this message. 

[W 2016-08-28 14:30:57.962 JupyterHub app:757] No admin users, admin interface will be unavailable. 
[W 2016-08-28 14:30:57.962 JupyterHub app:758] Add any administrative users to `c.Authenticator.admin_users` in config. 
[I 2016-08-28 14:30:57.962 JupyterHub app:785] Not using whitelist. Any authenticated user will be allowed. 
[I 2016-08-28 14:30:57.992 JupyterHub app:1231] Hub API listening on http://127.0.0.1:8081/hub/ 
[E 2016-08-28 14:30:57.998 JupyterHub app:963] Refusing to run JuptyterHub without SSL. If you are terminating SSL in another layer, pass --no-ssl to tell JupyterHub to allow the proxy to listen on HTTP. 

请注意,脚本已完成,但默认情况下不会为您在浏览器中打开窗口,就像标准 Jupyter 安装中那样。

更重要的是最后一行的输出(也以红色打印在屏幕上),拒绝在没有 SSL 的情况下运行 JupyterHub。JupyterHub 特别构建用于多用户登录和使用单个 Notebook,因此它在抱怨期望它具有运行 SSL(以确保用户交互安全)。

最后一行的后半部分提示我们应该怎么做 —— 我们需要告诉 JupyterHub 我们没有使用证书/SSL。我们可以使用 --no-ssl 参数来做到这一点,如下所示:

Jupyterhub --no-ssl 

这将导致在控制台中显示预期的结果,并使服务器继续运行:

[I 2016-08-28 14:43:15.423 JupyterHub app:622] Loading cookie_secret from /Users/dtoomey/jupyterhub_cookie_secret 
[W 2016-08-28 14:43:15.447 JupyterHub app:304]  
 Generating CONFIGPROXY_AUTH_TOKEN. Restarting the Hub will require restarting the proxy. 
 Set CONFIGPROXY_AUTH_TOKEN env or JupyterHub.proxy_auth_token config to avoid this message. 

[W 2016-08-28 14:43:15.450 JupyterHub app:757] No admin users, admin interface will be unavailable. 
[W 2016-08-28 14:43:15.450 JupyterHub app:758] Add any administrative users to `c.Authenticator.admin_users` in config. 
[I 2016-08-28 14:43:15.451 JupyterHub app:785] Not using whitelist. Any authenticated user will be allowed. 
[I 2016-08-28 14:43:15.462 JupyterHub app:1231] Hub API listening on http://127.0.0.1:8081/hub/ 
[W 2016-08-28 14:43:15.468 JupyterHub app:959] Running JupyterHub without SSL. There better be SSL termination happening somewhere else... 
[I 2016-08-28 14:43:15.468 JupyterHub app:968] Starting proxy @ http://*:8000/ 
14:43:15.867 - info: [ConfigProxy] Proxying http://*:8000 to http://127.0.0.1:8081 
14:43:15.871 - info: [ConfigProxy] Proxy API at http://127.0.0.1:8001/api/routes 
[I 2016-08-28 14:43:15.900 JupyterHub app:1254] JupyterHub is now running at http://127.0.0.1:8000/ 

如果现在我们访问最后一行输出中显示的 URL(http://127.0.0.1:8000/)),我们将进入 JupyterHub 的登录界面:

因此,我们已经避免了需要 SSL,但我们仍然需要为系统配置用户。

JupyterHub 软件使用配置文件来确定其工作方式。您可以使用 JupyterHub 生成配置文件,并使用以下命令提供默认值:

jupyterhub --generate-config 
Writing default config to: jupyterhub_config.py 

生成的配置文件可用近 500 行。示例文件的开头如下所示:

# Configuration file for jupyterhub.
c = get_config()

#------------------------------------------------------------------------------
# JupyterHub configuration
#------------------------------------------------------------------------------

# An Application for starting a Multi-User Jupyter Notebook server.
# JupyterHub will inherit config from: Application

# Include any kwargs to pass to the database connection. See
# sqlalchemy.create_engine for details.
# c.JupyterHub.db_kwargs = {}

# The base URL of the entire application

# c.JupyterHub.base_url = '/'

如您所见,大多数配置设置前面都有一个井号(#),表示它们已被注释掉。提到的设置是将要应用的默认值。如果您需要更改某个设置,您需要去掉前面的井号并更改等号(=)右侧的值。顺便说一下,这是一种测试更改的好方法:进行一次更改,保存文件,尝试更改,继续进行其他更改。如果过程中某个更改未按预期工作,您只需恢复井号前缀即可恢复到工作状态。

我们将查看一些可用的配置选项。值得注意的是,文件中的许多设置是 Python 设置,而不是特定于 JupyterHub 的。项目列表包括此处显示的项目:

区域描述
JupyterHubJupyterHub 本身的设置
LoggingConfigurable日志信息布局
SingletonConfigurable仅允许一个实例的可配置项
Application日期格式和日志级别
SecuritySSL 证书
SpawnerHub 如何为新用户启动新的 Jupyter 实例
LocalProcessSpawner使用 popen 作为用户启动本地进程
Authenticator主要 API 是一个方法 authenticate
PAMAuthenticator与 Linux 交互以登录
LocalAuthenticator检查本地用户,并且如果用户存在,可以尝试创建它们

正在继续操作

我没有更改配置文件就使我的安装正常运行。默认情况下,配置使用 PEM 系统,它会连接到您正在运行的操作系统,以传入凭据(就像登录到机器一样)进行验证。

如果在尝试登录到您的 JupyterHub 安装时,控制台日志中出现了 JupyterHub single-user server requires notebook >= 4.0 消息,您需要使用以下命令更新基础 Jupyter:

pip3 install jupyter

这将把您的基础 Jupyter 更新到最新版本,目前是 4.1。

如果您没有安装 pip3,您需要升级到 Python 3 或更高版本。有关您的系统需要采取的步骤,请参阅 python.org 上的文档。

现在,您可以使用以下命令行启动 JupyterHub:

jupyterhub --no-ssl

使用您登录机器时使用的相同凭据登录到登录页面(如前所示)(记住 JupyterHub 使用 PEM,它会调用您的操作系统来验证凭据)。您将进入一个非常像标准 Jupyter 首页的页面:

它看起来非常相似,只是现在屏幕右上角有两个额外的按钮:

  • 控制面板

  • 注销

单击注销按钮将您从 JupyterHub 注销并重定向到登录页面。

点击控制面板按钮会带你进入一个新屏幕,提供两个选项,如下所示:

  • 停止我的服务器

  • 我的服务器

点击“停止我的服务器”按钮会停止你的 Jupyter 安装,并带你进入一个只包含一个按钮的页面:我的服务器(如下节所示)。你可能还注意到命令行控制台日志中发生的变化:

[I 2016-08-28 20:22:16.578 JupyterHub log:100] 200 GET /hub/api/authorizations/cookie/jupyter-hub-token-dtoomey/[secret] (dtoomey@127.0.0.1) 13.31ms 
[I 2016-08-28 20:23:01.181 JupyterHub orm:178] Removing user dtoomey from proxy 
[I 2016-08-28 20:23:01.186 dtoomey notebookapp:1083] Shutting down kernels 
[I 2016-08-28 20:23:01.417 JupyterHub base:367] User dtoomey server took 0.236 seconds to stop 
[I 2016-08-28 20:23:01.422 JupyterHub log:100] 204 DELETE /hub/api/users/dtoomey/server (dtoomey@127.0.0.1) 243.06ms 

点击“我的服务器”按钮将带你回到 Jupyter 首页。如果你之前点击过“停止我的服务器”按钮,那么底层的 Jupyter 软件将会重新启动,正如你在控制台输出中可能会注意到的那样(如下所示):

I 2016-08-28 20:26:16.356 JupyterHub base:306] User dtoomey server took 1.007 seconds to start 
[I 2016-08-28 20:26:16.356 JupyterHub orm:159] Adding user dtoomey to proxy /user/dtoomey => http://127.0.0.1:50972 
[I 2016-08-28 20:26:16.372 dtoomey log:47] 302 GET /user/dtoomey (127.0.0.1) 0.73ms 
[I 2016-08-28 20:26:16.376 JupyterHub log:100] 302 GET /hub/user/dtoomey (dtoomey@127.0.0.1) 1019.24ms 
[I 2016-08-28 20:26:16.413 JupyterHub log:100] 200 GET /hub/api/authorizations/cookie/jupyter-hub-token-dtoomey/[secret] (dtoomey@127.0.0.1) 10.75ms 

JupyterHub 概述

总结一下,通过 JupyterHub,我们安装了一个 Jupyter 实例,它会为每个用户保持一个独立的 Jupyter 软件实例,从而避免变量值的冲突。软件能够识别是否需要实例化新的 Jupyter 实例,因为用户会登录应用程序,而系统会维护用户列表。

Docker

Docker 是另一种可以让多个用户共享同一个 Notebook 而不发生冲突的机制。Docker 是一个允许你将应用程序集成成镜像并在容器中运行的系统。Docker 适用于大多数环境。Docker 允许在同一台机器上运行多个镜像实例,但保持独立的地址空间。因此,Docker 镜像的每个用户都有自己的软件实例和独立的数据/变量集。

每个镜像是运行所需软件的完整堆栈,例如,Web 服务器、Web 应用程序、API 等。

将 Notebook 的镜像进行思考并不是一件难事。该镜像包含 Jupyter 服务器代码和你的 Notebook,结果是一个完全独立的单元,不与其他实例共享任何空间。

安装

安装 Docker 涉及下载最新的文件(macOS 的 docker.dmg 文件和 Windows 的 .exe 安装文件),然后将 Docker 应用程序复制到你的 Applications 文件夹中。Docker QuickStart 终端 是大多数开发者使用的主要应用程序。Docker QuickStart 会在本地机器上启动 Docker,为 Docker 应用程序分配一个 IP 地址 / 端口号,并进入 Docker 终端。一旦 QuickStart 完成,如果你已安装镜像,你可以访问你的应用程序(在本例中是 Jupyter Notebook)。

从 Docker 终端,你可以加载镜像、删除镜像、检查状态等。

启动 Docker

如果你运行 Docker QuickStart,你将进入 Docker 终端窗口,显示类似于以下内容:

bash --login '/Applications/Docker/Docker Quickstart Terminal.app/Contents/Resources/Scripts/start.sh' 
Last login: Tue Aug 30 08:25:11 on ttys000 
bos-mpdc7:Applications dtoomey$ bash --login '/Applications/Docker/Docker Quickstart Terminal.app/Contents/Resources/Scripts/start.sh' 

Starting "default"... 
(default) Check network to re-create if needed... 
(default) Waiting for an IP... 
Machine "default" was started. 
Waiting for SSH to be available... 
Detecting the provisioner... 
Started machines may have new IP addresses. You may need to re-run the `docker-machine env` command. 
Regenerate TLS machine certs?  Warning: this is irreversible. (y/n): Regenerating TLS certificates 
Waiting for SSH to be available... 
Detecting the provisioner... 
Copying certs to the local machine directory... 
Copying certs to the remote machine... 
Setting Docker configuration on the remote daemon... 

 ##         . 
 ## ## ##        == 
 ## ## ## ## ##    === 
 /"""""""""""""""""\___/ === 
 ~~~ {~~ ~~~~ ~~~ ~~~~ ~~~ ~ /  ===- ~~~ 
 \______ o           __/ 
 \    \         __/ 
 \____\_______/ 

docker is configured to use the default machine with IP 192.168.99.100 
For help getting started, check out the docs at [`docs.docker.com`](https://docs.docker.com) 

(显示末尾附近的奇怪图形是鲸鱼的字符表示——Docker 的标志。)

你可以从输出中看到以下内容:

  • Docker 机器已启动——Docker 机器控制着在你的空间中运行的镜像。

  • 如果你正在使用证书,证书将被复制到你的 Docker 空间中。

  • 最后,它会告诉你在访问 Docker 实例时使用的 IP 地址——应该是你正在使用的机器的 IP 地址。

为 Docker 构建你的 Jupyter 镜像

Docker 知道包含运行应用程序所需整个软件堆栈的镜像。我们需要构建一个包含 Notebook 的镜像,并将其放入 Docker 中。

我们需要下载所有必要的 Jupyter-Docker 编码。在 Docker 终端窗口中,我们运行以下命令:

$ docker pull jupyter/all-spark-notebook 
Using default tag: latest 
latest: Pulling from jupyter/all-spark-notebook 
8b87079b7a06: Pulling fs layer  
872e508604af: Pulling fs layer  
8e8d83eda71c: Pull complete  
... 

这是一个较大的下载过程,需要一些时间。它正在下载并安装运行 Jupyter 所需的所有可能组件。请记住,每个镜像都是完全自包含的,所以每个镜像都有运行 Jupyter 所需的全部软件。

下载完成后,我们可以使用以下命令启动一个 Notebook 镜像:

docker run -d -p 8888:8888 -v /disk-directory:/virtual-notebook jupyter/all-spark-notebook 
The parts of this command are: 
  • docker run:告诉 Docker 执行一个镜像的命令。

  • -d:将镜像作为服务器(守护进程)运行,直到用户手动停止。

  • -p 8888:8888:将内部端口 8888 暴露给外部用户,使用相同的端口地址。Notebook 默认已经使用端口 8888,所以我们只是表示暴露相同的端口。

  • -v /disk-directory:/virtual-notebook:将来自 disk-directory 的 Notebook 以 virtual-notebook 名称暴露出来。

  • 最后的参数是使用 all-spark-notebook 作为该镜像的基础。在我的例子中,我使用了以下命令:

$ docker run -d -p 8888:8888 -v /Users/dtoomey:/dan-notebook jupyter/all-spark-notebook 
b59eaf0cae67506e4f475a9861f61c01c5af3556489992104c4ce39343e8eb02

显示的长 hex 数字是镜像标识符。我们可以使用 docker ps -l 命令检查镜像是否正在运行,这个命令会列出我们 Docker 中的所有镜像:

$ docker ps -l 
CONTAINER ID        IMAGE                        COMMAND                  CREATED             STATUS              PORTS                    NAMES 
b59eaf0cae67        jupyter/all-spark-notebook   "tini -- start-notebo"   8 seconds ago       Up 7 seconds        0.0.0.0:8888->8888/tcp   modest_bardeen 

显示的部分如下:

  • 第一个名称 b59... 是容器的分配 ID。Docker 中的每个镜像都被分配给一个容器。

  • 镜像是 jupyter/all-spark-notebook,它包含运行 Notebook 所需的所有软件。

  • 该命令告诉 Docker 启动镜像。

  • 端口访问点是我们预期的:8888

  • 最后,Docker 为每个正在运行的镜像分配随机名称 modest_bardeen(不确定为什么这么做)。

如果我们此时尝试访问 Docker Jupyter,我们将被要求为系统设置安全性,如此显示所示:

一旦我们设置好安全性,我们应该能够通过浏览器访问 Notebook,地址为 http:// 127.0.0.1:8888。我们在 Docker 启动时看到过前面的 IP 地址(0.0.0.0),并且我们使用的是指定的端口 8888

你可以在左上角看到 URL。在其下方,我们有一个标准的空 Notebook。所使用的 Docker 镜像包含了所有最新版本,因此你无需做任何额外操作就能获取更新的软件或组件。你可以通过下拉 "New" 菜单来查看可用的语言选项:

Docker 总结

我们已经安装了 Docker,并且创建了一个包含我们的 Notebook 的镜像。我们还将该 Docker 镜像放入 Docker 中,并且访问了我们的 Docker Notebook 镜像。

总结

在本章中,我们学习了如何暴露一个 Notebook,以便多个用户可以同时使用该 Notebook。我们看到了一个错误发生的例子。我们安装了一个解决该问题的 Jupyter 服务器,并且使用 Docker 来缓解这个问题。

在下一章,我们将会看看 Jupyter 的一些即将发布的功能增强。