1. 先分清“本地”和“离线”:五种常见的离线执行场景
有一次我把一个做网页自动化的 Python 脚本拷到内网机器上,双击运行,然后盯着控制台等了半分钟,最后看到一行ModuleNotFoundError。旁边同事笑着说:“你不是说离线也能跑吗?”其实那句玩笑话戳中了很多人的误区:所谓离线执行自动化脚本,不是你让脚本离线,而是你提前把运行它所需要的所有东西都准备齐。自动化脚本要能在本地直接执行,尤其在内网隔离或断网环境下,核心工作从来不是写那几行业务代码,而是把解释器、依赖库、浏览器驱动、公共数据和入口脚本一起打包成一个“断网也能启动”的整体。
很多场景其实不是真的“完全没网”,而是限制程度不同。我把平时遇到的离线执行场景分成五类,每类的应对方式完全不一样。
1.1 五种离线场景,对应五种准备策略
- 完全断网的单机:这台机器不能访问任何外网,也没有内网镜像源。所有东西必须提前放到本机,pip、npm、apt 全部要走本地文件。
- 内网隔离但允许内网镜像:通常企业内部最少会提供一套软件源,这时候可以把离线包传到内网服务器上,让所有机器统一从内网源安装。
- 只能访问白名单域名:比如业务环境只允许访问公司官网或特定 API,其他域名全部拒绝。脚本里的在线资源、CDN、公共驱动的下载地址都要改成白名单内的渠道。
- 弱网环境:网络通但极不稳定,超时严重。这种环境下脚本不能默认在线资源一定可用,必须加缓存、超时、重试,否则跑着跑着就挂。
- 飞行模式下的本地开发:笔记本离线办公,也需要跑自动化脚本,但所有依赖都在本地缓存里。
在开始做离线方案之前,先花十分钟确认三个问题:脚本运行最依赖的外部资源是什么?是第三方库、浏览器驱动、数据库、模型权重,还是某个外部接口?这些资源多大?能否提前拷贝到目标机器?目标机器有没有内网仓库或者镜像源?搞清楚这三个问题,再决定是把依赖打进脚本目录,还是做一套离线包,或者直接 Docker 镜像搬运。
1.2 “直接执行”不是有脚本就行,而是五个指标同时满足
很多人觉得“在本地直接执行”就是拿到一台机器,敲一条命令就能跑。但实际落地要看五个检查项:
| 检查项 | 常见问题 |
|---|---|
| 解释器 | 系统里有没有python3/node,版本是否匹配 |
| 依赖包 | import xxx是否能成功,第三方库是否齐全 |
| 工作目录 | 脚本里的相对路径,是否总能指向预期位置 |
| 数据文件 | 配置文件、驱动、模型文件、CSV 数据是否在 |
| 权限 | 有没有写日志、创建临时目录、读取外设的权限 |
这五个检查项随便哪个出问题,都会让脚本“看起来不能离线跑”。我之前帮同事排查过一个脚本,代码本身完全没问题,但脚本里用了./config.json,结果他换了一台机器,人坐在/home/admin目录下执行/data/tools/run.py,那必然读不到/data/tools/config.json。这类问题后面专门有一章展开,这里先记住:本地离线执行的第一步,不是写代码,而是把这五个指标列成一个清单,逐项打勾。
1.3 最小可复现示例:一个断网也能运行的 Python 脚本
空谈容易,我给出一个最简单可复现的例子。假设有一个数据处理脚本run.py,它读取当前目录下的data.csv,做一次清洗后输出result.txt:
import csv from pathlib import Path base_dir = Path(__file__).resolve().parent input_file = base_dir / "data.csv" output_file = base_dir / "result.txt" with open(input_file, "r", encoding="utf-8") as f: rows = list(csv.DictReader(f)) valid_rows = [row for row in rows if row.get("score") and int(row["score"]) >= 60] with open(output_file, "w", encoding="utf-8") as f: for row in valid_rows: f.write(f"{row['name']}: {row['score']}\n") print(f"done, {len(valid_rows)} rows")如果它只用标准库,那确实不需要装任何第三方包。但我们大部分自动化脚本会用 requests、pandas、selenium 这类库,所以离线准备的核心是用另一台联网机器把依赖打包:
python3 -m venv venv source venv/bin/activate pip install -r requirements.txt pip download -r requirements.txt -d ./offline_packages然后把整个项目目录拷贝到内网机器上,重建环境:
python3 -m venv venv source venv/bin/activate pip install --no-index --find-links=./offline_packages -r requirements.txt python run.py这里有两个细节必须说清楚:--no-index表示不让 pip 去 PyPI 搜索,--find-links指定从本地目录查找包。如果不加--no-index,pip 仍然会尝试连接外网,一旦连不上就会来回重试,浪费时间。另一个细节是,pip download只能解决 Python 包层面的问题;如果脚本依赖libnss3.so这类系统动态库,或者依赖 Chrome、ChromeDriver,那还得另想办法,第四、五章会专门讲。
2. 入口脚本设计:从命令行到自动化脚本里调用别的脚本
“直接执行”听起来简单,但实际会面对两种完全不同的需求。第一种是你在命令行手敲命令,或者双击一个脚本文件;第二种是你本身在写一个自动化框架,想让框架去执行另一个本地离线脚本,比如通过 subprocess 调度 job。这两者看起来差不多,但设计思路不同。
2.1 命令行执行背后的三个隐藏变量
命令行执行一个 Python 脚本,很多人只敲python run.py,但背后有三个隐藏变量决定你能不能跑通:
- 用的是
python还是python3:某些老系统里python指向 Python 2,直接跑就跑飞了。 - 当前工作目录是哪:脚本里的相对路径,是相对于当前终端所在的目录,不是脚本所在目录。
PATH里能不能找到解释器:如果解释器安装在某个自定义目录,PATH没配好,命令行会直接提示命令不存在。
我在内网机上部署脚本时,习惯写一个run.sh入口,把这三个变量全部固化:
#!/usr/bin/env bash set -euo pipefail cd "$(dirname "$0")" source ./venv/bin/activate python run.py "$@"然后执行chmod +x run.sh。之后不管我在什么目录下,只要执行/path/to/run.sh,脚本一定会先进入它自己所在的目录,再激活它自带的虚拟环境,最后运行run.py。这样能避免“换一个执行位置就报 ModuleNotFoundError”的经典问题。
2.2 在自动化脚本里调用本地脚本:subprocess 的正确姿势
如果你正在写一个自动化测试框架或任务编排脚本,想要直接调用另一个本地离线脚本,最常用的不是os.system,而是subprocess.run。对比一下:
import subprocess import sys result = subprocess.run( ["python", "run.py", "--env", "offline"], cwd="/data/tools", capture_output=True, text=True, timeout=120, ) print("return code:", result.returncode) print("stdout:", result.stdout) print("stderr:", result.stderr)cwd指定子进程的工作目录,timeout防止离线脚本卡死,capture_output捕获控制台信息。我不建议用shell=True拼字符串,因为一旦脚本路径里包含空格或特殊字符,很容易出现想象不到的转义问题。列表形式的参数传递是最安全的。
还有一点:如果被调脚本使用了自己的虚拟环境,那subprocess里要显式指定虚拟环境中的 Python 路径,比如/data/tools/venv/bin/python,不要用系统默认的python。否则即使入口脚本里激活了 venv,subprocess起的新进程也不一定会继承这个激活状态。
2.3 让入口脚本可信:日志、错误码、退出码
直接执行的脚本最怕“没人盯着”。命令行下你还能看到报错,但如果是调度系统自动执行,脚本失败后如果没有返回非零退出码,调度系统会认为任务成功,问题就藏住了。所以在设计本地离线脚本时,我有三个习惯:
- 所有异常捕获后,至少打印到
stderr,并用sys.exit(1)结束,而不是让解释器抛出未捕获异常后退出码还是 0。 - 日志文件按日期命名,例如
logs/run_20250617_1530.log,并记录启动时间、关键参数、结束时间。 - 在脚本结束前,把处理结果和最后一条错误写入一个
status.txt文件,方便第二天直接看文件判断状态。
比如在run.py里加一句:
try: main() except Exception as exc: with open("status.txt", "w", encoding="utf-8") as f: f.write(f"FAILED: {exc}\n") raise else: with open("status.txt", "w", encoding="utf-8") as f: f.write("SUCCESS\n")这样即便没有任何人在终端旁边,自动化脚本被调度系统调用之后,也能通过退出码和状态文件双重确认结果。
2.4 Windows 下“双击执行”和定时任务的两个坑
Windows 环境下,很多人喜欢把脚本做成 .bat 文件直接双击运行。我先给出一个相对可靠的模板:
@echo off chcp 65001 >nul cd /d "%~dp0" call .\venv\Scripts\activate.bat python run.py %* >> logs\run.log 2>&1这里%~dp0是 bat 文件所在目录,cd /d是切换盘符和目录。如果不写这一行,双击运行后工作目录很可能是系统解释器的当前位置,脚本里的相对路径大概率出错。chcp 65001是把控制台代码页切到 UTF-8,否则脚本里输出中文容易乱码。还有个小细节:bat 文件本身的编码最好保存为 ANSI 或 GBK,如果带 BOM 的 UTF-8 反而会报错,这是 Windows 老毛病。
Windows 定时任务的坑更多。你在任务计划程序里配置python run.py,但那台机器的“当前目录”并不是脚本所在目录,所以脚本里如果是相对路径,定时执行基本都会失败。正确做法是任务计划里直接指向run.bat,让 bat 负责切目录和环境。这是最省心的方案。
3. 离线依赖搬运实战:pip、npm、Maven 三套方案
离线执行最常见的失败点,其实不是代码逻辑,而是依赖管理。不同语言生态有不同的搬运方式,这里针对 Python、Node、Java 三套主流方案做个实战总结。
3.1 Python 项目:pip download 制作离线依赖包
前面已经给了最基本的pip download命令。实际项目中还需要处理一个细节:某些包在 PyPI 上有多个版本,而pip download默认下载当前平台匹配的 wheel。如果内网机器是 Linux x86_64,联网下载机器也是 Linux x86_64,那没问题;但如果你的本机是 macOS,目标机是 centos 7,那就必须用--platform参数指定目标平台,否则拷过去的 wheel 根本装不上。
pip download -r requirements.txt \ --platform manylinux2014_x86_64 \ --python-version 3.8 \ --implementation cp \ --abi cp38 \ --only-binary=:all: \ -d ./offline_packages这条命令下载的包会尽量带上平台标签,避免把 mac 版包带到 Linux。不过它对一些只有源码包的库不友好,需要灵活处理。我的建议是:先在自己和目标机相同操作系统的联网环境把依赖打包,这是最省事的方案。如果实在做不到,再用平台参数区分。
在离线机器安装时,还可以加上--no-cache-dir减少磁盘缓存占用。如果你有多个内网机器,完全可以在一台机器上搭一个本地 PyPI 服务,用pip install --index-url http://内网地址/simple指向它。但这是比较重量的做法,脚本数量少的时候没必要。
3.2 Node 项目:npm pack 与直接拷贝 node_modules
Node 自动化脚本遇到离线环境时,常见的做法有两种。第一种是在联网机器执行:
npm pack然后会把项目打包成一个.tgz文件,拷贝到内网后执行:
npm install ./your-package-1.0.0.tgz这种方式适合你要分发的是一个 npm 包,或者在多台机器间同步。第二种是直接拷贝整个node_modules文件夹,这在小型项目里最粗暴也最有效。但要注意:如果项目里使用了原生模块(比如node-sass、sharp),它们依赖的系统库和 Node ABI 版本必须和目标机一致,否则直接拷贝过来后,启动时会出现NODE_MODULE_VERSION不匹配的报错。
无论哪种方式,一定记得锁版本。项目里保留package-lock.json,离线安装时才能保证依赖树和联网环境完全一致。如果你只是简单地在 package.json 里写了"express": "^4.18.0",那换一台机器安装时可能会拉到不一样的小版本,最终导致行为不一致。离线执行环境最怕这种隐蔽的版本漂移。
3.3 Maven 本地仓库有包,但项目就是引不进来
这个现象在 Java 项目里特别常见,也就是热词里提到的“maven本地有包但是引不进来”。明明~/.m2/repository下面文件都在,离线执行mvn package却报Failure to find。我踩过几次之后,总结出四个最可能的原因:
- 版本不一致:本地仓库里的 jar 版本,和 pom.xml 里声明的不完全一样,哪怕只是
1.0.0和1.0.0-RELEASE的差异,Maven 也认为不存在。 _remote.repositories文件作祟:Maven 在下载依赖时会记录来源仓库 ID,离线时如果它记录的远程仓库不可达,就会拒绝使用本地文件。解决方法是找到对应目录下的.lastUpdated文件和_remote.repositories文件,删掉后执行mvn install -o。- settings.xml 配置了错误的 mirror 或 profile:本地仓库被指向了一个内网不存在的镜像仓库,导致 Maven 每个依赖都要去镜像找不到,白白超时。
- IDE 与命令行用了不同的 Maven 配置:IDEA 里配的 Maven home 和 settings.xml 与终端里的不一样,导致你在命令行看着没问题,在 IDE 里执行却拉不到包。
排查时先执行两句话:
mvn dependency:tree -Dverbose mvn dependency:get -Dartifact=groupId:artifactId:version -o如果第二条命令在离线模式下能拉到,说明依赖本身存在;如果拉不到,就优先检查版本号和_remote.repositories。我曾经遇到一个模块,本地明明有spring-boot-starter-web-2.7.18.jar,但项目一直报找不到,最后发现该目录下有个_remote.repositories文件里写着central,而离线时central仓库无法访问,Maven 就认为这个 jar 不能直接用。删除之后问题彻底解决。
3.4 离线依赖包的通用管理铁律
针对不同语言,有三条通用的东西:
- 锁定版本:Python 用
requirements.txt(最好带哈希),Node 用 lock 文件,Java 用 pom 里的精确版本。 - 保留来源:离线包里放一个
README,记录下载日期、来源仓库、下载机器架构,方便半年后有人问“这个包哪来的”时能回答。 - 定期更新:离线环境不等于永远不变,除非业务要求,否则建议每季度做一次依赖更新,并形成新的离线包。
4. 网页自动化脚本离线执行:驱动和浏览器的准备
网页自动化脚本是离线执行里最容易踩坑的场景。你写好了 Selenium 脚本,本地能跑,拿到内网服务器上就报SessionNotCreatedException,或者干脆报chromedriver不存在。这里的核心问题是:Selenium 脚本运行需要的不是只有 Python 包,还包括一套完整的浏览器 + 驱动环境。
4.1 为什么离线环境下要单独准备浏览器和驱动
Selenium 的库本身是 Python 包,可以用 pip 离线安装。但webdriver.Chrome()启动时,底层它需要两个东西:一个可执行的 Chrome 浏览器,和一个与浏览器版本严格匹配的 ChromeDriver 驱动。正常情况下,webdriver-manager库会联网下载驱动;但在离线环境里,这个下载必然失败。所以你必须提前把对应版本的浏览器和驱动拷贝到目标机器,并且在脚本里显式指定路径。
浏览器和驱动的版本匹配是首要问题。Chrome 109、Chrome 110、Chrome 120,不同版本对应的 ChromeDriver 的主版本号必须一致,小版本不一定要完全相同,但最好接近。如果你使用 Chrome for Testing,它对应驱动版本完全锁定,下载时直接命名成一样的版本号,比较省心。
4.2 配置路径而不是依赖 PATH 自动查找
在脚本里,我建议显式设置路径,不要依赖系统 PATH。示例:
from selenium import webdriver from selenium.webdriver.chrome.service import Service options = webdriver.ChromeOptions() options.binary_location = "./tools/chrome/chrome" options.add_argument("--headless=new") options.add_argument("--no-sandbox") options.add_argument("--disable-dev-shm-usage") options.add_argument("--disable-gpu") service = Service(executable_path="./tools/driver/chromedriver") driver = webdriver.Chrome(service=service, options=options)binary_location指定 Chrome 可执行文件位置,executable_path通过Service指定驱动位置。如果你用的是比较老的 Selenium 3 写法,executable_path是直接传给webdriver.Chrome()的;Selenium 4.10 之后推荐用Service。关键点是:路径最好写成相对脚本目录,而不是写死/home/user/...,这样整个项目目录拷到任何机器都能用。配合前面说的run.sh里先切换目录,相对路径就安全了。
4.3 Linux 服务器上的系统库:最容易被忽略的一环
在 Ubuntu 或 CentOS 服务器上跑 Selenium,即使浏览器和驱动都对了,启动时也可能报error while loading shared libraries: libnss3.so。因为浏览器在显示页面时需要一堆图形和字体相关的动态链接库。完整列表很长,常见的有:
libnss3.so libatk-bridge-2.0.so.0 libgbm.so.1 libasound.so.2 libxkbcommon.so.0在有网的 Ubuntu 机器上可以执行:
sudo apt-get install -y libnss3 libatk-bridge2.0-0 libgbm1 libasound2 libxkbcommon0在完全离线的机器上,就需要提前下载这些 deb 包并拷贝过去安装。判断缺失哪个库的方法很简单:在目标机上执行:
ldd ./tools/driver/chromedriver | grep "not found"以及:
ldd ./tools/chrome/chrome | grep "not found"每一行输出都是一个缺失的动态库,按图索骥一个个补全。这个步骤比较枯燥,但它是离线跑通网页自动化的必经之路。
4.4 离线环境下为脚本加“稳定保险”
离线环境通常没有外部干预手段,所以脚本本身要设计得更谨慎。我建议至少加三点:
- 显式等待设短一些。网络不在线时,页面元素可能永远等不到,默认 10 秒超时看起来很合理,但 30 个元素等下来,一次失败就要卡好几分钟。
- 失败重试有上限。让浏览器操作失败时最多重试 3 次,每次间隔 2 秒,避免死循环。
- 关键步骤记录日志。每次点击、截图、输入都记录到结构化日志中,离线跑挂后回看日志能快速定位是哪一步。
5. 踩坑实录:五起离线脚本事故的完整排查链路
这一节我挑五个自己真实遇到、也经常在别人项目里见到的“离线脚本事故”,不直接给结论,而是带着排查思路走一遍。这个流程比答案本身更重要。
5.1 工作目录与相对路径:换机必挂的头号原因
第一个事故很典型。脚本在开发机上跑得好好的,拷到内网机器后直接报FileNotFoundError: [Errno 2] No such file or directory: 'config.json'。第一反应是配置文件没拷过去,但检查后发现文件确实在项目根目录。
排查链路:
- 在脚本开头增加
print(os.getcwd()),看当前目录到底是什么。 - 发现打印出的是执行命令时所在的目录,而不是项目目录。
- 确认根因:脚本用了
open("config.json"),这个相对路径是相对于当前工作目录的,而不是脚本文件所在目录。 - 修复:把路径改成基于脚本文件定位:
from pathlib import Path BASE_DIR = Path(__file__).resolve().parent config_path = BASE_DIR / "config.json"这样无论从哪个目录执行,路径都不会错。这个坑在所有语言里都存在,Node 里是__dirname,Java 里要看user.dir,Java 的File("xxx")默认也是基于进程工作目录。
5.2 系统编码与换行符:Windows 与 Linux 之间的隐形冲突
第二个事故发生在跨平台搬运脚本时。我在 Windows 上写了个脚本,输出 CSV 文件,拿到 Linux 服务器上执行后,文件里所有中文字符变成乱码,每行末尾还带\r。
排查链路:
- 怀疑是文件内容本身乱码,因为记事本打开 CSV 显示乱码。
- 用
file output.csv查看,发现编码是ISO-8859-1或application/octet-stream。 - 根因:Python 的
open()如果没有指定encoding,在 Windows 上默认用 GBK 写入,在 Linux 上默认用 UTF-8 写入。而 CSV 里的数据是 UTF-8 编码的字符串,被 Windows 默认 GBK 写出去,自然乱码。 - 修复:所有文件读写显式指定
encoding="utf-8",并且写入时设置newline=""避免多余换行:
with open("output.csv", "w", encoding="utf-8", newline="") as f: writer = csv.writer(f) writer.writerows(data)Windows 与 Linux 之间如果还要传 bat 脚本,也建议在入口处chcp 65001 >nul,并且让 bat 文件使用 CRLF 行尾,否则可能被系统识别失败。
5.3 动态库缺失:本机能跑,服务器上缺全家桶
第三个事故是 Selenium 跑在纯净版 CentOS 服务器上,启动 Chrome 时报error while loading shared libraries: libX11.so.6。因为开发机上开发工具链完整,图形库都有;但标准服务器镜像为了精简,很多 X11 库都没装。
排查链路:
- 先看完整报错,确认是缺共享库,不是驱动问题。
- 执行
ldd /path/to/chrome | grep "not found"列出所有缺失库。 - 逐个查询这些库属于哪个 RPM 包。在有网环境用
yum provides */libX11.so.6或apt-file search找。 - 下载对应的 rpm/deb 包,拷到内网安装。
这类问题还有一个加速技巧:与其一个个补库,不如直接在有网络且系统和目标机一致的环境里,先装好所有依赖,再用docker export或dpkg -l导出包清单,批量打包。
5.4 环境变量污染:全局 PYTHONPATH 干扰 venv
第四个事故是脚本通过run.sh执行,明明已经激活了 venv,但import requests时报的包路径来自系统全局 Python,导致版本不匹配。排查时我先打印了sys.path,发现里面有/usr/lib/python3/dist-packages,而这个路径不是 venv 里的。
根因:用户主目录里的.bashrc或系统环境变量设置了PYTHONPATH,当脚本以 shell 方式启动时,PYTHONPATH被传递给了 Python 进程,覆盖了虚拟环境默认的sys.path顺序。
修复:在入口脚本开头强制清除这个变量:
unset PYTHONPATH或者直接在 Python 内启动时强制使用 venv 解释器,避免依赖 shell 环境。这件事也是为什么我更推荐用subprocess.run调用显式路径的 Python,而不是依赖 shell 激活。
5.5 一个完整 Selenium 离线故障排查案例
第五个事故是内网服务器上跑 Selenium,一直报session not created: This version of ChromeDriver only supports Chrome version 114,但系统里安装的 Chrome 版本是 120。光看报错就感觉要重新下载驱动。
排查链路:
- 查看 Chrome 版本:
google-chrome --version,确认为 120.x。 - 查看 chromedriver 版本:
chromedriver --version,确认为 114.x。 - 确认版本不对,从有网机器下载匹配 Chrome 120 的 ChromeDriver,拷贝过来。
- 重新执行,又报缺
libatk相关动态库。 - 用
ldd查到缺失的库,下载对应 deb 包安装。 - 再执行,成功启动浏览器。
这类问题一定要按顺序排查,不要一看到驱动不匹配就只改驱动,改完之后还可能被系统库卡住。流程走完整下来,才能真正把“离线可执行”变成“离线可复现”。
6. 再进一步:把整套离线执行环境打包成可交付物
前面讲的都是怎么在目标机器上“拼积木”。如果这种环境要多台机器部署,或者要交付给其他团队,逐台安装依赖就不是好方案。这时候应该把整套执行环境打包成一个可交付物,拷过去直接跑。
6.1 用 Docker 离线镜像解决“环境不一致”
Docker 是目前最推荐的离线交付方式。在联网机器上构建一个包含 Python、Node、Chrome、ChromeDriver、所有依赖的镜像,然后导出:
docker save -o offline_runner.tar offline_runner:latest把 tar 文件拷到内网机器后导入:
docker load -i offline_runner.tar运行:
docker run --rm --network none -v /data/project:/app offline_runner python /app/run.py这里关键的参数是--network none,它强制容器创建的网络命名空间没有网络接口,真正做到“离线运行”。这样也能避免脚本在离线环境里某些地方尝试联网导致超时。-v是把宿主机上的项目目录挂载进去,这样镜像只管环境,数据文件跟脚本项目本身保持独立,更新脚本时不需要重新构建镜像。
需要注意docker save和docker export不是一回事。docker save保存的是镜像完整的分层结构,能够被docker load恢复并继续运行;docker export导出的是容器文件系统快照,导入后是一个裸文件系统,不具备镜像层的版本信息。做离线交付时,用docker save是正确选择。
6.2 内网下统一入口脚本的设计
即使有了 Docker,日常跑脚本时也不可能每次手敲一长串docker run。我会在项目根目录放一个run.sh,把所有命令封装起来:
#!/usr/bin/env bash set -euo pipefail cd "$(dirname "$0")" IMAGE_NAME="offline_runner:latest" PROJECT_DIR="$(pwd)" docker run --rm --network none \ -v "${PROJECT_DIR}:/app" \ -w /app \ "${IMAGE_NAME}" \ python /app/run.py "$@"这样内网机器上只需要提前安装好 Docker 并加载镜像,之后执行./run.sh就能跑。任何同事拿到项目目录,不需要在宿主机上装任何语言解释器,也不需要处理 venv,这比逐台机器装环境可靠得多。
如果目标机器连 Docker 都没装,那就要用离线方式安装 Docker。大体思路是:在有网机器下载 Docker 的 rpm 或 deb 包,外加所有依赖,拷到内网后本地安装。这也是比较成熟的做法,但要注意 Docker 和内核版本、systemd 的兼容性,不能只看网上的通用教程。
6.3 无人值守时的自愈与现场保留
离线环境通常意味着没人盯着终端,所以脚本失败后必须能“留下话”。我习惯在统一入口脚本里加入现场保留逻辑:
- 日志目录按日期归档,文件名带时间戳,比如
logs/20250617_1530.log。 - 失败时把当前目录文件列表、最近 50 行日志、环境变量快照写入
debug.txt。 - 如果脚本具有可重试性(比如从断点继续),失败后最多自动重试两次。
具体可以在 Python 中增加一个简单的重试装饰器,或者在run.sh里写一个带循环的调用逻辑。我的实际经验是:宁可让脚本多写一个 debug 文件,也不要让它在离线环境里悄悄失败。因为在线环境你可以上服务器看实时日志,离线环境一旦部署到用户现场,你只能依赖这些落盘信息。
整个离线执行项目做到这一步,基本就达到“拷贝即用”的状态了。这套方案我在公司的内网自动化测试、本地数据处理、模型推理脚本上都验证过,稳定性远高于“临时把文件拷过去试试”的做法。如果你现在正被内网机器的各种依赖问题折磨,建议直接从一个小脚本开始,先把入口脚本、离线依赖、日志落盘这三件事做扎实,再往后扩展。