Jupyter Notebook 图片插入、大小控制与对齐指南
Jupyter Notebook 里插一张图听起来是再基础不过的操作一行就能解决。可真正把它放进一份要交付的分析报告、一门课的讲义、或者一份机器学习实验记录里时问题就冒出来了图片默认按原始分辨率铺开一张手机截图能顶满整个屏幕想让图居中Markdown 原生语法压根不认导出成 HTML 分享给同事图片变成了一个碎掉的图标。jupyter notebook 插入图片并控制大小和对齐方式这个需求拆开看是三件互相牵连的事——图怎么进来、进来之后多大、摆在哪。任何一个环节没考虑到最后交付的东西都会显得不专业。这篇内容面向所有用 Notebook 干实事的人做数据分析的、跑实验记录的、写教学材料的、拿 Notebook 当个人知识库的不管你是刚学会新建单元格的新手还是已经能熟练用nbconvert批量导出的老手下面这些手法都能直接用上。我会从 Jupyter 的渲染管线讲起说清楚为什么原生 Markdown 做不到尺寸和对齐控制然后给出 HTML 标签、代码显示、base64 内嵌这几条路线配上尺寸换算过程、对齐的四种写法最后把导出兼容性和一堆踩过的坑整理成速查表。目标只有一个让你以后往 Notebook 里放图不用再靠试。1. 先搞清楚 Notebook 里的图片到底由谁渲染很多人卡在图片样式上根源是没分清单元格的类型差异。Markdown 单元格和 Code 单元格走的是两条完全不同的渲染链路能用的语法、能生效的样式都不一样。把这条链路理清楚后面所有操作都是顺水推舟。1.1 Markdown 单元格与代码单元格的渲染差异Markdown 单元格的内容会先被解析成 HTML再交给前端的渲染层显示。这个转换过程支持标准 Markdown 语法比如同时也允许你直接写裸 HTML——这一点非常关键意味着img、div、table这些标签在 Markdown 单元格里是合法且会被渲染的。但注意Markdown 解析器对 HTML 的处理是透传它不会帮你做任何样式增强你写多少属性浏览器就按多少属性渲染。Code 单元格则不同。它执行 Python输出结果由 IPython 的输出格式化器接管。文本走text/plain而IPython.display.Image这类对象会输出富媒体表示rich representation前端根据 MIME 类型选择渲染方式。换句话说代码单元里图片的尺寸是你在 Python 对象里指定的不是在 HTML 里指定的。这个区别决定了两件事想精细控制样式边框、间距、对齐走 Markdown 单元写 HTML 更直接想让图片由数据动态生成、批量处理、随计算结果变化走代码单元更合适。现实中我通常是混着用——示意图、流程图这种静态资源放 Markdown模型输出、可视化结果这种动态内容走代码。1.2 路径解析相对路径到底相对谁这是新手最容易翻车的地方。Jupyter Notebook 的相对路径基准是Notebook 服务启动时的工作目录或者更准确地说是当前 kernel 的工作目录一般等于你启动jupyter notebook命令时所在的目录而不是.ipynb文件所在的目录。比如你在~/work下执行启动命令然后打开~/work/project/report.ipynb此时相对路径images/a.png会被解析成~/work/images/a.png而不是~/work/project/images/a.png。这就是为什么很多人明明把图和 notebook 放在同一个文件夹图片却加载不出来。稳妥的做法有三种我按推荐程度排用绝对路径临时验证最快但不适合分享/Users/you/work/project/images/a.png自己能跑别人拿到就废。启动时切到 notebook 所在目录cd ~/work/project jupyter notebook之后相对路径就和 notebook 同级了这是我最常用的方式。代码里动态定位在 notebook 开头跑一段设置工作目录的代码保证路径基准稳定。import os # 把工作目录切到 notebook 文件所在目录路径基准就固定了 notebook_dir os.path.dirname(os.path.abspath(__file__)) if __file__ in globals() else os.getcwd() os.chdir(notebook_dir) print(当前工作目录:, os.getcwd())注意如果先用 Markdown 单元写了图片、后来才切换工作目录需要重新执行该 Markdown 单元双击进去按 ShiftEnter才会刷新渲染。2. 原生 Markdown 图片语法能走多远先把基线摸清楚。知道了原生语法的能力边界才能判断什么时候必须上 HTML。2.1 基础语法与实际表现标准写法就这一种渲染出来是一个imgsrc指向路径alt是替代文字。它的显示宽度默认等于图片的固有像素宽度但在容器宽度不够时会被压缩到容器宽度高度按比例自适应。行为上等价于 CSS 里的max-width: 100%; height: auto;。有一个不太常见的扩展写法用标题位传尺寸部分 Markdown 渲染器支持这里的引号内容会被解析成title属性鼠标悬停时显示提示文字跟尺寸无关。Jupyter 默认的 Markdown 解析器不认{width300}这类属性扩展语法那是某些静态站点生成器的方言你写了它只会当成普通文字显示出来。这一点我实测过很多次别抱侥幸。2.2 为什么纯 Markdown 控制不了尺寸和对齐Markdown 的设计哲学是内容与表现分离。它只描述这里有一张图不描述这张图多宽、摆哪。尺寸和对齐属于表现层归 CSS 管Markdown 语法里没有对应字段所以无论你怎么折腾方括号圆括号的组合都表达不出width400或者居中的意思。对齐更是如此。img是行内元素inline element它的水平位置由父容器的文本对齐方式决定。Markdown 段落默认左对齐所以你插的图默认贴左。想改就得引入能承载text-align的块级容器——而 Markdown 语法本身没有。所以结论很干脆要控制尺寸或对齐就必须用 HTML 标签或代码方式。原生语法只适合能把图显示出来就行的场景。3. 用 HTML 标签精准控制图片尺寸好在 Markdown 单元格允许裸 HTML这扇门一开尺寸控制就完全自由了。下面三种写法各有适用场景我按从粗到细的顺序说。3.1 img 标签的三种尺寸写法与取舍写法一width/height 属性像素img srcimages/result.png width480这是最省事的写法width只写一个高度会自动等比缩放。缺点是像素值写死换到窄屏比如手机上看导出的 HTML还是可能溢出。写法二内联 style百分比img srcimages/result.png stylewidth:60%; height:auto;百分比相对父容器宽度计算天然适配不同屏幕。我一般做报告都用这个容器是 notebook 的输出区宽度随窗口变图也跟着变不会撑破。写法三max-width 限制上限img srcimages/result.png stylemax-width:600px; width:100%; height:auto;这个组合兼顾了两头容器宽的时候最多 600 像素不至于太大容器窄的时候自动缩到 100%不溢出。这是我个人最推荐的默认模板写一次存成代码片段以后到处粘贴。三种写法的对比如下写法尺寸基准溢出风险适用场景width480固定像素窄屏会溢出快速查看、内部草稿stylewidth:60%父容器百分比无响应式报告、网页分享max-width:600px; width:100%两者结合无通用默认、正式交付3.2 尺寸怎么算从原图分辨率倒推显示宽度尺寸不是拍脑袋填的尤其当你要保证多张图视觉统一时。核心公式只有一个显示高度 原图高度 × (显示宽度 ÷ 原图宽度)举例一张原图 1920×1080 的截图你想放在宽度 800 的内容区里希望它占一半宽度目标显示宽度 800 × 0.5 400 像素显示高度 1080 × (400 ÷ 1920) 225 像素所以写stylewidth:400px;就够了高度不用管。再比如 4 张图要并排内容区宽 800每张占据 1/4 还留点间隙那么单张宽度控制在 180 上下比较舒服stylewidth:180px;。如果你的图原始宽度小于目标显示宽度那就别放大了——放大只会让图变糊。这时候应该改用max-width让它在小图时保持原样stylemax-width:400px; width:100%;。实操心得我习惯在 Notebook 里开一个隐藏的单元格专门记内容区宽度不同分辨率显示器上量一次记一次。写尺寸参数时直接照抄省得每张图都去试。3.3 让图片不撑破布局的三个细节第一永远给 height 留 auto。只要你写了width又手动写了固定height变形就来了。除非你明确知道要裁剪成特定宽高比否则高度交给浏览器算。第二注意高分屏Retina的观感。同样 400 像素宽的显示区域在 2 倍屏上实际需要 800 像素的图源才够清晰。所以图源别压缩得太狠宁可原图大一点、显示时缩小也不要原图就小、显示时放大。第三竖版长图和横版图区别对待。竖版长图比如手机截图、长流程图不要用width:100%那样高度会吓死人整屏都放不下。竖图更该限制heightimg srcimages/long.png styleheight:400px; width:auto;4. 对齐方式的四种落地手法尺寸搞定接着是位置。因为img是行内元素单靠它自己没法居中必须靠外层容器或者 CSS 手段。下面四种方法我都长期用过各有各的舒服场景。4.1 div 包裹加内联样式最通用的居中法div styletext-align:center; img srcimages/result.png stylemax-width:500px; width:100%; height:auto; /div原理很简单div是块级元素占满整行text-align:center让它的行内内容也就是 img水平居中。这是我最常用的方式兼容性最好导出 HTML 也不出幺蛾子。想右对齐就改成text-align:right左对齐text-align:left。三行代码覆盖全部需求。4.2 flex 布局同时控水平和垂直div styledisplay:flex; justify-content:center; align-items:center; img srcimages/result.png stylewidth:300px; /divjustify-content管水平center / flex-start / flex-end / space-betweenalign-items管垂直。需要并排多张图并均匀分布时flex 比 text-align 更合适div styledisplay:flex; justify-content:space-between; img srcimages/a.png stylewidth:30%; img srcimages/b.png stylewidth:30%; img srcimages/c.png stylewidth:30%; /div三张图等距排一行间距由space-between自动算不用手写 margin。4.3 表格法对齐老派但稳得离谱table tr td aligncenter img srcimages/result.png width400 /td /tr /table这写法在今天看来有点原始但它有两个别人比不了的好处在导出成 PDF 或某些邮件客户端渲染时表格布局的兼容性远好于 flex另外一张图配一行图注时表格能天然做到图与文字一起居中table tr td aligncenter img srcimages/result.png width400br em图 1模型收敛曲线/em /td /tr /tablebr换行加斜体图注整体被 td 的居中带着走。写实验报告时我用这个最多——图注必须跟图一起居中否则看着别扭。4.4 代码单元格里的对齐控制代码单元里display(Image(...))出来的图默认是块级居中Jupyter 给输出区加了居中样式。但如果你想自定义位置可以借助 HTML 包装from IPython.display import Image, HTML, display img_html img srcimages/result.png stylemax-width:500px; height:auto; display(HTML(fdiv styletext-align:center;{img_html}/div))注意这里传给HTML的是字符串src用相对路径同样受工作目录影响。这个套路的好处是可以把对齐方式参数化配合循环批量出图def show_centered(path, width500, captionNone): html fdiv styletext-align:center; html fimg src{path} stylemax-width:{width}px; width:100%; height:auto; if caption: html fbrem{caption}/em html /div display(HTML(html)) show_centered(images/result.png, width460, caption图 1损失曲线)一个函数把尺寸 居中 图注全包了后面几十张图重复调用即可样式绝对统一。这种小工具函数是我最推荐沉淀到个人 snippet 库里的东西。5. 代码路线IPython.display 与 base64 内嵌有些场景 HTML 标签解决不了比如图是运行时生成的、需要批量循环、或者要保证导出后不丢图。这时候就得靠代码。5.1 Image 对象的宽高参数from IPython.display import Image, display display(Image(filenameimages/result.png, width480))width和height接受整数像素或者字符串比如60%部分版本支持。只给width时会等比缩放。这个方式适合对已经落地的图片文件做统一展示。如果图片在内存里比如 matplotlib 画完还没存盘可以用retinaTrue提高清晰度display(Image(datapng_bytes, width480, retinaTrue))retinaTrue会让图片以两倍分辨率渲染、再缩回指定宽度在 2 倍屏上看锐利很多。这个参数很少人知道做精细报告时很值。5.2 循环批量展示并统一尺寸实验里经常要对比多组结果一屏放 6 张图。手写 6 段 HTML 太累循环更省事from IPython.display import HTML, display paths [fimages/run{i}.png for i in range(1, 7)] cells [fimg src{p} stylewidth:31%; margin:1%; for p in paths] grid div styledisplay:flex; flex-wrap:wrap; justify-content:flex-start; .join(cells) /div display(HTML(grid))flex-wrap:wrap保证放不下时自动换行width:31%配margin:1%刚好一行三张。这套三列网格布局我几乎在每个对比实验里都用比一张张手动插效率高一个量级。5.3 base64 内嵌让图片跟着 notebook 走前面所有方法都有一个共同前提——图片文件得在。一旦你把.ipynb单独发给别人或者导出成自包含 HTML路径就可能断掉。解决办法是把图片编码进文档本身import base64 from IPython.display import HTML, display def embed_image(path, width480, aligncenter): with open(path, rb) as f: b64 base64.b64encode(f.read()).decode(ascii) ext path.rsplit(., 1)[-1].lower() mime {png: image/png, jpg: image/jpeg, jpeg: image/jpeg, gif: image/gif}.get(ext, image/png) return ( fdiv styletext-align:{align}; fimg srcdata:{mime};base64,{b64} stylemax-width:{width}px; width:100%; height:auto; f/div ) display(HTML(embed_image(images/result.png, width460)))图片变成一长串 base64 字符串写进 notebook好处是单文件自包含、随便怎么传都不丢图代价是.ipynb体积迅速膨胀——一张 200KB 的 PNG 编码后大约 270KB 文本几十张图就能把文件顶到十几 MB。所以我的原则是草稿阶段用路径交付阶段再批量嵌 base64。而且嵌入前最好先用工具把图压一遍能省不少体积。注意Image(data...)也可以直接接收 base64 字节效果等价但控制对齐不如 HTML 灵活所以我更偏向 HTML 这条路。6. 导出与兼容性别等交报告时才发现图没了Notebook 里的效果是一回事导出后的效果是另一回事。这一步不提前测交付时必然翻车。6.1 导出 HTML 时图片的去向jupyter nbconvert --to html report.ipynb默认不会把图片打包进去它只在 HTML 里保留src路径。对方打开时如果图片不在相对同一位置就是一堆碎图标。三个应对方案加--embed-images新版 nbconvert 支持把本地图片转成 data URI 内嵌导出的 HTML 完全自包含。命令是jupyter nbconvert --to html --embed-images report.ipynb。手动走 base64 路线上一节的方法优点是过程透明可控。图片和 HTML 一起打包成 zip 交付顺手但不够优雅。我通常用第一个一条命令解决实测下来最稳。6.2 转 PDF / LaTeX 时的注意点--to pdf走的是 LaTeX 渲染链。问题在于LaTeX 不认识 flex 布局你写的display:flex在转 PDF 时会失效或报错。这时候前面提到的表格法 (tabletd aligncenter) 就成了救命稻草它在 LaTeX 里会被转成对应的表格环境居中效果保留得最好。尺寸上百分比宽度在 LaTeX 里也经常被忽略。要转 PDF 的文档建议尺寸统一写成像素固定值对齐统一用table把兼容性拉满。这个经验是我被 PDF 里跑偏的图坑过好几次之后才总结出来的。输出目标尺寸写法对齐写法备注Notebook 预览百分比 / 像素都行div / flex最自由导出 HTMLmax-width 百分比div / flex加--embed-images转 PDF / LaTeX固定像素table td align避免 flex7. 常见问题与排查技巧实录最后这部分是我这些年攒下来的问题清单几乎覆盖了所有图不听话的情况。7.1 图片不显示的完整排查表按从快到慢的顺序挨个排现象最可能原因解决动作显示碎图标路径不对打印os.getcwd()核对基准目录显示 alt 文字文件名拼错/大小写不符注意 Linux 下大小写敏感空白无任何提示HTML 标签写错未闭合检查img、div配对时好时坏工作目录被前面代码改了把切目录代码放最前面导出后不显示图片没跟着打包用--embed-images或 base64路径问题占了我遇到的所有图不显示里的八成以上。一个好习惯在 notebook 开头固定输出一次当前工作目录出问题时一眼就能对照。7.2 尺寸和对齐不生效的几种情况尺寸写了没变化多半是同时写了width和height两个固定值或者 CSS 优先级被上层样式覆盖。去掉多余属性只留一个max-width试。居中没效果检查img是不是被p单行包裹了。Markdown 会把独立成行的 HTML 块包进段落而段落的text-align可能覆盖你的设置。把div和img写在同一逻辑块里中间别留空行。图片被拉变形手动指定了固定高度却没管宽度比例。规规矩矩写width让高度 auto。竖图占满整屏没限制高度。竖版图用height:400px; width:auto;而不是宽度百分比。7.3 几条我踩出来的私房经验经验一把常用模板存成 snippet。我在 snippets 里存了三个模板——横图响应式、竖图限高、图配图注居中。写报告时直接调出粘贴不用每次从零敲属性。这个习惯至少省了我一半的排版时间。经验二图源统一命名加序号。fig01_xxx.png、fig02_xxx.png这种好处是循环批量插入时排序稳定路径也容易记。别用带空格和中文的文件名URL 编码后容易出问题。经验三大图先压缩再插入。一张 5MB 的原始截图放进 notebook翻页会明显卡顿。用图像工具压到 200KB 以内、分辨率控制在 1600 像素宽肉眼看几乎无损滚动丝滑得多。经验四对齐方式一次定全局。一份文档里图的对齐要统一要么全居中要么全左对齐配图注。混着来会显得很随意。我通常报告类全居中技术笔记类全左对齐配图注。经验五交付前一定走一遍完整导出。我自己就吃过亏Notebook 里看着完美导 PDF 后图飘了、图注错位。现在我的流程固定是——写完先导一遍 HTML 和 PDF两个都检查过再交。关于jupyter notebook 怎么生成 markdown 目录语法这个被问得很多的关联问题顺带说一句HTML 锚点在这里也有用武之地。你可以在图片外面套一个带id的容器div idfig1 aligncenter.../div然后在文档开头用[跳到图1](#fig1)建立内部链接长文档里用它做图片索引跳转相当方便和 markdown 目录是同一套锚点机制。至于那些jupyter notebook 打不开单元格执行没反应无法运行之类的环境问题和图片排版是两条线通常跟内核状态、依赖冲突、启动目录有关等哪天专门写一篇环境排查的。图片这块把路径、尺寸、对齐这三件套吃透再算上导出兼容性基本就能覆盖日常全部需求了。我个人的体会是别小看这些排版细节——同样一份分析结果图整齐、尺寸统一、位置规整的那份看起来就是更可信。