第七章:Bokeh Server进阶
前置知识:本章需要了解 ColumnDataSource(第二章 2.2)和 CustomJS 回调(第六章 6.7)。Bokeh 三层架构的概念见第二章 2.2.6。
7.1 为什么需要Bokeh Server?
CustomJS的局限:
- 只能执行JavaScript,无法调用Python库
- 无法访问数据库、文件系统
- 无法进行复杂计算
Bokeh Server的优势:
- 回调函数用Python编写
- 可以访问任何Python库
- 支持实时数据更新
- 支持多用户会话
运行方式提醒:本章中的 Python 回调示例必须通过
bokeh serve运行。直接使用python app.py、show()或save()生成静态 HTML 时,Python 端回调不会执行;静态 HTML 交互应使用CustomJS。交叉引用:
7.2 第一个Bokeh Server应用
创建文件app.py:
这是一个 Bokeh Server 应用,请保存为
app.py后使用bokeh serve app.py --show运行。
# app.py
from bokeh.io import curdoc
from bokeh.plotting import figure
from bokeh.models import Slider, ColumnDataSource
from bokeh.layouts import column
import numpy as np
# 数据
x = np.linspace(0, 10, 100)
source = ColumnDataSource(data={'x': x, 'y': np.sin(x)})
# 图表
p = figure(title="正弦波", width=600, height=400)
p.line('x', 'y', source=source, line_width=2)
# 滑块
freq = Slider(title="频率", start=0.1, end=5, value=1, step=0.1)
# 回调
def update(attr, old, new):
new_y = np.sin(freq.value * x)
source.data = {'x': x, 'y': new_y}
freq.on_change('value', update)
# 布局
curdoc().add_root(column(freq, p))
curdoc().title = "正弦波演示"
运行:
bokeh serve app.py --show
7.3 Widgets小部件详解
数值输入类:
from bokeh.models import Slider, RangeSlider, Spinner
# 滑块
slider = Slider(title="数值", start=0, end=100, value=50, step=1)
# 范围滑块
range_slider = RangeSlider(title="范围", start=0, end=100, value=(20, 80))
# 数字输入框
spinner = Spinner(title="数量", low=0, high=1000, value=100, step=10)
选择类:
from bokeh.models import Select, MultiSelect, CheckboxGroup, RadioGroup
# 下拉选择
select = Select(title="类别", options=["A", "B", "C"], value="A")
# 多选下拉
multi_select = MultiSelect(title="多选", options=["A", "B", "C"], value=["A"])
# 复选框
checkbox = CheckboxGroup(labels=["选项1", "选项2", "选项3"], active=[0])
# 单选按钮
radio = RadioGroup(labels=["选项1", "选项2", "选项3"], active=0)
按钮类:
from bokeh.models import Button, Toggle, Dropdown
# 按钮
button = Button(label="点击")
# 开关按钮
toggle = Toggle(label="开关", active=False)
# 下拉按钮
dropdown = Dropdown(label="菜单", menu=[("选项1", "1"), ("选项2", "2")])
文本类:
from bokeh.models import TextInput, TextAreaInput, Div
# 文本输入
text_input = TextInput(title="输入", value="")
# 多行文本
text_area = TextAreaInput(title="描述", value="")
# 文本显示
div = Div(text="<h2>标题</h2><p>段落内容</p>")
日期类:
from bokeh.models import DatePicker, DateRangeSlider
from datetime import date
# 日期选择
date_picker = DatePicker(title="日期", value=date.today())
# 日期范围
date_range = DateRangeSlider(
title="日期范围",
start=date(2020, 1, 1),
end=date.today(),
value=(date(2023, 1, 1), date.today())
)
7.4 回调机制
on_change - 属性变化回调:
def callback(attr, old, new):
print(f"属性 {attr} 从 {old} 变为 {new}")
slider.on_change('value', callback)
on_click - 点击回调:
def callback():
print("按钮被点击")
button.on_click(callback)
on_event - 事件回调:
from bokeh.events import DoubleTap, Press
def callback(event):
print(f"双击位置: ({event.x}, {event.y})")
p.on_event(DoubleTap, callback)
选择回调:
def callback(attr, old, new):
indices = source.selected.indices
print(f"选中索引: {indices}")
source.selected.on_change('indices', callback)
7.5 实时数据更新
periodic_callback - 定时更新:
import random
from bokeh.io import curdoc
source = ColumnDataSource(data={'x': [], 'y': []})
p = figure()
p.line('x', 'y', source=source)
counter = [0]
def update():
counter[0] += 1
new_data = {'x': [counter[0]], 'y': [random.random()]}
source.stream(new_data, rollover=100) # 保留最近100个点
curdoc().add_root(p)
curdoc().add_periodic_callback(update, 100) # 每100ms更新
stream - 流式添加:
# 添加数据,保留最近N个
source.stream(new_data, rollover=100)
# 添加数据,无限制
source.stream(new_data)
patch - 局部更新:
# 更新特定位置的值
source.patch({
'y': [(0, new_value), (2, another_value)] # 更新索引0和2
})
7.6 完整示例:交互式仪表板
# dashboard.py
from bokeh.plotting import figure, curdoc
from bokeh.models import (
ColumnDataSource, Select, Slider, DataTable,
TableColumn, Div, HoverTool
)
from bokeh.layouts import column, row, gridplot
import pandas as pd
import numpy as np
# 模拟数据
np.random.seed(42)
n = 1000
df = pd.DataFrame({
'date': pd.date_range('2023-01-01', periods=n, freq='D'),
'category': np.random.choice(['A', 'B', 'C'], n),
'value': np.random.randn(n).cumsum(),
'volume': np.random.randint(100, 1000, n),
})
df['month'] = df['date'].dt.strftime('%Y-%m')
# 数据源
source = ColumnDataSource(df)
# 标题
title = Div(text="<h1>销售数据分析仪表板</h1>", width=800)
# 控件
category_select = Select(title="类别", options=['All'] + list(df['category'].unique()), value='All')
month_slider = Slider(title="显示天数", start=30, end=n, value=n, step=30)
# 主图表
p_main = figure(x_axis_type="datetime", width=800, height=400, title="趋势图")
p_main.line('date', 'value', source=source, line_width=2)
p_main.add_tools(HoverTool(tooltips=[("日期", "@date{%F}"), ("值", "@value{0.00}")],
formatters={'@date': 'datetime'}))
# 分布图
p_dist = figure(width=400, height=400, title="分布")
hist, edges = np.histogram(df['value'], bins=30)
p_dist.quad(top=hist, bottom=0, left=edges[:-1], right=edges[1:],
fill_color="skyblue", line_color="white")
# 数据表格
columns = [
TableColumn(field="date", title="日期", formatter=DateFormatter()),
TableColumn(field="category", title="类别"),
TableColumn(field="value", title="值"),
TableColumn(field="volume", title="销量"),
]
table = DataTable(source=source, columns=columns, width=800, height=200)
# 回调
def update_data(attr, old, new):
# 过滤类别
cat = category_select.value
days = month_slider.value
filtered = df.copy()
if cat != 'All':
filtered = filtered[filtered['category'] == cat]
filtered = filtered.tail(days)
source.data = ColumnDataSource.from_df(filtered)
category_select.on_change('value', update_data)
month_slider.on_change('value', update_data)
# 布局
controls = column(category_select, month_slider)
layout = column(
title,
row(controls, p_main),
row(p_dist, table)
)
curdoc().add_root(layout)
curdoc().title = "销售仪表板"
运行:
bokeh serve dashboard.py --show
7.7 应用模板结构
在继续看部署之前,建议先把这一节和第五章连起来理解:
- 第五章解决的是“页面怎么排版、控件和图表怎么组织”
- 本章解决的是“这些控件变化后,Python 代码如何真正响应”
- 第九章会继续讨论“这些回调和实时更新在数据量变大时如何优化”
如果你发现自己在仪表板代码里同时处理布局、数据过滤、回调、部署,通常说明已经进入了 Bokeh Server 的核心使用场景。
推荐的项目结构:
myapp/
├── main.py # 主应用文件
├── templates/
│ └── index.html # 自定义模板(可选)
├── static/
│ ├── css/
│ └── js/
├── data/
│ └── data.csv
└── requirements.txt
main.py 示例:
from bokeh.plotting import figure, curdoc
from bokeh.models import ColumnDataSource
# 初始化数据
source = ColumnDataSource(data=dict(x=[], y=[]))
# 创建图表
p = figure(title="实时数据")
p.line('x', 'y', source=source)
# 添加到文档
curdoc().add_root(p)
curdoc().title = "My App"
7.8 生产环境部署
方式1:使用 Nginx 反向代理
server {
listen 80;
server_name example.com;
location / {
proxy_pass http://localhost:5006;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
}
}
方式2:使用 Docker
FROM python:3.11
WORKDIR /app
COPY requirements.txt .
RUN pip install -r requirements.txt
COPY . .
EXPOSE 5006
CMD ["bokeh", "serve", "--allow-websocket-origin=*", "."]
启动命令:
# 开发环境
bokeh serve myapp.py --show
# 生产环境
bokeh serve myapp.py \
--allow-websocket-origin=example.com \
--port=5006 \
--num-procs=4
7.9 高级部署方案
使用 Gunicorn + Supervisor:
# 安装 Gunicorn
pip install gunicorn
# 启动命令
gunicorn -w 4 -b 0.0.0.0:5006 \
-k bokeh.server.gunicorn.bokeh_worker.BokehWorker \
myapp:application
Supervisor 配置:
[program:bokeh_app]
command=/path/to/venv/bin/gunicorn -w 4 -b 127.0.0.1:5006 -k bokeh.server.gunicorn.bokeh_worker.BokehWorker myapp:application
directory=/path/to/app
user=www-data
autostart=true
autorestart=true
stderr_logfile=/var/log/bokeh/bokeh.err.log
stdout_logfile=/var/log/bokeh/bokeh.out.log
使用 Systemd:
[Unit]
Description=Bokeh Server Application
After=network.target
[Service]
User=www-data
Group=www-data
WorkingDirectory=/path/to/app
ExecStart=/path/to/venv/bin/bokeh serve --port=5006 --allow-websocket-origin=example.com
Restart=always
[Install]
WantedBy=multi-user.target
使用 Kubernetes:
apiVersion: apps/v1
kind: Deployment
metadata:
name: bokeh-app
spec:
replicas: 3
selector:
matchLabels:
app: bokeh
template:
metadata:
labels:
app: bokeh
spec:
containers:
- name: bokeh
image: my-bokeh-app:latest
ports:
- containerPort: 5006
env:
- name: BOKEH_ALLOW_WS_ORIGIN
value: "example.com"
---
apiVersion: v1
kind: Service
metadata:
name: bokeh-service
spec:
selector:
app: bokeh
ports:
- port: 80
targetPort: 5006
type: LoadBalancer
7.10 安全最佳实践
WebSocket安全配置:
# 限制WebSocket连接的来源
bokeh serve app.py \
--allow-websocket-origin=example.com \
--allow-websocket-origin=www.example.com
CORS设置:
# 在应用中配置CORS
from bokeh.server.server import Server
server = Server({'/app': app},
allow_websocket_origin=["example.com"],
port=5006)
身份验证集成:
# 使用Flask-Login进行身份验证
from flask_login import login_required
@app.route('/bokeh-app')
@login_required
def bokeh_app():
script = server_document('http://localhost:5006/app')
return render_template('app.html', script=script)
敏感数据保护:
# 使用环境变量存储敏感信息
import os
DB_PASSWORD = os.environ.get('DB_PASSWORD')
API_KEY = os.environ.get('API_KEY')
# 不要在代码中硬编码敏感信息
7.11 生产环境部署清单
部署前检查:
- 设置正确的
--allow-websocket-origin - 配置HTTPS证书
- 设置进程管理(systemd/supervisor)
- 配置反向代理(Nginx/Apache)
- 设置日志记录
- 配置监控和告警
环境配置:
# 生产环境启动命令
bokeh serve app.py \
--port=5006 \
--allow-websocket-origin=example.com \
--num-procs=4 \
--log-level=warning \
--address=127.0.0.1
性能调优:
# 使用数据流控制
source.stream(new_data, rollover=1000)
# 启用WebGL(大数据集)
p = figure(output_backend="webgl")
# 使用异步回调
curdoc().add_periodic_callback(update, 1000)
监控配置:
# 使用systemd监控
sudo systemctl enable bokeh-app
sudo systemctl status bokeh-app
# 查看日志
journalctl -u bokeh-app -f
Docker部署示例:
FROM python:3.11-slim
WORKDIR /app
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
COPY . .
EXPOSE 5006
HEALTHCHECK CMD curl -f http://localhost:5006/ || exit 1
CMD ["bokeh", "serve", "app.py", \
"--port=5006", \
"--allow-websocket-origin=*", \
"--num-procs=2"]
7.12 本章小结
这一章的重点不是“会写一个滑块回调”这么简单,而是理解 Bokeh Server 让浏览器里的交互继续连接到 Python。
到这里,你应该已经建立了下面这条主线:
- standalone HTML 适合浏览器端交互和
CustomJS - 一旦需要 Python 回调、周期更新、访问数据库或文件系统,就进入 Bokeh Server 场景
- Bokeh Server 应用本质上是围绕
curdoc()、数据源、控件和回调组织起来的一个 Python Web 应用 - 部署时不仅要考虑“能跑起来”,还要考虑 WebSocket、反向代理、安全、进程管理和资源限制
可以把这一章和前后章节连起来看:
- 本章前半部分是在承接第六章:当
CustomJS不够时,就进入 Bokeh Server - 本章中的
ColumnDataSource更新、stream()、patch()又和第二章是连在一起的 - 本章的部署、嵌入和访问方式,还要结合第八章理解
- 本章里的实时回调、频率控制和数据规模问题,会在第九章进一步展开
7.13 常见坑
坑 1:把 Bokeh Server 当成“高级版 show()”
Bokeh Server 不是简单把 show() 换成 bokeh serve。
一旦进入 Server 模式,你就同时进入了这些问题域:
- 会话状态
- WebSocket 同步
- 回调执行频率
- 多用户访问
- 部署与安全
所以不要把它当成“只是让 Python 回调能跑”的小补丁,而要把它当成一个真正的应用运行环境。
坑 2:所有问题都用 Python 回调解决
很多交互其实根本不需要 Server,例如:
- 改点大小
- 简单过滤显示
- 前端联动
- tooltip 展示
这些场景通常直接用 CustomJS 更轻量,也更容易部署。
经验上可以这样判断:
- 只改前端展示:优先
CustomJS - 需要 Python 参与计算或访问后端资源:使用 Bokeh Server
如果你不确定该怎么选,建议回看第六章。
坑 3:回调能跑,但数据更新方式不对
很多 Server 应用变卡,不是因为 Server 本身“太慢”,而是因为:
- 每次都整体替换
source.data - 定时器频率过高
- 明明只改一个值,却重传整份数据
- 保留了过长的历史数据窗口
更稳妥的思路通常是:
- 追加数据用
stream() - 局部修改用
patch() - 控制
rollover - 把耗时计算和高频 UI 更新分开
这部分建议和第九章一起看。
坑 4:本地能跑,部署后交互失效
这通常不是图表代码本身的问题,而是部署链路的问题。最常见的原因包括:
--allow-websocket-origin没配对- 反向代理没正确转发 WebSocket
- 页面是 HTTPS,但 Bokeh Server 仍然走 HTTP
- 多进程配置和会话状态设计不匹配
所以生产环境部署时,应该把“图表功能正确”和“访问链路正确”分开排查。
坑 5:应用代码越写越大,却没有及时拆结构
一个 Bokeh Server 文件一开始可能只有几十行,但很快就会同时包含:
- 数据加载
- 图表创建
- 控件定义
- 回调逻辑
- 布局组织
- 部署配置假设
如果不及时拆分,后面会很难维护。
建议尽早把代码拆成几类函数:
- 数据准备函数
- 图表创建函数
- 控件创建函数
- 回调绑定函数
- 页面组装函数
这样在继续扩展到多页面、多个数据源或复杂部署时,成本会低很多。
延伸阅读:Bokeh Server 应用的部署和安全见本章 7.10-7.11 节。Server 应用的性能优化见第九章。输出选项见第八章;如果你还想回到前端交互和
CustomJS的场景对比,请回看第六章。