0
2

ローカルファイルを使ったライブラリインストールでハマったお話

Last updated at Posted at 2024-04-06

きっかけ

ローカルにダウンロードしたライブラリのインストールのお話です。
訳あってインターネット接続の無いパソコンへのライブラリのインストールをしなきゃいけない。ま、いろいろありますわな。セキュリティーポリシーとかね。
それも複数台のパソコンに一気にインストールしたい。ああ、インターネットが欲しい。w

そんな時に、ライブラリのインストールを自動化しようとしてハマったお話。

おさらい:ローカルファイルからのインストール方法

欲しいライブラリはPypi.orgから探してきます。バージョンはよく気を付けて。
ほんで、ターミナルで以下を実行するとインストールできます。

pip install --no-index --find-links=ライブラリを入れたフォルダ名 ライブラリ名

環境

今回は訳あってWindows11(久々に使った)
Python3.10.8

ライブラリの依存関係

ライブラリには依存関係があります。(リンク参照)
Aと言うライブラリをインストールするためにはBとCのライブラリがインストールされてないとだめよ~とか、そんな関係があります。詳しい事は下記を見て頂ければいいんですが、わかんなかったら依存関係があるんだなというコトだけ知っておいてください。というか、僕も説明できるほど知らない。

実施したこと

ライブラリの依存関係を考慮した順番に並べたリストを作っておいて、for分で回してインストールしてたんですね。
subprocessはコマンドラインで実行するコマンドをpythonを使って実行する方法。

import subprocess
command = 'pip install --no-index --find-links=ライブラリを入れたフォルダ名 ライブラリ名'
result = subprocess.run(command, shell=True, capture_output=True, text=True)

でもね、どうしてもインストールできないライブラリがありやがる!怒

ちなみにコメントは以下でもOK
command = f'python -m pip install {lib_folder}{element}'
{lib_folder}{element}のところはダウンロードしたライブラリのインストールファイルへのパスを入れてちょ。

念の為、cmdから実行してみる

インストールできなかったライブラリをコマンドラインから実行。
できるじゃん!

tar -xzvf ライブラリ名
cd tar.gzファイルを解凍したのフォルダ
python setup.py install

解説すると
1行目:tar.gzファイルを解凍
2行目:解凍したフォルダに移動
3行目:このフォルダのsetup.pyに引数installを渡して実行

参考:pythonでもtar.gzファイルを解凍する方法

これ、tarっていうアーカイブファイルをgzという圧縮ファイルに変換したもの。
Pythonの標準ライブラリtarfileで解凍できます。

with tarfile.open(tar.gzファイルのパス, 'r:gz')as tr:
    tr.extractall(path=解凍するフォルダパス)

ここまで来て・・・

なんか、もやもやしません?
ちょっと僕はしたんですよ。モヤモヤ
そこで、インストールできたtar.gzファイルを解凍した後のsetup.pyを見比べてみました。

  • インストールできたsetup.py
import os
import re
from setuptools import setup

scriptFolder = os.path.dirname(os.path.realpath(__file__))
os.chdir(scriptFolder)
  • インストールできなかったsetup.py
import re
from setuptools import setup, find_packages

なるほど

os.chdir(scriptFolder)って、ディレクトリの移動してるよね?

ということで、以下の通りにしよう

  • whlファイル
    先ほど試したコメントでインストール

  • tar.gzファイル
    解凍してから、解凍したフォルダに移動
    setup.pyに引数`install’を渡して実行

プログラムを作る前に・・・ご利用は計画的に(愛があるんやでぇ〜のCM風)

  • プログラムの構造を考える
  1. ライブラリ名をインストール順に並べたリストを作る
  2. 拡張子が.whlだったらinstall_whl関数にファイルパスを入れてインストールする、拡張子が.tar.gzだったらinstall_targz関数に入れる
  3. 正確にインストールできたかどうかを確認したいので、pip listの出力にライブラリ名が入っていたらインストールできたと判断できるget_result関数を作って判定したい
  4. インストールできたファイル、できなかったファイルをリストに入れて最後に確認したい。

こんな感じで行こう

ファイル名をどうやって関数に渡すか。

つまり、「1.ライブラリ名をインストール順に並べたリストを作る」の部分
csvに入れちゃおう。こんな感じ
これをinstall_order.csvとして保存しておこう。
あ、1行目は文字コードでえらいことに会うことがあるので、何か文字を入れておいてください。
そう、BOM付きとかshift-jisとかああいう難しいやつ。w
ちゃんとutf-8で保存しても訳わからん状態になる。<- きっと勉強不足

ライブラリファイル名
numpy-1.25.2-cp310-cp310-win_amd64.whl
pillow-10.2.0-cp310-cp310-win_amd64.whl
numpy-1.25.2-cp310-cp310-win_amd64.whl
・・・
PyMsgBox-1.0.9.tar.gz

これで上から順にインストールだな!

まず、whlファイルをインストールする関数を作ろう

「2.拡張子が.whlだったらinstall_whl関数にファイルパスを入れてインストールする、拡張子が.tar.gzだったらinstall_targz関数に入れる」のwhlファイルの部分。

できたw
引数の意味はにコメントに書いといた。

# install whl file
def install_whl(lib_folder, element):
    """
    lib_folder: ライブラリを保存しているフォルダ
    element: インストールしたいライブラリのファイル名
    """
    # subprocessを使ってコマンドを実行(あ、pipで書いちゃった。まいっか)
    command = f'python -m pip install {lib_folder}{element}'
    print(command)
    result = subprocess.run(command, shell=True, capture_output=True, text=True)

    # 結果の取得(あ、いらんか)
    outputs, errors = result.stdout, result.stderr
    try:
        outputs, error_comment = outputs.split('\n'), errors.split('\n')
    except:
        outputs, error_comment = outputs, errors

    lib_name = element.split('-')[0]
    return get_result(lib_name) 

最後のgyounoget_result()の関数はインストールできたかどうか判別させる関数のつもり。(後で作る)

じゃ、今度はtar.gzファイルをインストールする関数を作ろう

def install_targz(lib_folder, element):
    """
    lib_folder: ライブラリを保存しているフォルダ
    element: インストールしたいライブラリのファイル名
    tar.gzファイルは解凍したフォルダに移動してからpython setup.py installを実行する。
    そのためフォルダ移動を伴う
    """
    # tar.gzの解凍
    with tarfile.open({lib_folder}{element}, 'r:gz')as tr:
    tr.extractall(path=lib_folder)

    targz = element.replace('.tar.gz', '') # tar.gzを解凍したフォルダ名
    lib_name = element.split('-')[0] # バージョン名の無いライブラリ名(_や-で繋がれている)

    # フォルダの移動
    scriptFolder = os.path.dirname(os.path.realpath(__file__))
    os.chdir(scriptFolder + lib_folder + targz + '/')

    # インストール 
    command2 = 'python setup.py install'
    # command2 = f'python {lib_folder}{targz}/setup.py install'
    print(command2)
    result2 = subprocess.run(command2, shell=True, capture_output=True, text=True)
    # 結果の取得(やっぱいらんな)
    outputs2, errors2 = result2.stdout, result2.stderr
    try:
        outputs2, error_comment2 = outputs2.split('\n'), errors2.split('\n')
    except:
        outputs2, error_comment = outputs2, errors2

    # この自動インストールファイルのフォルダに戻ってくる
    os.chdir(scriptFolder)
    
    return get_result(lib_name)

やっぱちょっと長くなるね。w
whl向けの関数もtar.gz向けの関数も結果の取得はいらんねぇ。(自分の備忘録ののために残しとく)

インストールした後にインストールできたかどうか判別する関数を作ろう

原始的にpip listを実行して、インストールしたライブラリ名があるかどうかで判断。
ただし、ライブラリ名に-や_を使っていたり、それがライブラリ名とpip listで帰ってくる出力と一致しなかったりするので、全部バラバラにして、一致回数を数える方法にしました。

def get_result(lib_name):
    # whlファイル、tar.gzファイル名から-_でスプリットして単語にしちゃう      A
    lib_name_list = re.split('_|-| ', lib_name) 
    
    # pip listを実行して返り値を取得して改行でスプリットする
    command = 'pip list'
    result = subprocess.run(command, shell=True, capture_output=True, text=True)
    outputs = result.stdout
    outputs = outputs.split('\n')

    # pip listの出力値を1行ごとにみていく
    for output in outputs:
        result_flag = 0 # ライブラリ名との一致回数を記録するフラグ
        
        # 毎行、スペースでスプリットして最初の部分(ここにライブラリ名が入るはず)
        output_1_line_list = output.split(' ')[0]
        # pip listの出力をファイル名と同じく、_-で分割      B
        output_1_line_list = re.split('_|-| ', output_1_line_list)
        
        # 上のA(whl、tar.gzファイルから抽出した単語)とB(pip listの出力から抽出した単語を)を比較する
        for name in lib_name_list:
            if name in output_1_line_list:
                # print('name', name)
                result_flag += 1
            else:
                pass
        # AとBの単語の一致回数が、lib_name_listの単語の数と一致したらインストールできたと判断する
        if result_flag == len(lib_name_list):
            return True
    # インストールできなかった時の処理
    if result_flag == 0:
        return False
    else:
        return None

案外、長くなってきたな。w
しかもごちゃごちゃしてウザい。この関数はもっといい方法がありそう。

じゃ、上の関数を実行するためのプログラムを作っていこう

細かいことはコメント見て!(ごめん)
難しかったのは正規表現と、whl / tar.gzファイルとpip listした時の出力の一致点探し。
たとえば、nest-asyncioの場合。
tar.gzファイル名:nest_asyncio-1.6.0.tar.gz
pip listの出力:nest-asyncio 1.6.0
nestとasyncioの間が-だったり_だったり。
tar.gz、whlファイル共にファイル名のルールが「ライブラリ名」+「-」+「バージョン」+「拡張子」
だという共通点を探したりってとこあたり。

# ライブラリを保管したフォルダ
lib_folder = './libs/'

# インストール順を記載したファイルの読み取り
with open('./install_order.csv', 'r', encoding='utf-8') as f:
    libraries = f.readlines()

# 結果の保存先
installed_list = []
uninstalled_list = []
unexpected_list = []

# 目安のイテレーター
iter = 0

for j, library in enumerate(libraries):
    # 1行目は飛ばす!csvの1行目は適当な文字をわざと入れてあるので。
    if j !=0:
        result = None

        elements = library.split(',')
        element = elements[0]
        element = element.replace('\n', '')

        ################################
        ##### 進行がわかるための表示 #######
        ################################

        print('*'*40)
        print('lib_name: ', element)

        iter += 1
        print('iter: ', iter)
        print(f'check char |{lib_folder}|, |{element}|')

        
        ################################
        #####      実行部分       #######
        ################################
        if '.whl' in element:
            result = install_whl(lib_folder, element)
        elif '.tar.gz' in element:
            result = install_targz(lib_folder, element)
        else:
            unexpected_list.append(element)
        print(element, result)

        

        ################################
        #####     結果を保存       #######
        ################################
        if result:
            installed_list.append(element)
        elif result == False:
            uninstalled_list.append(element)
        else:
            pass
        print('num installed: ', len(installed_list))


################################
#####    最終結果の表示    #######
################################
print('*'*40)
print('result')
print('installed_list: ', installed_list)
print('uninstalled_list: ', uninstalled_list)
print('unexpected_list: ', unexpected_list)
print('*'*40)

if iter == len(installed_list):
    print('install was finished')
elif (iter == len(unexpected_list) !=0) | (len(uninstalled_list) != 0):
    print(f'All libraries were not finished. Uninstalled libraries were {len(uninstalled_list)}.')
else:
    pass

終わりに

実はろくに調べることをせず、プログラムを書いてしまった。(反省)
pyinstalerを使って実行ファイルで配布しても良かったなとか、色々思うところはあります。

実行に移す前によく考えましょう!www

0
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
0
2