2
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

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

Posted at

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 になりますので、以下のようなパターンで扱うのがスマートです。


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

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()を使ってインストールコマンドのコピーやリンクも表示できるので、次のようにもできます:

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

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

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

例:utils.py

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など):

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 に入れると、かなりそれっぽく動きます:

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名が異なる場合(例:sklearnscikit-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)

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 ライブラリを使って判定する方法もあります:

pip install stdlib-list
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

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 を使っています。未インストールなら事前に以下を実行してください:

    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

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名の変換辞書を使う

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


🔧 辞書&変換ロジックを追加したコード(抜粋)

# --- よくある 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 ボタン内)

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 に次のようなコードがあるとします:

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

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 自動生成対応済み)

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 の例

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で始めるスマートな依存管理」

📖 読み物・ストーリーフォーカス系

  1. 「ChatGPTと作る、ちょっと賢いStreamlitのHomeページ」
  2. 「requirements.txtはもう手書きしない。Streamlitで賢く依存チェック」
  3. 「“あのページだけで使うライブラリ”のせいで詰むのを防ぐ、Streamlitの小ワザ」

💡 アイデア紹介・Tips系

  1. 「StreamlitのHomeページに足したい、地味に便利な機能」
  2. 「Streamlit × import自動チェック × pip installボタンで爆速開発」
  3. 「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パッケージ名が異なるケースに対応
    • 例: sklearnscikit-learn, PILPillow など。
  4. 不足ライブラリをボタンで個別にインストール可能
    • subprocess を使って pip install を実行。
  5. 不足ライブラリを requirements.txt にまとめて保存
    • ボタン一つで自動生成&保存が可能。

🎯 この仕組みのメリット

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

🧩 今後のアイデア

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

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


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

user:

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

assistant:

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

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

2
2
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
2
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?