n8n 2.x 没有内嵌 Python 环境?四条落地路线快速跑通自动化工作流
发布时间:2026/10/10 12:16:11 锦皓数字建站

先讲个我上周的现场翻车。我在本地用 n8n 2.x 版搭一条自动化工作流想把爬虫抓回来的 CSV 清洗一下再入库习惯性地在 Code 节点里写了import pandas as pd开头的一堆 Python 代码。一保存整屏红色波浪线连import都被标成了语法错误。我第一反应是 n8n 配置坏了翻设置、看容器日志折腾了半小时才终于想起一个早该记住的事实n8n 2.x 的 Code 节点只认 JavaScript 和 TypeScript压根没有内嵌 Python 环境。这不是我一个人的误区。在把 n8n 和扣子、dify、fastgpt 放在同一张表里对比选型的人几乎都会踩到同一个问题为什么 Coze 的代码节点能选 Pythondify 的代码节点也能写 Python偏偏 n8n 2.x 不行这篇文章我想把这件事一次说透——先说清为什么没有再说清没了它工作流怎么活下去最后给你几条从临时方案到生产方案的完整落地路径。无论你是刚把 n8n 跑起来的新手还是准备企业级部署的老手这篇文章都能解决一个共同困惑n8n 到底能不能跑 Python该怎么跑。1. 在 n8n 里写 Python 被当场拦下来问题到底出在哪1.1 复现一次Python 报错现场先还原一下常见的操作流程。你打开 n8n 画布从节点面板里拖一个 Code 节点双击进入编辑器默认给你的是这样一段 JS 骨架// 设置传入的数据 const items $input.all(); // 设置返回的数据 return items;这时候你想换成 Python 逻辑删掉模板写print(hello)或者更贪心一点直接import requests然后调接口。编辑器会在你输入第一个 Python 关键字时就开始报错保存按钮上的红点更像是在嘲笑你语法错误。有些朋友会说那我是不是装个 Python 插件就行了这就是问题所在——n8n 2.x 并没有为 Code 节点提供任何可选的 Python 解释器。Code 节点打开的是一个基于 VM 沙箱的 JavaScript 运行时你没法在节点设置里切换语言更不是在面板里装个插件就能解决的。这一点和很多低代码/自动化平台的代码节点完全不同。1.2 这个内嵌环境的期待从哪来我问过身边几个从扣子、dify 迁移到 n8n 的用户他们都觉得 n8n 缺了块拼图。扣子的代码节点可选 Python 或 JavaScriptdify 的代码节点默认就是 Python 函数fastgpt 虽说主打知识库很多场景下用户也会顺手写一段 Python 做预处理。于是大家默认这类工作流平台都应该内置 Python。再加上网上大量教程截图经常出现 n8n 画布上有个节点在跑 Python 脚本看起来就像官方功能。仔细看会发现截图里出现的其实是社区节点或者老版本的第三方节点。n8n 官方文档从很早就明确了内置 Code 节点的语言边界就是 JavaScript / TypeScript。所以2.x 版本没有内嵌 Python 环境这个说法更准确地讲应该是——n8n 从来就没有内嵌过 Python 环境这不是某个版本删掉的功能而是它的产品边界一开始就在那。2. n8n 2.x 的运行环境里装不进顺手可用的 Python三个层面的原因2.1 Code 节点不是通用脚本执行器很多初用 n8n 的人以为 Code 节点是在这个格子里跑任何代码。实际上 n8n 的 Code 节点内部是一个受限沙箱设计目标就是让用户做轻量数据变换、分支判断、字段映射。它给你暴露了$input、$json、$env这一组工作流上下文 API能 require 的模块也极其有限文件系统、网络栈、子进程默认都碰不到。为什么要设计成这样一是有利于安全你从公共模板库导入工作流时不至于让一段陌生 JS 直接拿到容器里的 Shell二是保证性能可控跑在隔离 VM 里的脚本就算死循环也不会把主进程拖死三是为了生态一致性——n8n 的节点体系用 TypeScript 写就让所有官方/社区节点共享同一套类型系统代码节点的返回值才能自动被下游节点识别。如果你把 Python 塞进这个沙箱等于在 VM 里再嵌套一个 Python 解释器开销和安全模型都不一样官方显然不想背这个包袱。2.2 官方镜像刻意保持轻量再看部署层面。n8n 官方 Docker 镜像基于 Node.js 运行时构建默认不包含 Python、不带 GPU 驱动、不含浏览器连 git 这类工具都要额外装。这不是疏忽而是一个自动化编排引擎的定位它负责调度不负责把所有计算能力都打包进去。如果你拉过一个n8nio/n8n镜像看大小你会明白几层叠加下来已经不小了再把 Python 解释器、pip、常见科学计算库塞进去镜像体积会失控启动速度、攻击面、维护成本一起上涨。何况不同工作流对 Python 依赖的需求天差地别——有人要 pandas有人要 requests有人要 selenium官方不可能内置一套满足所有人的 Python 环境。所以 n8n 的做法是把语言运行时当成基础设施的一部分让部署者自己决定容器里装什么这其实更贴近真实生产环境的管理方式。2.3 与扣子、dify 的产品哲学差异扣子和 dify 之所以代码节点里就能跑 Python是因为它们以 SaaS 或服务端容器的形式把 Python 运行时托管在了平台侧。你写的每个函数都会发到平台的一个执行容器里跑平台统一管理依赖、超时、资源配额。这对用户很友好但代价是你把执行环境绑定在了特定平台上——你没法控制里面装的是 Python 3.10 还是 3.11也没法装那些平台白名单之外的包。n8n 的自托管属性决定了它更接近你自己就是平台。它把运行环境的选择权交到你手里代价就是你得自己解决 Python 环境。**没有内嵌不等于没有出路只是出路需要你自己搭。**理解这一点后面看到各种跑 Python的骚操作你就不会迷惑了。3. 让 n8n 跑 Python的四条可行路线与选型逻辑3.1 路线 AExecute Command 容器内 Python最朴素的办法n8n 有一个官方核心节点叫Execute Command本质就是让你在运行 n8n 的容器或主机里执行 Shell 命令。那么只要这个环境里装了 Python你就能让它跑python3 /path/to/script.py。优点零新插件思维直白适合快速验证、跑一次性脚本。缺点脚本得提前放到执行环境里跨主机/容器部署时同步麻烦输出是一段字符串解析 JSON 还得再接一个节点安全性全靠自觉命令注入风险要自己扛。3.2 路线 B社区节点 n8n-nodes-pythonnpm 上有一个社区节点叫n8n-nodes-python装完以后画布上会出现一个Run Python Script或者类似名字的节点可以在节点属性里直接粘贴 Python 代码。优点上手快像在页面里写脚本对不熟悉命令行的用户友好。缺点它的实现本质上还是在背后用 child_process 调你容器里的 Python所以你的 n8n 环境里必须先装好 Python 和依赖节点本身并非官方维护版本兼容性要自己试遇到复杂依赖、长耗时任务时报错信息常常很含糊。3.3 路线 C自研自定义 Python 节点n8n 支持自定义节点扩展。你可以用 TypeScript 写一个节点内部通过child_process.execFile去调python3执行脚本然后把 stdout 解析成 JSON 返回给工作流。优点完全可控字段、校验、错误处理都能做成你想要的样子团队可复用封装一次全项目用。缺点开发门槛高需要懂 n8n 的节点规范、打包、放置目录后期升级 n8n 版本时可能踩 API 兼容的坑。3.4 路线 D独立 Python 服务 HTTP 调用把 Python 相关能力单独封装成一个 Web 服务最常见是 FastAPIn8n 这边只用 HTTP Request 节点去调用。优点语言栈彻底分离Python 环境升级、依赖安装、性能调优都在独立服务里做完全不污染 n8nn8n 侧只负责传 JSON 和收 JSON链路稳定服务可以被多个工作流复用甚至能被公司其他系统调用。缺点多一个服务要部署、监控、维护如果你只有一两条简单脚本用这条路显得重了一些。3.5 选型结论我实际用下来判断标准很简单场景推荐路线理由临时调试、几行脚本AExecute Command最快不需要额外工程本人在单机摸鱼玩B社区节点页面写码体验好团队要复用一个业务规则C自定义节点能把规则固化成节点字段生产环境、依赖复杂、多人协作D独立服务最稳边界最清晰如果你问我自己生产环境的答案我会直接告诉你能走 D 就不走 B 和 C。原因后面实操部分细说。4. 动手实操一Docker 部署的 n8n 里补装 Python 环境用 Execute Command 跑起来4.1 docker-compose Dockerfile最常见的是 Docker 部署的 n8n而官方镜像默认没装 Python。先看一个最基础的 docker-compose 长什么样version: 3.8 services: n8n: build: context: . dockerfile: Dockerfile ports: - 5678:5678 environment: - N8N_BASIC_AUTH_ACTIVEtrue - N8N_BASIC_AUTH_USERadmin - N8N_BASIC_AUTH_PASSWORDyourpassword - GENERIC_TIMEZONEAsia/Shanghai volumes: - ./n8n_data:/home/node/.n8n对应目录下放一个 Dockerfile关键是把 Python 装进去FROM n8nio/n8n:latest USER root RUN apt-get update \ apt-get install -y python3 python3-pip \ pip3 install --no-cache-dir pandas numpy requests \ rm -rf /var/lib/apt/lists/* USER node这里做了两件容易忽略的事第一USER root是为了有权限安装软件包装完立刻切回USER node保证 n8n 运行时仍然是非 root 用户避免权限问题第二直接 pip 安装你工作流里用得到的 Python 依赖省得每次在容器里手动装。4.2 验证容器内 Python 可用启动之后进容器验证一下docker-compose up -d --build docker compose exec n8n python3 --version # Python 3.x.x docker compose exec n8n python3 -c import pandas; print(pandas.__version__)如果这两条命令都能正常返回说明 n8n 的工作流已经可以通过 Execute Command 节点触达 Python。我把测试脚本放在./n8n_data/scripts/test.py由 n8n_data 目录挂载进容器这样脚本新增/修改后容器内立刻能看见不用重新 build 镜像。这个挂载思路在开发期非常省事。4.3 Execute Command 节点踩坑三个点在 n8n 画布上拖入 Execute Command 节点在 command 字段填python3 /home/node/.n8n/scripts/test.py就能看到 stdout 作为输出。但有三个坑我基本每次都踩路径别用相对路径Execute Command 的工作目录不一定是挂载目录最好写容器内的绝对路径。stdout 是文本不是 JSONPython 脚本里如果print(json.dumps(result))Execute Command 节点输出的仍是字符串你后面必须再接一个 Code 节点JSON.parse($json.stdout)否则下游节点拿不到结构化数据。它是按输入 item 逐条执行的如果上游传了 100 条 itemExecute Command 会被执行 100 次。如果你只想运行一次务必先用别的方式把 item 合并成一条比如用 Code 节点return [{ json: { ... } }]。这套方案我在测试环境用了很久但最后生产环境还是换成了独立服务。原因很简单脚本分散在各处版本没管起来而且 n8n 容器一旦重建容器里的 Python 又没了维护成本高安全感低。5. 动手实操二FastAPI 封装 Python 能力把 n8n 的 HTTP 节点变成Python 入口5.1 一个 20 行的 FastAPI 服务独立 Python 服务最轻量好上手的框架是 FastAPI。写一个服务端把 n8n 传进来的 JSON 接住用 Python 逻辑处理后返回 JSONfrom fastapi import FastAPI from pydantic import BaseModel from typing import List, Optional app FastAPI() class Row(BaseModel): id: int raw: str class RequestModel(BaseModel): rows: List[Row] keyword: Optional[str] None app.post(/clean) def clean(req: RequestModel): results [] for r in req.rows: text r.raw.strip() results.append({ id: r.id, length: len(text), contains_keyword: req.keyword in text if req.keyword else False, }) return {items: results} app.get(/health) def health(): return {status: ok}本地跑起来uvicorn main:app --host 0.0.0.0 --port 8000。完整逻辑可以再加 pandas、numpy、requests 等任意依赖这里不再赘述。关键是 n8n 只需要 HTTP 通信Python 世界里你想装什么就装什么再也没有n8n 没内嵌的束缚。5.2 在 n8n 里对接 HTTP Request 节点n8n 画布上拖入 HTTP Request 节点配置如下MethodPOSTURLhttp://python-service:8000/cleanBody Content TypeJSONBody用表达式{{ $json }}注意如果 n8n 和 FastAPI 服务在同一个 Docker 网络里URL 里的主机名直接写服务名比如python-service不要写 localhost。跨机器调用则写成内网 IP 或域名。响应解析没什么门槛n8n 自动把 JSON 解析成 items下游节点直接引用$json.items就行。5.3 生产落地镜像、端口、CORS、日志到了生产环境这个 FastAPI 服务最好也容器化。放到同一个 docker-compose 网络里加上健康检查python-service: build: ./python-service ports: - 8000:8000 healthcheck: test: [CMD, curl, -f, http://localhost:8000/health] interval: 30s timeout: 5s retries: 3如果你的 n8n 和 Python 服务不在同一个网络或前端页面需要跨域访问 Python 服务记得加 CORS 中间件from fastapi.middleware.cors import CORSMiddleware app.add_middleware( CORSMiddleware, allow_origins[*], allow_methods[*], allow_headers[*], )还有两个细节第一长时间任务要把 HTTP Request 节点的 timeout 调大默认值往往不够第二给服务加个日志配置把每次调用的入参、出参都打出来排错时依赖这些日志比什么都强。5.4 批量和循环最常见的对接失误n8n 的数据流是 item 数组HTTP Request 节点默认会对每个输入 item 发起一次请求。如果上游有 50 条数据你想把它们一次性发给 Python 服务直接拖 HTTP 节点是发 50 次请求——慢且低效。正确做法是在 HTTP 请求节点前面加一个聚合步骤用 Code 节点把所有 item 合并成一个数组再用一个字段传给服务const items $input.all().map(item item.json); return [{ json: { rows: items } }];把这条 Code 节点的输出接到 HTTP Request 节点Body 表达式配{{ $json }}这样服务端拿到的就是包含全部行的完整 JSON。这是我在对接过程中最容易忽略的一环在新手排障里出现频率极高。6. 从 dify/扣子迁移过来的 Python 脚本在 n8n 里要改三处6.1 数据流函数入参变成了 item 流如果你之前是在扣子或 dify 的代码节点里写 Python迁移到 n8n 的第一感觉通常是不知道该把脚本往哪放。这是正常现象因为两者的数据模型完全不同。dify 和扣子的代码节点更像一个函数你预先声明入参变量脚本里直接拿变量名用。而 n8n 的工作流是线性的数据管道每个节点都是一台小小的加工机器输入是上一条的 item 数组输出是下一条的 item 数组。所以在 n8n 里运行 Python你不是在写一个函数而是在设计一个数据在节点间流转时的处理环节。6.2 返回值从单个 dict 变成 item 数组dify 的代码节点要求返回一个结果变量扣子也类似通常是单个对象。而 n8n 的节点接口约定俗成地要求你返回至少一个经过{ json: ... }包装的 itemreturn [{ json: { result: hello } }];如果你把 Python 逻辑封装成了 FastAPI 服务服务端返回值可以是干净的字典但 n8n 拿到 response 后会把整个响应作为一个 item。如果你的服务端返回了{count: 10, rows: [...]}下游字段引用是$json.count、$json.rows跟你在函数里把返回值当作字典时的心态几乎一样。所以迁移的第一步是把自己的思维从函数式拨到HTTP 式。6.3 依赖和文件处理方式完全不同在扣子或 dify 的代码节点里平台会处理好依赖白名单你只需写业务逻辑。到了 n8n 自托管环境依赖得自己装。建议把依赖声明在 FastAPI 服务的requirements.txt里而不是靠记忆在容器里一个个 pip install。文件场景更麻烦。dify 里能直接读取上传的知识库文件n8n 的流程里文件往往以 Base64 字符串或文件路径的形式存在于 item 字段中。我的一般做法是如果文件不大直接在 n8n 里转成 Base64 字符串传给 Python 服务服务端解出来处理如果文件大则先让 Python 服务落地到共享存储n8n 传存储路径。不要在 n8n 容器和 Python 服务之间频繁拷贝大文件路径引用比内容传输可靠得多。6.4 迁移实例一个 keyword 过滤的改动举个简单例子。dify 里的 Python 代码节点大概是def main(keyword: str, rows: list) - dict: filtered [r for r in rows if keyword in r.get(text, )] return {count: len(filtered), rows: filtered}迁移到 n8n FastAPI 路线后服务端改成class Row(BaseModel): text: str class FilterRequest(BaseModel): keyword: str rows: List[Row] app.post(/filter) def filter_rows(req: FilterRequest): filtered [r for r in req.rows if req.keyword in r.text] return {count: len(filtered), rows: [r.dict() for r in filtered]}n8n 侧则用 Code 节点把上游多个 item 聚合成rows数组再交给 HTTP Request 节点。整个逻辑完全平移只是把平台执行 Python换成了n8n 调用 Python 服务。反过来如果只是几行简单变换也不必硬上 Python——n8n 的 Code 节点用 JavaScript 写起来同样简单能用原生 JS 解决的逻辑没必要引一个外部服务进来。7. 我最后在环境里长期运行的组合以及一句真心话折腾了一圈之后我现在的部署结构很固定n8n 2.x 作为编排层专门负责定时触发、条件分支、审批流、和各类系统对接所有 Python 相关能力全部收敛在独立的 FastAPI 服务里容器跟 n8n 走同一个 Docker 网络。数据清洗、聚合计算、复杂字符串处理、机器学习推理这些工作都从服务接口出去n8n 这边永远是传 JSON、收 JSON两个动作。这个组合的好处是升级 n8n 版本时我完全不用担心 Python 依赖被打回原形Python 服务的依赖、模型文件、脚本版本独立管理出现性能瓶颈也能单独扩容器换人接手后看 n8n 流程图就能明白数据怎么流转看 Python 服务代码就知道每一步在算什么边界非常清爽。最后说句真心话。遇到n8n 2.x 没有内嵌 Python 环境这类困惑先别急着怀疑自己选错了工具。大多数时候这不是功能的缺失而是产品给运行时的定位不同。只要把环境和编排解耦的思路理清楚n8n 不仅不会因为没有 Python 被限制住反而会因为这种解耦让你在后续做企业级部署、多语言扩展时走得更稳。怕的不是没有内嵌怕的是你把本应灵活部署的运行时当成了一个盒装软件来用。
锦
锦皓数字建站
深耕本土企业品牌数字化升级,专注原创端正雅致商务官网,从视觉设计到稳定运维全程保驾护航。