我們現在需要將某個環境已經安裝的 python 包離線傳遞到另外一個環境,且確保這種安裝行為最終不需要對 PYPI 中央倉庫的有效連接,也能完成。下面給出兩種辦法:
docker container
如果你的 python 環境位于某個容器內,那最好的辦法就是執行docker commit
操作構建鏡像:
docker commit <容器id> <自定義的鏡像名稱>:<tag>
比如
docker commit 123 456:78
tag不是必須的,若沒有指定,默認為latest
。容器id可以通過docker ps
獲取:
123 456 "789" 11 minutes ago Up 11 minutes 0123
其中的123
就是容器id
執行docker commit成功構建鏡像以后,就可以用docker save
保存鏡像:
docker save -o <tar包名稱>.tar <自定義的鏡像名稱>:<tag>
這一步可能存在交換分區不足,導致無法保存鏡像的問題,報錯如下:
Error response from daemon: write layer.tar: no space left on device
可以通過以下命令獲取docker交換空間的地址:
docker info | grep "Docker Root Dir"
至于空間不足,無外乎刪一點東西,或者重定向到其他位置,或者修改 docker 的配置。 這些修改大部分需要重啟 docker 進程,在生產環境下慎用。另外也可以在參數中指定中間位置,但是僅限高版本的 docker
轉移 python 包
如果你不是在容器環境內,那操作可以復雜一點。
導出
首先需要導出 python 包的清單文件(requirements.txt):
pip freeze > requirements.txt
根據清單文件在源環境下載包到本地:
pip download -r requirements.txt -d python-pkgs/ --no-deps
下載的包有whl文件,也有壓縮包,這是正常現象。
源環境已經安裝的包,有可能會互相沖突。也就是源環境的pip生態有可能已經依賴不自恰了。
我們需要先去掉清單文件的版本號,反正僅限本地目錄安裝,也裝不了別的版本。但根本目的是讓pip無法針對版本號進行依賴性檢查:
import argparse
import re
from pathlib import Pathdef remove_version_specifiers(input_file, output_file=None, inplace=False):"""從requirements文件中移除所有包的版本約束參數:input_file (str): 輸入的requirements文件路徑output_file (str): 輸出文件路徑,默認為None(與輸入文件同名但添加-cleaned后綴)inplace (bool): 是否直接在原文件上修改"""# 讀取文件內容with open(input_file, 'r', encoding='utf-8') as f:lines = f.readlines()# 定義正則表達式模式,匹配包名和版本約束pattern = re.compile(r'^([^\s!=<>#]+)([!=<>].*)?$')cleaned_lines = []for line in lines:line = line.strip()# 跳過空行和注釋if not line or line.startswith('#'):cleaned_lines.append(line + '\n')continue# 處理帶有注釋的行if '#' in line:code_part, comment_part = line.split('#', 1)code_part = code_part.strip()comment_part = '#' + comment_partelse:code_part = linecomment_part = ''# 移除版本約束match = pattern.match(code_part)if match:package_name = match.group(1)cleaned_lines.append(f"{package_name}{comment_part}\n")else:# 如果不匹配標準格式,保留原樣cleaned_lines.append(f"{line}\n")# 確定輸出文件路徑if inplace:output_path = input_fileelif output_file:output_path = output_fileelse:input_path = Path(input_file)output_path = input_path.with_name(f"{input_path.stem}-cleaned{input_path.suffix}")# 寫入清理后的內容with open(output_path, 'w', encoding='utf-8') as f:f.writelines(cleaned_lines)print(f"已成功清理文件: {input_file} → {output_path}")return output_pathif __name__ == "__main__":parser = argparse.ArgumentParser(description='移除requirements文件中的版本約束')parser.add_argument('input_file', help='輸入的requirements文件路徑')parser.add_argument('-o', '--output', help='輸出文件路徑,默認為輸入文件名添加-cleaned后綴')parser.add_argument('-i', '--inplace', action='store_true', help='直接在原文件上修改')args = parser.parse_args()try:remove_version_specifiers(args.input_file, args.output, args.inplace)except Exception as e:print(f"處理文件時出錯: {e}")
現在就可以將清單文件和包一起復制到目標環境了。
導入
如果目標環境有已經安裝的python包,需要全卸載掉,防止和從源環境導入的沖突。
在目標環境導出清單文件:
pip freeze > u-requirements.txt
然后卸載掉全部的依賴:
pip uninstall -r u-requirements.txt -y
此時將之前復制過來的離線文件全部安裝即可:
pip install --no-index --find-links=python-pkgs/ -r requirements-cleaned.txt --only-binary=:none: --no-build-isolation --no-deps
--no-index
和--find-links=python-pkgs/
的組合,可以讓pip只從本地的目錄里獲取要安裝的包。
--no-deps
會使 pip 在安裝軟件包時,不安裝它所依賴的其他包。也就是說,只會安裝requirements-cleaned.txt
文件里直接列出的軟件包。這是為了防止在安裝的時候出現依賴沖突。
--no-build-isolation
:正常情況下,pip 在構建軟件包時會創建一個隔離的環境。而使用這個參數后,就不會創建隔離環境,構建過程會依賴當前環境里已有的依賴項。這是防止 pip 無法檢測到本地已經安裝的setup-tools
。
--only-binary=:none::
這個參數表明不使用預編譯的二進制包(像.whl 文件)進行安裝.