第一次在 Notebook 里贴图,多数人都会先敲下,图是出来了,但一张 4000 像素宽的实验截图直接铺满整个页面,把周围的文字和代码挤得七零八落。等你回过头想把它缩到一半、再往中间挪一挪,才发现标准 Markdown 的图片语法里,压根就没有"尺寸"和"对齐"这两个参数。这不是你语法记错了,而是这套语法从设计之初就没打算管排版——它的定位是"把图放进来",不是"把图摆好看"。
要把图摆好看,就得换几条路走。这篇内容把在 Jupyter Notebook 里插图的几条路线拆开讲清楚:每条路分别能控制什么、尺寸和对齐该怎么写才不会在不同环境下翻车、路径为什么会莫名其妙失效、导出成 HTML 和 PDF 时图去哪了,以及并排图、图注、样式复用这些真正做报告时才需要的排版手法。不管你是刚装完 Jupyter 的新手,还是已经在用 Notebook 写实验记录、数据分析报告、教学课件的老手,下面这些写法都能直接抄走用。
1. 标准 Markdown 语法的能力上限在哪
1.1 感叹号语法只有一个参数
标准 Markdown 的图片语法长这样:。方括号里是替代文字,圆括号里是路径,就这两样,再无其他。你想加个尺寸,写?不认。写{width=500}?那是 Pandoc 的扩展语法,Jupyter 的解析器也不认。想加个对齐,写?那个引号位置是给 title 属性用的,只会变成鼠标悬停提示,跟对齐没有任何关系。
所以第一件要接受的事是:在标准 Markdown 语法里,尺寸和对齐这两个需求从根本上就无处安放。这不是"有个隐藏参数你没找到",而是语法层面就没有位置留给它。你想控制外观,必须借助下面的渲染链路绕出去。
1.2 Markdown 单元格最终变成了什么
理解这条路的关键,是想清楚 Notebook 里一个标记单元格到底被谁渲染。链路其实很短:你写的 Markdown 文本 → 被解析器转成 HTML 片段 → 塞进页面的 DOM 里由浏览器渲染。也就是说,只要能被浏览器认识的 HTML,你就能直接写进标记单元格,Jupyter 会把这一段原样交给浏览器。
这条结论相当重要,它意味着:
<img src="a.png" width="480">能直接用,因为浏览器认识img标签和width属性。<div style="text-align:center">...</div>也能直接用,因为style属性是标准的 CSS 内联写法。<table>、<figure>、<span>、<br>这些标签同样可以混在 Markdown 里写,甚至能和 Markdown 语法嵌套使用(缩进四空格或空行分隔比较稳妥)。
反过来说,凡是浏览器不认识的东西,写进去就是一段死文本,直接显示在页面上。所以判断一个写法能不能用,标准非常简单:把它单独存成一个.html文件用浏览器打开,能正常显示,那在 Notebook 的标记单元格里基本也能正常显示。
1.3 四条插入路线,各自能管什么
在动手之前,先把可选路线摆清楚,避免你在错误的路上反复试探。下面是四条最常用的路径的实际能力边界。
| 路线 | 写在哪种单元格 | 尺寸控制 | 对齐控制 | 典型用途 |
|---|---|---|---|---|
| 标准 Markdown 语法 | 标记单元格 | 不支持 | 不支持 | 快速预览、临时贴图 |
| HTML img 标签 | 标记单元格 | width 属性 / style 宽度 | 父容器居中、块级居中、浮动 | 绝大多数固定排版需求 |
| IPython.display.Image | 代码单元格 | width、height 参数 | 不支持,需外层包装 | 程序化生成、动态图片 |
| display(HTML(...)) | 代码单元格 | 完整 CSS 能力 | 完整 CSS 能力 | 数据流程里动态拼排版 |
这张表建议先记住结论:要排版就用 HTML,要动态就拿 IPython.display 拼一段 HTML 出来。剩下两节分别展开这两条主线,以及路径、导出这些让人抓狂的细节。
2. 用 HTML img 标签一次解决尺寸和对齐
2.1 最小可用写法与 width 的取值规则
最省事的写法就是在标记单元格里直接写 HTML:
<img src="./figs/result.png" width="480">这一行的效果是:图片按 480 像素宽渲染,如果原图比这个大就缩小,比这个小就拉伸到 480。注意这里有个很容易被忽略的点——width属性在 HTML 规范里要求是"非负整数",单位是像素,而且不能带px后缀。你写width="480px",在多数浏览器上会被忽略然后退回原图尺寸,看起来就像"改了没用"。这个坑我踩过,排查了半天才反应过来是单位写错了。
如果要用百分比(比如"占正文宽度的 60%"),属性方式是靠不住的,得换成内联样式:
<img src="./figs/result.png" style="width:60%;">style里的width是 CSS 层面的事,接受60%、32rem、calc(100% - 40px)这类写法。凡是想用百分比、想算动态宽度、想加最大宽度限制,一律走 style,不要走属性。两者的适用场景区分得非常清楚:固定像素用属性,相对尺寸用样式。
2.2 三种对齐写法与可靠性排序
对齐比尺寸更容易翻车,因为img标签本身没有"水平居中"这个属性。下面三种写法我都长期用过,按可靠性从高到低排:
第一种,父容器text-align。img默认是行内元素,所以把它包在一个块级容器里,让容器居中,图片自然跟着居中:
<div style="text-align:center;"> <img src="./figs/result.png" width="480"> </div>这是最稳的一种,兼容性最好,JupyterLab、Notebook 7、VS Code 的 Notebook 编辑器里表现一致。
第二种,块级 + 自动外边距。不想要外层容器的时候,把图片本身变成块级元素:
<img src="./figs/result.png" width="480" style="display:block; margin-left:auto; margin-right:auto;">写法稍长,但好处是图片独占一行,左右不会再有行内元素的空隙干扰,垂直方向也更好控制。
第三种,旧式align属性。像<img src="a.png" width="480" align="right">这种写法在浏览器里依然有效,效果是让图片浮动到右侧、文字在左侧环绕。要提醒的是:这个属性在 HTML5 里已经被标记为过时,而且它的合法取值只有left、right、top、middle、bottom——没有center。很多人凭直觉写align="center",结果就是什么也没发生。想居中,老老实实回到第一种或第二种。
提示:如果图片要右对齐且不希望文字环绕,可以用
<div style="display:flex; justify-content:flex-end;">包一层,这在现在的客户端里比align="right"更好预期。
2.3 只写一个维度的原因
我在培训新人时反复强调一件事:控制图片尺寸时,永远只写宽度,不写高度。理由很直接——图片的宽高比是固定的,你只给宽度,浏览器会自动按比例算出高度;你同时给宽度和高度,只要这两个值的比例和原图对不上,图片就会被拉伸变形,人像变胖、图表文字被压扁。
举个具体例子。假设原图是 1600×900,宽高比 16:9。你写width="400",浏览器自动渲染成 400×225,比例正确。你写width="400" height="400",渲染出来就是一个正方形,图被纵向拉长。所以只有一种情况可以同时写宽高:你确实打算把图片裁成另一个比例,并且接受变形,这种情况其实更适合先用图像工具裁好再来插入。
顺带一个实用参数:如果图片要放大显示(比如一张 400px 的小图标放大到 800px),放大后容易发虚,可以在样式里加image-rendering: pixelated;让像素边界更硬朗;反过来,照片类图片放大时可以加image-rendering: auto;保持平滑。这两个参数在放大部分截图时特别有用。
2.4 边框、圆角、阴影:可复制的样式片段
既然已经用上 HTML 了,一些提升观感的小样式顺手就能加上。下面这几段是我在写实验报告时最常用的:
<!-- 图片加细边框和浅阴影,适合截图 --> <img src="./figs/screenshot.png" width="640" style="border:1px solid #d0d7de; border-radius:6px; box-shadow:0 2px 6px rgba(0,0,0,0.08);"> <!-- 限制最大宽度,防止宽图撑破版面 --> <img src="./figs/wide.png" style="max-width:100%; height:auto;"> <!-- 居中 + 限制最大宽度,最通用的组合 --> <div style="text-align:center;"> <img src="./figs/chart.png" style="max-width:80%; height:auto;"> </div>这里重点说第二段里的max-width:100%和height:auto。这两个是防爆版面的保险丝:无论原图多宽,都不会超出内容区宽度;同时height:auto保证缩放后比例仍然正确。我在写包含几十张图的报告时,会把这两个值当成默认配置,只有确实需要固定尺寸时才覆盖掉。
有一个细节容易被忽略:height:auto在大多数情况下是多余的,因为不写 height 本身就是自动。但当你同时写了max-width又想覆盖某个继承来的高度约束时,显式写出来更保险。这属于"不写也行、写了更稳"的一类防御性写法。
3. IPython.display.Image:代码单元格那条路
3.1 display(Image(...)) 到底渲染出了什么
标记单元格适合手写排版,但有些图是算出来的——训练曲线、生成的对比图、预测结果可视化。这些图的路径只有在运行到那一步才知道,就得在代码单元格里完成插入。
from IPython.display import Image, display display(Image(filename="./figs/curve.png", width=520))这段代码的渲染结果,本质上还是往单元格输出里塞了一个 HTML 的img标签,只不过这个标签是 IPython 帮你生成的。这里的width=520会被写进标签的width属性,所以它遵循前面讲过的规则:单位是像素,不带后缀。想用百分比,只能在外面包一层 HTML。
3.2 三种数据来源的实际差别
Image的构造参数看起来简单,但不同来源的行为差别很大,直接影响你的路径怎么写。
| 传入方式 | 写法 | 路径相对谁解析 | 会不会把图片嵌进 ipynb |
|---|---|---|---|
| 本地文件 | Image(filename="a.png") | 内核进程的当前工作目录 | 会,转成 base64 内嵌 |
| 网络地址 | Image(url="https://.../a.png") | 由浏览器加载 | 不会,存的是链接 |
| 字节数据 | Image(data=png_bytes) | 无路径概念 | 会 |
这里最需要警惕的是第一行。filename参数是在 Python 进程里读文件的,所以它相对于内核的工作目录,而不是相对于 notebook 文件所在的目录。这两个目录在很多情况下根本不是一个地方:如果你是先cd到项目根目录再jupyter notebook,那内核的工作目录通常是根目录;而 notebook 文件可能躺在三层子目录里。于是同一个./figs/a.png,在标记单元格里能显示,在代码单元格里就报FileNotFoundError。
我的做法是,在写路径之前先在代码单元格里确认一次:
import os print(os.getcwd()) print(os.listdir("."))看到实际目录之后,再用os.path.join拼路径,或者干脆用一个相对 notebook 位置固定的变量:
from pathlib import Path fig_dir = Path("figs") display(Image(filename=fig_dir / "curve.png", width=520))用pathlib的好处是跨平台不用操心斜杠方向,Windows 上也不会因为反斜杠被当成转义字符而报错。
3.3 对齐能力缺失的补齐办法
Image本身没有对齐参数,这一点常被抱怨。解决办法是绕开它,自己拼 HTML:
from IPython.display import HTML, display path = "figs/curve.png" display(HTML(f''' <div style="text-align:center;"> <img src="{path}" style="width:70%; max-width:640px;"> </div> '''))这个写法的关键细节在于:这里的src是交给浏览器解析的,所以路径的相对基准又变回了 notebook 文件所在目录。也就是说,同一个路径字符串,用Image(filename=...)和用display(HTML(...))可能会指向两个不同的位置。这个现象第一次遇到会非常困惑,记住一句话就够了:谁读文件,路径就相对谁。Python 读文件看内核工作目录,浏览器读图看 notebook 所在目录。
3.4 什么时候必须放弃 Image
Image有一个明显的代价:filename模式会把图片转成 base64 塞进输出结果里。一张 2MB 的截图转 base64 之后约 2.7MB,如果一次循环里插入二十张,notebook 文件会迅速膨胀到几十兆,保存变慢、打开变卡、git diff直接卡死。
所以我给自己定了一条线:手动排版的成图,一律用标记单元格的 HTML 写法;只有图片内容每次运行都会变的场景,才用代码单元格动态生成。如果动态图确实很多,我会让代码先把图片存到磁盘,再只输出一个相对路径的 img 标签,而不是把字节数据留在输出里:
out = Path("figs/auto") out.mkdir(parents=True, exist_ok=True) fpath = out / f"epoch_{epoch:03d}.png" save_plot(fpath) display(HTML(f'<div style="text-align:center;"><img src="{fpath.as_posix()}" style="width:60%;"></div>'))注意这里用了as_posix(),把 Windows 的\统一换成/,避免路径进 HTML 之后出问题。这个小动作在跨系统协作的仓库里非常值钱。
4. 图片显示不出来的排查链路
4.1 相对路径到底相对谁
前面反复提到路径基准的问题,这里给一个能记住的结论:
- 标记单元格里的
<img src="...">:由浏览器发起请求,基准是 notebook 文件所在的目录。 - 代码单元格里的
Image(filename=...):由 Python 进程读取,基准是内核的工作目录。 - 代码单元格里
display(HTML('<img src=...>')):又回到浏览器解析,基准是 notebook 文件所在目录。 - 导出成 HTML 之后:基准变成导出的 HTML 文件所在的目录。
四条规则里最后一条最容易被忘。导出后的 HTML 如果和图片不在同一个相对关系下,图就全裂了。所以我习惯在项目里保持一个固定结构,把 notebook 和图片放在一起:
project/ report.ipynb figs/ result.png导出时用jupyter nbconvert --to html report.ipynb,HTML 落在同目录,figs/result.png的相对关系不变,图片照样能显示。
4.2 中文名、空格、反斜杠这三个雷
文件名里有空格:<img src="./my fig.png">会被截断成./my。要么把空格换成下划线或连字符,要么把路径做 URL 编码写成./my%20fig.png。我倾向于直接改文件名,省得后面每一处都要记得编码。
文件名里有中文:浏览器一般能正确显示,但在导出、跨平台、命令行工具链(比如某些 LaTeX 转换流程)里容易出问题。经验是项目内的资源文件一律用 ASCII 命名,中文标题写在图注里,不放文件名。
反斜杠路径:在 Windows 上复制来的路径是figs\result.png,写进 Markdown 之后\r可能被解释成回车,写进 HTML 之后也基本不会按预期解析。统一改成正斜杠figs/result.png,在 Windows 上照样能用。
4.3 裂图的四步定位法
图片显示成一个带破图标的占位框时,按下面顺序走,基本能在两分钟内定位。
| 步骤 | 操作 | 判断依据 |
|---|---|---|
| 1 | 在代码单元格执行import os; print(os.getcwd()); print(os.listdir('figs')) | 文件是否真的存在、内核工作目录在哪 |
| 2 | 换成绝对路径再试一次 | 如果绝对路径能显示,问题就在相对基准上 |
| 3 | 打开浏览器开发者工具的 Network 面板刷新页面 | 看那条图片请求返回的是 404 还是 200 |
| 4 | 检查文件名大小写与扩展名 | 服务器区分大小写,.PNG和.png不是一回事 |
第三步特别值得做一次。很多人只看页面上是裂图,就以为是路径写错了,其实有可能是图片太小看不出来、或者样式把width设成了 0(比如width:0%或者某个继承来的max-width把它压没了)。Network 面板里看到 200,说明请求成功了,问题就在样式;看到 404,问题才在路径。先分清是"找不到"还是"看不见",比盲目改路径高效得多。
注意:如果你把 notebook 分享给了别人,对方打开看到裂图,还有一种可能是图片文件没一起发过去。
.ipynb文件里默认只存路径,不存图片内容,除非你用了附件模式。
5. 并排图、图注与图组排版
5.1 用 table 做两图并排
对比实验最常需要两图并排。用表格是最稳的做法,因为表格天然就是等分的块级容器,两个单元格里的图片会各自在自己的格子里适配:
<table style="width:100%; border:none;"> <tr> <td style="width:50%; text-align:center; border:none;"> <img src="./figs/before.png" style="width:90%;"> <div style="font-size:0.85em; color:#57606a;">处理前</div> </td> <td style="width:50%; text-align:center; border:none;"> <img src="./figs/after.png" style="width:90%;"> <div style="font-size:0.85em; color:#57606a;">处理后</div> </td> </tr> </table>几个细节值得解释。图片宽度给90%而不是100%,是为了在两图之间留出视觉间隙,不然两张图会贴在一起,看起来像一张。text-align:center加在td上,让图片和图注一起居中。图注用div而不是<p>,因为p自带上下外边距,会把行高撑得很难看。最后显式写上border:none,因为很多主题会给表格加边框,并排图加了边框很像电子表格,观感不好。
这套写法还有个隐性好处:导出成 PDF 时表格结构通常能被保住,而 flex 布局在部分转换链路里会被拍平。如果你的 notebook 最终要出 PDF,这条是重要参考。
5.2 flex 容器做自适应并排
如果不追求对齐得像表格那么死板,flex 更省事,尤其在图片数量不固定的时候:
<div style="display:flex; gap:12px; justify-content:center; flex-wrap:wrap;"> <img src="./figs/a.png" style="width:30%; min-width:180px;"> <img src="./figs/b.png" style="width:30%; min-width:180px;"> <img src="./figs/c.png" style="width:30%; min-width:180px;"> </div>gap负责间隙,flex-wrap:wrap保证窗口变窄时自动折行,min-width保证折行之后图片不会缩成一条线。写三图并排时,宽度给30%加上gap的占位,刚好能排满一行;给33%就会因为间隙挤下去。
这里有个陷阱:不要把这几张图的宽度设成固定的width="300",那样在窄窗口下会溢出容器,出现横向滚动条。响应式排版里,百分比加min-width的组合几乎总是比固定像素更好用。
5.3 图注的写法与序号维护
图注这件事在 Notebook 里没有原生支持,只能手写。但手写有个麻烦:序号会在插入新图之后全乱。我用的办法是先把序号写成一个固定格式,插入新图时用编辑器的全局搜索替换逐个改。图多了确实会烦,所以如果报告里有十几张图,我会考虑两类替代方案。
一类是用 Python 生成图注。把图片清单和说明放进一个列表,用循环拼 HTML:
figs = [ ("figs/preprocess.png", "数据预处理流程"), ("figs/model.png", "模型结构"), ("figs/result.png", "最终结果对比"), ] html = "".join( f'<div style="text-align:center; margin:12px 0;">' f'<img src="{p}" style="max-width:80%;">' f'<div style="font-size:0.85em; color:#57606a;">图 {i} {c}</div>' f'</div>' for i, (p, c) in enumerate(figs, 1) ) display(HTML(html))序号自动递增,增删顺序改列表即可。这个脚本我用了很久,写教辅材料时尤其省事。
另一类是干脆接受无序号的图注,只在正文里说"上图展示了……"。这在实验记录里完全够用,正式报告再补编号。分清场景,别在内部笔记上花排版的功夫。
6. custom.css 与附件插入
6.1 custom.css 放哪、什么时候生效
如果你每个 notebook 都要写一遍同样的样式,说明该把它抽出来了。经典 Notebook 支持自定义 CSS 文件,位置是:
~/.jupyter/custom/custom.css写好之后重启服务才生效。可以在里面定义全局规则,比如让所有图片默认不超过内容区宽度:
/* ~/.jupyter/custom/custom.css */ .jp-OutputArea-output img, .text_cell_render img { max-width: 100%; height: auto; }这两条选择器覆盖了输出区和标记单元格两个位置,等于给整个 Notebook 装了一道"图片不会撑破版面"的保险。我强烈建议每个刚配好环境的人都加上这两行,它能省掉大量"图又爆了"的返工。
要提醒的是,JupyterLab 和 Notebook 7 的样式体系跟经典 Notebook 差别很大,~/.jupyter/custom/custom.css不一定被读取。如果你的环境是这两个新版本,更省事的做法是在每个 notebook 的第一个单元格里写一段注入样式的代码:
from IPython.display import HTML, display display(HTML(""" <style> .jp-OutputArea-output img, .text_cell_render img { max-width:100%; height:auto; } </style> """))这段代码只在当前 notebook 打开时生效,换个环境就没了,但胜在不用折腾配置文件。
6.2 定义自定义类与模板复用
有了全局样式表,就可以定义自己的类名,把冗长的内联样式收敛成一个短类:
.fig-center { text-align:center; margin:14px 0; } .fig-center img { max-width:80%; height:auto; border-radius:6px; } .fig-caption { font-size:0.85em; color:#57606a; margin-top:4px; }之后写图只需要三行:
<div class="fig-center"> <img src="./figs/result.png"> <div class="fig-caption">图 1 不同参数下的收敛曲线</div> </div>这就是模板化复用的思路:把"位置、尺寸、间距、说明文字样式"打包成一组类,正文里只留内容和路径。报告改版时只改 CSS 一处,几十张图的风格一起变。写长报告,这个投入产出比非常高。
提示:在部分客户端的消毒器配置下,标记单元格里的
<style>块会被过滤掉,类名就失效了。如果发现类不生效,先用内联style兜底,排查是不是被消毒器拦了。
6.3 Edit → Insert Image 的附件模式
经典 Notebook 的菜单里有 Edit → Insert Image(在标记单元格上右键也能找到)。这个功能的效果和前面几种都不一样:它把图片base64 编码后直接写进.ipynb文件,插入的语法是:
好处很明显:notebook 自包含,发给别人不用附带图片目录,换电脑打开也不会裂图。坏处同样明显:
.ipynb文件体积暴涨,一张高清截图能让文件从几百 KB 涨到几 MB。- 版本管理几乎失效。每次替换图片,base64 字符串整段变化,
git diff里是一坨看不懂的乱码,代码评审没法看。 - 想批量替换图片非常麻烦,得重新走一遍插入流程。
我的取舍是:临时分享、单文件交付、图片只有一两张的小 notebook,用附件模式;项目仓库里的正式文档,一律用外链图片目录。这个判断标准很实用,不用纠结。
6.4 和 LaTeX 里 \includegraphics 的思路对应
写过 LaTeX 或者 Overleaf 的人,进 Notebook 最容易带着\includegraphics的思维,结果处处碰壁。把两者的对应关系理一遍,迁移成本会降很多:
| 需求 | LaTeX / Overleaf 写法 | Notebook 对应写法 |
|---|---|---|
| 指定宽度 | \includegraphics[width=0.6\textwidth]{a.png} | style="width:60%;" |
| 固定像素宽 | \includegraphics[width=480px]{a.png} | width="480" |
| 水平居中 | 放进figure环境并\centering | 外层div加text-align:center |
| 并排两图 | subfigure或minipage | table两个单元格或 flex 容器 |
| 图注 | \caption{...}自动编号 | 手动写,或用脚本生成 |
核心差别在于:LaTeX 用"环境 + 参数"来描述排版意图,Notebook 用"HTML 结构 + CSS"来描述渲染结果。前者是文档语义,后者是页面渲染。想通这一点,就不会再去找"有没有一个 center 参数"了——直接问"浏览器怎么居中一个元素",答案自然就有了。
7. 导出、协作与版本管理里的图片
7.1 HTML 正常但 PDF 丢图的原因
jupyter nbconvert --to html report.ipynb导出通常没问题,因为 HTML 导出是把页面结构原样搬过去。但--to pdf走的是另一条路:先把 Markdown 和 HTML 转成 LaTeX,再用 LaTeX 引擎编译成 PDF。这条链路上,HTML 里的样式信息大量丢失——style="width:60%"很可能被丢掉,图片直接按原始尺寸插入,然后撑出页面边界。
如果你的目标产物确实是 PDF,有两个更稳的选择。
第一个是改用webpdf 导出,它用无头浏览器渲染页面再打印成 PDF,CSS 会被完整保留:
jupyter nbconvert --to webpdf --allow-chromium-download report.ipynb第二个是在需要精确控制尺寸的位置,用raw 单元格写 LaTeX。raw 单元格只有在导出目标匹配它的 mimetype 时才会被输出,所以写text/latex的 raw 单元格在 HTML 导出里会被跳过,在 PDF 导出里会被正确处理:
\begin{figure}[h] \centering \includegraphics[width=0.7\textwidth]{figs/result.png} \caption{结果对比} \end{figure}这个技巧的实际价值在于:同一份 notebook 里可以同时维护 HTML 版和 PDF 版的图片写法,导出时各取所需,互不干扰。代价是同一张图要写两遍,所以只在对版式有硬性要求的正式交付物上用,内部记录不必这么麻烦。
7.2 base64 内嵌的取舍
前面提到过Image(filename=...)和附件模式都会把图片转成 base64。这里给一组具体数字感受一下:一张 1.5MB 的 PNG,base64 之后大约 2MB,写进.ipynb就是 2MB 的文本。如果一份报告里嵌了十张,文件就是二十多兆。
判断标准可以简化成三条:
- 需要保证"发过去一定能打开",用内嵌。
- 需要版本管理、需要多人协作改图,用外链目录。
- 图片由代码生成且数量多,一定用外链,让磁盘文件承担体积。
还有一点常被忽略:内嵌之后,原图一改,notebook 不会自动更新,你得重新走一遍插入流程。而外链模式下改磁盘文件即可,notebook 不用动。这是外链在迭代期最大的优势。
7.3 Git 仓库里的目录约定与检查点文件
多人协作的 notebook 项目里,图片目录的约定比技术细节更重要。我用的一套规则是:
project/ notebooks/ analysis.ipynb figs/ analysis/ step1.png step2.png .gitignore要点有三个。图片按 notebook 名字再分一层子目录,避免几十张图挤在一个figs里互相覆盖同名文件。路径统一用相对路径../figs/analysis/step1.png,而不是绝对路径——绝对路径在别人机器上必裂。.gitignore里加上*.ipynb_checkpoints/,避免自动生成的检查点目录被提交进仓库。
另外,如果图片总量超过几百兆,考虑用 Git 的二进制大文件扩展来管理,或者干脆把图片放到对象存储、notebook 里引用链接。普通的纯文本仓库塞进大量二进制文件,克隆会变得非常慢。
7.4 不同客户端里的表现差异
最后说一个实际会遇到的问题:同一段 HTML 写法,在不同客户端里表现不完全一致。
JupyterLab 和 Notebook 7 用的是新的前端渲染管线,对 HTML 的消毒比经典 Notebook 严格一些,某些属性可能被过滤,但img的width、style、常见容器标签都没问题。VS Code 内置的 Notebook 编辑器对 HTML 的支持也比较完整,只是个别主题会覆盖图片的默认样式,遇到显示异常时可以加!important试试。而在终端里运行的编辑器前端,标记单元格的渲染能力受终端限制,复杂的 HTML 排版不一定能完整显示,这种环境下更适合保持写法简单,把复杂排版留到浏览器里看。
结论就是:如果你写的 notebook 需要多人、多环境打开,排版写法尽量保持在"基础 HTML + 内联样式"这个子集里,少用花哨的 CSS 特性,兼容性会好很多。
8. 几段可以直接抄的成品片段
把前面所有内容压成四段模板,用的时候改路径和宽就行。
单图居中、按比例控制,最通用的一段:
<div style="text-align:center; margin:14px 0;"> <img src="./figs/result.png" style="max-width:80%; height:auto; border:1px solid #d0d7de; border-radius:6px;"> <div style="font-size:0.85em; color:#57606a; margin-top:6px;">图 1 实验结果概览</div> </div>固定像素宽、需要精确对齐到某个视觉宽度的场景:
<img src="./figs/icon.png" width="480" style="display:block; margin-left:auto; margin-right:auto;">两图并排带图注,对比实验常用:
<table style="width:100%; border:none;"> <tr> <td style="width:50%; text-align:center; border:none;"> <img src="./figs/before.png" style="width:92%;"> <div style="font-size:0.85em; color:#57606a;">处理前</div> </td> <td style="width:50%; text-align:center; border:none;"> <img src="./figs/after.png" style="width:92%;"> <div style="font-size:0.85em; color:#57606a;">处理后</div> </td> </tr> </table>动态生成图片后自动排版,路径写死会失效时用这一段:
from pathlib import Path from IPython.display import HTML, display def show_fig(path, width="70%", caption=None): p = Path(path).as_posix() cap = f'<div style="font-size:0.85em; color:#57606a; margin-top:6px;">{caption}</div>' if caption else "" display(HTML( f'<div style="text-align:center; margin:14px 0;">' f'<img src="{p}" style="max-width:{width}; height:auto;">{cap}</div>' )) show_fig("figs/curve.png", width="65%", caption="图 2 训练曲线")我个人在实际操作中的体会是,这套东西真正花时间的从来不是标签怎么写,而是路径基准和导出链路这两件事。前者记住"谁读文件,路径就相对谁"这一句就能解决大半;后者只需要提前想清楚最终交付物是 HTML 还是 PDF,然后在动手贴第一张图的时候就选对写法,别等排了几十张图之后再回头改。至于尺寸和对齐,只要养成"宽度用 style 写百分比、对齐靠父容器"的习惯,后面几乎不会再遇到需要临时排查的情况。