2026年8月31日 星期一

Python一下:第三方模組怎麼選?從需求、授權到安全更新

《Python一下:從風土資料到智慧生活》第 29 篇

第三方模組怎麼選?從需求、授權到安全更新

會安裝套件只是起點;真正的能力,是知道為何選、如何驗證、何時更新與怎麼退回。

CH14 第三方模組虛擬環境供應鏈安全
學習目標
完成本篇後,你能區分標準函式庫、第三方發行套件與匯入模組;從功能、維護、授權、安全及部署條件評估套件;使用venv隔離環境;建立可重現的依賴清單;進行基本弱點稽核,並為USR場域制定安全更新與回復流程。

一、先問「需要什麼」,再問「裝哪一個」

水井村智慧生活專案可能需要網頁請求、影像處理、資料分析、資料庫、斷詞與硬體通訊。每多一個第三方套件,就多一份功能,也多一份版本、授權、弱點與維護責任。

USR需求先檢查可能使用
呼叫YouBike或場域API標準urllib是否足夠?是否需Session、重試?requests、httpx
處理水井三寶圖卡只需縮圖,還是要進階影像運算?Pillow、OpenCV
整理感測資料內建csv能否完成?資料量多大?pandas、NumPy
存取MySQL直接SQL還是需要ORM?PyMySQL、SQLAlchemy
中文文字處理語言、詞典與授權是否合適?依任務比較斷詞工具
不要只看下載量:熱門不等於適合。需要一起看官方文件、最近維護狀態、Python版本支援、授權、已知弱點、相依套件、安裝大小、硬體架構及是否能在PythonAnywhere、Raspberry Pi或Colab運作。

二、Package、Distribution與Module不是同一件事

日常口語都稱「套件」,但安裝名稱與匯入名稱可能不同。pip安裝的是發行套件(distribution package),Python程式匯入的是模組或import package。

import importlib.metadata


distribution_name = "Pillow"
print(importlib.metadata.version(distribution_name))

# 安裝名稱是Pillow,匯入名稱卻是PIL
from PIL import Image

print(Image.__name__)

因此不能只用 import名稱 == pip名稱 猜測。應以套件官方文件及PyPI專案頁為準,並小心拼字相近的惡意套件。

三、七項評估表:先評分再安裝

面向檢查問題證據
需求適配能否用最少功能完成任務?最小原型與測試
維護狀態是否仍支援所用Python版本?官方文件、發行紀錄
授權教學、商用、再散布是否允許?LICENSE與專案metadata
安全是否有未修補已知弱點?安全公告與依賴稽核
可部署性平台、CPU、記憶體與系統函式庫是否相容?乾淨環境安裝測試
資料治理資料是否會外傳?預設遙測為何?隱私政策與網路測試
可替換性若停止維護,能否退出?包裝介面與回復計畫

四、用Python建立簡易評估紀錄

from dataclasses import dataclass


@dataclass(frozen=True)
class PackageReview:
    name: str
    need_fit: int
    maintenance: int
    license_fit: int
    security: int
    deployability: int
    evidence: str

    @property
    def total(self):
        scores = (
            self.need_fit,
            self.maintenance,
            self.license_fit,
            self.security,
            self.deployability,
        )
        if any(score not in range(1, 6) for score in scores):
            raise ValueError("各項評分必須是1到5")
        return sum(scores)


review = PackageReview(
    name="示範套件",
    need_fit=5,
    maintenance=4,
    license_fit=4,
    security=4,
    deployability=5,
    evidence="官方文件、最小原型、授權檔與測試報告",
)
print(review.total)

分數不能取代判斷;若授權不合、存在無法接受的弱點或會把敏感資料送出,即使總分高也不應採用。

五、每個專案都建立自己的虛擬環境

PyPA建議使用虛擬環境管理第三方套件,避免不同專案互相干擾。Windows與macOS/Linux啟用方式不同:

$ py -m venv .venv
$ .venv\Scripts\activate
$ python -m pip install --upgrade pip

# macOS或Linux
$ python3 -m venv .venv
$ source .venv/bin/activate
$ python -m pip install --upgrade pip

在專案根目錄建立 .venv,並把它加入.gitignore。不要把整個虛擬環境提交到版本控制;應提交依賴宣告,讓環境可以重建。

六、確認目前真的在虛擬環境

import sys
from pathlib import Path


in_virtual_environment = sys.prefix != sys.base_prefix
print("Python:", Path(sys.executable))
print("虛擬環境:", in_virtual_environment)

啟用只是方便shell找到正確Python;關鍵是執行的直譯器路徑。Thonny、VS Code、排程或網站服務也要指定同一環境。

七、用python -m pip避免裝錯地方

import subprocess
import sys


result = subprocess.run(
    [sys.executable, "-m", "pip", "--version"],
    check=True,
    capture_output=True,
    text=True,
)
print(result.stdout.strip())

python -m pip可明確使用目前Python所屬的pip,減少電腦同時安裝多個Python時的混亂。

八、不要無條件安裝「最新版」

開發初期可指定一段相容範圍,正式部署則需要經測試的解析結果。以下只是格式示範,不宣稱這些版本永遠合適:

requests>=2.0,<3.0
Pillow>=10.0,<13.0
SQLAlchemy>=2.0,<3.0
PyMySQL>=1.1,<2.0

版本範圍要依支援政策與測試調整。對教室教材,可以提供一份已驗證版本;對正式系統,升級應經過測試、稽核、備份與回復流程。

九、requirements.txt與pip freeze

$ python -m pip install -r requirements.txt
$ python -m pip freeze > requirements-lock.txt
$ python -m pip check

pip freeze記錄目前環境所有已安裝版本,適合製作環境快照,但可能把未使用的套件也收入。較成熟的專案會分開維護「直接需求」與「解析後鎖定結果」。

十、用metadata查看版本、授權與依賴

from importlib.metadata import metadata, requires, version


def package_summary(distribution_name):
    info = metadata(distribution_name)
    return {
        "name": info.get("Name"),
        "version": version(distribution_name),
        "license_expression": info.get("License-Expression"),
        "license": info.get("License"),
        "home_page": info.get("Home-page"),
        "dependencies": requires(distribution_name) or [],
    }


print(package_summary("pip"))

Metadata可以協助盤點,但授權欄位可能缺漏或表達方式不同。真正採用前仍要讀專案LICENSE、例外條款與所包含資料或模型的授權。

十一、產生最小軟體物料清單

import importlib.metadata
import json
from datetime import datetime, timezone


packages = sorted(
    (
        {
            "name": item.metadata.get("Name", item.name),
            "version": item.version,
        }
        for item in importlib.metadata.distributions()
    ),
    key=lambda item: item["name"].lower(),
)

inventory = {
    "created_at": datetime.now(timezone.utc).isoformat(),
    "python": __import__("sys").version,
    "packages": packages,
}
print(json.dumps(inventory, ensure_ascii=False, indent=2))

這是教學用盤點,不是完整標準SBOM。至少要知道部署了哪些元件與版本,發生安全公告時才能判斷是否受影響。

十二、檢查已知弱點,但不要誤解結果

PyPA的pip-audit可掃描Python環境中套件的已知弱點。它不是原始碼掃描器,也不能保證發現所有風險;掃描前仍要把套件視為會被解析與處理的外部輸入。

$ python -m pip install pip-audit
$ python -m pip_audit
$ python -m pip_audit -r requirements-lock.txt
稽核不等於自動升級:先確認弱點是否影響實際使用路徑、修正版是否相容,再於測試環境更新並跑完整測試。若暫時無法更新,應記錄理由、降低暴露面、設定期限與負責人,而不是永久忽略。

十三、雜湊可以強化可重現安裝

pip支援hash-checking mode,requirements中的套件可附上允許的檔案雜湊,再用 --require-hashes 驗證。雜湊能確認下載檔案符合鎖定內容,但不能證明套件本身沒有惡意程式或設計缺陷。

$ python -m pip install \
    --require-hashes \
    -r requirements-hashed.txt

十四、授權不是只有「免費」與「付費」

檢查項目教學與USR要問的問題
程式碼授權可否修改、散布、商用?需否保留聲明?
資料/模型授權圖片、詞典、模型權重是否另有條款?
Copyleft義務與自有程式結合或部署服務時有何義務?
專利與商標程式碼授權是否同時處理專利或名稱使用?
隱私與服務條款雲端API是否保存、再利用或跨境傳輸資料?

遇到正式商用、再散布或複雜授權時,應由具資格的法務或授權專業人員確認,不能只靠AI摘要下結論。

十五、先做煙霧測試,再讓套件進場域

from importlib import import_module
from importlib.metadata import version


EXPECTED = {
    "requests": "requests",
    "Pillow": "PIL",
    "PyMySQL": "pymysql",
    "SQLAlchemy": "sqlalchemy",
}


def smoke_test(distribution_name):
    module_name = EXPECTED[distribution_name]
    module = import_module(module_name)
    return {
        "distribution": distribution_name,
        "version": version(distribution_name),
        "imported_as": module.__name__,
    }


for name in EXPECTED:
    try:
        print(smoke_test(name))
    except Exception as error:
        print(name, "FAIL", type(error).__name__)

煙霧測試只能確認基本匯入。還要測USR實際流程,例如圖片能否正確縮圖、MySQL能否回滾、Raspberry Pi能否安裝、斷線後資料能否補傳。

十六、用介面包住第三方套件

不要讓整個專案到處直接依賴某套件。把外部功能包在小型介面後,較容易測試、替換與集中處理錯誤:

class ImageThumbnailer:
    def __init__(self, image_module):
        self.image_module = image_module

    def create(self, source, destination, size=(800, 800)):
        with self.image_module.open(source) as image:
            image.thumbnail(size)
            image.save(destination)


from PIL import Image

thumbnailer = ImageThumbnailer(Image)

這也讓測試時能傳入替身物件,不必每次真的處理大型圖片或呼叫外部服務。

十七、安全更新的六步驟

  1. 盤點:確認直接與間接依賴、Python與作業系統版本。
  2. 閱讀:查看官方發行說明、安全公告與破壞性變更。
  3. 備份:保存可回復的程式版本、設定與資料庫備份。
  4. 測試:在乾淨環境重建,執行單元、整合與場域流程測試。
  5. 分段部署:先測試站或少量設備,觀察Log、效能與錯誤率。
  6. 記錄與回復:留下版本、日期、負責人、驗證結果及退版條件。
from dataclasses import dataclass
from datetime import date


@dataclass
class UpgradeRecord:
    package: str
    old_version: str
    new_version: str
    tested_on: date
    tests_passed: bool
    rollback_tag: str
    reviewer: str


record = UpgradeRecord(
    package="示範套件",
    old_version="1.0",
    new_version="1.1",
    tested_on=date.today(),
    tests_passed=True,
    rollback_tag="before-demo-upgrade",
    reviewer="課程小組",
)
print(record)

十八、USR場域的特別風險

場域常見限制選套件時的因應
養殖池邊緣設備網路不穩、ARM平台、儲存有限先測離線、wheel支援與資源用量
長者與社區資料語音、照片可能含個資優先本機處理,確認外傳與保存規則
學生共同開發環境不一致、帳密誤傳虛擬環境、鎖定檔、Secrets與程式審查
長期USR維運學生畢業、套件停更文件化、介面隔離、替代方案與交接

十九、課堂挑戰

挑戰A|基礎:選擇一個目前課程用到的第三方套件,完成七項評估表,所有判斷都附官方來源。
挑戰B|進階:在全新虛擬環境依鎖定清單重建專案,執行pip check、弱點稽核及三項煙霧測試,記錄結果。
挑戰C|USR場域:為Raspberry Pi智慧養殖閘道器設計「更新、觀察、退版」流程,包含斷網、套件無ARM wheel及資料不得遺失三種情境。

二十、用AI當評估助教,但證據要回到官方來源

請擔任Python第三方套件評估助教。
我的任務、Python版本、作業系統與部署硬體如下:〔請填寫〕。
請先問清楚必要功能,再比較不超過3個候選套件。
評估需求適配、官方維護、Python支援、授權、安全公告、
間接依賴、安裝大小、ARM與離線部署、資料是否外傳。
每項必須指向官方文件、PyPI metadata或專案LICENSE,
不以下載量或AI印象直接下結論。
最後提出最小驗證、更新與退版計畫。

二十一、延伸閱讀

沒有留言:

張貼留言