2024-2I プログラミング1 第27回 講義資料

2025年02月05日(水)1・2時限

1 クラスメイトの作品のクローンと実行

課題07としてGitHubに公開されているクラスメイトの制作物を、各自のローカルPCに「クローン」して実行するための手順について解説します。

1.1 リポジトリのクローン

ここでは久しぶりに Git Bash ターミナルを使用します。Windows のスタートボタンを右クリックして「ターミナル」を起動し、ターミナルタブを Git Bash に切り替えください。

img

Git Bash ターミナルで、ドキュメント (C:\Users\xxxx\Documents) などの適当なフォルダに移動して Classmates-Dev-Hub というフォルダを新規作成して、そのなかに移動してください。このフォルダの内部に、クラスメイトが制作した 課題07 のリモートリポジトリをクローンしていきます。

cd c:Users/xxxx/Documents
mkdir Classmates-Dev-Hub
cd Classmates-Dev-Hub/

GoogleClassroomに配置している「ポートフォリオ一覧」から、各ポートフォリオにアクセスし、さらにそこからGitHub のリモートリポジトリ (=制作物を公開しているプロジェクトのトップページ) にアクセスしてください。

プロジェクトのトップページにアクセスしたら、次の図のように [Code] - [Code] - [HTTPS] - の順にたどって「リモートリポジトリのURL」をクリップボードにコピーしてください。「リモートリポジトリのURL」とは https://github.com/Xxxx/Yyyy.git のように アドレスの末尾が「.git となっているものです。また、トップページに表示されている README.md についても同時に確認しておいてください。

img

README.mdの確認

第14回講義で学習したように、プロジェクトフォルダのトップ階層に README.md という名前のファイル (マークダウン形式) を配置しておくと、リポジトリのトップページに、その内容が表示されるようになります。一般には、プロジェクトの概要、制作期間、実行方法、ライセンス、操作方法などを記載します。

GitHub で公開するプロジェクトには、原則として README.md を配置するようにしてください。

Git Bash ターミナルに戻ります。

次のコマンドを入力して「クローン」を実行し、さらに、そのフォルダのなかに移動してください。ここでは「だれが制作したプロジェクトなのか」が分かりやすいようにクローン先(展開先)のフォルダを 38-Yamada のように変更しています (クローンの手順については第15回講義のなかで学習しました)。

git clone https://github.com/Xxxx/Yyyy.git 38-Yamada
cd 38-Yamada

ls コマンドなどでフォルダの内容を確認してください。

次に、これらのプログラムを実行するため、pip を最新版にしてから仮想環境を構築して有効化します。ただし、現在使用中のターミナル (シェル) は VSCode でデフォルト使用している「PowerShell」 ではなく、「Bash」であるため、いつものように .venv/Scripts/Activate.ps1 を実行しても、仮想環境は有効化されません( 拡張子 .ps1 はPowerShellスクリプトの拡張子です)。

Bash 環境において仮想環境を有効化するためには source .venv/Scripts/activate ようにコマンドを実行してください。sourceコマンドは 引数に与えたファイルに記述されたシェルスクリプトを実行 するためのコマンドで、主に Linuxで使用される Bash、tcsh、zsh などのシェル環境で利用ができます。

python -m pip install --upgrade pip
python -m venv .venv
source .venv/Scripts/activate

Git Bash ターミナルで仮想環境が有効化されると、入力プロンプトの前に (.venv) の文字列が付きます。なお、この仮想環境を抜けるためには、単に deactivate とコマンドと打ち込みます。仮想環境が無効化されれば (.venv) の文字列は消えます。

1.2 ライブラリのインストール

ls でファイルを一覧表示して requirements.txt が存在すれば、pip install -r requirements.txt でライブラリを 一括インストールします。requirements.txt は、プログラムの実行に必要なライブラリの一覧を示したテキストファイルで、第22回講義のなかで詳細を説明しています。

pip install -r requirements.txt

もし、フォルダのなかに requirements.txt が存在しなければ、各Pythonファイルの冒頭に記述される imoport から必要なライブラリを判断して、個々にインストールしてください。このような作業は、非常に面倒であるため、通常は requirements.txt を用意します。

今後、GitHub などのリモートリポジトリにPythonプロジェクトを公開する際は ( 課題08 を含む)、原則として requirements.txt を含めるようにしてください。requirements.txt の作成方法は第22回講義を参照してください。

1.3 プロジェクトの実行

プログラムの エントリポイント (=開始点) となる Pythonファイル を対象に、次のコマンドを実行します。プログラムが複数のファイルから構成される場合、エントリーポイントとなる Pythonファイル には慣例的に main.pyapp.py のような名前をつけます。

python main.py

基本的には、以上の手順でプログラムが起動するはずです。うまく動作しない場合は 制作者に、直接、質問や相談 してください。

また、Git Bashターミナルからは code . & コマンドでプロジェクトをVSCodeでオープンすることができるので、プログラムそのものを読むこともできます。自分が以外が書いたプログラムを読むことは、非常に勉強になるので、それも是非トライしてください。


演習1:「課題07」において requirements.txt を作成していない場合は、それをプロジェクトフォルダのトップ階層 (ルート) に作成して GitHub にプッシュせよ。

演習2 (Ex) :「課題07」において README.md を作成していない場合は、それプロジェクトフォルダのトップ階層 (ルート) に作成して GitHub にプッシュせよ。

演習3: 実際に誰かのプロジェクトをローカルPCにクローンして、プログラムを実行せよ。

(Ex) の演習は、授業時間外に取り組んでください

2 ライセンス

前々回の講義で説明したように PySide6 は Lesser General Public License 3.0 (LGPLv3) というライセンスのもとで公開・配布されています。このため、PySide6 を使用して開発したアプリケーションを公開・配布 する際には、LGPLv3 と互換性のあるライセンスを選択して、そのライセンスを適用する必要があります。

具体的には「LGPLv3」「MIT License」「GPLv3」「Apache License 2.0」などの互換性があるライセンスの下で自作のアプリを公開・配布する必要があります。各ライセンスの概要については ウェブ検索やChatGPTなど で把握してください。

ここでは、PySide6 と同じ LGPLv3 ライセンス で自作アプリを公開・配布する手順を解説します。これによりライセンス違反を指摘されることを避けることができます。

2.1 ライセンスファイルの追加

プロジェクトフォルダのトップ階層 (ルート) に LICENSE.txt という名前のファイルを作成してGNU公式ウェブサイトから「LGPLv3 ライセンス全文」をコピーして貼り付けます。

GNU LESSER GENERAL PUBLIC LICENSE
Version 3, 29 June 2007
Copyright © 2007 Free Software Foundation, Inc. https://fsf.org/

(中略)

If the Library as you received it specifies that a proxy can decide whether future versions of the GNU Lesser General Public License shall apply, that proxy’s public statement of acceptance of any version is permanent authorization for you to choose that version for the Library.

ここでは、(中略) としていますが、実際の LICENSE.txt では文章を略さないでください。

2.2 REAMDE.md に記載

プロジェクトフォルダのルートに配置した REAMDE.md の最後に以下のような内容を記載します。

(略)

# ライセンス

このアプリケーションは GNU Lesser General Public License v3.0 のもとで公開されています。詳細は `LICENSE.txt` ファイルを参照してください。

このアプリケーションは PySide6 を利用しています。PySide6 は LGPL v3 ライセンスの下で公開されています。詳細は [こちら](https://www.qt.io/licensing/) を参照してください。

以上の手順に従えば、ライセンス違反の問題は基本的に回避できます。

個人的な趣味の範囲でアプリを開発して、ひっそりとGitHubで公開しているレベルにおいては、ライセンス違反が指摘されることはほとんどありません。しかし、皆さんは将来的にはプロになる立場であり、いまのうちからライセンスについて意識を持っておくことは非常に重要です。

3 正規表現

正規表現 (Regular Expression) とは、特定のシンボルや記号で構成された特別な文字列 (これを パターン という) を使用し、文字列の 検索、置換、分析 を効率的かつ強力に行なうための仕組みです。正規表現は、Pythonをはじめとして様々なプログラミング言語でライブラリとして利用可能です。また、VSCodeなどのテキストエディタや、データ処理ツールでも対応している製品が多くあります。

例えば、正規表現を使用すると、ユーザが入力する「電話番号」「メールアドレス」「URL」「郵便番号」「IPアドレス」「日付」「時刻」などが 適切な形式で記述されているかどうかを、複雑なロジックを構築することなく確認すること、つまり、バリデーション (Validation) が可能になります。

さらに、文章中から特定の規則を持った文字列を検索・抽出することや、それらを一定の規則で置き換えることも可能です。例えば、テキストファイルのなかから「R29001」「r29031」「r29158」のように「大文字と小文字」「半角と全角」が混在した学籍番号の文字列を検索し、それらを全て半角大文字の学籍番号、つまり「R29001」「R29031」「R29158」のように置換する処理も可能です。

このように正規表現は、かなり強力なツールになりますが、利用に際しては 適切にパターンを記述 する必要があります。パターンの記述は経験と知識を必要とするものでしたが、現在では、複雑なパターンも ChatGPT などの 生成AI を活用して、自然言語を用いた対話的な方法で生成することが可能になりました。

ここでは、PySideと組み合わせて正規表現について学んでいきます。

3.1 バリデーション

ウェブアプリやGUIアプリにおける バリデーション (検証) とは、ユーザからの入力 (例えば電話番号や郵便番号、メールアドレス、パスワードなど) が「期待される形式や条件を満たしているか」を確認する手続きを意味します。

ウェブサービスの登録フォームなどで見かけることがあると思います。

ここでは、以下のようなGUIアプリの「携帯電話番号の入力」に関するバリデーションを 正規表現 (Regular Expression)を利用して実装してみたいと思います。

img

前々回に作成したPySideのプロジェクトフォルダに qt-11.py というファイルを新規作成して、以下の内容を貼付けて実行してください。

import sys
import PySide6.QtCore as Qc
import PySide6.QtWidgets as Qw
import re

# 都道府県タプル (末尾の「空欄」を含む)
pref_list = ('北海道','東京都','大阪府','鹿児島県','沖縄県','')

# 電話番号のバリエーション (検証)
def validate_tel_number(tel:str)->bool:
  pattern = r'^\d{3}-\d{4}-\d{4}$' # 正規表現パターン
  if re.match(pattern, tel):
    return True
  else:
    return False

class MainWindow(Qw.QMainWindow):
  
  def __init__(self):

    super().__init__() 
    self.setWindowTitle('MainWindow') 
    self.setGeometry(100, 50, 640, 130) 

    # 都道府県コンボボックスの見出し
    self.lb_pref = Qw.QLabel(self)
    self.lb_pref.setGeometry(15,10,300,25)
    self.lb_pref.setText('都道府県')

    # 都道府県コンボボックスの本体
    self.cmb_pref = Qw.QComboBox(self)
    self.cmb_pref.setGeometry(15,35,80,25)
    self.cmb_pref.setEditable(False)
    for p in pref_list:
      self.cmb_pref.addItem(p)
    # pref_list の末尾の「空白」を初期値にセットする。
    self.cmb_pref.setCurrentIndex(len(pref_list)-1) 
    self.cmb_pref.currentIndexChanged.connect(self.pref_changed)

    # 都道府県の検証ラベル vm: Validation Message
    self.lb_pref_vm = Qw.QLabel(self)
    self.lb_pref_vm.setGeometry(105,35,200,25)
    self.lb_pref_vm.setText('必須項目')
    self.lb_pref_vm.setStyleSheet('color: red;')
    self.lb_pref_vm.setVisible(True)

    # 電話番号テキストボックスの見出し
    self.lb_tel = Qw.QLabel(self)
    self.lb_tel.setGeometry(15,65,300,25)
    self.lb_tel.setText('携帯電話番号')

    # 電話番号テキストボックスの本体
    self.tb_tel = Qw.QLineEdit(self)
    self.tb_tel.setGeometry(15,90,110,25)
    self.tb_tel.setPlaceholderText('XXX-XXXX-XXXX')
    self.tb_tel.setAlignment(Qc.Qt.AlignmentFlag.AlignCenter)
    self.tb_tel.textChanged.connect(self.tel_changed)

    # 電話番号の検証ラベル vm: Validation Message
    self.lb_tel_vm = Qw.QLabel(self)
    self.lb_tel_vm.setGeometry(135,90,300,25)
    self.lb_tel_vm.setText('必須項目')
    self.lb_tel_vm.setStyleSheet('color: red;')

  # 都道府県コンボボックスが変更
  def pref_changed(self):
    idx = self.cmb_pref.currentIndex()
    if idx == len(pref_list)-1 :
      self.lb_pref_vm.setVisible(True)
      print(f'都道府県が未選択になりました。')
    else :
      self.lb_pref_vm.setVisible(False)
      print(f'「{pref_list[idx]}」が選択されました。')

  # 電話番号テキストボックスが変更
  def tel_changed(self):
    tel = self.tb_tel.text()
    if len(tel) == 0:
      self.lb_tel_vm.setText('必須項目')
      
    else:
      if validate_tel_number(tel):
        self.lb_tel_vm.setText('')
      else:
        self.lb_tel_vm.setText('書式に誤りがあります')

# 本体
if __name__ == '__main__':
  app = Qw.QApplication(sys.argv)
  main_window = MainWindow()
  main_window.show()
  sys.exit(app.exec())

ここでは、分かりやすさを優先して 第12行目 から 第15行目 のように記述していますが、実際には return re.match(r'^\d{3}-\d{4}-\d{4}$', tel) のように1行で記述することができます。

3.2 正規表現

正規表現 (Regular Expression) を使うために 第04行目re ライブラリを読み込んでいます。また、バリデーションのコアとなる処理は 第09行目 から 第15行目validate_tel_number 関数です。この関数は型ヒントから読み取れるように、文字列型の引数 tel を受け取って、それが携帯電話番号のパターン XXX-XXXX-XXXX にマッチしているかどうかをバリデーション (検証) して、TrueFalse を返すものです。

第11行目 において携帯電話番号を表すパターンを pattern = r'^\d{3}-\d{4}-\d{4}$' のように記述しています。まず、先頭の r ですが、これは、エスケープシーケンス (例えば \n で改行を意味するなど) を適用しない Raw String (生の文字列) であることを指示をするものになります。正規表現では バックスラッシュ (環境によっては円マーク) を多用 するため、一般には「r文字列」を使用してパターンを記述します。なお、「r文字列」は、正規表現だけではなく Python で広く使われる文法です。もし、「r文字列」を使用しない場合は、エスケープ処理されないように '^\\d{3}-\\d{4}-\\d{4}$' のようにパターンを記述する必要があります (読みづらくなります)。エスケープシーケンスについてはウェブを参照してください。

以下、パターンを構成する文字列の意味です。

ここで注意してほしいのは ^$ の取り扱いです。これを取り除いて '\d{3}-\d{4}-\d{4}' のようなパターンを使用すると「電話番号は090-1111-1111です。」のような文字列、つまり 文字列の内部に XXX-XXXX-XXXX を含む ような場合も、バリデーションを通過してしまうようになります。

以上のように正規表現のパターンの記述には ある程度の知識と経験 が必要になります (使えるようになると非常に便利なもののの、使えるようになるまで大変でした)。しかし、最近では「生成系AI」を利用することで、自然言語で情報を与えて、適切な正規表現パターンを簡単に得ることができるようになりました。例えば、次のように質問してパターンを得ることができます。

質問

Pythonの正規表現に関する質問です。携帯電話番号 XXX-XXXX-XXXX の形式にマッチするようなパターンを教えてください。ただし、番号の先頭は「090」「080」「070」のどれかからはじまっている必要があります。

回答こちらを参照してください。

なお、正規表現には、プログラミング言語によって方言 があるので、AIに質問する場合は、Pythonにおける正規表現についての話題であること を必ず指示してください。


演習1: 第11行目 を通常の文字列に変更すると、つまり、先頭の r を削除するとどのようになるか確認してください。

演習2: 第11行目 を r文字列を使わずに pattern = '^\\d{3}-\\d{4}-\\d{4}$' のように記述しても正常に機能することを確認せよ。

演習3: 第11行目pattern = '\d{3}-\d{4}-\d{4}' のように ^$ を除いた場合、「090-1111-1111」もバリデーションを通過するが、「xxx090-1111-1111zzzz」でもバリデーションを通過してしまうことを確認してください。

演習4: 次のようにプログラムを拡張してください。なお、電子メールアドレスのバリデーションを設定してください (ChatGPTなどを活用すること)。

img

演習5: メールアドレスのバリデーションとして ac.jp および ed.jp ドメインのみを許可するように設定せよ。

3.3 コンボボックス

第25行目から第45行目 および 第65行目から第73行目 では、新規のウィジェットとして「コンボボックス」を組み込んでいます。ラジオボタンと同じように1個の選択肢を得るものですが、選択候補が多数の場合に有効なウィジェットになります。

この他、PySide6 (Qt6) で使用可能なウィジェットの一覧は公式リファレンスから確認することができます。どのようなウィジェットがあるか必ず確認してください。

4 OpenCV と PySide6 の組合せ

OpenCVと PySide6 を組み合わせた例を以下に示します。これらを組み合わせると簡単な画像処理アプリ(GUI)を作成することができます。

img

OpenCV では画像を numpy.ndarray オブジェクトとして扱いました。この numpy.ndarray オブジェクトを PySide6 で表示するためには 第29行目 の手順で PySide6.QtGui.QImage' に変換して、第37行目 のようにQLabelウィジェットに貼り付けて表示します。

実行するためには pip install opencv-python でライブラリをインストール後、qt-12.py と同じフォルダににimg-01.jpgを配置してください。

import sys
import PySide6.QtWidgets as Qw
import PySide6.QtGui as Qg
import cv2
import numpy as np

class MainWindow(Qw.QMainWindow):
  
  def __init__(self):

    super().__init__() 
    self.setWindowTitle('MainWindow') 
    self.setGeometry(100, 50, 685, 280) 

    # OpenCVで画像ファイルを読込み
    img1 = cv2.imread('img-01.jpg')
    print(type(img1)) # => <class 'numpy.ndarray'>

    # img1を320x240にリサイズ
    img1 = cv2.resize(img1,(320,240),interpolation=cv2.INTER_LANCZOS4)
    height, width, ch = img1.shape

    # img1をimg2に複製。加工。
    img2 = img1.copy()
    img2[105:115,65:110] = np.array([0,0,0],dtype=np.uint8)

    # np.ndarray オブジェクトを QImageオブジェクトに変換
    bpl = ch * width # bpl : bytes_per_line
    qimg1 =Qg.QImage(img1.data, width, height, 
                      bpl, Qg.QImage.Format.Format_BGR888)
    qimg2 =Qg.QImage(img2.data, width, height, 
                      bpl, Qg.QImage.Format.Format_BGR888)
    print(type(qimg1)) # => <class 'PySide6.QtGui.QImage'>

    # QLabelに画像 qimg_1 を設定する
    self.lb_img1 = Qw.QLabel(self)
    self.lb_img1.setPixmap(Qg.QPixmap.fromImage(qimg1))
    self.lb_img1.setGeometry(15,10,width,height)

    # QLabelに画像 qimg_2 を設定する
    self.lb_img2 = Qw.QLabel(self)
    self.lb_img2.setPixmap(Qg.QPixmap.fromImage(qimg2))
    self.lb_img2.setGeometry(350,10,width,height)

    # ラベル1
    self.lb_title1 = Qw.QLabel(self)
    self.lb_title1.setText('加工前')
    self.lb_title1.setGeometry(15,250,100,25)

    # ラベル1
    self.lb_title2 = Qw.QLabel(self)
    self.lb_title2.setText('加工後')
    self.lb_title2.setGeometry(350,250,100,25)

# 本体
if __name__ == '__main__':
  app = Qw.QApplication(sys.argv)
  main_window = MainWindow()
  main_window.show()
  sys.exit(app.exec())

4.1 単純な画像表示

プログラムのなかで複雑な画像処理をせず、画像を表示するだけであれば、以下のようにシンプルに記述することができます。

import sys
import PySide6.QtCore as Qc
import PySide6.QtWidgets as Qw
import PySide6.QtGui as Qg

class MainWindow(Qw.QMainWindow):
  
  def __init__(self):

    super().__init__() 
    self.setWindowTitle('MainWindow') 
    self.setGeometry(100, 50, 350, 280) 

    # PySideの機能で画像の読込みとリサイズ
    width, height = 320, 240
    pm1 = Qg.QPixmap('img-01.jpg')
    pm1 = pm1.scaled( width, height, 
                      Qc.Qt.AspectRatioMode.KeepAspectRatio)

    # QLabelに「画像」をセット
    self.lb_img1 = Qw.QLabel(self)
    self.lb_img1.setPixmap(pm1)
    self.lb_img1.setGeometry(15,10,width,height)

    # QLabelに「文字列」をセット
    self.lb_title1 = Qw.QLabel(self)
    self.lb_title1.setText('加工前')
    self.lb_title1.setGeometry(15,250,100,25)

# 本体
if __name__ == '__main__':
  app = Qw.QApplication(sys.argv)
  main_window = MainWindow()
  main_window.show()
  sys.exit(app.exec())

5 EXEファイル化

Python で開発されたアプリを実行するには、実行環境に Python や ライブラリ がインストールされている必要があります。しかし、Python 環境をセットアップすることは、一般のPCユーザーにとってはかなりハードルが高い作業 になります。アプリを開発しても、他者に利用してもらえなければ、開発の意欲・モチベーションを保つこと は難しくなります。この問題を解決法として、Python プログラムを 実行ファイル (EXEファイル) に変換して配布する という手段があります。

EXE化するためのツールにはpy2exepy2appNuitkaなど様々なものがありますが、ここではpyinstallerというツールを使用します。EXE化したいアプリを開発している VSCode でターミナルを開き、pippyinstaller をインストールします。

pip install pyinstaller

次に、エントリーポイントとなるPythonファイル (この例では main.py ) を第1引数に与えて以下のコマンドを実行します。

pyinstaller main.py --onefile

しばらく処理がつづき、成功すると xxxxxx INFO: Building EXE from EXE-00.toc completed successfully. のようなメッセージが表示されます。これは、変換プロセスが成功したことを意味します。

プロジェクトフォルダに dist というフォルダが作成され、そこに main.exe のようなファイルが生成されます。distDistribution (配布・配給) の略語になります。また、プロジェクトフォルダに build という中間ファイルを収納するファルダも作成されます (このフォルダの内容は消しても問題ありません)。

なお、次のように --noconsole オプションをつけると コンソールが非表示 になります。完全 GUI アプリの場合はこのオプションを設定してください。

pyinstaller main.py --onefile --noconsole

実行ファイルに変換すると (EXEファイル内に Pythony環境 や 関連ライブラリ を内包するため) 非常にファイルサイズが大きく なります。小規模なアプリでも数十MB以上になります。そのため、通常、GitHub などのリモートリポジトリには distbuild を含めません。.gitignore にこれらを除外するように設定します (dist は目的に必要に応じてプッシュ対象に含めることがあります)。

ただし、GitHub では、ファイルサイズが 100 MB を超えるファイルをプッシュすることは基本的にできません。PythonプログラムをEXE化すると、このファイルサイズの制限に引っかかる可能性があります。そのときは、GitHub ではなく別の方法での配布を検討する必要があります。

Google Drive は、その選択肢となります。ただし、Google Drive では「EXEファイルの直接アップロードができない」あるいは「セキュリティ上の警告が表示されること」があります。そのため、EXE ファイルを ZIP 形式で圧縮してアップロードすることが推奨されます。これにより、ファイルのアップロードが容易になり、ダウンロード者も安全にファイルを受け取ることができます。


演習 : Pythonプログラム qt-11.py をEXEファイルに変換せよ。

6 課題08

プログラミング1の課題としては「最終の課題」となります。「PySide を利用した GUIアプリ (自由作品)」を作成し、そのプログラムを GitHubに公開 するとともに 課題06で作成したポートフォリオに概要を掲載してください。


ポートフォリオに作品として掲載する際、必ず、次の内容を含めてください。これらに漏れがあると 6点以下の評価 になります。細かな書式や形式については自由です。