我接触过不少刚开始用Python做数据分析或者机器学习的朋友几乎每个人都会遇到同一个问题辛辛苦苦在Jupyter Notebook里调试好的代码转头要交付或者部署的时候对面只要一份干净的.py文件。很多人第一反应是直接把.ipynb后缀改成.py然后发现要么打不开要么打开了一堆乱七八糟的文本。.ipynb文件和.py文件看起来都是代码文件但两者的组织逻辑完全不同。.ipynb本质上是JSON格式的文档里面藏了单元格、输出结果、元数据这些信息而.py就是纯文本的Python脚本。这篇文章就围绕这个转换需求把各种可行的转换方式、转换之后怎么处理、实际项目里有哪些坑一次说清楚。1. 先搞清楚ipynb究竟藏了什么东西1.1 一个.ipynb文件的结构拆解.ipynb文件是Jupyter Notebook的专用格式它的全名是IPython Notebook。很多人误以为它就是个普通的文本文件实际上它是由JSONJavaScript Object Notation组织的结构数据。JSON的特点是层级分明、可读性好所以.ipynb文件本质上是一个可以被解析的数据文档。用任意文本编辑器打开一个.ipynb文件你会看到类似这样的结构{ cells: [ { cell_type: markdown, metadata: {}, source: [ # 这是一个标题\n, 这是说明文字 ] }, { cell_type: code, execution_count: 3, metadata: {}, outputs: [ { output_type: stream, name: stdout, text: [ Hello World\n ] } ], source: [ print(Hello World) ] } ], metadata: { kernelspec: { display_name: Python 3, language: python, name: python3 }, language_info: { name: python, version: 3.9.13 } }, nbformat: 4, nbformat_minor: 5 }关键信息都在cells这个数组里。每个单元格有类型、源代码、输出结果、执行次数等属性。cell_type分两种markdown文本说明和code可执行代码。source是单元格内容的列表每行字符串一个元素。outputs记录的是代码执行后产生的输出包括stdout、stderr、图片、富文本等。execution_count是执行顺序编号。理解了这种结构你就明白为什么直接把.ipynb改成.py是不可行的了。.py文件期望的是逐行可执行的纯代码而.ipynb里的代码被层层包裹在JSON的字段里直接改名后Python解释器根本识别不了。1.2 为什么需要专门转换而不是手动复制有的同学说我直接打开Notebook把每个代码单元格的内容复制到一个.py文件里不就行了吗这个办法对于只有几个单元格的小脚本确实可行但一旦涉及几十个单元格、有些代码被分割成多个片段、中间穿插大量markdown笔记手动复制就非常容易出错。你可能会漏掉某个变量定义也可能不小心把markdown内容当成代码粘进去。更重要的是Notebook的单元格执行顺序并不一定是从上到下。有些人习惯先跑第5个单元格再回头跑第2个单元格这时候代码之间的依赖关系非常隐蔽。手动复制时你根本无法保证最终的.py文件执行顺序是合理的。专业工具可以按照Notebook中从上到下的顺序把所有代码单元格提取并拼接至少保证基本框架是对的。另外还有代码复用的问题。如果团队里的同事需要把别人的Notebook集成到他自己的项目里没有经过规范转换的代码往往需要大量二次调试。转换这件事听上去简单里面门道不少。2. jupyter nbconvert命令行转换最靠谱的一条路2.1 安装环境和第一条转换命令Jupyter生态里自带一个专门做格式转换的工具叫nbconvert。如果你安装的是Anaconda它已经包含在默认环境里了。如果你用的是纯Python环境需要先安装pip install nbconvert最基础的转换命令非常短jupyter nbconvert --to script 你的文件名.ipynb执行完这条命令当前目录下会出现一个同名但以.py结尾的文件。我把这个命令的各个组成部分拆开讲一下jupyter调用Jupyter的命令行入口。nbconvert指定使用nbconvert这个子命令。--to script目标格式是Python脚本。script这个词表示生成纯代码文件。最后的参数你要转换的.ipynb文件路径。这条命令会读取整个Notebook把所有cell_type为code的单元格里的源代码按顺序提取出来用注释块标记每个单元格的来源最后写入一个.py文件。我在一个示例项目上实测过一个包含38个单元格的Notebook转换耗时不到1秒生成的.py文件有500多行。整个过程是自动且精确的不会丢代码。2.2 转换生成的文件长什么样转换完成后生成的.py文件开头部分会包含一段元数据注释#!/usr/bin/env python # coding: utf-8 # In[1]: # 一些代码 import pandas as pd # In[2]: # 一些代码 df pd.read_csv(data.csv)每个代码单元格对应一个# In[x]:标记。这个标记标明原本在Notebook里是第几个执行单元便于你对照排查问题。markdown单元格默认不会出现在.py文件里除非你用模板自定义输出。代码单元格里的注释会原样保留这有助于阅读。如果Notebook里有%matplotlib inline这种魔法命令转换后的.py文件也会原样保留这行。这行在Jupyter环境里是有意义的但在纯Python脚本里会直接报错除非你的环境里装了IPython并启用了相应magic。这个问题在第3节我会详细讲怎么处理。2.3 高级用法批量转换、指定输出目录、模板定制如果你手上有一堆.ipynb文件要一次性转换写个for循环或者用通配符都不太优雅。nbconvert支持在命令行直接传入多个文件jupyter nbconvert --to script notebook1.ipynb notebook2.ipynb notebook3.ipynb它会依次处理每个文件输出到当前目录。如果想输出到指定目录加--output-dir参数jupyter nbconvert --to script --output-dir ./converted notebook.ipynbnbconvert还支持自定义模板。默认模板只保留代码单元格如果你想在生成的.py文件开头加上一段版权声明或者导入公共模块可以创建自定义模板。不过对于绝大多数场景默认输出已经够用了模板定制属于进阶玩法。我这里有另一个常用技巧如果你只想要代码不想要那些# In[x]:标记可以加--template generic。但我不推荐这个做法因为标记能帮你对照Notebook里的原始位置调试时非常有用。3. 转换之后的py文件还不能直接跑常见问题排查3.1 魔法命令必须手动处理的第一个坑把.ipynb转换成.py只是第一步。很多人的Notebook里充斥着魔法命令比如%matplotlib inline %%time %load_ext autoreload %autoreload 2这些命令在Jupyter环境中是可用的但脱离Notebook环境后Python解释器不一定认识它们。%matplotlib inline在IPython里可以运行但在普通Python脚本里会抛出语法错误因为它不是标准的Python语法。处理方式有几种直接注释掉或删除。如果你需要matplotlib出图改成plt.show()然后用Python脚本的方式运行。图像会弹窗显示或者保存到本地。用IPython来运行最终脚本。比如ipython your_file.py这样IPython会解析魔法命令但有一个前提你的.py文件里要导入必要的模块比如from IPython import get_ipython否则那些命令仍然无法执行。模块化的处理方式——把魔法命令收敛到统一配置里不要在逻辑代码里满天飞。比如把%matplotlib inline放在文件顶部的配置区块这样清理起来方便有时候甚至可以写个脚本自动删除这些行。3.2 交互式输出与状态问题Notebook的核心特点之一是交互式执行。你在一个单元格里定义了变量results []然后在另一个单元格里不断添加内容靠的是同一个内核里的全局变量状态。转换成.py后代码变成了从上到下顺序执行这种状态依赖依然成立只要变量定义在引用之前即可。但有一个容易忽略的差异Notebook里可以分次执行、重复执行同一个单元格变量会被覆盖或者更新。而.py文件运行后从头到尾只执行一次如果某个变量的值依赖于之前运行时的副作用代码输出就会和Notebook里的结果对不上。另外Notebook里如果直接写df并作为单元格最后一行表格会以富文本形式显示数据。在.py文件中这种表达式不会产生任何输出。如果你依赖这种显示方式查看中间结果转换成.py后那些看不到结果不代表代码挂了只是没有打印。排查逻辑时要明白这一点。3.3 路径问题working directory变了Notebook运行时当前工作目录通常是Notebook文件所在的目录。很多人在Notebook里写df pd.read_csv(data.csv)然后一切正常。但转换成的.py文件如果被放到项目的子目录里运行这个相对路径就可能失效因为工作目录是启动脚本的位置。解决方法是使用绝对路径或者基于脚本所在位置动态构建路径import os BASE_DIR os.path.dirname(os.path.abspath(__file__)) df pd.read_csv(os.path.join(BASE_DIR, data.csv))这样无论从哪里启动都能准确定位到文件所在目录下的数据文件。这个问题在项目交接时特别常见值得提前预防。3.4 主逻辑入口和防御式编程Notebook是线性执行的最后几个单元格往往就是模型的训练、预测或者可视化输出。但转换成.py后如果整个文件只是裸露的顶层代码将来你把它作为模块导入时会立刻执行全部内容这往往是你不希望的。所以我通常在转换后会做一次轻量级重构——把核心流程包进函数然后加一个主入口def main(): # 原本Notebook里的主要逻辑 pass if __name__ __main__: main()这里还有一个更隐蔽的问题Notebook里可能有!pip install xxx这种shell命令。这种代码转换成.py后直接运行会执行系统命令非常危险建议转换成注释或者明确标注为需要手动执行。4. 另外几种转格式的野路子值不值得用4.1 在线转换工具与IDE自带功能如果你手头只有一个.ipynb文件身边又没有Python环境你可能会搜到很多在线转换工具。把文件上传网站帮你生成.py免费省事。表面看很美好实际用下来问题不少隐私风险。Notebook里往往包含数据路径、字段名、内部API逻辑上传到第三方服务器存在泄露风险。转换质量参差不齐。有些在线工具只是提取source字段对魔法命令、特殊编码处理不到位。文件大小限制。Notebook稍微有点规模比如內嵌了图片输出在线工具可能直接拒绝上传。部分IDE也提供了导出功能比如VS Code的Notebook编辑界面自带导出Python脚本选项。这个功能本质上也是调用nbconvert底层原理相同。使用IDE的导出功能优点是不用记命令缺点是可定制性差而且有些版本导出的脚本会有编码问题。4.2 jupytext另一种同步思路如果你经常需要在.ipynb和.py之间来回切换比如用IDE写代码但想用Notebook展示结果那我推荐了解一下jupytext。它允许你以.py文件作为Notebook的另一种存储格式。安装后你可以配置jupytext把Notebook同时保存为.ipynb和.py两种格式两者内容保持一致。编辑.py时Notebook同步更新反之亦然。这个工具适合那些需要长期维护、频繁双向转换的项目。如果只是一次性转换用nbconvert就够了没必要引入额外依赖。4.3 手动备份式转换的适用场景有一种极端情况可以手动复制Notebook的代码量很少、没有复杂依赖、一次性使用。比如你只是写了个快速清洗脚本总共5行代码直接复制出来粘贴到.py里10秒钟搞定。这种情况下引入任何工具反而是负担。不过我心里还是有一个标准只要超过10个单元格或者需要对外交付我就不会手动复制。手动复制意味着无法追踪代码与Notebook的对应关系别人拿到手也没法快速理解代码的来龙去脉。5. 从工具人角度说说我平时怎么管这些文件5.1 什么场景值得转、什么不值得转并不是所有.ipynb都值得转成.py。我的判断标准很简单值得转的场景要部署到服务器或定时任务里运行。要作为模块被其他代码导入调用。要提交到Git仓库和团队协作因为.py更容易做diff和code review。要对代码做单元测试。不值得转的场景只是自己分析数据、做探索性可视化写完就完事。Notebook本身就是交付物汇报时展示流程。代码还处于频繁变动阶段转成.py反而会增加维护负担。很多人一听到工程化就急着把所有Notebook转成.py其实没必要。Notebook在数据探索阶段有天然优势交互式查看中间结果远比.py方便。等逻辑稳定了再转换效果最好。5.2 转换前的三个检查习惯我在转换前会固定做三件事你可以直接参考第一清理输出。Notebook里的图片输出、打印日志会让文件体积暴增转换后虽然不影响代码执行但会给Git仓库带来烦恼。用Edit - Clear All Outputs清理一遍文件瞬间小很多。第二调整单元格顺序。确保单元格从上到下是你想要的执行顺序。方法很简单Kernel - Restart Run All如果全程无报错说明这个执行顺序是自洽的转换后的.py顺序执行也不会有问题。第三检查敏感信息。Notebook里可能会有硬编码的数据库连接串、api_token、内部文件路径。转换成.py后这些信息会明文存在如果文件要提交到Git务必先用环境变量替代或者放进.env文件。5.3 转换成模块的工程化思路如果你要转换的Notebook是某个业务流程的核心逻辑我建议你转换后更进一步把它改造成标准的Python模块结构而不是让生成的.py文件继续当一个大脚本用。大体思路是将数据加载部分抽出来单独成一个函数或独立的loader.py。将特征处理、模型训练、结果导出分别封装成函数。顶层只保留一个调用这些函数的入口。这样后续测试、替换模型、调整参数都方便很多。Notebook文件保留作为算法演示文档转换出的.py文件作为实际运行的代码单元两者各司其职。如果你在乎代码风格可以把转换生成的.py丢给格式化工具整理一遍比如autopep8 --in-place your_file.py经过格式化后代码的缩进和空行更规范阅读体验好很多。但要注意如果你之后又修改了Notebook并重新转换格式化的改动会被覆盖所以最好确定Notebook内容不再变化后再格式化。5.4 一个偷懒但有效的自动化脚本我在团队内部给同事提供了一个简单脚本这里分享给大家它可以把一个目录下所有.ipynb文件批量转换为.py并且自动跳过已经存在且时间戳更新的.py文件#!/bin/bash for notebook in $(find . -name *.ipynb); do python_file${notebook%.ipynb}.py if [ ! -f $python_file ] || [ $notebook -nt $python_file ]; then jupyter nbconvert --to script $notebook fi done这个脚本的思路是只有Notebook文件比已有.py文件新的时候才转换避免重复劳动。你还可以变通一下加入--output-dir参数把所有转换结果集中到一个文件管理起来更清爽。5.5 关于格式转换的一些长期考虑格式转换看起来是个小需求但它背后的核心其实是代码可复用性和可维护性的问题。Notebook很适合讲故事和探索但它不适合做“被长期维护的代码单元”。一个成熟的团队往往会制定一套工作流Notebook负责研究和演示.py负责工程和交付。转换工具本身的官方性和稳定性也很重要。nbconvert是Jupyter项目的一部分意味着它会长期维护、和Jupyter生态兼容。相比之下一些第三方个人开发的小工具可能在某个版本后就不再更新使用时需要谨慎。如果只是想解决今天这个把.ipynb转成.py的需求记住一条主线就够了用jupyter nbconvert --to script命令转换然后手动处理魔法命令、路径和主入口。这三个步骤做完你的.py文件大概率能直接跑通。我在实际使用中还有一个感触转换完之后别急着删掉原Notebook。不少人在部署完成后立刻把.ipynb删了后来回头想改个逻辑只能对着几百行的.py发愁。保留Notebook作为设计和推导过程的存档保留.py作为可执行的代码结果两者配合使用才是最优解。最后分享一个小技巧转换后先跑一遍python -m py_compile your_file.py检查语法再实际运行能够把大多数低级错误提前挡在门外。希望这些经验能帮你少走点弯路。
阅读完成 · 觉得有帮助?