複数の.venvからrequirements.txtを一括生成するWindowsバッチ

「複数の.venvからrequirements.txtを一括生成するWindowsバッチ」の内容を表す技術イラスト

Pythonプロジェクトが増えてくると、それぞれのフォルダに .venv が作られ、依存パッケージの管理も分散していきます。

各プロジェクトで次のコマンドを一つずつ実行することもできます。

pip freeze > requirements.txt

しかし、プロジェクトが10件、20件と増えれば、仮想環境を切り替えながら同じ操作を繰り返すのは手間です。また、長期間触っていないプロジェクトでは、requirements.txt が作られていなかったり、現在の仮想環境と内容が一致していなかったりすることもあります。

そこで本記事では、指定フォルダ以下の .venv を再帰的に検索し、それぞれの仮想環境から requirements.txt を一括生成するWindowsバッチを作ります。

目次

このバッチでできること

このバッチには、次の機能があります。

  • 指定フォルダ以下にある .venv を再帰検索する
  • 仮想環境を有効化せず、各環境のPythonを直接実行する
  • .venv の親フォルダへ requirements.txt を生成する
  • 既存ファイルを一時ファイル経由で置き換える
  • 1件失敗しても、残りの仮想環境の処理を継続する
  • 成功、失敗、スキップ件数を最後に表示する
  • .git、node_modules、__pycache__ の内部を検索しない

想定するフォルダ構成は次のようなものです。

projects
├─ project-a
│  ├─ .venv
│  └─ requirements.txt  ← ここに生成
├─ project-b
│  ├─ .venv
│  └─ requirements.txt  ← ここに生成
└─ tools
   └─ project-c
      ├─ .venv
      └─ requirements.txt  ← ここに生成

バッチファイル全文

次のコードを freeze_all_venvs.bat という名前で保存します。

@echo off
setlocal EnableExtensions DisableDelayedExpansion

rem Recursively find .venv folders and write requirements.txt
rem beside each .venv. Virtual environments are not activated.

if "%~1"=="" (
    set "SCAN_ROOT=%~dp0"
) else (
    for %%R in ("%~1") do set "SCAN_ROOT=%%~fR"
)

if not exist "%SCAN_ROOT%\" (
    echo [ERROR] Folder not found: "%SCAN_ROOT%"
    exit /b 1
)

for %%R in ("%SCAN_ROOT%") do set "SCAN_ROOT=%%~fR"

set /a FOUND=0, SUCCEEDED=0, FAILED=0, SKIPPED=0 >nul

echo Scan root: "%SCAN_ROOT%"
echo.

call :scan "%SCAN_ROOT%"

echo.
echo Finished. Found=%FOUND%  Updated=%SUCCEEDED%  Failed=%FAILED%  Skipped=%SKIPPED%

if not "%FAILED%"=="0" exit /b 1
exit /b 0

:scan
for /d %%D in ("%~1\*") do call :visit "%%~fD"
exit /b 0

:visit
if /I "%~nx1"==".venv" (
    set /a FOUND+=1 >nul
    call :freeze_one "%~f1"
    exit /b 0
)

rem Skip directories that cannot contain relevant project environments.
if /I "%~nx1"==".git" exit /b 0
if /I "%~nx1"=="node_modules" exit /b 0
if /I "%~nx1"=="__pycache__" exit /b 0

call :scan "%~f1"
exit /b 0

:freeze_one
set "VENV_DIR=%~f1"

if not exist "%VENV_DIR%\Scripts\python.exe" (
    echo [SKIP] No Windows Python executable: "%VENV_DIR%"
    set /a SKIPPED+=1 >nul
    exit /b 0
)

for %%P in ("%VENV_DIR%\..") do set "PROJECT_DIR=%%~fP"
set "REQ_FILE=%PROJECT_DIR%\requirements.txt"
set "TMP_FILE=%PROJECT_DIR%\requirements.txt.freeze.tmp"

echo [RUN ] "%VENV_DIR%"
"%VENV_DIR%\Scripts\python.exe" -m pip freeze > "%TMP_FILE%"

if errorlevel 1 (
    if exist "%TMP_FILE%" del /q "%TMP_FILE%" >nul 2>&1
    echo [FAIL] pip freeze failed: "%VENV_DIR%"
    set /a FAILED+=1 >nul
    exit /b 1
)

move /y "%TMP_FILE%" "%REQ_FILE%" >nul

if errorlevel 1 (
    echo [FAIL] Could not replace: "%REQ_FILE%"
    echo        Generated data remains at: "%TMP_FILE%"
    set /a FAILED+=1 >nul
    exit /b 1
)

echo [ OK ] "%REQ_FILE%"
set /a SUCCEEDED+=1 >nul
exit /b 0

実行方法

BATを置いたフォルダ以下を検索する

引数を付けずに実行すると、バッチファイル自身が置かれているフォルダを検索の起点にします。

freeze_all_venvs.bat

たとえば、次の場所にバッチファイルを置いたとします。

C:\Users\user\projects\freeze_all_venvs.bat

この場合は、C:\Users\user\projects 以下にあるすべての .venv が検索対象になります。

任意のフォルダを指定する

検索したいルートフォルダを第1引数として渡すこともできます。

freeze_all_venvs.bat "C:\Users\user\projects"

パス全体をダブルクォーテーションで囲んでいるため、フォルダ名に空白が含まれていても実行できます。

仮想環境を有効化しなくてよい理由

一般的には、Windowsで仮想環境を利用するときに次のような有効化操作を行います。

.venv\Scripts\activate

しかし、今回のバッチでは仮想環境内のPythonをフルパスで直接呼び出します。

"%VENV_DIR%\Scripts\python.exe" -m pip freeze

どのPythonを使用するかが明示されているため、仮想環境を有効化する必要がありません。また、単に pip を呼ぶのではなく、python.exe -m pip としているので、その仮想環境に対応するpipを確実に実行できます。

再帰検索の仕組み

:scan サブルーチンは、現在のフォルダにある子フォルダを列挙します。

:scan
for /d %%D in ("%~1\*") do call :visit "%%~fD"
exit /b 0

見つかったフォルダは、:visit サブルーチンへ渡されます。フォルダ名が .venv なら pip freeze の対象とし、それ以外ならさらに下の階層を検索します。

if /I "%~nx1"==".venv" (
    set /a FOUND+=1 >nul
    call :freeze_one "%~f1"
    exit /b 0
)

.venv を見つけた時点でその内部には入らないため、通常は大量のファイルがある site-packages まで走査せずに済みます。

また、Python環境の検索と関係しないことが多い次のフォルダも除外しています。

if /I "%~nx1"==".git" exit /b 0
if /I "%~nx1"=="node_modules" exit /b 0
if /I "%~nx1"=="__pycache__" exit /b 0

一時ファイルを使用する理由

単純に次のように実行すると、処理の開始時点で既存の requirements.txt が空になってしまう可能性があります。

pip freeze > requirements.txt

その状態で pip freeze が失敗すると、以前の正常な内容を失うおそれがあります。

そこで、このバッチでは最初に一時ファイルへ出力します。

"%VENV_DIR%\Scripts\python.exe" -m pip freeze > "%TMP_FILE%"

pip freeze が正常終了した場合だけ、次の処理で正式な requirements.txt と置き換えます。

move /y "%TMP_FILE%" "%REQ_FILE%" >nul

これにより、pip freeze 自体が失敗したときに、既存の requirements.txt を空ファイルで上書きする事故を防いでいます。

途中で失敗しても検索を継続する

:freeze_one 内では、エラー発生時に exit /b 1 を返しています。

exit /b 1

ここで終了するのはサブルーチンであり、バッチファイル全体ではありません。そのため、1件の .venv が壊れていても、残りの環境に対する処理は継続されます。

最後に FAILED の値を確認し、1件でも失敗していればバッチ全体の終了コードを 1 にします。

if not "%FAILED%"=="0" exit /b 1

人が画面で実行結果を確認できるだけでなく、別のスクリプトやCIから呼び出した場合にも、失敗の有無を終了コードで判定できます。

「No Python at …」と表示された場合

次のようなエラーが表示されることがあります。

No Python at "C:\Users\old-user\AppData\Local\Programs\Python\Python312\python.exe"

これは、.venv が作成時に使用したPythonを見つけられない状態です。たとえば、次のような場合に発生します。

  • 別のPCからプロジェクトと .venv をまとめてコピーした
  • Windowsのユーザー名が変わった
  • 親フォルダを別の場所へ移動した
  • 元になったPythonをアンインストールした

Pythonの仮想環境には、作成時の環境を基準とした絶対パスが含まれるため、一般にそのまま別の場所へ移動して使うことはできません。Python公式ドキュメントでも、仮想環境を移動した場合は、新しい場所で再作成することが推奨されています。

既存の requirements.txt がある場合は、壊れた環境を一度退避し、新しい環境を作り直せます。

ren .venv .venv_broken
py -3.12 -m venv .venv
.venv\Scripts\python.exe -m pip install --upgrade pip
.venv\Scripts\python.exe -m pip install -r requirements.txt

requirements.txt が存在しない場合は、古い .venv をすぐに削除しないでください。インストールされていたパッケージを確認・救出できる可能性があるため、まず .venv_broken などの名前に変更して退避するのが安全です。

pip freezeを使う際の注意点

pip freeze は、プロジェクトが直接必要としているライブラリだけを抽出するコマンドではありません。現在の環境にインストールされているパッケージとバージョンを、再インストールに利用できる形式で出力します。

したがって、生成されるファイルには、直接利用しているパッケージだけでなく、そのパッケージが依存する間接依存関係も含まれます。

pip公式ドキュメントでも、pip freeze は現在インストールされている状態を報告するものであり、ロックファイルや依存関係の解決結果を生成するものではないと説明されています。

このため、生成した requirements.txt は、次の用途に向いています。

  • 現在動いているPython環境のスナップショット
  • プロジェクトを別のPCで再構築するための記録
  • 長期間保管するプロジェクトの依存パッケージ保存
  • Gitで依存環境の変化を確認するための資料

一方、プロジェクトが直接依存するライブラリを厳密に管理したい場合は、pyproject.toml、pip-tools、Poetry、uvなどを利用する方法もあります。

使用前に確認したい注意事項

既存のrequirements.txtは更新される

このバッチは、正常に pip freeze が完了すると、既存の requirements.txt を置き換えます。Gitで管理しているプロジェクトでは、実行後に必ず差分を確認してください。

git diff -- requirements.txt

編集可能インストールも出力される

pip install -e . などで編集可能インストールしたパッケージも、通常は出力対象になります。除外したい場合は、次のオプションを追加できます。

python -m pip freeze --exclude-editable

ただし、編集可能インストールも環境再現に必要な情報である場合があります。常に除外するのではなく、用途に応じて選択するのが適切です。

Windows用のバッチである

このコードは、次のWindows形式の仮想環境を対象としています。

.venv\Scripts\python.exe

LinuxやmacOSでは通常、Pythonの場所が次のようになります。

.venv/bin/python

そのため、LinuxやmacOSではBashなどで同等のスクリプトを作成する必要があります。

今後追加できる機能

公開用のユーティリティとして発展させるなら、次の機能が考えられます。

  • --dry-run:対象を表示するだけでファイルを書き換えない
  • --apply:明示的に指定したときだけ更新する
  • --backup:更新前のファイルを .bak として保存する
  • --exclude-editable:編集可能インストールを除外する
  • [BROKEN VENV]:移動やPython削除で壊れた環境を分類表示する
  • 更新前後の差分表示
  • CSVまたはJSONによる実行結果の出力
  • PowerShell版、Bash版の追加

特に、不特定多数へ公開する場合は、デフォルトでは書き換えを行わない --dry-run と、明示的に更新する --apply の組み合わせが安全です。

まとめ

このバッチを利用すると、複数のPythonプロジェクトに分散した .venv から、requirements.txt をまとめて生成できます。

単純な繰り返し作業を自動化するだけでなく、長期間使っていなかった仮想環境や、移動によって壊れた仮想環境を発見する点でも役立ちます。

ただし、pip freeze の出力は「プロジェクトが本来必要とする最小依存関係」ではなく、「現在の環境にインストールされているパッケージのスナップショット」です。この性質を理解したうえで、Gitによる差分確認や、pyproject.toml などの依存関係管理と組み合わせると、より安全に運用できます。

参考になったらシェアしてください
  • URLをコピーしました!
  • URLをコピーしました!

この記事を書いた人

機械設計・油圧・CAD・Python・AIなど、ものづくりに関わる技術を扱っています。工学知識を整理・構造化し、設計や自動化に再利用できる形へ変えていくことを目指しています。

目次