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?

pip-licenses を使った「やらかさないため」の依存ライセンスチェック

2
Last updated at Posted at 2026-01-12

個人開発のPythonプロジェクトをGitHubで公開しようとしたとき、
「依存ライブラリのライセンスって何か見ておいた方がいいよな」
と思い、pip-licensesを触ってみました。

正直、ライセンスに詳しいわけでもなく、法的に完璧な判断をしたいわけでもありません。

この記事は、

  • よく分からないなりに手を動かしてみた
  • 完璧は諦めた
  • やらかさないためのガードレールだけ作った

という、その結果の共有です。

この記事の内容は法的判断を代替するものではありません。
商用利用や大規模公開の場合は、専門家への確認を前提としてください。

やりたかったこと

  • 明らかにNGそうなライセンスは自動で止めたい
  • OKと言えそうなものは分かるようにしたい
  • それ以外は「あとで自分が見る」で済ませたい

つまり、
完全に自動で判定するのではなく、立ち止まるきっかけが欲しかった
という感じです。

判定ルール(暫定版)

区分 判定 扱い
🚫 ブラック 含まれていたらNG エラー停止
✅ ホワイト 正規化して完全一致 OK
⚠️ グレー それ以外 人が確認

厳密さよりも
自分が説明できるルールかどうか を優先しています。

設定(ここだけ自分で決める)

REQUIREMENTS_FILE = "requirements.txt"
OUTPUT_FILE = "THIRD-PARTY-NOTICES.md"

# 含まれていたらアウトにするもの
# → コピーレフト色が強く、個人開発では判断が難しいため
BLACKLIST = [
    "GPL",
    "AGPL",
    "LGPL",
    "MPL"
]

# 正規化後に完全一致したら OK とするもの
# → 利用条件が比較的シンプルで実績が多いもの
WHITELIST = [
    "MIT",
    "BSD",
    "APACHE-2.0"
]

参考にしたサイト:

requirements.txt に書いた依存だけを見る

result = !pip-licenses

↑だとローカル環境の全部が対象になるのですが、
使っていないライブラリまで大量に出てしまいました。
(Google Colabで実行する時などは特に)

そのため requirements.txt に書いたものだけ を見ることにしました。

from pathlib import Path

def load_requirements(path):
    return [
        line.split("==")[0].strip()
        for line in Path(path).read_text().splitlines()
        if line and not line.startswith("#")    #コメントアウトされたものは無視
    ]

packages = load_requirements(REQUIREMENTS_FILE)

print(f"ライセンスチェック対象: {packages}")

pip-licenses をそのまま使う

深く考えず、まずはそのまま実行します。

result = !pip-licenses --packages {" ".join(packages)}
result_text = "\n".join(result)

JSON にしたり、細かくパースしたりはしていません。
読める形で出てくれれば十分 という判断です。

表記揺れは最低限だけ吸収する

出力を見ていると、

  • MIT
  • MIT License
  • BSD
  • BSD License

のような表記揺れがありました。

全部きれいに揃えるのは大変そうだったので、
今回は 接尾語を落とすだけ にしました。

def normalize_license_name(text: str) -> str:
    text = text.upper().strip() # 大文字に変換
    text = text.replace("SOFTWARE LICENSE", "")
    text = text.replace("LICENSE", "")
    return  text.strip()  # 最後にもう一度 strip() して空白除去

ブラック / ホワイト / グレー判定

normalized_text = normalize_license_name(result_text)

# ブラック:含まれていたら NG
black_hits = [
    k for k in BLACKLIST
    if k in normalized_text
]

# ホワイト:正規化後に完全一致
white_hits = []
for line in normalized_text.splitlines():
    license_name = line.strip()
    if license_name in WHITELIST:
        white_hits.append(license_name)
  • ブラックにも当たらない
  • ホワイトにも完全一致しない

ものは、すべて グレー(WARN) 扱いです。

LGPLLGPL-2.1 にもヒットしますが、
今回は「含まれていたら止める」ため、あえて粗い文字列判定にしています。

判定結果の扱い

if black_hits:
    print(f"🚫 LICENSE CHECK FAILED: {sorted(set(black_hits))}")
    print(result_text)
    assert False, "禁止ライセンスが見つかりました"

if not white_hits:
    print("⚠️ LICENSE CHECK WARNING: gray licenses detected")
    print("目視でレビューしてください")
  • ブラックだけは止める
  • グレーはログに出して、人が見る

「全部は自動化しない」前提です。

THIRD-PARTY-NOTICES を生成する

!pip-licenses \
  --format=markdown \
  --packages {" ".join(packages)} \
  --output-file={OUTPUT_FILE}

print("✅ ライセンスチェック完了。NOTICEファイルを作成しました")

OSS 公開用のファイルが一応できるので、 ここはそのまま使っています。

  • ブラック / ホワイト / グレー判定のロジック
  • requirements.txt から依存パッケージを読み込む処理
  • NOTICE ファイル (THIRD-PARTY-NOTICES.md) の生成

これら一式のコードは GitHub にまとめてあります。
JSON出力に対応し、Notebook・CLI・CI のいずれでも実行可能です:

まとめ

  • ライセンスをちゃんと判定するのは難しい
  • でも「何も見ない」よりはマシ
  • ブラック / ホワイト / グレーくらいが自分には扱いやすかった

自分と同じように
「よく分からないけど、最低限のガードレールは欲しい」
と思った人の参考になれば嬉しいです。

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?