Streamlitアプリの依存関係を自動チェック＆インストールしてくれるHome.pyを作った話

# **user**:

次のように、多数のページから成るディレクトリ構成のStreamlitアプリがあります。

```
streamlit_app/
├── Home.py              ← トップページ（自動で最初に読み込まれる）
├── 📂 pages/
│   ├── Page1.py         ← ページ1
│   ├── Page2.py         ← ページ2
├── utils.py             ← 共通関数など
├── requirements.txt
```

ページごとに異なるPythonライブラリをimportするのですが、めったに使わないページもあるので、requirements.txtですべてimportするようなことはせず、使うことになったらimportするようにしたいです。そのような操作をHome.pyで行うことができれば良いなと思っていますが、良い考えはありませんか？

# **assistant**:

そのような「**ページによって使うライブラリが異なるが、全体のrequirements.txtには含めたくない**」という要望に応えるためには、次のようなアプローチが考えられます。

---

## ✅ アイデア：**必要なときにimport＆エラーメッセージ表示**
Streamlitはページを開くたびに該当する `.py` ファイルが実行されるため、**各ページで必要なときに `import` を実行する**のが自然です。

ただし、`import` したいライブラリが未インストールであれば `ImportError` になりますので、以下のようなパターンで扱うのがスマートです。

---

### 💡 各ページでの遅延インポート＋例外処理

```python
import streamlit as st

try:
    import some_rare_library  # あまり使わないライブラリ
except ImportError:
    st.error("このページには 'some_rare_library' が必要ですが、インストールされていません。\n以下のコマンドでインストールしてください:\n\n`pip install some-rare-library`")
    st.stop()

# ここから本来の処理
st.write("このページでは some_rare_library を使っています！")
```

このようにすれば、**必要になったときにだけエラーメッセージを表示して処理を止める**ことができます。

---

### 👑 さらに親切に：インストール補助リンクを表示

Streamlitは`st.code()`や`st.markdown()`を使ってインストールコマンドのコピーやリンクも表示できるので、次のようにもできます：

```python
st.warning("必要なライブラリが見つかりませんでした。")
st.code("pip install some-rare-library", language="bash")
```

---

### ❓Home.pyで共通的に制御したい場合は？

基本的にStreamlitのページ構成（マルチページ）では、`Home.py` は他ページを直接制御できません。ですが、**共通の関数で「必要なときにインポートを試す」ラッパー関数**を `utils.py` に書いておくと便利です：

#### 例：`utils.py`
```python
import streamlit as st
import importlib

def try_import(module_name, install_hint=None):
    try:
        return importlib.import_module(module_name)
    except ImportError:
        st.error(f"このページには `{module_name}` が必要です。")
        if install_hint:
            st.code(f"pip install {install_hint}", language="bash")
        st.stop()
```

#### 使う側（Page1.pyなど）：
```python
from utils import try_import

rare_lib = try_import("some_rare_library", install_hint="some-rare-library")

# 以後 rare_lib.xxx として使用
```

---

## ✅ まとめ

| 方法 | メリット | デメリット |
|------|----------|------------|
| 各ページでtry-import | シンプル・必要なときだけ | コードが冗長になりやすい |
| 共通関数でラップ | 再利用性・読みやすさ向上 | 若干抽象化される |
| requirements.txtに全部書く | 確実に全ページ動く | サイズ膨張・無駄なインストール |

---

興味があれば、次に「使われているページだけを自動で抽出して依存ライブラリを収集するスクリプト」なんかも提案できますよ！

# **user**:

pages 以下にある全ての .py ファイルから import 文を確認して、インポートされていないライブラリを.py ファイルごとに教えてくれて、ボタンを押したらインポートしてくれるような機能をHome.pyに書けませんか？

# **assistant**:

めっちゃ面白いアイデアですね！そして、**技術的に実現可能**です。

要するに、
- `pages/` 以下の `.py` ファイルをスキャンして、
- 使われている外部ライブラリを調べて、
- それがインストールされているか確認して、
- 足りないものがあれば `pip install` でインストールするボタンを表示する

という **簡易的な依存管理ツール**を `Home.py` に埋め込むイメージですね。

---

## ✅ 実装プラン（Home.py）

1. `pages/` 内の `.py` ファイルを列挙  
2. 各ファイルから `import xxx` を抽出（AST解析 or 正規表現）  
3. `pkg_resources` で現在の環境にインストール済みかチェック  
4. 足りないライブラリをページ別に表示  
5. ボタンを押すと `subprocess.run(["pip", "install", xxx])` でインストール

---

## 💻 サンプルコード（Home.py）

以下のコードを `Home.py` に入れると、かなりそれっぽく動きます：

```python
import streamlit as st
import os
import ast
import pkg_resources
import subprocess

PAGES_DIR = "pages"

def extract_imports_from_file(file_path):
    with open(file_path, "r", encoding="utf-8") as f:
        tree = ast.parse(f.read(), filename=file_path)
    
    imports = set()
    for node in ast.walk(tree):
        if isinstance(node, ast.Import):
            for n in node.names:
                imports.add(n.name.split('.')[0])
        elif isinstance(node, ast.ImportFrom):
            if node.module:
                imports.add(node.module.split('.')[0])
    return imports

def is_installed(pkg_name):
    try:
        pkg_resources.get_distribution(pkg_name)
        return True
    except pkg_resources.DistributionNotFound:
        return False

st.title("ページ別依存ライブラリチェッカー")

missing_packages = {}

for filename in os.listdir(PAGES_DIR):
    if filename.endswith(".py"):
        filepath = os.path.join(PAGES_DIR, filename)
        imported = extract_imports_from_file(filepath)
        
        not_installed = [pkg for pkg in imported if not is_installed(pkg)]
        if not_installed:
            missing_packages[filename] = not_installed

if not missing_packages:
    st.success("すべてのページに必要なライブラリがインストールされています 🎉")
else:
    for file, pkgs in missing_packages.items():
        with st.expander(f"📄 {file} に不足しているパッケージ"):
            for pkg in pkgs:
                col1, col2 = st.columns([3, 1])
                col1.write(f"- `{pkg}`")
                if col2.button(f"インストール: {pkg}", key=f"{file}-{pkg}"):
                    with st.spinner(f"{pkg} をインストール中..."):
                        result = subprocess.run(["pip", "install", pkg], capture_output=True, text=True)
                        if result.returncode == 0:
                            st.success(f"{pkg} をインストールしました")
                        else:
                            st.error(f"{pkg} のインストールに失敗しました:\n{result.stderr}")
```

---

## ⚠️ 注意点

- `ast` 解析で取得できるのは「`import` で静的に書かれているライブラリ」のみです（`importlib`など動的importは対象外）。
- PyPIのパッケージ名とimport名が異なる場合（例：`sklearn` → `scikit-learn`）は手動で調整が必要です。
- `subprocess.run(["pip", "install", ...])` は Streamlit 実行環境によっては権限がないかも（Colabや一部のクラウド環境）。

---

## 💡 拡張アイデア

- `requirements_checker.py` として外部ファイルにしておいて、`Home.py` から読み込む
- インストール予定のパッケージを一覧にして `pip install -r` 相当のまとめてインストールもできる
- `importlib.metadata`（Python 3.8+）を使えばよりモダンに判定可能

---

この方向で進めたいですか？改善ポイントや、カスタマイズしたい部分があれば聞かせてください！

# **user**:

ありがとうございます。いくつかはインポートできるようですが、

```
io のインストールに失敗しました: ERROR: Could not find a version that satisfies the requirement io (from versions: none) ERROR: No matching distribution found for io 
```

のように失敗するものが多いですね。

# **assistant**:

めちゃくちゃ鋭い観察です！そのエラーの原因は、**標準ライブラリを PyPI パッケージとしてインストールしようとしてしまっているから**です。

---

## 🧠 背景：標準ライブラリ vs 外部ライブラリ

Python には「標準ライブラリ」と「外部ライブラリ（PyPIからインストールするやつ）」の2種類があります：

| 例 | ライブラリの種類 | インストール必要？ |
|-----|------------------|-------------------|
| `os`, `sys`, `math`, `io` | 標準ライブラリ | ❌ もとからある |
| `pandas`, `numpy`, `matplotlib` | 外部ライブラリ | ✅ `pip install` が必要 |

なので、`io`, `sys`, `re`, `os` などを `pip install` しようとしてもエラーになります。

---

## ✅ 解決策：標準ライブラリを除外するフィルターを追加する

次のように `standard_libs` を参照して、インストール対象から除外できます。

### 🔧 修正する箇所（Home.py）

```python
import sys

standard_libs = sys.builtin_module_names  # タプル（'sys', 'os', 'math' など）

# is_installed 関数の中で標準ライブラリを除外
def is_installed(pkg_name):
    if pkg_name in standard_libs:
        return True  # 標準ライブラリなのでOK
    try:
        pkg_resources.get_distribution(pkg_name)
        return True
    except pkg_resources.DistributionNotFound:
        return False
```

---

## 👌 さらに高精度にしたい場合

`sys.builtin_module_names` だけでは不十分なことがあります（例：`re`, `json`, `typing` はビルトインではないけど標準ライブラリ）。なので、次のように `stdlib_list` ライブラリを使って判定する方法もあります：

```bash
pip install stdlib-list
```

```python
from stdlib_list import stdlib_list

stdlibs = set(stdlib_list("3.10"))  # Pythonのバージョンに応じて変更

def is_installed(pkg_name):
    if pkg_name in stdlibs:
        return True
    try:
        pkg_resources.get_distribution(pkg_name)
        return True
    except pkg_resources.DistributionNotFound:
        return False
```

---

## ✨ まとめ

| 修正ポイント | 内容 |
|--------------|------|
| ❌ 標準ライブラリをpip installしないようにする | `sys.builtin_module_names` または `stdlib-list` を使って判定 |
| ✅ 必要な外部ライブラリだけインストール対象にする | `pip install` が必要なものだけ表示・ボタン化 |

---

もしご希望があれば、この機能全体をモジュール化（utils化）して、任意のプロジェクトに組み込みやすくする案も出せますよ！

# **user**:

ご提案ありがとうございます。ご提案通りの機能を Home.py に組み込んだ全コードを書いてください。

# **assistant**:

こちらが、標準ライブラリを除外し、`pages/` 以下の `.py` ファイルで使われている外部ライブラリを自動検出・インストールできるようにした `Home.py` の完全版コードです：

---

## ✅ 完全版 `Home.py`

```python
import streamlit as st
import os
import ast
import pkg_resources
import subprocess
from stdlib_list import stdlib_list

# --- 設定 ---
PAGES_DIR = "pages"
PYTHON_VERSION = "3.10"  # ご自身のPythonバージョンに合わせてください（例: "3.11"）

# --- 標準ライブラリ一覧を取得 ---
stdlibs = set(stdlib_list(PYTHON_VERSION))

# --- ファイルから import 文を抽出 ---
def extract_imports_from_file(file_path):
    with open(file_path, "r", encoding="utf-8") as f:
        tree = ast.parse(f.read(), filename=file_path)

    imports = set()
    for node in ast.walk(tree):
        if isinstance(node, ast.Import):
            for n in node.names:
                imports.add(n.name.split('.')[0])
        elif isinstance(node, ast.ImportFrom):
            if node.module:
                imports.add(node.module.split('.')[0])
    return imports

# --- パッケージがインストールされているか判定 ---
def is_installed(pkg_name):
    if pkg_name in stdlibs:
        return True
    try:
        pkg_resources.get_distribution(pkg_name)
        return True
    except pkg_resources.DistributionNotFound:
        return False

# --- メイン処理 ---
st.title("📦 ページ別ライブラリチェック＆インストーラー")

missing_packages = {}

# 各ページの.pyファイルを走査
for filename in os.listdir(PAGES_DIR):
    if filename.endswith(".py"):
        filepath = os.path.join(PAGES_DIR, filename)
        imported = extract_imports_from_file(filepath)

        not_installed = [pkg for pkg in imported if not is_installed(pkg)]
        if not_installed:
            missing_packages[filename] = not_installed

# 結果の表示
if not missing_packages:
    st.success("✅ すべてのページに必要なライブラリはインストール済みです 🎉")
else:
    for file, pkgs in missing_packages.items():
        with st.expander(f"📄 {file} に不足しているパッケージ"):
            for pkg in pkgs:
                col1, col2 = st.columns([3, 1])
                col1.write(f"- `{pkg}`")
                if col2.button(f"インストール: {pkg}", key=f"{file}-{pkg}"):
                    with st.spinner(f"{pkg} をインストール中..."):
                        result = subprocess.run(
                            ["pip", "install", pkg],
                            capture_output=True, text=True
                        )
                        if result.returncode == 0:
                            st.success(f"{pkg} をインストールしました ✅")
                        else:
                            st.error(f"{pkg} のインストールに失敗しました ❌\n\n{result.stderr}")
```

---

## ✅ 補足

- このコードは `pages/` 以下の `.py` を自動でスキャンし、標準ライブラリを除いた外部ライブラリだけを検出します。
- 標準ライブラリの判定には `stdlib-list` を使っています。未インストールなら事前に以下を実行してください：
  
  ```bash
  pip install stdlib-list
  ```

- Pythonのバージョンによって `stdlib_list("3.10")` の部分を変えてください（例：3.11を使っていれば `"3.11"` に）。

---

ご希望があれば、次は：
- 複数パッケージをまとめてインストールするボタン
- エクスポート用 requirements.txt を自動生成
などの追加機能にも対応できます！

# **user**:

ありがとうございます。予期していたことでしたが、ご提案いただいた Home.py を実行すると

```
ModuleNotFoundError: No module named 'stdlib_list'
Traceback:
File "/Users/ikemen/streamlit-app/Home.py", line 6, in <module>
    from stdlib_list import stdlib_list
```

というエラーになりました。このエラーを予期して、stdlib_list がなかったらインストールするように Home.py を改変していただけませんか？

# **assistant**:

もちろんです！ご希望の通り、`stdlib_list` がインストールされていなかった場合に **自動でインストールし、インポートをリトライする** 処理を `Home.py` の冒頭に追加します。

---

## ✅ 自動インストール対応済みの `Home.py`

```python
import streamlit as st
import os
import ast
import pkg_resources
import subprocess
import sys

# --- stdlib_list の自動インストール ---
try:
    from stdlib_list import stdlib_list
except ModuleNotFoundError:
    with st.spinner("📦 stdlib_list をインストール中..."):
        subprocess.run([sys.executable, "-m", "pip", "install", "stdlib-list"], check=True)
    from stdlib_list import stdlib_list

# --- 設定 ---
PAGES_DIR = "pages"
PYTHON_VERSION = f"{sys.version_info.major}.{sys.version_info.minor}"  # 自動でバージョン取得

# --- 標準ライブラリ一覧を取得 ---
stdlibs = set(stdlib_list(PYTHON_VERSION))

# --- ファイルから import 文を抽出 ---
def extract_imports_from_file(file_path):
    with open(file_path, "r", encoding="utf-8") as f:
        tree = ast.parse(f.read(), filename=file_path)

    imports = set()
    for node in ast.walk(tree):
        if isinstance(node, ast.Import):
            for n in node.names:
                imports.add(n.name.split('.')[0])
        elif isinstance(node, ast.ImportFrom):
            if node.module:
                imports.add(node.module.split('.')[0])
    return imports

# --- パッケージがインストールされているか判定 ---
def is_installed(pkg_name):
    if pkg_name in stdlibs:
        return True
    try:
        pkg_resources.get_distribution(pkg_name)
        return True
    except pkg_resources.DistributionNotFound:
        return False

# --- メイン処理 ---
st.title("📦 ページ別ライブラリチェック＆インストーラー")

missing_packages = {}

# 各ページの.pyファイルを走査
for filename in os.listdir(PAGES_DIR):
    if filename.endswith(".py"):
        filepath = os.path.join(PAGES_DIR, filename)
        imported = extract_imports_from_file(filepath)

        not_installed = [pkg for pkg in imported if not is_installed(pkg)]
        if not_installed:
            missing_packages[filename] = not_installed

# 結果の表示
if not missing_packages:
    st.success("✅ すべてのページに必要なライブラリはインストール済みです 🎉")
else:
    for file, pkgs in missing_packages.items():
        with st.expander(f"📄 {file} に不足しているパッケージ"):
            for pkg in pkgs:
                col1, col2 = st.columns([3, 1])
                col1.write(f"- `{pkg}`")
                if col2.button(f"インストール: {pkg}", key=f"{file}-{pkg}"):
                    with st.spinner(f"{pkg} をインストール中..."):
                        result = subprocess.run(
                            [sys.executable, "-m", "pip", "install", pkg],
                            capture_output=True, text=True
                        )
                        if result.returncode == 0:
                            st.success(f"{pkg} をインストールしました ✅")
                        else:
                            st.error(f"{pkg} のインストールに失敗しました ❌\n\n{result.stderr}")
```

---

## 🔧 補足

- `sys.executable` を使うことで、仮想環境でも正しい Python 実行環境に `pip install` できます。
- 最初の実行時のみ `stdlib-list` がなければ自動インストールされます。
- Pythonバージョンも自動検出されるので、特に指定する必要はありません。

---

このまま常用していただけるはずですが、さらに改善したい点があれば遠慮なく教えてください！

# **user**:

ありがとうございます。ほぼ完璧なんですが、「PyPIのパッケージ名とimport名が異なる場合（例：sklearn → scikit-learn）は手動で調整が必要です。」の部分だけ、どうにかならないですかね？

# **assistant**:

めちゃくちゃいい質問です！それ、**まさにPythonあるあるの落とし穴**なんですよね。

---

## 💡 問題の本質

Pythonには次のように **import名とPyPIパッケージ名が異なる**ライブラリが結構あります：

| import名 | pip install名 |
|-----------|----------------|
| `sklearn` | `scikit-learn` |
| `cv2`     | `opencv-python` |
| `PIL`     | `Pillow` |
| `bs4`     | `beautifulsoup4` |
| `yaml`    | `PyYAML` |
| `Crypto`  | `pycryptodome` |

---

## ✅ 解決策：import名 → PyPI名の変換辞書を使う

完全自動は難しいですが、**よくある例をカバーした変換辞書**を使えばかなり実用的になります！

---

### 🔧 辞書＆変換ロジックを追加したコード（抜粋）

```python
# --- よくある import名 → pip名 の変換マッピング ---
import_to_pip = {
    "sklearn": "scikit-learn",
    "cv2": "opencv-python",
    "PIL": "Pillow",
    "bs4": "beautifulsoup4",
    "yaml": "PyYAML",
    "Crypto": "pycryptodome",
    "tensorflow": "tensorflow",  # 明示しておくと安心
    # 必要に応じて追加可能
}

# --- パッケージがインストールされているか判定（変換考慮） ---
def is_installed(pkg_name):
    pip_name = import_to_pip.get(pkg_name, pkg_name)
    if pkg_name in stdlibs:
        return True
    try:
        pkg_resources.get_distribution(pip_name)
        return True
    except pkg_resources.DistributionNotFound:
        return False

# --- インストール時も変換名を使う ---
# 呼び出し時の例：
# subprocess.run([sys.executable, "-m", "pip", "install", import_to_pip.get(pkg, pkg)])
```

---

### ✨ 使い方の例（streamlit ボタン内）

```python
pip_pkg = import_to_pip.get(pkg, pkg)
result = subprocess.run(
    [sys.executable, "-m", "pip", "install", pip_pkg],
    capture_output=True, text=True
)
```

---

## ✅ これでどうなる？

たとえば `Page1.py` に次のようなコードがあるとします：

```python
import sklearn
import cv2
```

`Home.py` はこう判断します：

| 見つけたimport名 | pip install名として使う | インストール成功？ |
|------------------|--------------------------|--------------------|
| `sklearn`        | `scikit-learn`           | ✅ OK |
| `cv2`            | `opencv-python`          | ✅ OK |

---

## 🔄 改良の余地

- `import_to_pip` は拡張可能です（JSONなどで外出ししてもいい）
- 自動推論まではしない（精度が保証できないので手動マップが安心）

---

## ✍️ 必要なら全体コードに組み込んだ版もお出しできます！

ご希望あれば、これを完全に `Home.py` に組み込んだバージョンとしてコード出しますね！どうします？

# **user**:

はい、よろしくお願いします！

# **assistant**:

了解です！それでは、**import名とPyPIパッケージ名の変換マッピング（辞書）を含めた完全版の `Home.py`** を以下にお届けします！

---

## ✅ import名 → pip install名 変換対応済み `Home.py`

```python
import streamlit as st
import os
import ast
import pkg_resources
import subprocess
import sys

# --- stdlib_list の自動インストール ---
try:
    from stdlib_list import stdlib_list
except ModuleNotFoundError:
    with st.spinner("📦 stdlib_list をインストール中..."):
        subprocess.run([sys.executable, "-m", "pip", "install", "stdlib-list"], check=True)
    from stdlib_list import stdlib_list

# --- よくある import名 → pip install名 の変換辞書 ---
import_to_pip = {
    "sklearn": "scikit-learn",
    "cv2": "opencv-python",
    "PIL": "Pillow",
    "bs4": "beautifulsoup4",
    "yaml": "PyYAML",
    "Crypto": "pycryptodome",
    "Image": "Pillow",
    "matplotlib": "matplotlib",
    "seaborn": "seaborn",
    "numpy": "numpy",
    "pandas": "pandas",
    "tensorflow": "tensorflow",
    "torch": "torch",
    "plotly": "plotly",
    "flask": "flask",
    "fastapi": "fastapi",
}

# --- 設定 ---
PAGES_DIR = "pages"
PYTHON_VERSION = f"{sys.version_info.major}.{sys.version_info.minor}"

# --- 標準ライブラリ一覧を取得 ---
stdlibs = set(stdlib_list(PYTHON_VERSION))

# --- ファイルから import 文を抽出 ---
def extract_imports_from_file(file_path):
    with open(file_path, "r", encoding="utf-8") as f:
        tree = ast.parse(f.read(), filename=file_path)

    imports = set()
    for node in ast.walk(tree):
        if isinstance(node, ast.Import):
            for n in node.names:
                imports.add(n.name.split('.')[0])
        elif isinstance(node, ast.ImportFrom):
            if node.module:
                imports.add(node.module.split('.')[0])
    return imports

# --- パッケージがインストールされているか判定（変換対応） ---
def is_installed(import_name):
    pip_name = import_to_pip.get(import_name, import_name)
    if import_name in stdlibs:
        return True
    try:
        pkg_resources.get_distribution(pip_name)
        return True
    except pkg_resources.DistributionNotFound:
        return False

# --- メイン処理 ---
st.title("📦 ページ別ライブラリチェック＆インストーラー")

missing_packages = {}

# 各ページの.pyファイルを走査
for filename in os.listdir(PAGES_DIR):
    if filename.endswith(".py"):
        filepath = os.path.join(PAGES_DIR, filename)
        imported = extract_imports_from_file(filepath)

        not_installed = [pkg for pkg in imported if not is_installed(pkg)]
        if not_installed:
            missing_packages[filename] = not_installed

# 結果の表示
if not missing_packages:
    st.success("✅ すべてのページに必要なライブラリはインストール済みです 🎉")
else:
    for file, pkgs in missing_packages.items():
        with st.expander(f"📄 {file} に不足しているパッケージ"):
            for pkg in pkgs:
                pip_pkg = import_to_pip.get(pkg, pkg)
                col1, col2 = st.columns([3, 1])
                col1.write(f"- `{pkg}` ➜ `{pip_pkg}`")
                if col2.button(f"インストール: {pip_pkg}", key=f"{file}-{pkg}"):
                    with st.spinner(f"{pip_pkg} をインストール中..."):
                        result = subprocess.run(
                            [sys.executable, "-m", "pip", "install", pip_pkg],
                            capture_output=True, text=True
                        )
                        if result.returncode == 0:
                            st.success(f"{pip_pkg} をインストールしました ✅")
                        else:
                            st.error(f"{pip_pkg} のインストールに失敗しました ❌\n\n{result.stderr}")
```

---

## 💡 このバージョンのポイント

- **`import_to_pip` 辞書**を導入し、変換名を表示＆インストールに使用
- 不足ライブラリを明示的に「`import名 ➜ pip名`」と表示
- 初回実行時に `stdlib_list` が未インストールでも自動対応
- Pythonバージョンは自動取得で汎用性高め

---

## ✨ 拡張のアイデア（必要なら実装できます）

- 不足ライブラリをまとめて「一括インストール」ボタン追加
- 自動で `requirements.txt` を生成して保存
- `import_to_pip` を外部JSONファイルで管理

---

必要に応じてお好きな方向にカスタマイズしていきましょう！何か追加したい機能ありますか？

# **user**:

ありがとうございます。自動で requirements.txt を生成して保存する機能を追加した全コードを書いてください。

# **assistant**:

喜んで対応します！  
以下は、**不足しているライブラリを `requirements.txt` に自動出力する機能を追加した完全版の `Home.py`** です。

---

## ✅ 主な追加機能

- 各ページで見つかった不足ライブラリをすべて収集し、`requirements.txt` にまとめて保存するボタンを追加。
- すでに存在する `requirements.txt` は上書き（必要ならバックアップ機能を付けることも可能）。

---

## 📄 `Home.py`（requirements.txt 自動生成対応済み）

```python
import streamlit as st
import os
import ast
import pkg_resources
import subprocess
import sys

# --- stdlib_list の自動インストール ---
try:
    from stdlib_list import stdlib_list
except ModuleNotFoundError:
    with st.spinner("📦 stdlib_list をインストール中..."):
        subprocess.run([sys.executable, "-m", "pip", "install", "stdlib-list"], check=True)
    from stdlib_list import stdlib_list

# --- よくある import名 → pip install名 の変換辞書 ---
import_to_pip = {
    "sklearn": "scikit-learn",
    "cv2": "opencv-python",
    "PIL": "Pillow",
    "bs4": "beautifulsoup4",
    "yaml": "PyYAML",
    "Crypto": "pycryptodome",
    "Image": "Pillow",
    "matplotlib": "matplotlib",
    "seaborn": "seaborn",
    "numpy": "numpy",
    "pandas": "pandas",
    "tensorflow": "tensorflow",
    "torch": "torch",
    "plotly": "plotly",
    "flask": "flask",
    "fastapi": "fastapi",
}

# --- 設定 ---
PAGES_DIR = "pages"
PYTHON_VERSION = f"{sys.version_info.major}.{sys.version_info.minor}"

# --- 標準ライブラリ一覧を取得 ---
stdlibs = set(stdlib_list(PYTHON_VERSION))

# --- ファイルから import 文を抽出 ---
def extract_imports_from_file(file_path):
    with open(file_path, "r", encoding="utf-8") as f:
        tree = ast.parse(f.read(), filename=file_path)

    imports = set()
    for node in ast.walk(tree):
        if isinstance(node, ast.Import):
            for n in node.names:
                imports.add(n.name.split('.')[0])
        elif isinstance(node, ast.ImportFrom):
            if node.module:
                imports.add(node.module.split('.')[0])
    return imports

# --- パッケージがインストールされているか判定（変換対応） ---
def is_installed(import_name):
    pip_name = import_to_pip.get(import_name, import_name)
    if import_name in stdlibs:
        return True
    try:
        pkg_resources.get_distribution(pip_name)
        return True
    except pkg_resources.DistributionNotFound:
        return False

# --- メイン処理 ---
st.title("📦 ページ別ライブラリチェック＆インストーラー")

missing_packages = {}

# 各ページの.pyファイルを走査
for filename in os.listdir(PAGES_DIR):
    if filename.endswith(".py"):
        filepath = os.path.join(PAGES_DIR, filename)
        imported = extract_imports_from_file(filepath)

        not_installed = [pkg for pkg in imported if not is_installed(pkg)]
        if not_installed:
            missing_packages[filename] = not_installed

# --- 結果表示 ---
if not missing_packages:
    st.success("✅ すべてのページに必要なライブラリはインストール済みです 🎉")
else:
    st.subheader("📄 不足パッケージの確認とインストール")
    all_missing = set()
    for file, pkgs in missing_packages.items():
        with st.expander(f"📄 {file} に不足しているパッケージ"):
            for pkg in pkgs:
                pip_pkg = import_to_pip.get(pkg, pkg)
                all_missing.add(pip_pkg)
                col1, col2 = st.columns([3, 1])
                col1.write(f"- `{pkg}` ➜ `{pip_pkg}`")
                if col2.button(f"インストール: {pip_pkg}", key=f"{file}-{pkg}"):
                    with st.spinner(f"{pip_pkg} をインストール中..."):
                        result = subprocess.run(
                            [sys.executable, "-m", "pip", "install", pip_pkg],
                            capture_output=True, text=True
                        )
                        if result.returncode == 0:
                            st.success(f"{pip_pkg} をインストールしました ✅")
                        else:
                            st.error(f"{pip_pkg} のインストールに失敗しました ❌\n\n{result.stderr}")

    # --- requirements.txt への出力 ---
    st.subheader("📄 requirements.txt を自動生成")
    if st.button("📥 requirements.txt に保存"):
        try:
            with open("requirements.txt", "w", encoding="utf-8") as f:
                for pkg in sorted(all_missing):
                    f.write(f"{pkg}\n")
            st.success("✅ requirements.txt を保存しました！")
            st.code("\n".join(sorted(all_missing)), language="text")
        except Exception as e:
            st.error(f"❌ 保存に失敗しました: {e}")
```

---

## 📝 保存される `requirements.txt` の例

```text
opencv-python
Pillow
PyYAML
beautifulsoup4
scikit-learn
```

---

## 🔧 拡張アイデア（必要あれば実装可）

- `requirements.txt` に **すでに存在する内容とマージ**する
- バージョン指定（例: `scikit-learn==1.3.2` など）も付ける
- 保存先を選択可能にする（例：`out/requirements.txt` など）

---

お気に召しましたら、他の便利機能の追加もどんどんできますよ！  
何か他にも「こうできたら最高」みたいなアイデアありますか？

# **user**:

ありがとうございます。これで充分です。今回の話をブログに書きたいと思うので、タイトル案を複数提案していただけますか？

# **assistant**:

それは楽しみですね！今回の内容は「Streamlit × Python × 自動依存チェック＆管理」という技術的にも面白いテーマなので、以下に**いくつかタイプ別にタイトル案**を出してみました：

---

### 🧑‍💻 実用・技術フォーカス系
1. **「Streamlitで作る！ページ別ライブラリ管理＆requirements自動生成ツール」**
2. **「Streamlitアプリの依存関係を自動チェック＆インストールしてくれるHome.pyを作った話」**
3. **「めったに使わないページのために全部pip installしたくない！Streamlitで解決」**
4. **「動的にimport＆pip install！Streamlitで始めるスマートな依存管理」**

---

### 📖 読み物・ストーリーフォーカス系
5. **「ChatGPTと作る、ちょっと賢いStreamlitのHomeページ」**
6. **「requirements.txtはもう手書きしない。Streamlitで賢く依存チェック」**
7. **「“あのページだけで使うライブラリ”のせいで詰むのを防ぐ、Streamlitの小ワザ」**

---

### 💡 アイデア紹介・Tips系
8. **「StreamlitのHomeページに足したい、地味に便利な機能」**
9. **「Streamlit × import自動チェック × pip installボタンで爆速開発」**
10. **「importに応じてpip installするStreamlitアプリを書いてみた」**

---

ご希望に応じて「初心者向け」「業務活用」「ユーモア系」などにも振れますので、テイストの希望があればお聞かせください！  
どれか気になるタイトルありましたか？または、そこから調整して一緒に決めてもOKです 🙌

# **user**:

「Streamlitアプリの依存関係を自動チェック＆インストールしてくれるHome.pyを作った話」にしようと思います。最後に、今回の話のまとめを詳しく書いてください。

# **assistant**:

素敵なタイトルですね！「Streamlitアプリの依存関係を自動チェック＆インストールしてくれるHome.pyを作った話」にふさわしい**まとめ文**を、ブログ記事のラストにそのまま貼れるような形でご用意しました：

---

## 🧾 今回のまとめ：Home.pyでStreamlitアプリの依存関係を“賢く”管理する方法

Streamlitで複数ページ構成のアプリを作っていると、**ページごとに必要なライブラリが異なる**というケースが増えてきます。特に、あまり使わないページにだけ必要なライブラリのために、全体の `requirements.txt` に書いて毎回インストールするのは非効率ですよね。

そこで今回は、**StreamlitアプリのHome.pyに以下の機能を組み込む**ことで、効率的に依存関係を管理できるようにしました。

---

### ✅ Home.py に組み込んだ主な機能

1. **各ページ（pagesフォルダ以下）の `.py` ファイルを走査**
   - `ast` モジュールを使って `import` 文を解析。
2. **標準ライブラリかどうかを `stdlib_list` で判定**
   - 不足している外部ライブラリのみを抽出。
3. **import名とPyPIパッケージ名が異なるケースに対応**
   - 例: `sklearn` ➜ `scikit-learn`, `PIL` ➜ `Pillow` など。
4. **不足ライブラリをボタンで個別にインストール可能**
   - `subprocess` を使って `pip install` を実行。
5. **不足ライブラリを `requirements.txt` にまとめて保存**
   - ボタン一つで自動生成＆保存が可能。

---

### 🎯 この仕組みのメリット

- **使うページにだけ必要なライブラリをインストールできる**ので、開発環境がスリムになる。
- `requirements.txt` も **実際に必要なものだけを自動で反映できる**。
- チーム開発時も、**ページ追加時の依存漏れチェックが簡単**になる。
- `pip install` に失敗しがちな import名とpip名の不一致も自動で吸収。

---

### 🧩 今後のアイデア

- バージョン付きの `requirements.txt` に対応する
- すでにある `requirements.txt` にマージする
- 不足パッケージを「一括インストール」するボタンを追加
- 使用頻度が低いページに応じた **動的インポート** に拡張する（例：`importlib`を使う）

---

**「ライブラリの依存関係は賢く自動で管理したい」**という開発スタイルにぴったりな仕組みになりました。  
ちょっとした工夫で、開発体験がぐっとスムーズになりますよ。

---

ご希望に応じて文体をカジュアル寄りや技術ドキュメント風にも調整できます。  
他にもまとめたい視点があれば教えてください！

# **user**:

ありがとうございました！次回もまたよろしくお願いしますね！

# **assistant**:

こちらこそ、楽しくご一緒できて嬉しかったです！  
次回もぜひ、どんなテーマでも大歓迎ですので、またいつでもお声がけください 😊  
ブログ執筆、応援してます📣✍️

またお会いできるのを楽しみにしてます！🌟