Python独習!

習得したPython知識をペイフォワード

Pythonで設定ファイル(.xml)の読み込みと書き換えをする

ソフトウェアの設定(好みの画面設定、出力ファイルの保存場所とか)を保存するには、ini、xmljsonどれがいいの?
ソフトウェアの目的に合わせて選択するしかない。ネットに沢山の情報が転がっているが、それらは異なる背景を持つプログラマーたちの意見である。そこからは何がベストなのか決めることはできない、という結論に至った。流行り廃りだけで言えば、iniファイルは時代遅れ、xmlが普及している、jsonが伸びてきている、といったところ。

サンプルプログラム

今回はxmlを取り上げることにして、気が向いたらjsonを試してみる。
作ったプログラムはありふれた内容だが、[Read]ボタンで設定ファイルから値を読み込んでテキストボックスに表示、[Write]ボタンでテキストボックス上で編集した内容を設定ファイルに書き込む、というもの。

〇[Read]ボタンを押すとconfig.xmlの値をテキストボックスに表示する
python_xml_read

〇[Write]ボタンを押すとconfig.xmlにテキストボックスの値を書き込む
python_xml_write

ソースコード

# ReadWriteXML_001.py
# Python 3.8.1

import tkinter as tk
import xml.etree.ElementTree as ET

class App(tk.Frame):
    def __init__(self, master=None):
        super().__init__(master)
        self.pack()
        self.create_widgets()

    def create_widgets(self):
        ## Make instances
        self.ent1 = tk.Entry(self, width=50)
        self.ent2 = tk.Entry(self, width=50)
        self.ent3 = tk.Entry(self, width=50)

        self.lb1 = tk.Label(self, text="ent1:")
        self.lb2 = tk.Label(self, text="ent2:")
        self.lb3 = tk.Label(self, text="ent3:")

        self.bt1 = tk.Button(self, text="Read",  width=6, command=self.read_xml)          
        self.bt2 = tk.Button(self, text="Write", width=6, command=self.write_xml)

        ## Place them
        self.lb1.grid(row=0, column=0, padx=5, pady=5, sticky=tk.W)
        self.lb2.grid(row=1, column=0, padx=5, pady=5, sticky=tk.W)
        self.lb3.grid(row=2, column=0, padx=5, pady=5, sticky=tk.W)

        self.ent1.grid(row=0, column=1, columnspan=2, padx=5, pady=5)
        self.ent2.grid(row=1, column=1, columnspan=2, padx=5, pady=5)
        self.ent3.grid(row=2, column=1, columnspan=2, padx=5, pady=5)
        
        self.bt1.grid(row=3, column=1, padx=5, pady=5)
        self.bt2.grid(row=3, column=2, padx=5, pady=5)

    def read_xml(self):
        try:
            tree = ET.parse('config.xml')
            root = tree.getroot()

            text_ent1 = root.find("./setting/ent1").text
            text_ent2 = root.find("./setting/ent2").text
            text_ent3 = root.find("./setting/ent3").text

            self.ent1.delete(0, tk.END)
            self.ent2.delete(0, tk.END)
            self.ent3.delete(0, tk.END)        
        
            self.ent1.insert(0, text_ent1)
            self.ent2.insert(0, text_ent2)
            self.ent3.insert(0, text_ent3)
        except Exception:
            print("例外エラー")
        else:
            print("XML読み込み完了")

    def write_xml(self):
        try:
            tree = ET.parse('config.xml')
            root = tree.getroot()

            text_ent1 = self.ent1.get()
            text_ent2 = self.ent2.get()
            text_ent3 = self.ent3.get()

            root.find("./setting/ent1").text = text_ent1
            root.find("./setting/ent2").text = text_ent2
            root.find("./setting/ent3").text = text_ent3

            tree.write('config.xml')
        except Exception:
            print("例外エラー")
        else:
            print("XML書き込み完了")

if __name__ == "__main__":
    root = tk.Tk()
    app = App(master=root)
    app.mainloop()

PythonでBASLERのカメラを制御する ~インストールの忘備録~

2021/09/22 更新

basler-python-pypylon
Source of photo:https://github.com/basler/pypylon

マシンビジョン業界で有名なBASLER社。産業用カメラの豊富なラインナップを展開していて、仕事で何かとお世話になることが多い。そんなBASLERカメラをPythonで制御するための環境を構築したので忘備録を残しておく。
インストール手順は色んなサイトで紹介されているので割愛する。



環境

パソコン

windows10 64bit

pylon

インストーラー : Basler_pylon_6.1.1.19832.exe
pylonバージョン詳細 : 下図参照
f:id:greenhornprofessional:20210106222054p:plain

python

Python3.8.1 32bit

pypylon

whlファイル : pypylon-1.5.4-cp38-cp38-win32.whl

※ whlファイルのインストール方法は他でも使えるので記載しておく。
 1. whlファイルを適当なフォルダに保存する。
 2. コマンドプロンプトでwhlファイルを保存したディレクトリに移動する。

C:\***\***>cd C:\***\***\***\Python\Python38-32

 3. pipでwhlファイルを指定してインストールする。

C:\***\***\***\Python\Python38-32>pip install pypylon-1.5.4-cp38-cp38-win32.whl

動作確認

f:id:greenhornprofessional:20210106225305p:plain

Pythonでsecretsモジュールを使ってパスワードを自動生成する

Pythonでパスワード作るにはsecrets.choice()を使った方が良いと言われているけど、random.choice()と何が違うのか?
random.choice()で作成されたパスワードは予測される可能性がある。パスワードを構成するすべての文字がランダムに選ばれたように見えて実はどんな文字が選ばれているのかを第三者が計算することができる。 対して、secrets.choice()はより予測が難しくなったもの。

注意事項

secretsモジュールはより安全であると紹介しているサイトを見かけますが、PEP506のQ/Aに「専用の暗号化ソフトウェアの代用にはならない」との記述があります。過度な信頼をしてはいけないと私は解釈しました。

この記事はrandom.choice()よりはsecrets.choice()のほうがベターだよね、ということをお伝えするのが目的であり、安全であるとお伝えする意図は一切ありません。ご使用は自己責任でお願いします。

解説

random.choice()

random.Randomクラスの関数であり、Mersenne Twisterという乱数発生器(アルゴリズム)を使用しているが、暗号化の目的には向いていない。
実は、randomモジュールは別に random.SystemRandomクラス も提供していて、このクラスはOSがもつ乱数発生器を利用して乱数を作成する。

secrets.choice()

secrets.SystemRandomクラスの関数であり、このクラスもOSがもつ乱数発生器を利用して乱数を作成する。

結局は…

random.SystemRandomのインスタンスを作ってchoice()メソッドを使えば、OSがもつ乱数発生器を利用することができるのに、このことを知らないユーザーが多いので新たにsecretsモジュールを設けて間違いを防ぐことにしたらしい。というようなことがPEP506に書いてあった。

サンプルプログラム

アルファベットの大文字と小文字、数字0~9の中から10文字をランダムに選びパスワードを作成する。そして、パスワードをクリップボードにコピーし、念のためテキストファイル(.txt)にしてデスクトップにも保存する。というプログラム。
python_create_password_secrets

ソースコード

import os
import datetime
import string
import secrets
import ctypes
import pyperclip

#Determine password length
len_pw = 10

#Create password
def create_pw(length):
    chars = string.ascii_uppercase + string.ascii_lowercase + string.digits
    pw = ''.join([secrets.choice(chars) for i in range(length)])

    return pw

#Save as txt
def save_txt(password):
    now = datetime.datetime.now()
    filename = "PassWord_{:%Y%m%d%H%M%S}.txt".format(now)
    desktop = os.getenv("HOMEDRIVE") + os.getenv("HOMEPATH") + "\\Desktop"

    with open(os.path.join(desktop, filename), mode='a') as f:
        f.write(password)

def main():
    x = create_pw(len_pw)
    save_txt(x)
    pyperclip.copy(x)
    ctypes.windll.user32.MessageBoxW(
        0,
        "パスワードをクリップボードにコピーしました",
        "Complete!!",
        0x00000040)

if __name__ == '__main__':
    main()

Pythonでグラフのプロットとプロットの間の値を読み取る

実験で〇〇を測定したけど測定間隔が粗かった。データとデータの間にある値が欲しんだけど…
interpolateのスプライン補間でデータ数を増やす。※あくまでもデータの“補間”ですのでその意味をちゃんと理解してご使用ください。

解説

スプライン補間の詳細はググってもらうとして、interpolateの使い方は以下のサイトが非常にわかりやすかった。応用例は↓↓のサンプルプログラムを参照してほしい。
org-technology.com

サンプルプログラム

エクセルからデータを読み込んで、任意の「横軸の値」を与えるとそれに対応するデータを返すというプログラム。
元データは以下のようにエクセルで表形式で保存してあるとする。A列が横軸データ、B列が縦軸データとなっている。見てのとおりデータ数は26個。

元データ(エクセルシート)

これをデータ数1000個でスプライン補間すると以下の青線グラフになる。赤線は元データのグラフ。

python-spline-スプライン補間
スプライン補間結果

ここで、例えば課題のレポートに横軸が10.5における測定値が必要だったことが後からわかったとする。元データには存在しないので再測定するか…もしくは、スプライン補間したデータを求めるか。サンプルプログラムでは44行目、48行目のようにすることでスプライン補間結果から値を得ている。

x=10.5に対応するyは205.713です


ソースコード

# 37_SplineCurve_001.py
# python 3.8.1
# openpyxl 3.0.3
# coding: utf-8

import numpy as np
from scipy import interpolate
from matplotlib import pyplot as plt
import openpyxl


def read_excel(wb_path):
    col_A = []
    col_B = []
    
    wb = openpyxl.load_workbook(wb_path)
    sheet_list = wb.sheetnames
    sheet = wb[sheet_list[0]]
    
    num_data = sheet.max_row

    for i in range(1, num_data+1):
        cell = sheet.cell(row=i, column=1)
        col_A.append(cell.value)

        cell = sheet.cell(row=i, column=2)
        col_B.append(cell.value)

    return col_A, col_B


def interpolate_spline(data_x, data_y, xoi):
    data_x_min = min(data_x)
    data_x_max = max(data_x)

    interpolated_func = interpolate.interp1d(data_x, data_y, kind="quadratic")
    corr_data_x = np.linspace(data_x_min, data_x_max, 1000)
    corr_data_y = interpolated_func(corr_data_x)

#    plt.plot(data_x, data_y,"r")
#    plt.plot(corr_data_x, corr_data_y)
#    plt.show()

    return interpolated_func(xoi)

if __name__ == '__main__':

    xoi = 10.5  #欲しいy値に対応するx値 (X of Interest) 
        
    wb_path = 'OriginalData.xlsx'
    x, y = read_excel(wb_path)
    yoi = interpolate_spline(x, y, xoi)  #欲しいy値を求める (Y of Interest)
    print("x={}に対応するyは{:.3f}です".format(xoi, yoi))


Pythonで自動化!Outlookメールを自動送信する

STMPサーバー?よく分からない…それを使わずにメールを自動送信する方法あるか?
win32comモジュールを使ってOutlookを操作する

解説

Pythonでメール操作する方法をググるstmplibを使用する方法が多く出てくる。この方法はSTMPサーバーやパスワードを指定する必要があり、詳しくない人だとちょっと忌避感があるかもしれない。そこで、手軽に実現する方法としてwin32comを使用する。サーバーとかパスワードの指定不要!
コードも簡単これだけ↓↓で、勝手にOutlookが立ち上がってメールが送信される。

from win32com import client
outlook = client.Dispatch('Outlook.Application')
new_email = outlook.CreateItem(0)
new_email.BodyFormat = 1
new_email.To = '送信メールアドレス' 
new_email.Subject = 'タイトル'
new_email.Body = '本文'
new_email.Send()

Line 4

メールの書式設定を選択する。
1:テキスト 2:HTML 3:リッチテキスト
python-win32com-outlook

Line 5,6,7

必要事項をここにべた書きするか、変数でもよい。

Line 8

このタイミングでメールが送信される。動作確認したい場合は代わりにnew_email.Display(True)を入れておく。送信前のメール画面が確認できる。


サンプルプログラム

毎日、朝9時と夕方18時に上司にメール連絡を入れるプログラムを作成した。リモートワークの勤怠管理のための定時連絡を自動化する、という想定。

  • 朝9時に送信されるメール画面。キャプチャーするためにnew_email.Display(True)で送信しないように止めている。
Outlook 自動作成されたメール画面①
  • 夕方18時に送信されるメール画面。
Outlook 自動作成されたメール画面②


ソースコード

# 36_EmailAutomatically_001.py
# python 3.8.1
# schedule 0.6.0
# coding: utf-8

import time
from win32com import client
import schedule

def makeEmail(set_subject, set_body):
    outlook = client.Dispatch('Outlook.Application')
    new_email = outlook.CreateItem(0)

    #フォーマットを指定する。1:テキスト 2:HTML 3:リッチテキスト
    new_email.BodyFormat = 1

    #送信先メールアドレス
    new_email.To = 'test1@test.com; test2@test.com'
    new_email.CC = 'test3@test.com'

    #メールタイトル
    new_email.Subject = set_subject

    #メール本文
    new_email.Body = set_body

    #メール送信実行
    new_email.Display(True)   #Outlookの新規メール画面が開くとこまで
#   new_email.Send()          #問答無用で送信するモード

def main():
    #朝用の定型文
    subj_morning = "[連絡]業務開始"
    body_morning = "〇〇さん\n業務開始します。\n□□"

    #夕方用の定型文
    subj_afternoon = "[連絡]業務終了"
    body_afternoon = "〇〇さん\n業務終了します。\nお疲れさまでした。\n□□"

    #朝9時と、夕方18時にタイマーをセットする
    schedule.every().day.at("9:00").do(makeEmail, subj_morning, body_morning)
    schedule.every().day.at("18:00").do(makeEmail, subj_afternoon, body_afternoon)

    while True:
        schedule.run_pending()
        time.sleep(1)

if __name__ == '__main__':
    main()

参考サイト

schedule · PyPI

Pythonで自動化!複数画像から輝度値を取得してテキストに出力する

画像から輝度値を抽出したいが、大量にあるので自動化したい。
filedialog.askopenfilenamesを使ってファイルダイアログで複数画像を一括で選択できるようにする。

解説

filedialog.askopenfilenamesの使い方

  • filetypesで開くファイルの種類を指定、initialdirでデフォルトパスを指定するだけ。ちなみに...namesのsを抜くとファイルが1個しか選択できない。複数選択したい場合はsを忘れないこと。
  • filesはタプル型になり、選択したファイルのパスが格納される。なので、このタプルをfor文でアンパックしてあげれば一括処理ができる。
files = filedialog.askopenfilenames(filetypes = ***, initialdir = ***)


サンプルプログラム

デスクトップに保存されている赤、青、緑の3枚の画像から一括で輝度値を取得してテキストに出力するプログラムを作成した。

  • 下の画像はファイルダイアログで画像を選択するところ。



  • 下の画像は出力結果。※見やすいように後から編集で適当にスペースを入れてある。


ソースコード

# 35_GetIntensity_001.py
# python 3.8.1
# opencv-python 4.1.2.30 
# coding: utf-8

import os
import datetime
import cv2
import tkinter as tk
from tkinter import filedialog

def getBGR(path):
    imgBGR = cv2.imread(path, cv2.IMREAD_COLOR)

    #開いた画像からRGBを抽出する。
    b = imgBGR.T[0].flatten().mean()
    g = imgBGR.T[1].flatten().mean()
    r = imgBGR.T[2].flatten().mean()

    return b, g, r

def main():
    root = tk.Tk()
    root.withdraw()

    #出力するファイルの名前を定義する。タイムスタンプ付き。
    now = datetime.datetime.now()
    fo = 'ImageIntensityList_{:%Y%m%d%H%M%S}.txt'.format(now)
    
    #ファイルダイアログのデフォルトパスをデスクトップにする。
    desktop_path = os.getenv("HOMEDRIVE") + os.getenv("HOMEPATH") + "\\Desktop"

    #対象ファイルのタイプをTiffに限定する。
    target_format = [('TIF image', '*.tif')]
    
    #ファイルダイアログオブジェクトの作成。複数ファイル選択できる。
    files = filedialog.askopenfilenames(filetypes = target_format, initialdir = desktop_path)
    
    #ファイルダイアログで選択したすべてのファイルを処理する。
    with open(os.path.join(desktop_path, fo), mode='a') as f:
        f.write('File path,B,G,R,Intensity\n')
        for file in files:
            b, g, r = getBGR(file)
            i = 0.298912 * r + 0.586611 * g + 0.114478 * b  #RGBから輝度値を算出
            line = '{0},{1:.2f},{2:.2f},{3:.2f},{4:.2f}\n'.format(file, b, g, r, i)
            f.write(line)

if __name__ == '__main__':
    main()


Pythonのlist.sort()とsorted(list)の違いは新たにリストが作られるかどうか

list.sort()とsorted(list)の使い分けは?
対象となるリストを作り替えてもいい場合は list.sort() を、対象となるリストを残しておきたい場合は sorted(list) を使う

解説

list.sort()

sortメソッドは対象リストに直接変更を加えるソートを行う。これを「インプレイスでソートする」と言う。以下コード内の#1と#2が示すように、ソート前後でオブジェクトIDが変わっていないことがわかる。

data = ["Green", "Blue", "Red", "Yellow", "Orange", "Brown", "Black"]
print("Object ID:", id(data))   #1
data.sort()
print("Result   :", data)
print("Object ID:", id(data))   #2
Object ID: 86722536   #1
Result   : ['Black', 'Blue', 'Brown', 'Green', 'Orange', 'Red', 'Yellow']
Object ID: 86722536   #2


sorted(list)

sorted関数は対象リストとは別の新しいリストを作り、ソートした要素を入れる。以下コード内の#3と#4が示すように、sort関数から返ってくるとオブジェクトIDが変わっていることからも分かる。

data = ["Green", "Blue", "Red", "Yellow", "Orange", "Brown", "Black"]
print("Object ID:", id(data))             #3
print("Result   :", sorted(data))
print("Object ID:", id(sorted(data)))   #4
Object ID: 86722504   #3
Result   : ['Black', 'Blue', 'Brown', 'Green', 'Orange', 'Red', 'Yellow']
Object ID: 86721800   #4


Tips ~Pythonのソートは安定している~

『安定』とは、ソート基準が同等のデータはソート前の順番が保持される、ということを意味する。細かいことだが、これを知らないとkeyパラメータに複数条件を入れる…なんて無駄なことをやってしまいかねない。と思ったら、過去に挙げた記事でまんまそれをやってしまっていた。
greenhornprofessional.hatenablog.com

文字数でソートする例

Green, Brown, Blackは5文字なので、同等データとなるがちゃんとソート前の順序が保持されていることがわかる。

data = ["Green", "Blue", "Red", "Yellow", "Orange", "Brown", "Black"]
data.sort(key=len)
print(data)
['Red', 'Blue', 'Green', 'Brown', 'Black', 'Yellow', 'Orange']
/* -----codeの行番号----- */