首页 / 资讯中心 / 文章详情

.ipynb转.py实用指南:Jupyter导出、nbconvert批量与jupytext双向同步

.ipynb转.py实用指南:Jupyter导出、nbconvert批量与jupytext双向同步 ★ FEATURED ARTICLE
手里拿着一个 .ipynb 文件想把它当成普通 Python 脚本来用——丢到服务器上跑定时任务、放进项目仓库做代码评审或者就是习惯了在 IDE 里看代码这个需求我遇到过太多次了。每次都有新人跑来问直接把后缀改成 .py 行不行答案是显而易见的但我猜你在问之前已经悄悄试过一次了。今天这篇就专门讲 .ipynb 转 .py 这件事从最无脑的图形界面导出一路到命令行批量转换、再到长期维护更友好的双向同步方案把每一步的坑和体验都摊开讲。不管你是在校学生处理课程实验还是工程师要把同事发来的 notebook 收编进项目这篇文章都能让你少走几圈弯路。我会先解释两种文件的本质区别再给四套方案最后附上高频踩坑记录。内容不追求大而全只求每一句都是实操里用得上的。1. 先搞清楚 .ipynb 和 .py 的本质区别再谈转换1.1 .ipynb 不是代码文件而是一个 JSON 容器很多人第一次拿到 .ipynb 会下意识双击打开结果发现系统根本不知道拿什么程序关联它。这是因为 .ipynb 全称是 Jupyter Notebook 文件它的本体是一个 JSON 文档用来承载代码和代码的产出物以及说明文字的复合内容。你可以用任何文本编辑器打开一个 .ipynb 看一眼会看到类似下面这样的结构{ cells: [ { cell_type: markdown, metadata: {}, source: [ # 这是标题\n, 这里是说明文字 ] }, { cell_type: code, execution_count: 3, metadata: {}, outputs: [ { name: stdout, output_type: stream, text: [ hello world\n ] } ], source: [ print(hello world) ] } ], metadata: { language_info: { name: python } }, nbformat: 4, nbformat_minor: 5 }这里有几个关键字段你得眼熟cells所有内容单元每个单元由cell_type区分是 markdown 还是 code。source单元格里的源码文本。outputs代码执行后产生的输出、图表、富文本结果这部分是导成 .py 时会被丢弃的。metadata内核信息、语言信息、单元格运行状态等杂项。nbformatnotebook 格式版本目前主流是 4。理解了这层结构你就明白了Notebook 从设计上就不只是能运行的代码它是一个把代码、运行结果、分析思路打包在一起的互动文档。而 .py 是纯文本的 Python 源码文件解释器直接按语句顺序执行没有任何单元格的概念。1.2 直接把后缀改成 .py 会发生什么这是新手最常踩的坑我一律建议先别看答案自己亲手试一次印象深刻。把demo.ipynb改名为demo.py然后在命令行执行python demo.py你会看到一大片语法错误。因为 Python 解释器拿到的是一堆 JSON 字符串里面全是引号、冒号、方括号、花括号而 Python 语法要求这些符号有特定语义。最典型的是缩进报错和字符串引号不匹配错误信息可能指向文件第一行也可能指向第几百行完全看命。也就是说.ipynb 是用 JSON 打包的文档容器.py 是纯代码文本两者不能靠改后缀互通。正确的思路是把 notebook 里的source字段提取出来重新拼成一个符合 Python 语法的文本文件也就是做一次解包。1.3 转换的本质从文档容器变成纯文本代码转换的核心逻辑其实是三件事第一遍历所有 cells把cell_typecode的源码依次拼在一起保持它们在 notebook 里的顺序。第二把cell_typemarkdown的内容变成注释通常是逐行加#这样既能保留分析思路又不会干扰脚本运行。第三丢弃outputs字段因为脚本文件没有执行记录区。你只要用几十行 Python 代码配合标准库 json 就能手工实现这个过程很多公开的转换脚本本质就是这个思路。但现实中你不需要自己写因为官方生态里已经有成熟的工具链接下来四章分别介绍。2. 不想装任何东西的话用 Jupyter 自带的导出功能2.1 点几下鼠标就能完成如果你手里有 Jupyter Notebook 或者 JupyterLab 环境最简单的方案就在界面里。打开目标 notebook按照下面的路径操作Jupyter Notebook 经典界面File→Download as→Python (.py)JupyterLab 界面File→Save and Export Notebook As...→Python点完之后浏览器会下载一个同名但后缀为 .py 的文件整个过程不超过十秒钟。转换出来的文件内容非常规整code 单元格就是源码markdown 单元格被处理成带#的注释单元格之间还有# In[n]:这样的窗口分隔标记。这个功能背后调用的其实就是 nbconvert 的内核所以输出质量很高不是简单的字符串拼接。2.2 这个方案的优缺点它的优点只有一个零配置、零学习成本任何人打开 Jupyter 都能操作。但缺点也很明显每次转换都需要手动打开浏览器点菜单如果手里有几十个 notebook 要批量处理点一百多下鼠标想想都崩溃。另一个局限是下载这个动作把文件存到了浏览器默认下载目录不像命令行工具能精确控制输出位置。所以我的建议是文件数量少、只转一次、身边正好有 Jupyter 环境用这个方案足够。但凡涉及批量、自动化、或者要嵌进自己的工作流里直接跳到第三章的命令行方案。2.3 一个很多人不知道的技巧%save 魔法命令如果你正在 Jupyter 里编辑 notebook只想把某几个单元格导出成 .py而不想要全部单元格这里有个隐藏操作。在 notebook 里新建一个单元格输入%save output_part.py 1 2 3-5这条魔法命令会把第 1 个、第 2 个、第 3 到第 5 个单元格保存到当前工作目录下的output_part.py文件。注意这里的编号是单元格的执行序号不是它在 notebook 里的排列序号。你可以通过 In[序号] 左侧的标签判断。这个技巧最大的价值是精准导出适合你要把一个 notebook 里的精华代码单独抽出来做工具模块的场景。缺点是会连带生成一些注释文本而且不保留 markdown 单元格至于格式美观度就别太指望了。3. 命令行扛把子jupyter nbconvert 的使用手册3.1 一条命令完成单个文件转换nbconvert 是 Jupyter 官方提供的文档转换工具也是前面图形界面导出功能的后台引擎。只要你的环境里能跑jupyter命令说明 nbconvert 已经包含了它是 Jupyter 生态自带组件。单个文件转换的完整命令jupyter nbconvert --to script demo.ipynb执行完成后当前目录会生成demo.py终端还会输出一句执行日志。打开生成的 .py 文件你会看到大概这种格式#!/usr/bin/env python # coding: utf-8 # In[1]: import pandas as pd # In[2]: print(hello)注意三点第一行是 shebang方便在 Linux/macOS 下直接当脚本执行。# coding: utf-8声明编码虽然 Python3 默认就是 UTF-8但保留声明能避免老工具链误判。最显眼的是# In[1]这种标记它对应原 notebook 里的单元格编号相当于一种锚点注释能帮你在代码和 notebook 之间建立对应关系。这个转换是纯文本级别的操作不执行任何代码所以速度快、无副作用。哪怕 notebook 里的代码本身有语法错误转换也会正常完成——它只是提取文本不检查语法。3.2 批量转换和输出文件控制真正让 nbconvert 和图形界面拉开差距的是它在批量和自动化场景下的灵活性。一次转换目录下所有 notebookfor f in *.ipynb; do jupyter nbconvert --to script $f done在 Windows PowerShell 里对应写法是Get-ChildItem -Filter *.ipynb | ForEach-Object { jupyter nbconvert --to script $_.Name }如果想自定义输出文件名用--output参数jupyter nbconvert --to script demo.ipynb --output my_script生成的文件就是my_script.py而不是demo.py。还可以指定输出目录jupyter nbconvert --to script demo.ipynb --output-dir ./converted这几个参数组合起来基本能满足任何批量处理需求。我自己平时最常用的是先批量转换再配合 grep 检查生成的代码里有没有残留的魔法命令相当于给转换结果过一次安检。3.3 自定义模板把转换结果敲干净默认生成的 .py 里有#!/usr/bin/env python、# coding: utf-8、# In[1]这些注释。绝大多数情况下它们是无害的甚至有利于定位代码来源。但如果你要把转换结果直接提交到公司代码仓库或者分发给不太懂这些标记的同事有些人会觉得它们碍眼。这时候有两种处理思路。第一种接受这些标记。它们就是注释不影响运行运行时会自动忽略。第二种自己写一个 Jinja2 模板覆盖默认行为。nbconvert 的 script 转换支持自定义模板文件比如创建一个clean.tpl{%- extends script.j2 -%} {%- block header -%} #!/usr/bin/env python {%- endblock header -%} {%- block in_markdown -%} {# 去掉 In[] 标记 #} {%- endblock in_markdown -%}然后指定模板转换jupyter nbconvert --to script demo.ipynb --template clean.tpl注意clean.tpl需要放在当前目录nbconvert 会按相对路径找。这个做法的问题是模板语法得现学现查对不是搞前端的人有点费神。我建议除非团队有硬性代码格式要求否则保留默认标记完全够用核心代码逻辑一点都不会丢。3.4 转换后的脚本能不能直接跑这是绕不开的问题。转换只是把文本搬了个家能不能跑取决于 notebook 里的代码本身。有几种情况需要特别关注。魔法命令。以%和%%开头的 IPython 魔法命令比如%matplotlib inline、%timeit、%%capture在普通 Python 脚本里很多是不能用的。转换器不会帮你删除或改写这些命令原样放进 .py 里运行时就会报错。路径问题。notebook 里如果用了相对路径比如pd.read_excel(data/xxx.xlsx)转换后的脚本假设你从当前目录运行脚本路径才能对上。如果你把 .py 挪到别的目录再跑路径就断了。输出丢失。notebook 里保存的图片、表格、交互组件这些输出不会进入 .py因为脚本本身没有输出记录区。你只能重新运行代码让 print 重新在终端输出。转换后的脚本最常见的问题是魔法命令这个在第五章统一讲怎么处理。4. 长期使用的最优解jupytext 双向同步4.1 为什么单向转换不够用前两章讲的方案本质上都是一次性的单向转换——构成了 .py 之后你再修改 notebook.py 不会自动跟着变。反过来如果你在 IDE 里改了 .pynotebook 里的代码也不受影响。这意味着同一个项目里出现两份代码改着改着就分叉了。对于长期项目来说这非常痛苦。你会有选择困难症改哪个以哪个为准git 提交的时候两个文件都变动了reviewer 看哪份我见过不少项目因为维护两套代码导致手动同步遗漏最后爆出线上跑的代码和 notebook 里根本不是一回事的问题。解决思路是让 .ipynb 和 .py 配对存在一方修改另一方自动同步。这就是 jupytext 干的事。4.2 安装和两种实用的工作流jupytext 是一个开源工具安装非常简单pip install jupytext装完推荐使用第一种工作流一次性转换效果类似 nbconvert但生成的 .py 更干净jupytext --to py demo.ipynb生成的demo.py顶部会多一个 YAML 元数据头类似--- jupyter: jupytext: formats: ipynb,py text_representation: extension: .py ---这个头是 jupytext 识别配对关系的关键别手欠删掉删了之后就失去双向同步能力了。第二种工作流是配对模式分两步走。先设置配对格式并同步jupytext --set-formats ipynb,py --sync demo.ipynb这会在当前目录生成demo.py同时给demo.ipynb的 metadata 写入 jupytext 配置。之后每次你保存 notebook对应的 .py 会自动更新。反过来你直接用编辑器修改 .py再执行同步命令jupytext --sync demo.py对应的 .ipynb 也会跟着变。这个保存即同步的效果需要让 Jupyter 使用 jupytext 的文件管理器才会真正顺手具体做法是编辑 Jupyter 配置文件jupyter_notebook_config.py加上两行c.NotebookApp.contents_manager_class jupytext.TextFileContentsManager c.ContentsManager.default_jupytext_formats ipynb,py加了之后Jupyter 里所有新建的 notebook 都会自动生成配对 .py 文件保存时自动同步。对于以文本编辑器为主场的开发者这个配置能让 Git 世界里的代码审查回到一个舒服的节奏。4.3 我在项目里是怎么用 jupytext 的分享一个实际项目经验。团队里有人习惯用 notebook 做数据分析原型但生产环境的调度框架只能跑 .py。以前是 notebook 完事之后手工导出 .py再手动改魔法命令、改路径每次发布都心惊胆战。后来配了 jupytext开发者在 notebook 里写代码保存即同步 .py运维从 git 拉下代码用 .py 部署评审 diff 直接用 .py 看逻辑清晰没有 JSON 噪音。整个流程跑下来最大感受是再也用不着导出这一步了两边始终是同一份代码的两种表现形式。jupytext 还支持把 notebook 映射成 Markdown 或 R 脚本原理一样如果你有跨语言协作需求可以查阅官方文档但这篇文章聚焦 Python不多展开。5. 转换过程中的高频坑和不传经验5.1 魔法命令在 .py 里失效这是转换后报错频率最高的原因几乎每个用过%matplotlib的人都会遇到。看一下常见魔法命令在纯 Python 脚本里的命运魔法命令作用转成 .py 后的表现%matplotlib inline让图表显示在 notebook 内脚本中无效需改为plt.show()%timeit计时单行语句直接报错建议用timeit模块替代%%time计时整个单元格直接报错换成time模块%%capture捕获单元格输出直接报错需要删除后重新设计输出逻辑!ls执行 shell 命令可以运行但注意路径和输出格式%load_ext autoreload自动加载外部模块脚本里可保留不报错处理策略分两种。一种在转换前就在 notebook 里少用魔法命令把%matplotlib inline直接删掉用plt.show()替代。另一种是转换后全局搜索%开头的行逐行评估能删则删能换则换。grep -n ^% output.py这条命令快速列出所有带魔法命令的行号处理完再运行基本就没有这块的报错了。5.2 中文字符编码问题如果你在国内 Windows 环境工作转换后的 .py 文件用默认的 cp936GBK编码读取时遇到 emoji 或生僻字可能直接抛UnicodeDecodeError。Jupyter 生成的 .py 是 UTF-8 编码而老版本 Windows 控制台默认编码不是 UTF-8。排查思路先用文本编辑器打开看有没有乱码如果文件本身是好的那就是运行环境的问题。临时解决办法是脚本开头加import sys import io sys.stdout io.TextIOWrapper(sys.stdout.buffer, encodingutf-8)或者转换后统一把编码转成 GBK 也是办法。不过现在 Python3.7 和 Windows 新版系统对 UTF-8 的支持已经好很多建议优先升级环境而不是改文件编码。5.3 输出结果不见了这不是 bug很多人在转换后跑脚本发现原来 notebook 里的 result 图形没出现。这是正常行为因为 .py 文件不保存任何执行结果它只有源码。notebook 里的图表、DataFrame 预览、交互空间在转换时全部丢弃。能做的不是在转换工具里找选项而是在代码里把结果显式输出。比如原来 notebook 里最后一格只有一个变量名df就可以显示表格在 .py 里你得手动加print(df)才能看到同样内容。图表同理必须调用plt.show()或fig.savefig()。5.4 乱序执行依赖这个坑最隐蔽notebook 允许你跳着执行单元格比如先跑后面的代码再回来跑前面的。如果开发者在交互式探索中大量使用这种乱序执行最终 notebook 的执行状态和单元格顺序是不一致的。转换后的 .py 是按照单元格排列顺序从上到下执行的。如果 A 单元格依赖 B 单元格产生的变量而 A 在 B 前面脚本一开始就报NameError。我自己处理这个问题的方法是先重启内核然后Run All从头跑一遍 notebook确认能顺利通过再做转换。这个跑通则转的习惯能规避九成以上乱序依赖问题。如果重启后Run All本来就跑不通那问题不在转换先把 notebook 改成顺序可执行状态再转。5.5 转换问题速查表现象原因解决办法改后缀后 Python 报语法错误.ipynb 是 JSON 不是源码用 nbconvert 或 jupytext 转换生成的 .py 运行报Magic command not found魔法命令不被 Python 支持删除或替换为等价标准库代码图表一片空白或者说没有显示notebook 输出不保存在 .py在代码里加plt.show()或保存图片中文字符乱码或编码异常UTF-8 与本地默认编码不一致设置环境编码或在脚本开头声明 UTF-8脚本一启动就NameErrornotebook 乱序执行依赖先重启内核 Run All 通过后再转换转换后的文件路径找不到数据相对路径随脚本位置变化统一使用绝对路径或以脚本位置定位每次想要 .py 都得手动导出工作流不够自动化换 jupytext 配对模式6. 到底该选哪种方案结合场景的选型建议看了上面四套方案你可能会有点选择困难。其实没有绝对的优劣只看场景我按平时接触最多的情况给个参考。场景推荐方案理由偶发一次手上开着 Jupyter图形界面导出零成本、零记忆负担批量转换几十个 old notebooknbconvert 命令行一条命令处理所有文件将 notebook 部署进 CI/CD 流水线nbconvert 配合脚本可自动化、可参数化长期维护的 Python 项目想要代码双份同步jupytext 配对模式保存即同步免去手动导出只抽取 notebook 中部分代码做参考%save或手工复制精准控制范围再补充一个额外的判断维度你要的是结果文件还是工作流。如果只是今天需要一个 .py 临时用那选最简单的方式就行如果你发现 notebook 和脚本的同步问题已经困扰你超过两周说明需要从工作流层面换取 jupytext 的长期机制。6.1 我的个人体会用了那么多年 notebook 和脚本我最深的感受是.ipynb 和 .py 不是竞争关系它们是同一个项目的两种不同视图。notebook 更适合探索、演示、教学.py 更适合工程化、自动化、版本对比。转换不是抛弃 notebook 投奔 py而是让代码在合适的生命周期阶段出现在合适的载体里。在 jupytext 出来之前我都是到项目发布前才匆匆忙忙导一次 .py导完还要手动清理。现在配好配对模式之后这份牵扯精力已经彻底没了。建议每个经常用 notebook 的开发者都花十分钟试试 jupytext配置一次后面省心很多。最后分享一个小经验无论用哪种方案转换完成后第一件事永远是打开生成的 .py 文件从头到尾读一遍——不是看代码逻辑对不对而是看有没有魔法命令、路径硬编码、或者明显的转换垃圾。这五分钟的检查往往能省下后面好几个小时的排障时间。
阅读完成 · 觉得有帮助?
咨询建站