20191002のPythonに関する記事は30件です。

Tkinterを使うのであればPySimpleGUIを使ってみたらという話

ページを読んでできるもの

  • Pythonで以下の入力内容のGUIが簡単に作れます
  • 基本的な入力(テキストボックス、チェックボックス)
    • テキストボックスにはデフォルトで入力が、チェックボックスで入力できます
    • 入力された内容をポップアップで表示する

起動直後の画面

image.png

実行ボタンを押した結果

image.png

Tkinterについて

Tkinterは標準のPythonのGUIライブラリです。個人的に考えるメリット、デメリットは以下の通りです。

  • メリット

    • 標準だからインストール不要
    • サンプルはそこそこ多い
  • デメリット

    • ウィジェット(UIパーツ)を配置する際、種類、オプション、レイアウトと順を追って設定しないといけない
    • ウィジェットの配置が増えると、レイアウトがコードを見てもよくわからない

Tkinterの例

実際にTkinterについて実際にコードを載せます

PythonでGUIアプリを作る方法【Tkinter】
の記事の最後のコードと実行結果をこちらに転載します

実行結果

image.png

テキストボックスに入力後に「実行ボタン」を押した結果

image.png

コードは以下になります

import tkinter
from tkinter import messagebox

#ボタンがクリックされたら実行
def button_click():
    input_value = input_box.get()
    messagebox.showinfo("クリックイベント",input_value + "が入力されました。")

#ウインドウの作成
root = tkinter.Tk()
root.title("Python GUI")
root.geometry("360x240")

#入力欄の作成
input_box = tkinter.Entry(width=40)
input_box.place(x=10, y=100)

#ラベルの作成
input_label = tkinter.Label(text="ラベル")
input_label.place(x=10, y=70)

#ボタンの作成
button = tkinter.Button(text="実行ボタン",command=button_click)
button.place(x=10, y=130)

#ウインドウの描画
root.mainloop()

ウィジェット(UIのパーツ)が縦一列に並んだ場合は比較的簡単です。
問題は横一列に「ラベル」+「テキストボックス」というようなよくあるレイアウトで複数のウィジェットを配置した場合です

実行結果

image.png

テキストボックスに入力後に「実行ボタン」を押した結果

image.png

コードはいかになります。

import tkinter
from tkinter import messagebox

#ボタンがクリックされたら実行
def button_click():
    input_name_value = input_name.get()
    input_address_value = input_address.get()
    input_phone_value = input_phone.get()

    show_message = "名前:" + input_name_value + 'が入力されました。\n'
    show_message += "住所:" + input_address_value + 'が入力されました。\n'
    show_message += "電話番号:" + input_phone_value + "が入力されました。"
    print(show_message)

    # 入力内容をポップアップ画面で表示
    messagebox.showinfo("入力内容" ,show_message)

#ウインドウの作成
root = tkinter.Tk()
root.title("Python GUI")
root.geometry("360x120")

# 名前
input_name_label = tkinter.Label(text="名前")
input_name_label.grid(row=1, column=1, padx=10,)

# 入力欄の作成
input_name = tkinter.Entry(width=40)
input_name.grid(row=1, column=2)


# 住所
input_address_label = tkinter.Label(text="住所")
input_address_label.grid(row=2, column=1, padx=10,)

# 住所入力欄の作成
input_address = tkinter.Entry(width=40)
input_address.grid(row=2, column=2)


# 電話番号
input_phone_label = tkinter.Label(text="名前")
input_phone_label.grid(row=3, column=1, padx=10,)

# 電話番号入力欄の作成
input_phone = tkinter.Entry(width=40)
input_phone.grid(row=3, column=2)

#ボタンの作成
button = tkinter.Button(text="実行ボタン",command=button_click)
button.place(x=10, y=80)

#ウインドウの描画
root.mainloop()

問題なのは以下のところです。

# 電話番号
input_phone_label = tkinter.Label(text="名前")
input_phone_label.grid(row=3, column=1, padx=10,)

Tkinterはウィジェットを配置する際はまず自分が何(ラベル、ボタン、テキストボックスetc)を設定します。
そしてそのあとに配置する場所を指定します。

Tkinterでグリッドレイアウトを用いる際は、grid関数にrow,columnを使ってレイアウトを調整します。今回は縦×横が3×2のレイアウトなのでまだいいですが、例えば5×10などウィジェットが増えていくと、コードをぱっと見ただけではレイアウトがよくわからなくなります。

PySimpleGUI

PySimpleGUIは2018年から開発が始まったライブラリーです。

tkinter、Qt、WxPython、Remiののラッパーで公式ではもとのライブラリーで書く場合を比べて、コード量が2分の1から10分の1程度でかけると書いてあります。
私自身も実際に書いてみてTkinterで書くよりもはるかに短いコード量でかけると思いました。

最大の特徴は、リストを使用して、UIのレイアウトを配置することです。
実際に使用して見ましょう。

インストール方法はpipで簡単にインストールできます

pip install pysimplegui
or
pip3 install pysimplegui

簡単な使い方は以下のとおりです。公式の以下のサンプルをもとに作成しています。

import PySimpleGUI as sg


layout = [      
          [sg.Text('Python GUI')],      
          [sg.Text('名前', size=(15, 1)), sg.InputText('○○〇×××')],      
          [sg.Text('住所', size=(15, 1)), sg.InputText('△△△△村')],      
          [sg.Text('電話番号', size=(15, 1)), sg.InputText('xxx-xxx-xxx')],     
          [sg.Submit(button_text='実行ボタン')]      
         ]

window = sg.Window('Simple data entry window').Layout(layout)         

while True:      
    event, values = window.Read()      
    # if event == None or event == 'Exit':
    if event == None:  
        print('exit')
        break      

    if event == '実行ボタン':  
        show_message = "名前:" + values[0] + 'が入力されました。\n'
        show_message += "住所:" + values[1] + 'が入力されました。\n'
        show_message += "電話番号:" + values[2] + "が入力されました。"
        print(show_message)

        # ポップアップ
        sg.Popup(show_message)

実行結果はいかになります。

実行結果

image.png

実行ボタンを押した場合

image.png

いかがでしょうか?
レイアウトは以下で定義しています。2次元のリストでレイアウトを定義しています。

layout = [      
          [sg.Text('Python GUI')],      
          [sg.Text('名前', size=(15, 1)), sg.InputText('○○〇×××')],      
          [sg.Text('住所', size=(15, 1)), sg.InputText('△△△△村')],      
          [sg.Text('電話番号', size=(15, 1)), sg.InputText('xxx-xxx-xxx')],     
          [sg.Submit(button_text='実行ボタン')]      

そして取得した値は以下で取得できます

    if event == '実行ボタン':  
        show_message = "名前:" + values[0] + 'が入力されました。\n'
        show_message += "住所:" + values[1] + 'が入力されました。\n'
        show_message += "電話番号:" + values[2] + "が入力されました。"

詳しくは公式ドキュメントを読むことをおすすめしますが、
Tkinterでは54行だったものがPySimpleGUIですと28行でかけます。
なにより、Tkinterで書いたものよりもレイアウトや値の取得方法が格段にわかりやすいです。

checkboxを使ってみる

先程のコードにデフォルトでチェックが入っているチェックボックスを追加してみます。

コードはいかになります

import PySimpleGUI as sg


layout = [
          [sg.Text('Python GUI')],
          [sg.Text('名前', size=(15, 1)), sg.InputText('○○〇×××')],
          [sg.Text('住所', size=(15, 1)), sg.InputText('△△△△村')],
          [sg.Text('電話番号', size=(15, 1)), sg.InputText('xxx-xxx-xxx')],
          [sg.Checkbox('同意する', default=True)],
          [sg.Submit(button_text='実行ボタン')]
         ]

window = sg.Window('Simple data entry window').Layout(layout)

while True:
    event, values = window.Read()
    # if event == None or event == 'Exit':
    if event is None:
        print('exit')
        break

    if event == '実行ボタン':
        show_message = "名前:" + values[0] + 'が入力されました。\n'
        show_message += "住所:" + values[1] + 'が入力されました。\n'
        show_message += "電話番号:" + values[2] + "が入力されました。\n"
        show_message += "同意のチェックは:" + str(values[3]) + "です"
        print(show_message)

        # ポップアップ
        sg.Popup(show_message)

実行結果はいかになります。

起動直後の画面

image.png

実行ボタンを押した結果

image.png

チェックボックスの値はBool方でture,falseで取得できます。
Tkinterでチェックボックスでデフォルトの値を設定しようとおもうとちょっとめんどうだったものが簡単に設定できます。

Tkinterのチェックボックスの設定方法については以下の記事が参考になります。

PySimpleGUIは他にもTkinterを生で使うよりも、簡単にウィジェットが使用できます。また公式のサンプルが豊富です。matplotlibやopenCvを使用した例などが豊富にあります。

exe化について

exe化はPyInstllerを使用して簡単にできます

以下は公式の以下の説明からの引用です

単一ファイルの作り方は以下になります

pip install PyInstaller
pyinstaller -wF my_program.py

-wF をつけることで単一ファイルができます

また以下のエラーが出た場合は、--hidden-import tkinter をオプションに加えます。

ValueError: script '.......\src\tkinter' not found

Macだと以下でできるそうです(公式では確かめていないそうです)

pyinstaller --onefile --add-binary = '/ System / Library / Frameworks / Tk.framework / Tk': 'tk' --add-binary = '/ System / Library / Frameworks / Tcl.framework / Tcl': ' tcl 'your_program.py

この情報はRedditにあり、ソースはhttps://github.com/pyinstaller/pyinstaller/issues/1350 から引用しています。

ライセンスについて

PySimpleGUIはライセンスはLGPL3.0になります。
MITライセンスではないですが、再配布しない限り社内や個人で利用するにあたっての使用には問題がないはずですので通常使用する分には問題がないはずです。

今後の予定

時間があれば公式のサンプルプログラムからいくつかの紹介とTkinterで作られたGUIをPySimpleGUIで書き直した場合のサンプルを書こうかと考えています。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Tkinterを使うのであればpySympleGUIを使ってみたらという話

Tkinterを使うのであればpySympleGUIを使ってみたらという話

ページを読んでできるもの

  • Pythonで以下の入力内容のGUIが簡単に作れます
  • 基本的な入力(テキストボックス、チェックボックス)
    • テキストボックスにはデフォルトで入力が、チェックボックスで入力できます
    • 入力された内容をポップアップで表示する

起動直後の画面

image.png

実行ボタンを押した結果

image.png

Tkinterについて

Tkinterは標準のPythonのGUIライブラリです。個人的に考えるメリット、デメリットは以下の通りです。

  • メリット

    • 標準だからインストール不要
    • サンプルはそこそこ多い
  • デメリット

    • ウィジェット(UIパーツ)を配置する際、種類、オプション、レイアウトと順を追って設定しないといけない
    • ウィジェットの配置が増えると、レイアウトがコードを見てもよくわからない

Tkinterの例

実際にTkinterについて実際にコードを載せます

PythonでGUIアプリを作る方法【Tkinter】
の記事の最後のコードと実行結果をこちらに転載します

実行結果

image.png

テキストボックスに入力後に「実行ボタン」を押した結果

image.png

コードは以下になります

import tkinter
from tkinter import messagebox

#ボタンがクリックされたら実行
def button_click():
    input_value = input_box.get()
    messagebox.showinfo("クリックイベント",input_value + "が入力されました。")

#ウインドウの作成
root = tkinter.Tk()
root.title("Python GUI")
root.geometry("360x240")

#入力欄の作成
input_box = tkinter.Entry(width=40)
input_box.place(x=10, y=100)

#ラベルの作成
input_label = tkinter.Label(text="ラベル")
input_label.place(x=10, y=70)

#ボタンの作成
button = tkinter.Button(text="実行ボタン",command=button_click)
button.place(x=10, y=130)

#ウインドウの描画
root.mainloop()

ウィジェット(UIのパーツ)が縦一列に並んだ場合は比較的簡単です。
問題は横一列に「ラベル」+「テキストボックス」というようなよくあるレイアウトで複数のウィジェットを配置した場合です

実行結果

image.png

テキストボックスに入力後に「実行ボタン」を押した結果

image.png

コードはいかになります。

import tkinter
from tkinter import messagebox

#ボタンがクリックされたら実行
def button_click():
    input_name_value = input_name.get()
    input_address_value = input_address.get()
    input_phone_value = input_phone.get()

    show_message = "名前:" + input_name_value + 'が入力されました。\n'
    show_message += "住所:" + input_address_value + 'が入力されました。\n'
    show_message += "電話番号:" + input_phone_value + "が入力されました。"
    print(show_message)

    # 入力内容をポップアップ画面で表示
    messagebox.showinfo("入力内容" ,show_message)

#ウインドウの作成
root = tkinter.Tk()
root.title("Python GUI")
root.geometry("360x120")

# 名前
input_name_label = tkinter.Label(text="名前")
input_name_label.grid(row=1, column=1, padx=10,)

# 入力欄の作成
input_name = tkinter.Entry(width=40)
input_name.grid(row=1, column=2)


# 住所
input_address_label = tkinter.Label(text="住所")
input_address_label.grid(row=2, column=1, padx=10,)

# 住所入力欄の作成
input_address = tkinter.Entry(width=40)
input_address.grid(row=2, column=2)


# 電話番号
input_phone_label = tkinter.Label(text="名前")
input_phone_label.grid(row=3, column=1, padx=10,)

# 電話番号入力欄の作成
input_phone = tkinter.Entry(width=40)
input_phone.grid(row=3, column=2)

#ボタンの作成
button = tkinter.Button(text="実行ボタン",command=button_click)
button.place(x=10, y=80)

#ウインドウの描画
root.mainloop()

問題なのは以下のところです。

# 電話番号
input_phone_label = tkinter.Label(text="名前")
input_phone_label.grid(row=3, column=1, padx=10,)

Tkinterはウィジェットを配置する際はまず自分が何(ラベル、ボタン、テキストボックスetc)を設定します。
そしてそのあとに配置する場所を指定します。

Tkinterでグリッドレイアウトを用いる際は、grid関数にrow,columnを使ってレイアウトを調整します。今回は縦×横が3×2のレイアウトなのでまだいいですが、例えば5×10などウィジェットが増えていくと、コードをぱっと見ただけではレイアウトがよくわからなくなります。

pySympleGUI

pySympleGUIは2018年から開発が始まったライブラリーです。

tkinter、Qt、WxPython、Remiののラッパーで公式ではもとのライブラリーで書く場合を比べて、コード量が2分の1から10分の1程度でかけると書いてあります。
私自身も実際に書いてみてTkinterで書くよりもはるかに短いコード量でかけると思いました。

最大の特徴は、リストを使用して、UIのレイアウトを配置することです。
実際に使用して見ましょう。

インストール方法はpipで簡単にインストールできます

pip install pysimplegui
or
pip3 install pysimplegui

簡単な使い方は以下のとおりです。公式の以下のサンプルをもとに作成しています。

import PySimpleGUI as sg


layout = [      
          [sg.Text('Python GUI')],      
          [sg.Text('名前', size=(15, 1)), sg.InputText('○○〇×××')],      
          [sg.Text('住所', size=(15, 1)), sg.InputText('△△△△村')],      
          [sg.Text('電話番号', size=(15, 1)), sg.InputText('xxx-xxx-xxx')],     
          [sg.Submit(button_text='実行ボタン')]      
         ]

window = sg.Window('Simple data entry window').Layout(layout)         

while True:      
    event, values = window.Read()      
    # if event == None or event == 'Exit':
    if event == None:  
        print('exit')
        break      

    if event == '実行ボタン':  
        show_message = "名前:" + values[0] + 'が入力されました。\n'
        show_message += "住所:" + values[1] + 'が入力されました。\n'
        show_message += "電話番号:" + values[2] + "が入力されました。"
        print(show_message)

        # ポップアップ
        sg.Popup(show_message)

実行結果はいかになります。

実行結果

image.png

実行ボタンを押した場合

image.png

いかがでしょうか?
レイアウトは以下で定義しています。2次元のリストでレイアウトを定義しています。

layout = [      
          [sg.Text('Python GUI')],      
          [sg.Text('名前', size=(15, 1)), sg.InputText('○○〇×××')],      
          [sg.Text('住所', size=(15, 1)), sg.InputText('△△△△村')],      
          [sg.Text('電話番号', size=(15, 1)), sg.InputText('xxx-xxx-xxx')],     
          [sg.Submit(button_text='実行ボタン')]      

そして取得した値は以下で取得できます

    if event == '実行ボタン':  
        show_message = "名前:" + values[0] + 'が入力されました。\n'
        show_message += "住所:" + values[1] + 'が入力されました。\n'
        show_message += "電話番号:" + values[2] + "が入力されました。"

詳しくは公式ドキュメントを読むことをおすすめしますが、
Tkinterでは54行だったものがpySympleGUIですと28行でかけます。
なにより、Tkinterで書いたものよりもレイアウトや値の取得方法が格段にわかりやすいです。

checkboxを使ってみる

先程のコードにデフォルトでチェックが入っているチェックボックスを追加してみます。

コードはいかになります

import PySimpleGUI as sg


layout = [
          [sg.Text('Python GUI')],
          [sg.Text('名前', size=(15, 1)), sg.InputText('○○〇×××')],
          [sg.Text('住所', size=(15, 1)), sg.InputText('△△△△村')],
          [sg.Text('電話番号', size=(15, 1)), sg.InputText('xxx-xxx-xxx')],
          [sg.Checkbox('同意する', default=True)],
          [sg.Submit(button_text='実行ボタン')]
         ]

window = sg.Window('Simple data entry window').Layout(layout)

while True:
    event, values = window.Read()
    # if event == None or event == 'Exit':
    if event is None:
        print('exit')
        break

    if event == '実行ボタン':
        show_message = "名前:" + values[0] + 'が入力されました。\n'
        show_message += "住所:" + values[1] + 'が入力されました。\n'
        show_message += "電話番号:" + values[2] + "が入力されました。\n"
        show_message += "同意のチェックは:" + str(values[3]) + "です"
        print(show_message)

        # ポップアップ
        sg.Popup(show_message)

実行結果はいかになります。

起動直後の画面

image.png

実行ボタンを押した結果

image.png

チェックボックスの値はBool方でture,falseで取得できます。
Tkinterでチェックボックスでデフォルトの値を設定しようとおもうとちょっとめんどうだったものが簡単に設定できます。

Tkinterのチェックボックスの設定方法については以下の記事が参考になります。

PySympleGUIは他にもTkinterを生で使うよりも、簡単にウィジェットが使用できます。また公式のサンプルが豊富です。matplotlibやopenCvを使用した例などが豊富にあります。

exe化について

exe化はPyInstllerを使用して簡単にできます

以下は公式の以下の説明からの引用です

単一ファイルの作り方は以下になります

pip install PyInstaller
pyinstaller -wF my_program.py

-wF をつけることで単一ファイルができます

また以下のエラーが出た場合は、--hidden-import tkinter をオプションに加えます。

ValueError: script '.......\src\tkinter' not found

Macだと以下でできるそうです(公式では確かめていないそうです)

pyinstaller --onefile --add-binary = '/ System / Library / Frameworks / Tk.framework / Tk': 'tk' --add-binary = '/ System / Library / Frameworks / Tcl.framework / Tcl': ' tcl 'your_program.py

この情報はRedditにあり、ソースはhttps://github.com/pyinstaller/pyinstaller/issues/1350 から引用しています。

ライセンスについて

PySympleGUIはライセンスはLGPL3.0になります。
MITライセンスではないですが、再配布しない限り社内や個人で利用するにあたっての使用には問題がないはずですので通常使用する分には問題がないはずです。

今後の予定

時間があれば公式のサンプルプログラムからいくつかの紹介とTkinterで作られたGUIをPySympleGUIで書き直した場合のサンプルを書こうかと考えています。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Tkinterを使うのであればPySympleGUIを使ってみたらという話

Tkinterを使うのであればPySympleGUIを使ってみたらという話

ページを読んでできるもの

  • Pythonで以下の入力内容のGUIが簡単に作れます
  • 基本的な入力(テキストボックス、チェックボックス)
    • テキストボックスにはデフォルトで入力が、チェックボックスで入力できます
    • 入力された内容をポップアップで表示する

起動直後の画面

image.png

実行ボタンを押した結果

image.png

Tkinterについて

Tkinterは標準のPythonのGUIライブラリです。個人的に考えるメリット、デメリットは以下の通りです。

  • メリット

    • 標準だからインストール不要
    • サンプルはそこそこ多い
  • デメリット

    • ウィジェット(UIパーツ)を配置する際、種類、オプション、レイアウトと順を追って設定しないといけない
    • ウィジェットの配置が増えると、レイアウトがコードを見てもよくわからない

Tkinterの例

実際にTkinterについて実際にコードを載せます

PythonでGUIアプリを作る方法【Tkinter】
の記事の最後のコードと実行結果をこちらに転載します

実行結果

image.png

テキストボックスに入力後に「実行ボタン」を押した結果

image.png

コードは以下になります

import tkinter
from tkinter import messagebox

#ボタンがクリックされたら実行
def button_click():
    input_value = input_box.get()
    messagebox.showinfo("クリックイベント",input_value + "が入力されました。")

#ウインドウの作成
root = tkinter.Tk()
root.title("Python GUI")
root.geometry("360x240")

#入力欄の作成
input_box = tkinter.Entry(width=40)
input_box.place(x=10, y=100)

#ラベルの作成
input_label = tkinter.Label(text="ラベル")
input_label.place(x=10, y=70)

#ボタンの作成
button = tkinter.Button(text="実行ボタン",command=button_click)
button.place(x=10, y=130)

#ウインドウの描画
root.mainloop()

ウィジェット(UIのパーツ)が縦一列に並んだ場合は比較的簡単です。
問題は横一列に「ラベル」+「テキストボックス」というようなよくあるレイアウトで複数のウィジェットを配置した場合です

実行結果

image.png

テキストボックスに入力後に「実行ボタン」を押した結果

image.png

コードはいかになります。

import tkinter
from tkinter import messagebox

#ボタンがクリックされたら実行
def button_click():
    input_name_value = input_name.get()
    input_address_value = input_address.get()
    input_phone_value = input_phone.get()

    show_message = "名前:" + input_name_value + 'が入力されました。\n'
    show_message += "住所:" + input_address_value + 'が入力されました。\n'
    show_message += "電話番号:" + input_phone_value + "が入力されました。"
    print(show_message)

    # 入力内容をポップアップ画面で表示
    messagebox.showinfo("入力内容" ,show_message)

#ウインドウの作成
root = tkinter.Tk()
root.title("Python GUI")
root.geometry("360x120")

# 名前
input_name_label = tkinter.Label(text="名前")
input_name_label.grid(row=1, column=1, padx=10,)

# 入力欄の作成
input_name = tkinter.Entry(width=40)
input_name.grid(row=1, column=2)


# 住所
input_address_label = tkinter.Label(text="住所")
input_address_label.grid(row=2, column=1, padx=10,)

# 住所入力欄の作成
input_address = tkinter.Entry(width=40)
input_address.grid(row=2, column=2)


# 電話番号
input_phone_label = tkinter.Label(text="名前")
input_phone_label.grid(row=3, column=1, padx=10,)

# 電話番号入力欄の作成
input_phone = tkinter.Entry(width=40)
input_phone.grid(row=3, column=2)

#ボタンの作成
button = tkinter.Button(text="実行ボタン",command=button_click)
button.place(x=10, y=80)

#ウインドウの描画
root.mainloop()

問題なのは以下のところです。

# 電話番号
input_phone_label = tkinter.Label(text="名前")
input_phone_label.grid(row=3, column=1, padx=10,)

Tkinterはウィジェットを配置する際はまず自分が何(ラベル、ボタン、テキストボックスetc)を設定します。
そしてそのあとに配置する場所を指定します。

Tkinterでグリッドレイアウトを用いる際は、grid関数にrow,columnを使ってレイアウトを調整します。今回は縦×横が3×2のレイアウトなのでまだいいですが、例えば5×10などウィジェットが増えていくと、コードをぱっと見ただけではレイアウトがよくわからなくなります。

PySympleGUI

PySympleGUIは2018年から開発が始まったライブラリーです。

tkinter、Qt、WxPython、Remiののラッパーで公式ではもとのライブラリーで書く場合を比べて、コード量が2分の1から10分の1程度でかけると書いてあります。
私自身も実際に書いてみてTkinterで書くよりもはるかに短いコード量でかけると思いました。

最大の特徴は、リストを使用して、UIのレイアウトを配置することです。
実際に使用して見ましょう。

インストール方法はpipで簡単にインストールできます

pip install pysimplegui
or
pip3 install pysimplegui

簡単な使い方は以下のとおりです。公式の以下のサンプルをもとに作成しています。

import PySimpleGUI as sg


layout = [      
          [sg.Text('Python GUI')],      
          [sg.Text('名前', size=(15, 1)), sg.InputText('○○〇×××')],      
          [sg.Text('住所', size=(15, 1)), sg.InputText('△△△△村')],      
          [sg.Text('電話番号', size=(15, 1)), sg.InputText('xxx-xxx-xxx')],     
          [sg.Submit(button_text='実行ボタン')]      
         ]

window = sg.Window('Simple data entry window').Layout(layout)         

while True:      
    event, values = window.Read()      
    # if event == None or event == 'Exit':
    if event == None:  
        print('exit')
        break      

    if event == '実行ボタン':  
        show_message = "名前:" + values[0] + 'が入力されました。\n'
        show_message += "住所:" + values[1] + 'が入力されました。\n'
        show_message += "電話番号:" + values[2] + "が入力されました。"
        print(show_message)

        # ポップアップ
        sg.Popup(show_message)

実行結果はいかになります。

実行結果

image.png

実行ボタンを押した場合

image.png

いかがでしょうか?
レイアウトは以下で定義しています。2次元のリストでレイアウトを定義しています。

layout = [      
          [sg.Text('Python GUI')],      
          [sg.Text('名前', size=(15, 1)), sg.InputText('○○〇×××')],      
          [sg.Text('住所', size=(15, 1)), sg.InputText('△△△△村')],      
          [sg.Text('電話番号', size=(15, 1)), sg.InputText('xxx-xxx-xxx')],     
          [sg.Submit(button_text='実行ボタン')]      

そして取得した値は以下で取得できます

    if event == '実行ボタン':  
        show_message = "名前:" + values[0] + 'が入力されました。\n'
        show_message += "住所:" + values[1] + 'が入力されました。\n'
        show_message += "電話番号:" + values[2] + "が入力されました。"

詳しくは公式ドキュメントを読むことをおすすめしますが、
Tkinterでは54行だったものがPySympleGUIですと28行でかけます。
なにより、Tkinterで書いたものよりもレイアウトや値の取得方法が格段にわかりやすいです。

checkboxを使ってみる

先程のコードにデフォルトでチェックが入っているチェックボックスを追加してみます。

コードはいかになります

import PySimpleGUI as sg


layout = [
          [sg.Text('Python GUI')],
          [sg.Text('名前', size=(15, 1)), sg.InputText('○○〇×××')],
          [sg.Text('住所', size=(15, 1)), sg.InputText('△△△△村')],
          [sg.Text('電話番号', size=(15, 1)), sg.InputText('xxx-xxx-xxx')],
          [sg.Checkbox('同意する', default=True)],
          [sg.Submit(button_text='実行ボタン')]
         ]

window = sg.Window('Simple data entry window').Layout(layout)

while True:
    event, values = window.Read()
    # if event == None or event == 'Exit':
    if event is None:
        print('exit')
        break

    if event == '実行ボタン':
        show_message = "名前:" + values[0] + 'が入力されました。\n'
        show_message += "住所:" + values[1] + 'が入力されました。\n'
        show_message += "電話番号:" + values[2] + "が入力されました。\n"
        show_message += "同意のチェックは:" + str(values[3]) + "です"
        print(show_message)

        # ポップアップ
        sg.Popup(show_message)

実行結果はいかになります。

起動直後の画面

image.png

実行ボタンを押した結果

image.png

チェックボックスの値はBool方でture,falseで取得できます。
Tkinterでチェックボックスでデフォルトの値を設定しようとおもうとちょっとめんどうだったものが簡単に設定できます。

Tkinterのチェックボックスの設定方法については以下の記事が参考になります。

PySympleGUIは他にもTkinterを生で使うよりも、簡単にウィジェットが使用できます。また公式のサンプルが豊富です。matplotlibやopenCvを使用した例などが豊富にあります。

exe化について

exe化はPyInstllerを使用して簡単にできます

以下は公式の以下の説明からの引用です

単一ファイルの作り方は以下になります

pip install PyInstaller
pyinstaller -wF my_program.py

-wF をつけることで単一ファイルができます

また以下のエラーが出た場合は、--hidden-import tkinter をオプションに加えます。

ValueError: script '.......\src\tkinter' not found

Macだと以下でできるそうです(公式では確かめていないそうです)

pyinstaller --onefile --add-binary = '/ System / Library / Frameworks / Tk.framework / Tk': 'tk' --add-binary = '/ System / Library / Frameworks / Tcl.framework / Tcl': ' tcl 'your_program.py

この情報はRedditにあり、ソースはhttps://github.com/pyinstaller/pyinstaller/issues/1350 から引用しています。

ライセンスについて

PySympleGUIはライセンスはLGPL3.0になります。
MITライセンスではないですが、再配布しない限り社内や個人で利用するにあたっての使用には問題がないはずですので通常使用する分には問題がないはずです。

今後の予定

時間があれば公式のサンプルプログラムからいくつかの紹介とTkinterで作られたGUIをPySympleGUIで書き直した場合のサンプルを書こうかと考えています。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

sklearn.cross_varidationが使えない件

sklearn.cross_varidationがimportできない

sklearn.cross_varidationからimportしようと思ったが下記のようなエラーが出る.

SyntaxError: invalid syntax

調べてみたあっさり原因が見つかった.

cross_varidationの廃止

原因は,cross_varidationの廃止だったそうな.じゃあなに使えばいいのっていうとsklearn.model_selectionというのを代わりに使えばいいらしい.
具体的には...

from sklearn.model_selection import cross_val_score

こんな感じ

大体のクラスや関数は置き換えなのでcross_validationを書き換えるだけでいいっぽい

ソースはここ
ImportError: No module named 'sklearn.cross_validation'の対処

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

tkinterのタイトル横のアイコンをコードに埋め込む方法

この記事では、Windows10の環境で、Python 3.7.4を使っています。

pythonのデスクトップアイコンの変更方法

 pythonのデスクトップアイコンは通常pyinstallerを使うと、

pyinstaller
pyinstaller ソース.py --icon=アイコンファイル.ico

で変更することができます。
ですが、tkinterでウインドウを作成したときに、
dialog.png
が変更できない。
ここも変更したいと思う。

iconbitmapを使う方法

icon.icoを作成してある場合、

ico.py
import tkinter as tk

root = tk.Tk()
tk.iconbitmap(default='icon.ico')
root.mainloop()

とすると、同じフォルダにあるicon.icoが表示されます。
ただし、pyinstallerで--onefileを指定してコンパイルしても同一ファイルにコンパイルされずに、icon.icoファイルを同一フォルダに準備しないといけません。これでは、少しかっこ悪いです。

アイコンをコードに埋め込む

アイコンをbase64でエンコードして埋め込む方法があります。まず画像ファイル(a.gif)を用意します。(gifフィアルがいいようです。)
これをbase64でエンコードするために、Windows PowerSellを起動し、

certutil
certutil -encode a.gif text.txt

とすると、同じフォルダにtext.txtというファイルができます。中身は、

certutil
-----BEGIN CERTIFICATE-----
R0lGODlhEAAQAPcAAAAAAAAAMwAAZgAAmQAAzAAA/wArAAArMwArZgArmQArzAAr
/wBVAABVMwBVZgBVmQBVzABV/wCAAACAMwCAZgCAmQCAzACA/wCqAACqMwCqZgCq
mQCqzACq/wDVAADVMwDVZgDVmQDVzADV/wD/AAD/MwD/ZgD/mQD/zAD//zMAADMA
MzMAZjMAmTMAzDMA/zMrADMrMzMrZjMrmTMrzDMr/zNVADNVMzNVZjNVmTNVzDNV
/zOAADOAMzOAZjOAmTOAzDOA/zOqADOqMzOqZjOqmTOqzDOq/zPVADPVMzPVZjPV
mTPVzDPV/zP/ADP/MzP/ZjP/mTP/zDP//2YAAGYAM2YAZmYAmWYAzGYA/2YrAGYr
M2YrZmYrmWYrzGYr/2ZVAGZVM2ZVZmZVmWZVzGZV/2aAAGaAM2aAZmaAmWaAzGaA
/2aqAGaqM2aqZmaqmWaqzGaq/2bVAGbVM2bVZmbVmWbVzGbV/2b/AGb/M2b/Zmb/
mWb/zGb//5kAAJkAM5kAZpkAmZkAzJkA/5krAJkrM5krZpkrmZkrzJkr/5lVAJlV
M5lVZplVmZlVzJlV/5mAAJmAM5mAZpmAmZmAzJmA/5mqAJmqM5mqZpmqmZmqzJmq
/5nVAJnVM5nVZpnVmZnVzJnV/5n/AJn/M5n/Zpn/mZn/zJn//8wAAMwAM8wAZswA
mcwAzMwA/8wrAMwrM8wrZswrmcwrzMwr/8xVAMxVM8xVZsxVmcxVzMxV/8yAAMyA
M8yAZsyAmcyAzMyA/8yqAMyqM8yqZsyqmcyqzMyq/8zVAMzVM8zVZszVmczVzMzV
/8z/AMz/M8z/Zsz/mcz/zMz///8AAP8AM/8AZv8Amf8AzP8A//8rAP8rM/8rZv8r
mf8rzP8r//9VAP9VM/9VZv9Vmf9VzP9V//+AAP+AM/+AZv+Amf+AzP+A//+qAP+q
M/+qZv+qmf+qzP+q///VAP/VM//VZv/Vmf/VzP/V////AP//M///Zv//mf//zP//
/wAAAAAAAAAAAAAAACH5BAEAAPwALAAAAAAQABAAAAh7APcJHEiwoMGDCAXWU8aQ
mDKCDRlmYsgQorKF0JQR26exYSaBkz46pHgRWr2BysQIZLhwJUeN+9A85Diyo0Zo
+8TcQJmxIs6VAD4qdLhv4UmBYmIUJFlxHzEAGwlunDkwxs6lTqPuywRgJlVlPwfq
LBq26MOeMRNSTRgQADs=
-----END CERTIFICATE-----

という中身のファイルができます。これの文字の羅列の部分をとってきて、コードに埋め込みます。

base64.py
import tkinter as tk 
root = tk.Tk() 
data = '''R0lGODlhEAAQAPcAAAAAAAAAMwAAZgAAmQAAzAAA/wArAAArMwArZgArmQArzAAr
        /wBVAABVMwBVZgBVmQBVzABV/wCAAACAMwCAZgCAmQCAzACA/wCqAACqMwCqZgCq
        mQCqzACq/wDVAADVMwDVZgDVmQDVzADV/wD/AAD/MwD/ZgD/mQD/zAD//zMAADMA
        MzMAZjMAmTMAzDMA/zMrADMrMzMrZjMrmTMrzDMr/zNVADNVMzNVZjNVmTNVzDNV
        /zOAADOAMzOAZjOAmTOAzDOA/zOqADOqMzOqZjOqmTOqzDOq/zPVADPVMzPVZjPV
        mTPVzDPV/zP/ADP/MzP/ZjP/mTP/zDP//2YAAGYAM2YAZmYAmWYAzGYA/2YrAGYr
        M2YrZmYrmWYrzGYr/2ZVAGZVM2ZVZmZVmWZVzGZV/2aAAGaAM2aAZmaAmWaAzGaA
        /2aqAGaqM2aqZmaqmWaqzGaq/2bVAGbVM2bVZmbVmWbVzGbV/2b/AGb/M2b/Zmb/
        mWb/zGb//5kAAJkAM5kAZpkAmZkAzJkA/5krAJkrM5krZpkrmZkrzJkr/5lVAJlV
        M5lVZplVmZlVzJlV/5mAAJmAM5mAZpmAmZmAzJmA/5mqAJmqM5mqZpmqmZmqzJmq
        /5nVAJnVM5nVZpnVmZnVzJnV/5n/AJn/M5n/Zpn/mZn/zJn//8wAAMwAM8wAZswA
        mcwAzMwA/8wrAMwrM8wrZswrmcwrzMwr/8xVAMxVM8xVZsxVmcxVzMxV/8yAAMyA
        M8yAZsyAmcyAzMyA/8yqAMyqM8yqZsyqmcyqzMyq/8zVAMzVM8zVZszVmczVzMzV
        /8z/AMz/M8z/Zsz/mcz/zMz///8AAP8AM/8AZv8Amf8AzP8A//8rAP8rM/8rZv8r
        mf8rzP8r//9VAP9VM/9VZv9Vmf9VzP9V//+AAP+AM/+AZv+Amf+AzP+A//+qAP+q
        M/+qZv+qmf+qzP+q///VAP/VM//VZv/Vmf/VzP/V////AP//M///Zv//mf//zP//
        /wAAAAAAAAAAAAAAACH5BAEAAPwALAAAAAAQABAAAAh7APcJHEiwoMGDCAXWU8aQ
        mDKCDRlmYsgQorKF0JQR26exYSaBkz46pHgRWr2BysQIZLhwJUeN+9A85Diyo0Zo
        +8TcQJmxIs6VAD4qdLhv4UmBYmIUJFlxHzEAGwlunDkwxs6lTqPuywRgJlVlPwfq
        LBq26MOeMRNSTRgQADs=
     ''' 
img = tk.PhotoImage(data=data) 
label = tk.Label(image=img) 
label.pack() 
root.mainloop()

とすると、
base64.png
 ↑のようなコード埋め込み画像を表示することができます。

タイトル横のアイコンをコードに埋め込む方法

ここまで来れば、あと少しです。今埋め込んだ画像をタイトル横に表示してあげればいいのです。ここで

title.py
root.tk.call('wm', 'iconphoto', root._w, tk.PhotoImage(data=data))

をつかって、アイコンを表示します。

title.py
import tkinter as tk 
root = tk.Tk() 
data = '''R0lGODlhEAAQAPcAAAAAAAAAMwAAZgAAmQAAzAAA/wArAAArMwArZgArmQArzAAr
        /wBVAABVMwBVZgBVmQBVzABV/wCAAACAMwCAZgCAmQCAzACA/wCqAACqMwCqZgCq
        mQCqzACq/wDVAADVMwDVZgDVmQDVzADV/wD/AAD/MwD/ZgD/mQD/zAD//zMAADMA
        MzMAZjMAmTMAzDMA/zMrADMrMzMrZjMrmTMrzDMr/zNVADNVMzNVZjNVmTNVzDNV
        /zOAADOAMzOAZjOAmTOAzDOA/zOqADOqMzOqZjOqmTOqzDOq/zPVADPVMzPVZjPV
        mTPVzDPV/zP/ADP/MzP/ZjP/mTP/zDP//2YAAGYAM2YAZmYAmWYAzGYA/2YrAGYr
        M2YrZmYrmWYrzGYr/2ZVAGZVM2ZVZmZVmWZVzGZV/2aAAGaAM2aAZmaAmWaAzGaA
        /2aqAGaqM2aqZmaqmWaqzGaq/2bVAGbVM2bVZmbVmWbVzGbV/2b/AGb/M2b/Zmb/
        mWb/zGb//5kAAJkAM5kAZpkAmZkAzJkA/5krAJkrM5krZpkrmZkrzJkr/5lVAJlV
        M5lVZplVmZlVzJlV/5mAAJmAM5mAZpmAmZmAzJmA/5mqAJmqM5mqZpmqmZmqzJmq
        /5nVAJnVM5nVZpnVmZnVzJnV/5n/AJn/M5n/Zpn/mZn/zJn//8wAAMwAM8wAZswA
        mcwAzMwA/8wrAMwrM8wrZswrmcwrzMwr/8xVAMxVM8xVZsxVmcxVzMxV/8yAAMyA
        M8yAZsyAmcyAzMyA/8yqAMyqM8yqZsyqmcyqzMyq/8zVAMzVM8zVZszVmczVzMzV
        /8z/AMz/M8z/Zsz/mcz/zMz///8AAP8AM/8AZv8Amf8AzP8A//8rAP8rM/8rZv8r
        mf8rzP8r//9VAP9VM/9VZv9Vmf9VzP9V//+AAP+AM/+AZv+Amf+AzP+A//+qAP+q
        M/+qZv+qmf+qzP+q///VAP/VM//VZv/Vmf/VzP/V////AP//M///Zv//mf//zP//
        /wAAAAAAAAAAAAAAACH5BAEAAPwALAAAAAAQABAAAAh7APcJHEiwoMGDCAXWU8aQ
        mDKCDRlmYsgQorKF0JQR26exYSaBkz46pHgRWr2BysQIZLhwJUeN+9A85Diyo0Zo
        +8TcQJmxIs6VAD4qdLhv4UmBYmIUJFlxHzEAGwlunDkwxs6lTqPuywRgJlVlPwfq
        LBq26MOeMRNSTRgQADs=
     ''' 
root.tk.call('wm', 'iconphoto', root._w, tk.PhotoImage(data=data))
root.mainloop()

実行すると、
title.png

という風になります。
これでpyinstallerでインストールしても、icon.icoを置く必要もなく一つのファイルで起動することができます。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

RaspberryPiで監視カメラ 暗い部屋の撮影編

はじめに

前回、RaspberryPiで監視カメラを作成したのですが、暗い部屋でベビーモニタとして利用するには工夫が必要という状況になっていました。

暗い部屋で使うために

  • USBライトの制御
  • カメラの設定変更

について調査してみたので、まとめておきます。

USBライトの制御

ハードウェア

Can☆Doで買ったUSBライトを使用します。

usb.jpg

RaspberryPiに差してもlsusbで認識されないので、クラスとして認識するような仕組みはなくUSBコネクタから電力供給しているだけの動作のようです。

たぶん、どのメーカのUSBライトも同じ仕様だと思います。
Linux側からUSBポートの電源供給を制御する方法を探してみます。

hub-ctrl

ぐぐったら、hub-ctrlというソフトを使うことでUSBの制御ができるとのこと。

手順通りにgccでビルドしてインストールできました。

gcc -o hub-ctrl hub-ctrl.c -lusb

以下のコマンドで点灯、消灯ができました。

sudo hub-ctrl -h 0 -P 2 -p 0
sudo hub-ctrl -h 0 -P 2 -p 1

権限設定等

pythonからhub-ctrlを起動する場合にsudoのパスワード入力が問題になるので回避策を探します。

最初はsudoしなくてもUSB操作ができるグループを追加すればいけると思いましたが、そういうグループは無いんですね。
グループを追加はあきらめてsudoのパスワード要求をしないように設定を変更します。

sudo visudo

/usr/local/bin/hub-ctrl にコピーしたのでこれにNOPASSWDを指定した行を追加します

monitor_user    ALL=NOPASSWD: /usr/local/bin/hub-ctrl

これでsudoでのパスワード要求されなくなりますので、pythonからの呼び出しに支障はなくなりました。

クライアント側

HTML

点灯制御するトリガーのボタンを用意します。

usb.html
<html>
  <head>
    <script src="./usb.js"></script>
  </head>
  <body onload="on_load();">
    <input type="button" value="usb" onclick="on_button_light();">
    <div>
      <canvas id="canvas_image" width="640" height="480"></canvas>
    </div>
  </body>
</html>


javascript

ボタンのハンドラでUSBライトの点灯制御を要求するコマンドを送信します。

usb.js
var image_socket = null;

var usb_socket = null;
var mode_usb = true;


function on_load()
{
  // 画像通信用WebSocket接続
  img_url = "ws://" + location.hostname + ":60002"
  image_socket = new WebSocket(img_url);
  image_socket.binaryType = 'arraybuffer';
  image_socket.onmessage = on_image_message;

  usb_url = "ws://" + location.hostname + ":60004"
  usb_socket = new WebSocket(usb_url);
  usb_socket.binaryType = 'arraybuffer';
}

function on_button_light()
{
  mode_usb = mode_usb ? false : true;

  var command_data = { mode: mode_usb };
  usb_socket.send(JSON.stringify(command_data));
}


function on_image_message(recv_data)
{
  // 受信したデータをbase64文字列に変換
  var recv_image_data = new Uint8Array(recv_data.data);
  var base64_data = ""
  for (var i=0; i < recv_image_data.length; i++) {
    base64_data += String.fromCharCode(recv_image_data[i]);
  }

  // 画像をcanvasに描画
  var canvas_image = document.getElementById('canvas_image');
  var ctx = canvas_image.getContext('2d');
  var image = new Image();
  image.onload = function() {
    ctx.drawImage(image, 0, 0);
  }
  image.src = 'data:image/jpeg;base64,' + window.btoa(base64_data);
}


サーバー側

USBの電源制御するWebSocketのサーバーを用意します。

USB制御サーバー

クライアント側で送信したON/OFFの要求を受けてhub-ctrlを呼び出します。

UsbServer.py
import asyncio
import websockets
import json
import subprocess


class UsbServer:
    def __init__(self, loop, address, port):
        self.loop = loop
        self.address = address
        self.port = port


    async def _handler(self, websocket, path):
        while True:
            try:
                recv_data = await websocket.recv()
                dic_data = json.loads(recv_data)
            except:
                print('usb recv Error.')
                break

            if dic_data['mode']:
                command = ['sudo', 'hub-ctrl', '-h', '0', '-P', '2', '-p', '0']
            else:
                command = ['sudo', 'hub-ctrl', '-h', '0', '-P', '2', '-p', '1']

            print(command)
            try:
                process = subprocess.Popen(command, stdout=subprocess.PIPE)
                output = process.stdout.read().decode('utf-8')
                print(output)

            except:
                print('usb proc Error.')

    def run(self):
        self._server = websockets.serve(self._handler, self.address, self.port)
        self.loop.run_until_complete(self._server)
        self.loop.run_forever()

if __name__ == '__main__':
    loop = asyncio.get_event_loop()
    wss = UsbServer(loop, '0.0.0.0', 60004)
    wss.run()

USBの親HUBごと電源をOFFにするのでUSB AudioもOFFになるのは問題ですが、電源制御対応の子USB HUBを挟めば対応できるそうです。(手持ちのHUBでは動作確認できませんでした)

カメラの設定変更

USBライトの明かりだけはうまく撮影できなかったので、カメラ側の設定もいじることにします。

PiCameraで変更できるプロパティで明るさを制御します。

明るく撮影する設定

前回作成したImageサーバーにフレームレート、シャッタースピード、露出補正の設定を追加します。

公式の説明を読んでいろいろ設定してみましたが、バージョンで動作が違うので正しい設定がよくわかりませんでした。ネットの情報もばらばらで正解がよくわからないです。試行錯誤してうまくいった設定値にしています。

ImageServer.py
import asyncio
import websockets
import io
from picamera import PiCamera

class ImageServer:

    def __init__(self, loop, address , port):
        self.loop = loop
        self.address = address
        self.port = port
        self.camera = PiCamera()
        self.camera.framerate = 1
        self.camera.exposure_compensation = 10
        self.camera.shutter_speed = 1000 * 8000

    async def _handler(self, websocket, path):
        with io.BytesIO() as stream:
            for _ in self.camera.capture_continuous(stream, format='jpeg', use_video_port=True, resize=(640,480)):
                stream.seek(0)

                try:
                    await websocket.send(stream.read())
                except:
                    print('image send Error.')
                    break

                stream.seek(0)
                stream.truncate()

    def run(self):
        self._server = websockets.serve(self._handler, self.address, self.port)
        self.loop.run_until_complete(self._server)
        self.loop.run_forever()

if __name__ == '__main__':
    loop = asyncio.get_event_loop()
    ws_is = ImageServer(loop, '0.0.0.0', 60002)
    ws_is.run()


動作確認

シャッタースピードが遅いのでライトのON/OFFしてから少し待たないと更新されないですが、それなりに撮影できています。

anime_usb.gif

おわりに

USBの電源制御とシャッタースピードの制御でなんとか実用レベルになりました。

omake.jpg

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【Python Pandas メモ】

始めに

Python DataFrameに読み込んだ日付型の項目で絞り込みをする方法に手間取ったので、その時のメモ。

ポイント1

日付型データは pd.to_datetime関数を使って列ごと型変換する。

from ibmdbpy import IdaDataBase, IdaDataFrame
idadb = ... # (省略)

df_result = IdaDataFrame(idadb, 'TBL_RESULT').as_dataframe()
df_result['CREATE_DATE'] = pd.to_datetime(df_result['CREATE_DATE'])

ポイント2

検索時は検索条件側も datetime.date関数で変換した値を使う。

import datetime

w1= df_plan.query("EMPLOYEE_CD=='1234' and CREATE_DATE==datetime.date(2019,1,10)")
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【Python Pandas メモ】DataFrameで日付型項目で検索する方法

始めに

Python DataFrameに読み込んだ日付型の項目で絞り込みをする方法に手間取ったので、その時のメモ。

ポイント1

日付型データは pd.to_datetime関数を使って列ごと型変換する。

from ibmdbpy import IdaDataBase, IdaDataFrame
idadb = ... # (省略)

df_plan = IdaDataFrame(idadb, 'TBL_RESULT').as_dataframe()
df_plan['CREATE_DATE'] = pd.to_datetime(df_plan['CREATE_DATE'])

ポイント2

検索時は検索条件側も datetime.date関数で変換した値を使う。

import datetime

w1= df_plan.query("EMPLOYEE_CD=='1234' and CREATE_DATE==datetime.date(2019,1,10)")
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

シャニマスの全R~SSR画像を保存するスクリプト

アイドルマスター シャイニーカラーズ、絵が良い。そんなシャイニーカラーズ(以下、シャニマス)の画像を全部保存したくありませんか?

ただ、シャニマスのキャラって今(2019/10/01現在)252種類あるわけで...一つ一つページ探して保存するのはめんどくさいんですよね...

そこで!!スクレイピングを使って全部勝手に保存してもらおう!というわけです。

注意!!

https://topcourt-law.com/internet_security/scraping-illegal

上を読んでもらった方が早いんですけど著作権法などの絡みがあるっていうのとサーバーに負担をかけてしまう行為なので慎重に行ってください。

使用環境

Windows 10 Education
Python 3.7.3

ソースコード

shiny_scraping.py
import requests 
from bs4 import BeautifulSoup as bs
import os
import urllib.request
import urllib.error
j = 0

for num in range(1, 253):
    URL = 'https://imas-shinycolors.boom-app.wiki/entry/card-' + str(num) # URL入力
    images = [] # 画像リストの配列
    soup = bs(requests.get(URL).content,'lxml') # bsでURL内を解析
    for link in soup.find_all("img"): # imgタグを取得しlinkに格納
        s = link.get("src")
        if s.endswith("w=960") or s.endswith("w=600"):
            images.append(link.get("src"))

    for img in images:
        j += 1
        try:
            with urllib.request.urlopen(img) as w:
                data = w.read()
                with open('C:/Users/[ユーザー名]/Pictures/ShinyColors/' + str(j) + '.jpg', mode='wb') as m:
                    m.write(data)
        except urllib.error.URLError as e:
            print(e) #保存できなかった時のため

    print("No." + str(num) + " is ok") # 確認

解説

前半部分


for num in range(1, 253):
    URL = 'https://imas-shinycolors.boom-app.wiki/entry/card-' + str(num) # URL入力
    images = [] # 画像リストの配列
    soup = bs(requests.get(URL).content,'lxml') # bsでURL内を解析
    for link in soup.find_all("img"): # imgタグを取得しlinkに格納
        s = link.get("src")
        if s.endswith("w=960") or s.endswith("w=600"):
            images.append(link.get("src"))

1行目...今回はシャニマス攻略wikiを使って抜き出していこうと思います。252種類あるのでrangeを1~253にして(Pythonを使ったことある人は分かると思いますが、範囲指定は必要な数+1します)、
2行目...URLに番号を含めたものを指定します。
3行目...画像urlを格納するリストを初期化します
4行目...2行目で定義したURLの中身を取得します。この時の内容はlxml形式で取得されます。

基本的にスクレイピングは、ページを構成しているhtmlから必要な情報をタグやid、クラスなどを指定することで取得する感じになります。例えば、【ほわっとスマイル】櫻木真乃の場合だと、

URL: https://imas-shinycolors.boom-app.wiki/entry/card-1 

の中は以下の通りになっています。

card-1
<!DOCTYPE html>
<html lang="ja" xmlns:og="http://ogp.me/ns#">
<head>
<meta charset="UTF-8">
(略)

    <div class="breadcrumbs">
        <ul>
            <li><a href="/"><i class="fa fa-home" aria-hidden="true"></i> シャニマス攻略Wiki</a></li>
            <li><a href="https://imas-shinycolors.boom-app.wiki/entry/card-list">全カード一覧</a></li>            <li>SSR【ほわっとスマイル】櫻木真乃のスキルとステータス</li>

(略)

<div class="imgList1">

<div class="ss"><div class="image-zoom" data-url="https://image.boom-app.wiki/wiki/5a824ae1b1b4b803847278d1/35a59c77b6e647b3c56a66662fa46329.jpg?w=600">
<img src="https://image.boom-app.wiki/wiki/5a824ae1b1b4b803847278d1/35a59c77b6e647b3c56a66662fa46329.jpg?w=600" alt="">
</div>
</div>
</div>

(略)

<h3>フェスアイドルイラスト</h3>
<div class="imgList1">
<div class="ss"><div class="image-zoom" data-url="https://image.boom-app.wiki/wiki/5a824ae1b1b4b803847278d1/card/fes/001.jpg?w=960">
<img src="https://image.boom-app.wiki/wiki/5a824ae1b1b4b803847278d1/card/fes/001.jpg?w=960" alt="【ほわっとスマイル】櫻木真乃">
</div>
</div>
</div>


<a name="toc2" id="toc2"></a>
<h2>SSR【ほわっとスマイル】櫻木真乃の演出まとめ</h2>


(略)

</body>
</html>

今回取得したい画像(通常絵、フェスアピール時絵)は、いずれも.jpgの後に?w=600 or 960がついているのがわかると思います、これによって画像の幅を指定しているのですが、今回はこれを逆手にとってこの2種類の画像のみを取得するようにします。

5行目から...4行目で取得したもののなかからimgタグの文のみを指定し、その中でも上記の通り、?w=960?w=600で終わっているものを指定し、取得します。(これがないと、結構色々な画像が入ってしまうので選別が大変なことになります。)取得したいもの(リンク形式になっている)をimagesリストに格納します。

後半部分

    for img in images:
        j += 1
        try:
            with urllib.request.urlopen(img) as w:
                data = w.read()
                with open('C:/Users/[ユーザー名]/Pictures/ShinyColors/' + str(j) + '.jpg', mode='wb') as m:
                    m.write(data)
        except urllib.error.URLError as e:
            print(e) #保存できなかった時のため
    print("No." + str(num) + " is ok") # 確認

1~2行目...1imagesリストのなかから1枚ずつ取り出します。今回は保存するときの画像名を「(番号).jpg」にしたかったので上でjという引数を用意し、1枚画像を保存するごとに1ずつ加えています。
3~7行目...1枚ずつリンクの画像を読み込んで取得したものをopenの第一引数で指定したフォルダに保存しています。
8~9行目...取得できなかったらどういうエラーが起こったかをターミナルに出力します
10行目...コンソールに進捗を書き出しています。

実行結果

a.png
実行すると1つずつこんな感じで実行されます。だいたい1つ実行するのに0.5~1秒かかるので3分ほどで終わると思います。

image.png

この通り、見事に保存されていることがわかります。是非法の範囲で使って見てください~

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

ssl.get_server_certificateで証明書を取得できない場合の対処

概要

Python でSSL証明書の情報を確認したい場合、以下のようなコードが紹介されていることが多い。

import ssl
import OpenSSL # required pyopenssl

cert = ssl.get_server_certificate(('www.google.com', 443))
x509 = OpenSSL.crypto.load_certificate(OpenSSL.crypto.FILETYPE_PEM, cert)

試してみると、多くの場合はこの方法で正常に情報が取得できたが、正常に情報が取得できないケースがあったため、調査を実施した。

実際のところ invoke (Fabric 2) でローカルの openssl コマンド叩けばうまくいったけれど、何故 Python でやるとうまくいかなかったのかが疑問だったので。

原因

Server Name Indication(SNI) の指定がないため。
openssl s_client コマンドで言えば -servername が未指定のため。

IPアドレス直打ちの場合などにアクセスさせないためや、1つのサーバー上で複数のドメイン管理をしている場合などにSNI設定なしでアクセスすると別設定の証明書を返してきたり、そもそも返さなかったりする。

わざと openssl コマンドで -servername example.com など全く別のサーバーネームを与えた場合に同様の事象が再現した。

対処

原因がSNIと分かれば同じような質問があった。 結果としては、ssl.get_server_certificate は使わず、その中身と同じようなことを自分で書いてやる必要がある。

import socket
import ssl
import OpenSSL

def get_server_certificate(hostname):
    context = ssl.create_default_context()
    with socket.create_connection((hostname, 443)) as sock:
        with context.wrap_socket(sock, server_hostname=hostname) as sslsock:
            der_cert = sslsock.getpeercert(True)
            return ssl.DER_cert_to_PEM_cert(der_cert)  

cert = get_server_certificate('www.google.com')
x509 = OpenSSL.crypto.load_certificate(OpenSSL.crypto.FILETYPE_PEM, cert)
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Atcoderコンテスト用部品まとめ

この記事の目的

・Atcoderでのコンテスト本番中にあたふた慌てないように整理すること
・問題を解いてる途中に参照しやすい状態にすること
・アウトプット作業に慣れること
・Markdown記法になれること
・ページ内リンクがやりたかった(githubではうまくいかなかった)

随時更新予定

目次
1. 入力編
2. リスト編
3. 数値編
4. 書き方編
5. 出力編

1. 入力編

1.0 入力用

import sys
input = sys.stdin.readline

これでinput()だけより早くなるらしい

1.2 単入力

n=int(input())

1.3 複数入力

n,m=(int(x) for x in input().split())

1.4 リスト入力

l = [int(i) for i in input().split()]

2. リスト編

2.1 0リスト,空リスト

l=[0]*n
l=[]

2.2 リスト要素追加

l.append(n)

2.3 リスト重複なし

l=set(l)

2.4 リスト大きい順ソート

l.sort(reverse=True)

3. 数値編

3.1 素因数分解リスト列挙

def factorize(n):
    b = 2
    fct = []
    while b * b <= n:
        while n % b == 0:
            n //= b
            fct.append(b)
        b = b + 1
    if n > 1:
        fct.append(n)
    return fct

3.2 偶数True奇数False

def evenjudge(obj):
    if obj %2==0:
        return True
    else:
        return False

4. 書き方編

4.1 2重ループ

import itertools
for i,j in itertools.product(range(n), range(n)):
    pass

4.2 リスト内組み合わせ

import itertools
itertools.combinations(list,n)

5. 出力編

5.1 スペース間隔出力

print(s,end=' ')

大体ループの中に入れてる

github

https://github.com/kobayu0902art/AtCoder

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

AtCoderコンテスト用部品まとめ

この記事の目的

・Atcoderでのコンテスト本番中にあたふた慌てないように整理すること
・問題を解いてる途中に参照しやすい状態にすること
・アウトプット作業に慣れること
・Markdown記法になれること
・ページ内リンクがやりたかった(githubの.mdではうまくいかなかった)
→(2019/10/02時点)あれ、うまくリンクとんでない、、、

随時更新予定

目次
1. 入力編
2. リスト編
3. 数値編
4. 書き方編
5. 出力編

1. 入力編

1.0 入力用

import sys
input = sys.stdin.readline

これでinput()だけより早くなるらしい

1.2 単入力

n=int(input())

1.3 複数入力

n,m=(int(x) for x in input().split())

1.4 リスト入力

l = [int(i) for i in input().split()]

2. リスト編

2.1 空リスト,要素が0だけのリスト

l=[]
l=[0]*n

2.2 リストに要素追加

l.append(n)

2.3 重複なしのリスト

l=set(l)

2.4 リストを大きい順(降順)にソート

l.sort(reverse=True)

3. 数値編

3.1 素因数分解リスト列挙

def factorize(n):
    b = 2
    fct = []
    while b * b <= n:
        while n % b == 0:
            n //= b
            fct.append(b)
        b = b + 1
    if n > 1:
        fct.append(n)
    return fct

3.2 偶奇判断:偶数True奇数False

def evenjudge(obj):
    if obj %2==0:
        return True
    else:
        return False

4. 書き方編

4.1 2重ループを1行に

import itertools
for i,j in itertools.product(range(n), range(n)):
    pass

計算量は変わらないからAtcoder的にはいらない?

4.2 リスト内組み合わせ

import itertools
itertools.combinations(list,n)

計算量多めだから注意

5. 出力編

5.1 スペース間隔出力

print(s,end=' ')

大体ループの中に入れてる

github

https://github.com/kobayu0902art/AtCoder

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

AtCoderコンテスト用まとめ【python】

この記事の目的

・Atcoderでのコンテスト本番中にあたふた慌てないように整理すること
・問題を解いてる途中に参照しやすい状態にすること
・アウトプット作業に慣れること
・Markdown記法に慣れること
・ページ内リンクがやりたかった(githubの.mdではうまくいかなかった)
→(2019/10/02時点)あれ、うまくリンクとんでない、、、

随時更新予定

目次
1. 入力編
2. リスト編
3. 数値編
4. 書き方編
5. 出力編

1. 入力編

1.0 入力用

import sys
input = sys.stdin.readline

これでinput()だけより早くなるらしい

1.2 単入力

n=int(input())

1.3 複数入力

n,m=(int(x) for x in input().split())

1.4 リスト入力

l = [int(i) for i in input().split()]

2. リスト編

2.1 空リスト,要素が0だけのリスト

l=[]
l=[0]*n

2.2 リストに要素追加

l.append(n)

2.3 重複なしのリスト

l=set(l)

2.4 リストを大きい順(降順)にソート

l.sort(reverse=True)

3. 数値編

3.1 素因数分解リスト列挙

def factorize(n):
    b = 2
    fct = []
    while b * b <= n:
        while n % b == 0:
            n //= b
            fct.append(b)
        b = b + 1
    if n > 1:
        fct.append(n)
    return fct

3.2 偶奇判断:偶数True奇数False

def evenjudge(obj):
    if obj %2==0:
        return True
    else:
        return False

4. 書き方編

4.1 2重ループを1行に

import itertools
for i,j in itertools.product(range(n), range(n)):
    pass

計算量は変わらないからAtcoder的にはいらない?

4.2 リスト内組み合わせ

import itertools
itertools.combinations(list,n)

計算量多めだから注意

5. 出力編

5.1 スペース間隔出力

print(s,end=' ')

大体ループの中に入れてる

ToDo

itertools編とか増やしていきたい

github

https://github.com/kobayu0902art/AtCoder

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

AtCoderコンテスト用まとめ【Python】

AtCoder

https://atcoder.jp/

この記事の目的

  • Atcoderでのコンテスト本番中にあたふた慌てないように整理すること
  • 問題を解いてる途中に参照しやすい状態にすること
  • アウトプット作業に慣れること
  • Markdown記法に慣れること
  • ページ内リンクがやりたかった(githubの.mdではうまくいかなかった)
    →(2019/10/02時点)あれ、うまくリンクとんでない、、、どうして、、、(勉強中)

随時更新予定

目次
0. フォーマット編
1. 入力編
2. リスト編
3. 数値編
4. 書き方編
5. 出力編

0. フォーマット編

contest_base.py
import sys
import itertools
input = sys.stdin.readline

#単入力
n=int(input())

#複数入力
n,m=(int(x) for x in input().split())

#リスト入力
l = [int(i) for i in input().split()]

基本的にこれをファイルコピーして使いまわしてます

1. 入力編

1.0 入力用

import sys
input = sys.stdin.readline

これでinput()だけより早くなるらしい

1.1 単入力

n=int(input())

mono.PNG
こういうときの入力

1.2 複数入力

n,m=(int(x) for x in input().split())

multi.PNG
N Kのようなときの入力

1.3 リスト入力

l = [int(i) for i in input().split()]

multi.PNG
h1 h2 ... hN のようなときに使う

j1
j2
.
.
.
jN
のようにタテに並んでいるときは

j=[]
for _ in range(n):
    j.append(input())

2. リスト編

2.1 空リスト,要素が0だけのリスト

l=[]
l=[0]*n

2.2 リストに要素追加

l.append(n)

2.3 重複なしのリスト

l=set(l)

2.4 リストを大きい順(降順)にソート

l.sort(reverse=True)

2.5 優先度付きキュー

例として、【「リストaの最大値を1/2してリストaに戻す」をm回繰り返す】

import heapq
a = list(map(lambda x: int(x)*(-1), input().split()))
heapq.heapify(a)
for _ in range(m):
    temp = heapq.heappop(a)
    heapq.heappush(a, (-1)*(-temp//2))

優先度付きキューを使わない場合:

a = [int(i) for i in input().split()]
a.sort(reverse=True)
for _ in range(m):
    a[0]=a[0]//2
    a.sort(reverse=True)

この場合いちいち全要素をソートしなおすので計算量が膨大になってTLEとなる

3. 数値編

3.1 素因数分解リスト列挙

def factorize(n):
    b = 2
    fct = []
    while b * b <= n:
        while n % b == 0:
            n //= b
            fct.append(b)
        b = b + 1
    if n > 1:
        fct.append(n)
    return fct

3.2 偶奇判断:偶数True奇数False

n%2==0

3.3 最大公約数,最小公倍数

nとmの最大公約数ansを求める

import math
ans=math.gcd(n,m)

最小公倍数はgcdを用いて

def lcm(n,m):
    return (n*m)//math.gcd(n,m)

3.4 約数列挙,公約数列挙

def make_divisors(n):
    divisors = []
    for i in range(1, int(n**0.5)+1):
        if n % i == 0:
            divisors.append(i)
            if i != n // i:
                divisors.append(n//i)
    divisors.sort()
    return divisors

公約数列挙はmake_divisorsを用いて、

ansdiv=set(make_divisors(n))&set(make_divisors(m))

4. 書き方編

4.1 2重ループを1行に

import itertools
for i,j in itertools.product(range(n), range(n)):
    pass

計算量は変わらないから競プロ的にはいらない?

4.2 リスト内組み合わせ

import itertools
itertools.combinations(list,n)

計算量多めだから注意

5. 出力編

5.1 スペース区切り出力

print(s,end=' ')

大体ループの中に入れてる

ToDo

  • もっとバリエーション増やす
  • itertools編とか増やしていきたい

github

https://github.com/kobayu0902art/AtCoder

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

同Directory配下のClassのimportの注意点

概要

久しぶりにPythonでハマったので、備忘録的なあれ。
PythonにてあるProductを作成している際に、Class間で相互にimportする必要が出てきたが、、、、できない!
というか、すべきではない

対策

互いにimportしたいClassがあるのであれば、同じファイルに記載しましょう

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Pythonでbool型のオプション引数を与えたらハマった話

やりたいこと

実行時のオプション引数でTrue/Falseを設定したい。

$ py test.py --hogeFlag False
False

みたいな感じでオプション引数からbool値を取って内部で処理を分けたいと思ってました。

コード

最初はこんな感じのコードでオプション引数を取得していました。
bool以外のオプション引数を使ったスクリプトを組んだことがあったのでその応用でいけるやろと思ってました。

def get_args():
  parser = argparse.ArgumentParser()
  parser.add_argument("--hogeFlag", type=bool, default=True)

  return parser.parse_args()

結果

py test.py --hogeFlag False
True

:rolling_eyes::question:

Falseを指定したつもりがTrueになってますね・・・

原因

bool() でbool値に変換されているのが原因だった。
type=bool と指定しているとPythonは、設定された引数を bool() に入れてbool型にしてくれるようです。
type=int とかにすると int() を通してint型にしてくれていたおかげで文字列が入るとエラーが起こっていたんですね。
確かにどの言語でも、コマンドライン引数も最初はstring型として取得されてましたね。

今回のケースでは下記のコマンドで動かしていました。

$ py test.py --hogeFlag False

type=bool なので、この場合は bool('False') という処理を動かしてbool値に変換しようとします。
この bool('False') はどういう結果になるのか。サクッとPythonWinで動かしてみます

>>> print(bool('False'))
True

Trueになってるやん・・・

理由

pythonの真偽値でFalseと判定されるのは次のものらしいです(公式ドキュメントより抜粋

偽であると定義されている定数: None と False
数値型におけるゼロ: 0, 0.0, 0j, Decimal(0), Fraction(0, 1)
空のシーケンスまたはコレクション: '', (), [], {}, set(), range(0)

コマンドライン引数やオプション引数は、取得時にはstr型として扱われます。
なので3つ目の空のシーケンスまたはコレクションの部分に判定されます。
'' のような空文字だけがFalseと判定され、それ以外はTrueと判定されるようです。
bool('False') がTrueと判定される理由がわかりました。

対応

add_argument関数の引数には type の他に action というのもありました。
今回のケースではこちらを使用します。 参考

コード

parser.add_argument("--hogeFlag", action='store_true')

action='store_true' というのが新しいところですね。
動きとしてはこうなります

$ py test.py --hogeFlag
True

$ py test.py
False

--hogeFlag を指定した時だけ Trueで、指定しない時は False になりました!

余談ですが真逆の store_false というのも設定することができます。動きは store_true と反対なだけなのでここでは省きます。

まとめ

オプション引数でbool値を与えたいときは type=bool ではなく action='store_true(またはfalse)' で設定する!

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Pythonのargparseでbool型のオプション引数を与えたらハマった話

やりたいこと

実行時のオプション引数でTrue/Falseを設定したい。

$ py test.py --hogeFlag False
False

みたいな感じでオプション引数からbool値を取って内部で処理を分けたいと思ってました。

コード

最初はこんな感じのコードでオプション引数を取得していました。
bool以外のオプション引数を使ったスクリプトを組んだことがあったのでその応用でいけるやろと思ってました。

def get_args():
  parser = argparse.ArgumentParser()
  parser.add_argument("--hogeFlag", type=bool, default=True)

  return parser.parse_args()

結果

py test.py --hogeFlag False
True

:rolling_eyes::question:

Falseを指定したつもりがTrueになってますね・・・

原因

bool() でbool値に変換されているのが原因だった。
type=bool と指定しているとPythonは、設定された引数を bool() に入れてbool型にしてくれるようです。
type=int とかにすると int() を通してint型にしてくれていたおかげで文字列が入るとエラーが起こっていたんですね。
確かにどの言語でも、コマンドライン引数も最初はstring型として取得されてましたね。

今回のケースでは下記のコマンドで動かしていました。

$ py test.py --hogeFlag False

type=bool なので、この場合は bool('False') という処理を動かしてbool値に変換しようとします。
この bool('False') はどういう結果になるのか。サクッとPythonWinで動かしてみます

>>> print(bool('False'))
True

Trueになってるやん・・・

理由

pythonの真偽値でFalseと判定されるのは次のものらしいです(公式ドキュメントより抜粋

偽であると定義されている定数: None と False
数値型におけるゼロ: 0, 0.0, 0j, Decimal(0), Fraction(0, 1)
空のシーケンスまたはコレクション: '', (), [], {}, set(), range(0)

コマンドライン引数やオプション引数は、取得時にはstr型として扱われます。
なので3つ目の空のシーケンスまたはコレクションの部分に判定されます。
'' のような空文字だけがFalseと判定され、それ以外はTrueと判定されるようです。
bool('False') がTrueと判定される理由がわかりました。

対応

add_argument関数の引数には type の他に action というのもありました。
今回のケースではこちらを使用します。 参考

コード

parser.add_argument("--hogeFlag", action='store_true')

action='store_true' というのが新しいところですね。
動きとしてはこうなります

$ py test.py --hogeFlag
True

$ py test.py
False

--hogeFlag を指定した時だけ Trueで、指定しない時は False になりました!

余談ですが真逆の store_false というのも設定することができます。動きは store_true と反対なだけなのでここでは省きます。

まとめ

オプション引数でbool値を与えたいときは type=bool ではなく action='store_true(またはfalse)' で設定する!

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Jinja2でincludeしたファイルに変数を渡す方法

はじめに

ちょっとした社内・部内向けのツールを作るのに、Python + bottle + jinja2 + SQLiteを使ってる。jinja2でincludeしたファイルに変数、パラメータを渡す方法がわからなかったのであれこれ調べたのでメモ。

やりかた

sample.py
{% with 変数名 = 渡す変数名 %}
{% include 'インクルードするファイル' %}
{% endwith %}

というように、{% with %} ~ {% endwith %} で囲んで、withのパラメータに渡したい変数を書く。

template.html
{% with name = name %} 
{% include 'include.html' %}
{% endwith %}
include.html
名前:{{ name }}

複数の変数を渡す場合

template.html
{% with name = name, address = address %} 
{% include 'include.html' %}
{% endwith %}
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

「にじさんじ」で学ぶNeo4j

解説動画リンク

この記事を解説したものです。
解説動画を作成後リンクを貼ります。

はじめに

研究でNeo4jを扱う必要が出てきました。
これは問題です。

Neo4jの使い方を僕は知らないのです。

しかし、インターネットの記事は「Kevin」を「Chris」と繋いでどうこうしていて、これはやりたくありません。

そこで、自分の好きなコンテンツである、にじさんじの関係性をNeo4jを使って作りながら学んでいこうと思います。

Dex1OROV4AY6QaA.jpg

卯月、愛してるぞ。

この記事で扱う内容

この記事で扱う内容は以下のとおりです。

  • Neo4jのグラフ・エッジの概念
  • 基本操作
  • PythonDriver
  • Pythonによるスクレイピング
  • スクレイピング + Neo4jPythonDriverによるにじさんじネットワークの構築
  • 以上の記事内容を解説・実践する動画(Youtubeへのリンク)

使用OS・バージョン

使用OSはMacです。
また、Neo4j Desktopのバージョンは「Neo4j Desktop v1.2.1」
Neo4j自体のバージョンは3.5.9を使用しています。

GraphDB・Neo4jとは

GraphDB・Neo4jの特徴はグラフデータベースNeo4jハンズオン(インストールからプログラミングまで)で非常に分かりやすく解説されています。

Neo4jのインストール

Neo4jのインストールはNeo4j 入門 #1 (インストールおよび起動)のGUIのインストールを参考にしてください。
非常にわかりやすく画像もいっぱいで嬉しいです。

グラフ・エッジの概念

Now Writing...

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

pymatgenで結晶構造情報フォーマットcifを扱う

Cifについて

Cif:Crystallographic Information Fileは
結晶学情報共通データ・フォーマット:結晶構造についての情報を詰め込んだ、国際結晶学連盟が推奨しているフォーマットです。
詳細はここ表面分析研究会 無料記事vol.21にかなり詳しい記載があります。

例えば、NaCl(塩化ナトリウム)のCifはこんな感じです。

# generated using pymatgen
data_NaCl
_symmetry_space_group_name_H-M   'P 1'
_cell_length_a   3.50218997
_cell_length_b   3.50218997
_cell_length_c   3.50218997
_cell_angle_alpha   90.00000000
_cell_angle_beta   90.00000000
_cell_angle_gamma   90.00000000
_symmetry_Int_Tables_number   1
_chemical_formula_structural   NaCl
_chemical_formula_sum   'Na1 Cl1'
_cell_volume   42.95553177
_cell_formula_units_Z   1
loop_
 _symmetry_equiv_pos_site_id
 _symmetry_equiv_pos_as_xyz
  1  'x, y, z'
loop_
 _atom_site_type_symbol
 _atom_site_label
 _atom_site_symmetry_multiplicity
 _atom_site_fract_x
 _atom_site_fract_y
 _atom_site_fract_z
 _atom_site_occupancy
  Na  Na0  1  0.000000  0.000000  0.000000  1
  Cl  Cl1  1  0.500000  0.500000  0.500000  1


このままではただの文字列で分かりにくいので、pymatgenで読み込み数値データを利用しやすくします。

まずはMaterial Projectから上記の塩化ナトリウムのCifを取得します。

#material projectから塩化ナトリウムのデータを取得する
import pandas as pd
from pymatgen.ext.matproj import MPRester

API_KEY = 'Your API' # Materials Project の API キー

with MPRester(API_KEY) as m:
    # m.get_data(chemsys_formula_id)は Material Projectから指定化合物のデータを取得
   #chemsys_formula_id は、元素の組み合わせ(e.g.,Li-Fe-O)、化合物名(e.g.,Fe2O3)、
  # またはMaterial Projectの材料id (e.g., mp-1234)のどれかを入れる.
    NaCl_df = pd.DataFrame(m.get_data('NaCl',data_type='vasp'))

NaClのデータは2個あるらしい。

NaCl_df['cif']
>>>
0    # generated using pymatgen\ndata_NaCl\n_symmet...
1    # generated using pymatgen\ndata_NaCl\n_symmet...
Name: cif, dtype: object

両方のNaClをpymatgenのStructureオブジェクトで取得します。

import pymatgen.io.cif as pycif 
from pymatgen.io.cif import CifParser

#cifを取り出す
NaCl_cif_1 = CifParser.from_string(NaCl_df['cif'][0])
NaCl_cif_2 = CifParser.from_string(NaCl_df['cif'][1])

#CifParser.get_structure メソッドは cifのデータを入力するとStructureの部分をリスト型に整形して返す
NaCl_1 =CifParser.get_structures(NaCl_cif_1)
NaCl_2 =CifParser.get_structures(NaCl_cif_2)

NaCl_1,NaCl_2

Cifの名データがだいぶ見やすい形で出力されました。

([Structure Summary
  Lattice
      abc : 4.02463512 4.02463511817503 4.024635115608324
   angles : 59.99999991890345 59.99999987171031 60.000000015
   volume : 46.09613775346724
        A : -3.4854362538293855 0.0 -2.0123175618249705
        B : -3.4854362538293855 0.0 2.0123175581750297
        C : -2.323624174916067 3.286100806051642 -3.78335363038218e-09
  PeriodicSite: Na (0.0000, 0.0000, 0.0000) [0.0000, 0.0000, 0.0000]
  PeriodicSite: Cl (-4.6472, 1.6431, -0.0000) [0.5000, 0.5000, 0.5000]],
 [Structure Summary
  Lattice
      abc : 3.50218997 3.50218997 3.50218997
   angles : 90.0 90.0 90.0
   volume : 42.95553176567333
        A : 3.50218997 0.0 2.1444728683832323e-16
        B : -2.1444728683832323e-16 3.50218997 2.1444728683832323e-16
        C : 0.0 0.0 3.50218997
  PeriodicSite: Na (0.0000, 0.0000, 0.0000) [0.0000, 0.0000, 0.0000]
  PeriodicSite: Cl (1.7511, 1.7511, 1.7511) [0.5000, 0.5000, 0.5000]])

この2つを見比べると'angles'の値が60-60-60と90-90-90とあるので、結晶構造違いのようです。
塩化ナトリウムの結晶構造は立方格子(ccp)のものが普通なので、60-60-60のものは高圧下での構造ですかね?

81306024014311.jpg

現状のNaCl_1はリスト型ですが、その中にはStructure型というpymatgenで広く使うオブジェクトが入っています。

type(NaCl_1)
>>>list

type(NaCl_1[0])
>>>pymatgen.core.structure.Structure

たとえば、latticeメソッド(lattice:格子)を使うと,
格子に関するデータを表示できます。

NaCl_1_Lattice = NaCl_1[0].lattice

NaCl_1_Lattice
>>>
Lattice
    abc : 4.02463512 4.02463511817503 4.024635115608324
 angles : 59.99999991890345 59.99999987171031 60.000000015
 volume : 46.09613775346724
      A : -3.4854362538293855 0.0 -2.0123175618249705
      B : -3.4854362538293855 0.0 2.0123175581750297
      C : -2.323624174916067 3.286100806051642 -3.78335363038218e-09

さらにvolumeメソッドでは体積の値を取得できます。

NaCl_1_Lattice.volume
>>>
46.09613775346724

これで扱いにくいcifから数値データを取得できたので、機械学習に使う素材データとして扱えそうです。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Jupyter notebookでコードを折返したい。

Jupyter notebookでコードの折返しをしたかったが、日本語での解説記事が少なかったので、メモ程度に記しておきます。

Jupyterの設定フォルダ(私のWin環境ではC:\Users\(UserName)\.jupyter)の下、フォルダnbconfoigの中にあるnotebook.jsonに以下を追記する。

notebook.json
{
    "Cell": {
        "cm_config": {
            "lineWrapping": true
        }
    }
}

より良いコーディング環境を!

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

redis-pyからRedis Sentinelにアクセスする

TL;DR

  • redis-pyはRedis Sentinelに対応したモジュールがあるので、そちらを使ってRedis Sentinelにアクセスが可能
  • マスターがダウンした際に使っていた接続(コネクションプール)は、フェイルオーバーに追従してくれる

環境

今回の環境は、こちら。

$ cat /etc/redhat-release 
CentOS Linux release 7.6.1810 (Core)


$ uname -a
Linux localhost.localdomain 3.10.0-957.12.2.el7.x86_64 #1 SMP Tue May 14 21:24:32 UTC 2019 x86_64 x86_64 x86_64 GNU/Linux


$ redis-server -v
Redis server v=5.0.6 sha=00000000:0 malloc=jemalloc-5.1.0 bits=64 build=c3d7ebb6b1a2844b


$ redis-sentinel -v
Redis server v=5.0.6 sha=00000000:0 malloc=jemalloc-5.1.0 bits=64 build=c3d7ebb6b1a2844b


$ python -V
Python 3.6.8


$ pip3 freeze
redis==3.3.8

CentOS 7、Redis 5.0.6、Python 3.6.8、redis-py 3.3.8です。

各サーバーは、以下のように用意します。

  • マスター … 192.168.33.10
  • レプリカ … 192.168.33.11
  • Sentinel 1〜3 … 192.168.33.12194.168.33.14
  • クライアント … 192.168.33.15

Redis Sentinelの構築

まずは、Redis Sentinalを構築しましょう。

EPELから、Redisをインストールします。

$ sudo yum install epel-release
$ sudo yum install https://rpms.remirepo.net/enterprise/remi-release-7.rpm
$ sudo yum --enablerepo=remi install redis

まずは、Redisのレプリケーションを構成します。

Redisのマスターは、/etc/redis.confのデフォルト設定から以下のように変更。

※デフォルト設定の全体は、最後に記載します

bind 0.0.0.0

レプリカ側は、/etc/redis.confのデフォルト設定から以下のように変更。

bind 0.0.0.0

replicaof 192.168.33.10 6379

Redisのマスター、レプリカをそれぞれ起動して

$ sudo systemctl start redis

レプリケーションが動作していることを確認。

マスター側。

$ redis-cli -h 192.168.33.10
192.168.33.10:6379> info replication
# Replication
role:master
connected_slaves:1
slave0:ip=192.168.33.11,port=6379,state=online,offset=28,lag=0
master_replid:54810ab6453ffdf12f21ff34157e2e0d655f8a81
master_replid2:0000000000000000000000000000000000000000
master_repl_offset:28
second_repl_offset:-1
repl_backlog_active:1
repl_backlog_size:1048576
repl_backlog_first_byte_offset:1
repl_backlog_histlen:28

レプリカ側。

$ redis-cli -h 192.168.33.11
192.168.33.11:6379> info replication
# Replication
role:slave
master_host:192.168.33.10
master_port:6379
master_link_status:up
master_last_io_seconds_ago:6
master_sync_in_progress:0
slave_repl_offset:56
slave_priority:100
slave_read_only:1
connected_slaves:0
master_replid:54810ab6453ffdf12f21ff34157e2e0d655f8a81
master_replid2:0000000000000000000000000000000000000000
master_repl_offset:56
second_repl_offset:-1
repl_backlog_active:1
repl_backlog_size:1048576
repl_backlog_first_byte_offset:1
repl_backlog_histlen:56

続いて、Redis Sentinelを構成します。/etc/redis-sentinel.confのデフォルト設定から、以下のように変更します。

※デフォルト設定の全体は、最後に記載します

sentinel monitor mymaster 192.168.33.10 6379 2

Redis Sentinel起動(3台で行います)。

$ sudo systemctl start redis-sentinel

確認。

$ redis-cli -h 192.168.33.12 -p 26379
192.168.33.12:26379> info sentinel
# Sentinel
sentinel_masters:1
sentinel_tilt:0
sentinel_running_scripts:0
sentinel_scripts_queue_length:0
sentinel_simulate_failure_flags:0
master0:name=mymaster,status=ok,address=192.168.33.10:6379,slaves=1,sentinels=3

マスター、レプリカがそれぞれひとつ、Sentinelが3つある状態ですね。

redis-pyで、Redis Sentinelにアクセスする

それでは、今度はRedis Sentinelにredis-pyからアクセスしてみます。まずはインストール。

$ pip3 install redis

今回のredis-pyのバージョンです。

$ pip3 freeze
redis==3.3.8

redis-pyからRedis Sentinelへのアクセスですが、以下を参考に

Sentinel support

コマンドラインプログラムを書いてみました。

sentinel_client.py

import re
from redis.exceptions import ReadOnlyError
from redis.sentinel import Sentinel
import sys

try:
    print('start sentinel client')

    sentinels = [('192.168.33.12', 26379), ('192.168.33.13', 26379), ('192.168.33.14', 26379)]
    sentinel = Sentinel(sentinels, decode_responses = True)

    redis = None

    while True:
        if redis != None:
            print('{}: > '.format(redis), end = '', flush = True)
        else:
            print('> ', end = '', flush = True)

        command = sys.stdin.readline().strip()

        if command == 'info master':
            print(sentinel.discover_master('mymaster'))
        elif command == 'info replicas':
            print(sentinel.discover_slaves('mymaster'))
        elif command == 'use master':
            redis = sentinel.master_for('mymaster')
        elif command == 'use replica':
            redis = sentinel.slave_for('mymaster')
        elif command == 'info':
            print(str(redis.info()))
        elif command.startswith('set '):
            tokens = re.split('\s+', command)
            key = tokens[1]
            value = tokens[2]

            try:
                redis.set(key, value)
                print('set {} = {}'.format(key, value))
            except ReadOnlyError as e:
                print('{}: {}'.format(e.__class__.__name__, e))

        elif command.startswith('get '):
            tokens = re.split('\s+', command)
            key = tokens[1]
            print('get[{}] = {}'.format(key, redis.get(key)))
        elif command.startswith('del '):
            tokens = re.split('\s+', command)
            key = tokens[1]

            try:
                redis.delete(key)
                print('del, {}'.format(key))
            except ReadOnlyError as e:
                print('{}: {}'.format(e.__class__.__name__, e))
        elif command == 'exit':
            print('bye bye!!')
            break
        elif not command:
            pass
        else:
            print('unknown command = {}'.format(command))

except KeyboardInterrupt:
    print('bye bye!!')

Sentinelへのアクセスは、各Sentinelプロセスへのアクセス先を使って、Sentinelインスタンスを作成することで行います。

    sentinels = [('192.168.33.12', 26379), ('192.168.33.13', 26379), ('192.168.33.14', 26379)]
    sentinel = Sentinel(sentinels, decode_responses = True)

マスターおよびレプリカへのアクセスは、Sentinel#master_forまたはSentinel#slave_forで行います。

            redis = sentinel.master_for('mymaster')

            redis = sentinel.slave_for('mymaster')

各メソッドの戻り値は、SentinelConnectionPoolのインスタンスです。

あとは、SentinelConnectionPoolに対してRedisコマンドを実行すればOKです。

        elif command.startswith('set '):
            tokens = re.split('\s+', command)
            key = tokens[1]
            value = tokens[2]

            try:
                redis.set(key, value)
                print('set {} = {}'.format(key, value))
            except ReadOnlyError as e:
                print('{}: {}'.format(e.__class__.__name__, e))

        elif command.startswith('get '):
            tokens = re.split('\s+', command)
            key = tokens[1]
            print('get[{}] = {}'.format(key, redis.get(key)))
        elif command.startswith('del '):
            tokens = re.split('\s+', command)
            key = tokens[1]

            try:
                redis.delete(key)
                print('del, {}'.format(key))
            except ReadOnlyError as e:
                print('{}: {}'.format(e.__class__.__name__, e))

あとは、情報取得系のコマンドを…。

        if command == 'info master':
            print(sentinel.discover_master('mymaster'))
        elif command == 'info replicas':
            print(sentinel.discover_slaves('mymaster'))


        elif command == 'info':
            print(str(redis.info()))

discover〜はSentinelにおけるマスター、レプリカの情報で、infoは接続しているRedisの情報です。

では、実行してみます。

$ python3 sentinel_client.py
start sentinel client
> 

マスター、レプリカの情報。

> info master
('192.168.33.10', 6379)
> info replicas
[('192.168.33.11', 6379)]

マスターに接続。

> use master

set、get。

Redis<SentinelConnectionPool<service=mymaster(master)>: > set key1 value1
set key1 = value1
Redis<SentinelConnectionPool<service=mymaster(master)>: > get key1
get[key1] = value1

レプリカにつなぎなおして、データ取得。

Redis<SentinelConnectionPool<service=mymaster(master)>: > use replica
Redis<SentinelConnectionPool<service=mymaster(slave)>: > get key1
get[key1] = value1

レプリカ側では、データの更新は不可です。

Redis<SentinelConnectionPool<service=mymaster(slave)>: > set key2 value2
ReadOnlyError: You can't write against a read only replica.

ここで、再度マスターに接続。

Redis<SentinelConnectionPool<service=mymaster(slave)>: > use master

マスターのRedisを停止してみます。

$ sudo systemctl stop redis

redis-cliで、マスターが切り替わったことを確認します。

$ redis-cli -h 192.168.33.12 -p 26379
192.168.33.12:26379> info sentinel
# Sentinel
sentinel_masters:1
sentinel_tilt:0
sentinel_running_scripts:0
sentinel_scripts_queue_length:0
sentinel_simulate_failure_flags:0
master0:name=mymaster,status=ok,address=192.168.33.11:6379,slaves=1,sentinels=3

この状態で、先程のマスターにつないだコマンドからデータを取得してみます。

Redis<SentinelConnectionPool<service=mymaster(master)>: > get key1
get[key1] = value1

なんか、動きました…。

このRedisの情報を見てみます。

Redis<SentinelConnectionPool<service=mymaster(master)>: > info
{'redis_version': '5.0.6', 'redis_git_sha1': 0, 'redis_git_dirty': 0, 'redis_build_id': 'c3d7ebb6b1a2844b', 'redis_mode': 'standalone', 'os': 'Linux 3.10.0-957.12.2.el7.x86_64 x86_64', 'arch_bits': 64, 'multiplexing_api': 'epoll', 'atomicvar_api': 'atomic-builtin', 'gcc_version': '4.8.5', 'process_id': 24544, 'run_id': 'ad1e763f073c8fcce092a773f2bbd2c5933d2bf9', 'tcp_port': 6379, 'uptime_in_seconds': 440050, 'uptime_in_days': 5, 'hz': 10, 'configured_hz': 10, 'lru_clock': 9707634, 'executable': '/usr/bin/redis-server', 'config_file': '/etc/redis.conf', 'connected_clients': 7, 'client_recent_max_input_buffer': 2, 'client_recent_max_output_buffer': 0, 'blocked_clients': 0, 'used_memory': 2070088, 'used_memory_human': '1.97M', 'used_memory_rss': 4554752, 'used_memory_rss_human': '4.34M', 'used_memory_peak': 2173256, 'used_memory_peak_human': '2.07M', 'used_memory_peak_perc': '95.25%', 'used_memory_overhead': 2024062, 'used_memory_startup': 791416, 'used_memory_dataset': 46026, 'used_memory_dataset_perc': '3.60%', 'allocator_allocated': 2637720, 'allocator_active': 3035136, 'allocator_resident': 7573504, 'total_system_memory': 510861312, 'total_system_memory_human': '487.20M', 'used_memory_lua': 37888, 'used_memory_lua_human': '37.00K', 'used_memory_scripts': 0, 'used_memory_scripts_human': '0B', 'number_of_cached_scripts': 0, 'maxmemory': 0, 'maxmemory_human': '0B', 'maxmemory_policy': 'noeviction', 'allocator_frag_ratio': 1.15, 'allocator_frag_bytes': 397416, 'allocator_rss_ratio': 2.5, 'allocator_rss_bytes': 4538368, 'rss_overhead_ratio': 0.6, 'rss_overhead_bytes': -3018752, 'mem_fragmentation_ratio': 2.25, 'mem_fragmentation_bytes': 2526672, 'mem_not_counted_for_evict': 0, 'mem_replication_backlog': 1048576, 'mem_clients_slaves': 0, 'mem_clients_normal': 183998, 'mem_aof_buffer': 0, 'mem_allocator': 'jemalloc-5.1.0', 'active_defrag_running': 0, 'lazyfree_pending_objects': 0, 'loading': 0, 'rdb_changes_since_last_save': 0, 'rdb_bgsave_in_progress': 0, 'rdb_last_save_time': 1569988537, 'rdb_last_bgsave_status': 'ok', 'rdb_last_bgsave_time_sec': 0, 'rdb_current_bgsave_time_sec': -1, 'rdb_last_cow_size': 221184, 'aof_enabled': 0, 'aof_rewrite_in_progress': 0, 'aof_rewrite_scheduled': 0, 'aof_last_rewrite_time_sec': -1, 'aof_current_rewrite_time_sec': -1, 'aof_last_bgrewrite_status': 'ok', 'aof_last_write_status': 'ok', 'aof_last_cow_size': 0, 'total_connections_received': 26, 'total_commands_processed': 2737329, 'instantaneous_ops_per_sec': 3, 'total_net_input_bytes': 202595677, 'total_net_output_bytes': 1041974292, 'instantaneous_input_kbps': 0.2, 'instantaneous_output_kbps': 0.54, 'rejected_connections': 0, 'sync_full': 0, 'sync_partial_ok': 0, 'sync_partial_err': 0, 'expired_keys': 0, 'expired_stale_perc': 0.0, 'expired_time_cap_reached_count': 0, 'evicted_keys': 0, 'keyspace_hits': 3, 'keyspace_misses': 2, 'pubsub_channels': 1, 'pubsub_patterns': 0, 'latest_fork_usec': 277, 'migrate_cached_sockets': 0, 'slave_expires_tracked_keys': 0, 'active_defrag_hits': 0, 'active_defrag_misses': 0, 'active_defrag_key_hits': 0, 'active_defrag_key_misses': 0, 'role': 'master', 'connected_slaves': 0, 'master_replid': '56d4bc022b7a089800bfe723201c821f570f2fe4', 'master_replid2': '54810ab6453ffdf12f21ff34157e2e0d655f8a81', 'master_repl_offset': 91797500, 'second_repl_offset': 91786762, 'repl_backlog_active': 1, 'repl_backlog_size': 1048576, 'repl_backlog_first_byte_offset': 90748925, 'repl_backlog_histlen': 1048576, 'used_cpu_sys': 1280.370212, 'used_cpu_user': 41.081586, 'used_cpu_sys_children': 0.004907, 'used_cpu_user_children': 0.0, 'cluster_enabled': 0, 'db0': {'keys': 1, 'expires': 0, 'avg_ttl': 0}}

レプリカがいないことになっていますね。

'connected_slaves': 0

ということは、接続しているのはフェイルオーバーしたレプリカのようです。

この状態で、レプリカに接続してデータを取得してみると、なんと動きます。

Redis<SentinelConnectionPool<service=mymaster(master)>: > use replica
Redis<SentinelConnectionPool<service=mymaster(slave)>: > get key1
get[key1] = value1

discover〜で見てみると、こういう状態です。

Redis<SentinelConnectionPool<service=mymaster(slave)>: > info master
('192.168.33.11', 6379)
Redis<SentinelConnectionPool<service=mymaster(slave)>: > info replicas
[]

となると、この場合はレプリカにつないでも、つなぐべきレプリカがいないのでマスターに接続することになるわけですね。

実際、更新も可能です。

Redis<SentinelConnectionPool<service=mymaster(slave)>: > use replica
Redis<SentinelConnectionPool<service=mymaster(slave)>: > set key2 value2
set key2 = value2

というわけで、マスターを使っている時に接続先がダウンした場合、フェイルオーバーしたマスターに昇格した旧レプリカにつないでくれるということですね(Sentinelが検知するまでの間の挙動にについては、今回は見ていませんが)。

とりあえず、挙動はなんとなくわかった感じです。

付録:EPELからインストールした、RedisおよびRedis Sentinelのデフォルト値

EPELからインストールした、/etc/redis.confのデフォルト値。

$ sudo grep -v '#' /etc/redis.conf | grep -v '^$'
bind 127.0.0.1
protected-mode yes
port 6379
tcp-backlog 511
timeout 0
tcp-keepalive 300
daemonize no
supervised no
pidfile /var/run/redis_6379.pid
loglevel notice
logfile /var/log/redis/redis.log
databases 16
always-show-logo yes
save 900 1
save 300 10
save 60 10000
stop-writes-on-bgsave-error yes
rdbcompression yes
rdbchecksum yes
dbfilename dump.rdb
dir /var/lib/redis
replica-serve-stale-data yes
replica-read-only yes
repl-diskless-sync no
repl-diskless-sync-delay 5
repl-disable-tcp-nodelay no
replica-priority 100
lazyfree-lazy-eviction no
lazyfree-lazy-expire no
lazyfree-lazy-server-del no
replica-lazy-flush no
appendonly no
appendfilename "appendonly.aof"
appendfsync everysec
no-appendfsync-on-rewrite no
auto-aof-rewrite-percentage 100
auto-aof-rewrite-min-size 64mb
aof-load-truncated yes
aof-use-rdb-preamble yes
lua-time-limit 5000
slowlog-log-slower-than 10000
slowlog-max-len 128
latency-monitor-threshold 0
notify-keyspace-events ""
hash-max-ziplist-entries 512
hash-max-ziplist-value 64
list-max-ziplist-size -2
list-compress-depth 0
set-max-intset-entries 512
zset-max-ziplist-entries 128
zset-max-ziplist-value 64
hll-sparse-max-bytes 3000
stream-node-max-bytes 4096
stream-node-max-entries 100
activerehashing yes
client-output-buffer-limit normal 0 0 0
client-output-buffer-limit replica 256mb 64mb 60
client-output-buffer-limit pubsub 32mb 8mb 60
hz 10
dynamic-hz yes
aof-rewrite-incremental-fsync yes
rdb-save-incremental-fsync yes

EPELからインストールした、/etc/redis-sentinel.confのデフォルト設定。

$ sudo grep -v '#' /etc/redis-sentinel.conf | grep -v '^$'
port 26379
daemonize no
pidfile "/var/run/redis-sentinel.pid"
logfile "/var/log/redis/sentinel.log"
dir "/tmp"
sentinel myid 88cdee281108c92337965c782f60653f4f5d00fe
sentinel deny-scripts-reconfig yes
sentinel monitor mymaster 127.0.0.1 6379 2
sentinel config-epoch mymaster 0
sentinel leader-epoch mymaster 0
protected-mode no
supervised systemd
sentinel known-replica mymaster 192.168.33.11 6379
sentinel known-sentinel mymaster 192.168.33.14 26379 4cdd5e38eccc965513db0d8be46b9eb6614418da
sentinel known-sentinel mymaster 192.168.33.13 26379 22249d49a0b530709b7897cbbb88a2db6da6a1b1
sentinel current-epoch 0
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

redis-pyからRedis Sentinelにアクセスしてみる

TL;DR

  • redis-pyはRedis Sentinelに対応したモジュールがあるので、そちらを使ってRedis Sentinelにアクセスが可能
  • Redis Sentinel経由で取得したRedisへの接続はコネクションプールであり、通常のRedisへの操作と同様に利用可能
  • マスターがダウンした際に使っていた接続(コネクションプール)は、フェイルオーバーに追従してくれる

環境

今回の環境は、こちら。

$ cat /etc/redhat-release 
CentOS Linux release 7.6.1810 (Core)


$ uname -a
Linux localhost.localdomain 3.10.0-957.12.2.el7.x86_64 #1 SMP Tue May 14 21:24:32 UTC 2019 x86_64 x86_64 x86_64 GNU/Linux


$ redis-server -v
Redis server v=5.0.6 sha=00000000:0 malloc=jemalloc-5.1.0 bits=64 build=c3d7ebb6b1a2844b


$ redis-sentinel -v
Redis server v=5.0.6 sha=00000000:0 malloc=jemalloc-5.1.0 bits=64 build=c3d7ebb6b1a2844b


$ python -V
Python 3.6.8


$ pip3 freeze
redis==3.3.8

CentOS 7、Redis 5.0.6、Python 3.6.8、redis-py 3.3.8です。

各サーバーは、以下のように用意します。

  • マスター … 192.168.33.10
  • レプリカ … 192.168.33.11
  • Sentinel 1〜3 … 192.168.33.12194.168.33.14
  • クライアント … 192.168.33.15

Redis Sentinelの構築

まずは、Redis Sentinalを構築しましょう。

EPELから、Redisをインストールします。

$ sudo yum install epel-release
$ sudo yum install https://rpms.remirepo.net/enterprise/remi-release-7.rpm
$ sudo yum --enablerepo=remi install redis

まずは、Redisのレプリケーションを構成します。

Redisのマスターは、/etc/redis.confのデフォルト設定から以下のように変更。

※デフォルト設定の全体は、最後に記載します

bind 0.0.0.0

レプリカ側は、/etc/redis.confのデフォルト設定から以下のように変更。

bind 0.0.0.0

replicaof 192.168.33.10 6379

Redisのマスター、レプリカをそれぞれ起動して

$ sudo systemctl start redis

レプリケーションが動作していることを確認。

マスター側。

$ redis-cli -h 192.168.33.10
192.168.33.10:6379> info replication
# Replication
role:master
connected_slaves:1
slave0:ip=192.168.33.11,port=6379,state=online,offset=28,lag=0
master_replid:54810ab6453ffdf12f21ff34157e2e0d655f8a81
master_replid2:0000000000000000000000000000000000000000
master_repl_offset:28
second_repl_offset:-1
repl_backlog_active:1
repl_backlog_size:1048576
repl_backlog_first_byte_offset:1
repl_backlog_histlen:28

レプリカ側。

$ redis-cli -h 192.168.33.11
192.168.33.11:6379> info replication
# Replication
role:slave
master_host:192.168.33.10
master_port:6379
master_link_status:up
master_last_io_seconds_ago:6
master_sync_in_progress:0
slave_repl_offset:56
slave_priority:100
slave_read_only:1
connected_slaves:0
master_replid:54810ab6453ffdf12f21ff34157e2e0d655f8a81
master_replid2:0000000000000000000000000000000000000000
master_repl_offset:56
second_repl_offset:-1
repl_backlog_active:1
repl_backlog_size:1048576
repl_backlog_first_byte_offset:1
repl_backlog_histlen:56

続いて、Redis Sentinelを構成します。/etc/redis-sentinel.confのデフォルト設定から、以下のように変更します。

※デフォルト設定の全体は、最後に記載します

sentinel monitor mymaster 192.168.33.10 6379 2

Redis Sentinel起動(3台で行います)。

$ sudo systemctl start redis-sentinel

確認。

$ redis-cli -h 192.168.33.12 -p 26379
192.168.33.12:26379> info sentinel
# Sentinel
sentinel_masters:1
sentinel_tilt:0
sentinel_running_scripts:0
sentinel_scripts_queue_length:0
sentinel_simulate_failure_flags:0
master0:name=mymaster,status=ok,address=192.168.33.10:6379,slaves=1,sentinels=3

マスター、レプリカがそれぞれひとつ、Sentinelが3つある状態ですね。

redis-pyで、Redis Sentinelにアクセスする

それでは、今度はRedis Sentinelにredis-pyからアクセスしてみます。まずはインストール。

$ pip3 install redis

今回のredis-pyのバージョンです。

$ pip3 freeze
redis==3.3.8

redis-pyからRedis Sentinelへのアクセスですが、以下を参考に

Sentinel support

コマンドラインプログラムを書いてみました。

sentinel_client.py

import re
from redis.exceptions import ReadOnlyError
from redis.sentinel import Sentinel
import sys

try:
    print('start sentinel client')

    sentinels = [('192.168.33.12', 26379), ('192.168.33.13', 26379), ('192.168.33.14', 26379)]
    sentinel = Sentinel(sentinels, decode_responses = True)

    redis = None

    while True:
        if redis != None:
            print('{}: > '.format(redis), end = '', flush = True)
        else:
            print('> ', end = '', flush = True)

        command = sys.stdin.readline().strip()

        if command == 'info master':
            print(sentinel.discover_master('mymaster'))
        elif command == 'info replicas':
            print(sentinel.discover_slaves('mymaster'))
        elif command == 'use master':
            redis = sentinel.master_for('mymaster')
        elif command == 'use replica':
            redis = sentinel.slave_for('mymaster')
        elif command == 'info':
            print(str(redis.info()))
        elif command.startswith('set '):
            tokens = re.split('\s+', command)
            key = tokens[1]
            value = tokens[2]

            try:
                redis.set(key, value)
                print('set {} = {}'.format(key, value))
            except ReadOnlyError as e:
                print('{}: {}'.format(e.__class__.__name__, e))

        elif command.startswith('get '):
            tokens = re.split('\s+', command)
            key = tokens[1]
            print('get[{}] = {}'.format(key, redis.get(key)))
        elif command.startswith('del '):
            tokens = re.split('\s+', command)
            key = tokens[1]

            try:
                redis.delete(key)
                print('del, {}'.format(key))
            except ReadOnlyError as e:
                print('{}: {}'.format(e.__class__.__name__, e))
        elif command == 'exit':
            print('bye bye!!')
            break
        elif not command:
            pass
        else:
            print('unknown command = {}'.format(command))

except KeyboardInterrupt:
    print('bye bye!!')

Sentinelへのアクセスは、各Sentinelプロセスへのアクセス先を使って、Sentinelインスタンスを作成することで行います。

    sentinels = [('192.168.33.12', 26379), ('192.168.33.13', 26379), ('192.168.33.14', 26379)]
    sentinel = Sentinel(sentinels, decode_responses = True)

マスターおよびレプリカへのアクセスは、Sentinel#master_forまたはSentinel#slave_forで行います。

            redis = sentinel.master_for('mymaster')

            redis = sentinel.slave_for('mymaster')

各メソッドの戻り値は、SentinelConnectionPoolのインスタンスです。

あとは、SentinelConnectionPoolに対してRedisコマンドを実行すればOKです。

        elif command.startswith('set '):
            tokens = re.split('\s+', command)
            key = tokens[1]
            value = tokens[2]

            try:
                redis.set(key, value)
                print('set {} = {}'.format(key, value))
            except ReadOnlyError as e:
                print('{}: {}'.format(e.__class__.__name__, e))

        elif command.startswith('get '):
            tokens = re.split('\s+', command)
            key = tokens[1]
            print('get[{}] = {}'.format(key, redis.get(key)))
        elif command.startswith('del '):
            tokens = re.split('\s+', command)
            key = tokens[1]

            try:
                redis.delete(key)
                print('del, {}'.format(key))
            except ReadOnlyError as e:
                print('{}: {}'.format(e.__class__.__name__, e))

あとは、情報取得系のコマンドを…。

        if command == 'info master':
            print(sentinel.discover_master('mymaster'))
        elif command == 'info replicas':
            print(sentinel.discover_slaves('mymaster'))


        elif command == 'info':
            print(str(redis.info()))

discover〜はSentinelにおけるマスター、レプリカの情報で、infoは接続しているRedisの情報です。

では、実行してみます。

$ python3 sentinel_client.py
start sentinel client
> 

マスター、レプリカの情報。

> info master
('192.168.33.10', 6379)
> info replicas
[('192.168.33.11', 6379)]

マスターに接続。

> use master

set、get。

Redis<SentinelConnectionPool<service=mymaster(master)>: > set key1 value1
set key1 = value1
Redis<SentinelConnectionPool<service=mymaster(master)>: > get key1
get[key1] = value1

レプリカにつなぎなおして、データ取得。

Redis<SentinelConnectionPool<service=mymaster(master)>: > use replica
Redis<SentinelConnectionPool<service=mymaster(slave)>: > get key1
get[key1] = value1

レプリカ側では、データの更新は不可です。

Redis<SentinelConnectionPool<service=mymaster(slave)>: > set key2 value2
ReadOnlyError: You can't write against a read only replica.

ここで、再度マスターに接続。

Redis<SentinelConnectionPool<service=mymaster(slave)>: > use master

マスターのRedisを停止してみます。

$ sudo systemctl stop redis

redis-cliで、マスターが切り替わったことを確認します。

$ redis-cli -h 192.168.33.12 -p 26379
192.168.33.12:26379> info sentinel
# Sentinel
sentinel_masters:1
sentinel_tilt:0
sentinel_running_scripts:0
sentinel_scripts_queue_length:0
sentinel_simulate_failure_flags:0
master0:name=mymaster,status=ok,address=192.168.33.11:6379,slaves=1,sentinels=3

この状態で、先程のマスターにつないだコマンドからデータを取得してみます。

Redis<SentinelConnectionPool<service=mymaster(master)>: > get key1
get[key1] = value1

なんか、動きました…。

このRedisの情報を見てみます。

Redis<SentinelConnectionPool<service=mymaster(master)>: > info
{'redis_version': '5.0.6', 'redis_git_sha1': 0, 'redis_git_dirty': 0, 'redis_build_id': 'c3d7ebb6b1a2844b', 'redis_mode': 'standalone', 'os': 'Linux 3.10.0-957.12.2.el7.x86_64 x86_64', 'arch_bits': 64, 'multiplexing_api': 'epoll', 'atomicvar_api': 'atomic-builtin', 'gcc_version': '4.8.5', 'process_id': 24544, 'run_id': 'ad1e763f073c8fcce092a773f2bbd2c5933d2bf9', 'tcp_port': 6379, 'uptime_in_seconds': 440050, 'uptime_in_days': 5, 'hz': 10, 'configured_hz': 10, 'lru_clock': 9707634, 'executable': '/usr/bin/redis-server', 'config_file': '/etc/redis.conf', 'connected_clients': 7, 'client_recent_max_input_buffer': 2, 'client_recent_max_output_buffer': 0, 'blocked_clients': 0, 'used_memory': 2070088, 'used_memory_human': '1.97M', 'used_memory_rss': 4554752, 'used_memory_rss_human': '4.34M', 'used_memory_peak': 2173256, 'used_memory_peak_human': '2.07M', 'used_memory_peak_perc': '95.25%', 'used_memory_overhead': 2024062, 'used_memory_startup': 791416, 'used_memory_dataset': 46026, 'used_memory_dataset_perc': '3.60%', 'allocator_allocated': 2637720, 'allocator_active': 3035136, 'allocator_resident': 7573504, 'total_system_memory': 510861312, 'total_system_memory_human': '487.20M', 'used_memory_lua': 37888, 'used_memory_lua_human': '37.00K', 'used_memory_scripts': 0, 'used_memory_scripts_human': '0B', 'number_of_cached_scripts': 0, 'maxmemory': 0, 'maxmemory_human': '0B', 'maxmemory_policy': 'noeviction', 'allocator_frag_ratio': 1.15, 'allocator_frag_bytes': 397416, 'allocator_rss_ratio': 2.5, 'allocator_rss_bytes': 4538368, 'rss_overhead_ratio': 0.6, 'rss_overhead_bytes': -3018752, 'mem_fragmentation_ratio': 2.25, 'mem_fragmentation_bytes': 2526672, 'mem_not_counted_for_evict': 0, 'mem_replication_backlog': 1048576, 'mem_clients_slaves': 0, 'mem_clients_normal': 183998, 'mem_aof_buffer': 0, 'mem_allocator': 'jemalloc-5.1.0', 'active_defrag_running': 0, 'lazyfree_pending_objects': 0, 'loading': 0, 'rdb_changes_since_last_save': 0, 'rdb_bgsave_in_progress': 0, 'rdb_last_save_time': 1569988537, 'rdb_last_bgsave_status': 'ok', 'rdb_last_bgsave_time_sec': 0, 'rdb_current_bgsave_time_sec': -1, 'rdb_last_cow_size': 221184, 'aof_enabled': 0, 'aof_rewrite_in_progress': 0, 'aof_rewrite_scheduled': 0, 'aof_last_rewrite_time_sec': -1, 'aof_current_rewrite_time_sec': -1, 'aof_last_bgrewrite_status': 'ok', 'aof_last_write_status': 'ok', 'aof_last_cow_size': 0, 'total_connections_received': 26, 'total_commands_processed': 2737329, 'instantaneous_ops_per_sec': 3, 'total_net_input_bytes': 202595677, 'total_net_output_bytes': 1041974292, 'instantaneous_input_kbps': 0.2, 'instantaneous_output_kbps': 0.54, 'rejected_connections': 0, 'sync_full': 0, 'sync_partial_ok': 0, 'sync_partial_err': 0, 'expired_keys': 0, 'expired_stale_perc': 0.0, 'expired_time_cap_reached_count': 0, 'evicted_keys': 0, 'keyspace_hits': 3, 'keyspace_misses': 2, 'pubsub_channels': 1, 'pubsub_patterns': 0, 'latest_fork_usec': 277, 'migrate_cached_sockets': 0, 'slave_expires_tracked_keys': 0, 'active_defrag_hits': 0, 'active_defrag_misses': 0, 'active_defrag_key_hits': 0, 'active_defrag_key_misses': 0, 'role': 'master', 'connected_slaves': 0, 'master_replid': '56d4bc022b7a089800bfe723201c821f570f2fe4', 'master_replid2': '54810ab6453ffdf12f21ff34157e2e0d655f8a81', 'master_repl_offset': 91797500, 'second_repl_offset': 91786762, 'repl_backlog_active': 1, 'repl_backlog_size': 1048576, 'repl_backlog_first_byte_offset': 90748925, 'repl_backlog_histlen': 1048576, 'used_cpu_sys': 1280.370212, 'used_cpu_user': 41.081586, 'used_cpu_sys_children': 0.004907, 'used_cpu_user_children': 0.0, 'cluster_enabled': 0, 'db0': {'keys': 1, 'expires': 0, 'avg_ttl': 0}}

レプリカがいないことになっていますね。

'connected_slaves': 0

ということは、接続しているのはフェイルオーバーしたレプリカのようです。

この状態で、レプリカに接続してデータを取得してみると、なんと動きます。

Redis<SentinelConnectionPool<service=mymaster(master)>: > use replica
Redis<SentinelConnectionPool<service=mymaster(slave)>: > get key1
get[key1] = value1

discover〜で見てみると、こういう状態です。

Redis<SentinelConnectionPool<service=mymaster(slave)>: > info master
('192.168.33.11', 6379)
Redis<SentinelConnectionPool<service=mymaster(slave)>: > info replicas
[]

となると、この場合はレプリカにつないでも、つなぐべきレプリカがいないのでマスターに接続することになるわけですね。

実際、更新も可能です。

Redis<SentinelConnectionPool<service=mymaster(slave)>: > use replica
Redis<SentinelConnectionPool<service=mymaster(slave)>: > set key2 value2
set key2 = value2

というわけで、マスターを使っている時に接続先がダウンした場合、フェイルオーバーしたマスターに昇格した旧レプリカにつないでくれるということですね(Sentinelが検知するまでの間の挙動にについては、今回は見ていませんが)。

とりあえず、挙動はなんとなくわかった感じです。

付録:EPELからインストールした、RedisおよびRedis Sentinelのデフォルト値

EPELからインストールした、/etc/redis.confのデフォルト値。

$ sudo grep -v '#' /etc/redis.conf | grep -v '^$'
bind 127.0.0.1
protected-mode yes
port 6379
tcp-backlog 511
timeout 0
tcp-keepalive 300
daemonize no
supervised no
pidfile /var/run/redis_6379.pid
loglevel notice
logfile /var/log/redis/redis.log
databases 16
always-show-logo yes
save 900 1
save 300 10
save 60 10000
stop-writes-on-bgsave-error yes
rdbcompression yes
rdbchecksum yes
dbfilename dump.rdb
dir /var/lib/redis
replica-serve-stale-data yes
replica-read-only yes
repl-diskless-sync no
repl-diskless-sync-delay 5
repl-disable-tcp-nodelay no
replica-priority 100
lazyfree-lazy-eviction no
lazyfree-lazy-expire no
lazyfree-lazy-server-del no
replica-lazy-flush no
appendonly no
appendfilename "appendonly.aof"
appendfsync everysec
no-appendfsync-on-rewrite no
auto-aof-rewrite-percentage 100
auto-aof-rewrite-min-size 64mb
aof-load-truncated yes
aof-use-rdb-preamble yes
lua-time-limit 5000
slowlog-log-slower-than 10000
slowlog-max-len 128
latency-monitor-threshold 0
notify-keyspace-events ""
hash-max-ziplist-entries 512
hash-max-ziplist-value 64
list-max-ziplist-size -2
list-compress-depth 0
set-max-intset-entries 512
zset-max-ziplist-entries 128
zset-max-ziplist-value 64
hll-sparse-max-bytes 3000
stream-node-max-bytes 4096
stream-node-max-entries 100
activerehashing yes
client-output-buffer-limit normal 0 0 0
client-output-buffer-limit replica 256mb 64mb 60
client-output-buffer-limit pubsub 32mb 8mb 60
hz 10
dynamic-hz yes
aof-rewrite-incremental-fsync yes
rdb-save-incremental-fsync yes

EPELからインストールした、/etc/redis-sentinel.confのデフォルト設定。

$ sudo grep -v '#' /etc/redis-sentinel.conf | grep -v '^$'
port 26379
daemonize no
pidfile "/var/run/redis-sentinel.pid"
logfile "/var/log/redis/sentinel.log"
dir "/tmp"
sentinel myid 88cdee281108c92337965c782f60653f4f5d00fe
sentinel deny-scripts-reconfig yes
sentinel monitor mymaster 127.0.0.1 6379 2
sentinel config-epoch mymaster 0
sentinel leader-epoch mymaster 0
protected-mode no
supervised systemd
sentinel known-replica mymaster 192.168.33.11 6379
sentinel known-sentinel mymaster 192.168.33.14 26379 4cdd5e38eccc965513db0d8be46b9eb6614418da
sentinel known-sentinel mymaster 192.168.33.13 26379 22249d49a0b530709b7897cbbb88a2db6da6a1b1
sentinel current-epoch 0
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

PythonでStateモナドを書いてみる

Stateモナド

  • ”状態”を受け取り、状態に対する計算結果と、計算後の”状態”を返す(状態付き計算とよぶ)ような関数を、モナドとして扱いたいときに用いるモナド
  • 状態付き計算処理を簡潔に記述できる(関数合成できる)

参考

実装するまえに

状態付き計算とは

sを状態、sに対する何らかの計算結果をa、計算後の状態をs'としたとき、
以下の形で入出力が表現できる計算のこと。

s -> (a, s')

状態とは変数の値とかリストの中身とか、一般的な意味合いでの状態と思えばいい。

なぜ状態付き計算を使うのか

モナドが多く利用される純粋関数型プログラミング言語のHaskellのコードを例にとる。
以下は、スタック処理の例。

stackExample :: State [Int] Int
stackExample = do
  push 1
  push 2
  push 3
  pop
  push 4
  x <- pop
  return x
> runState stackExample []
> (4,[2,1])

(筆者はHaskellには触れたことがないので、コードそのものには特に言及しない。)
なにをやっているかに注目する。
上記プログラムは、1~4のデータを空の箱にに入れたり取り出したりし、最終的に 4 を取り出して 2 と 1 が残っている状態を表す、いわゆるスタック処理の一例である。
ここでは、変数への再代入なしに状態を更新しているようにふるまっている点が重要で、
このような処理を、Pythonで普通に実装しようとすると、例えば以下のように、
スタックの状態を保持するリストに対し、元々備わっている状態更新の処理を用いて、リストの中身を更新していく書き方になると思う。

def stackExample(state):
    state.insert(0,1)
    state.insert(0,2)
    state.insert(0,3)
    state.pop(0)
    state.insert(0,4)
    ret = (state.pop(0), state)
    return ret

stackExample([])

(4, [2, 1])

このままでも簡潔に記述できている。
しかしここで問題なのは、stateの変数がいまどのような状態なのか、どんな状態になりうるのかがわからないというところにある。
これを他の外部処理でも更新したり、また別のところでは参照したりしようとすると、
各処理において、stateになにが入っているのか、十分注意して実装することになる。
しかしそこに想定外の状態が入り込むとプログラムはバグってしまうわけだ。
もちろん、実際には変数のチェック処理を実装することで想定外の挙動を起こさないよう努力する。
しかしながら未知の値を持ちうる変数はできるかぎりプログラムから排除したい。
とはいえ状態を更新するような処理をきちんと記述できなければ書きたいプログラムを書けなくなる。
そこで登場するのが状態付き関数であり、Stateモナドである。と思う。

実装

pushとpop

まずは、リストを受け取り処理結果と処理後のリストを返すという形式で、
pushとpopを実装してみる。
入力したリストの中身を更新しないことに注意する。

push

def push(a):
    return lambda x: (None, [a] + x)

x = [1,2,3]
y = push(4)(x)
print(x)
print(y)

[1, 2, 3] ←push後でも中身は変わってないx
(None, [4, 1, 2, 3]) ←y

pop

def pop():
    return lambda x: (x[0], x[1:])

x = [1,2,3]
y = pop()(x)
print(x)
print(y)

[1, 2, 3] ← pop後も中身が変わってないx
(1, [2, 3]) ← y

それぞれの処理はラムダ式を返すことで関数をバインドできるようにし、
モナドに拡張できるようにしている。

def _bind(func):
    return lambda x: func(x)

_bind(push(4))([1,2,3])

(None, [4, 1, 2, 3])

_bind(pop())([1,2,3])

(1, [2, 3])

stateモナドへの拡張

pushとpopの処理はつくった。
ここで、スタックの例に戻ってみる。

stackExample :: State [Int] Int
stackExample = do
  push 1
  push 2
  push 3
  pop
  push 4
  x <- pop
  return x
> runState stackExample []
> (4,[2,1])

さて、自前でつくった push・pop を用いて、上記のようにスタック処理を記述できるだろうか?
このままではできない。
push・pop において入力と出力の形式が異なるからだ。

  • 入力
    • [状態]
  • 出力
    • (計算結果, [状態])

push pop を関数合成できるようにするためには、
両者の入力も出力も同じように抽象的に扱う仕組みが必要である。

  • 入力
    • State ( , [状態])
  • 出力
    • State (計算結果, [状態])

そこでいよいよStateモナドだ。

モナドクラス(仮)

class Monad():

    def __init__(self, a):
        raise NotImplementedError

    def _bind(self, func):
        raise NotImplementedError

    def __or__ (self, func):
        return self._bind(func)

    @staticmethod
    def call_state(s):
        return State(lambda : (s, s))

Stateモナド

class State(Monad):
    def __init__(self, a):
        self.run_state = a

    def _bind(self, func):
        _, s = self.run_state()
        return State(lambda :func(s)) 

    def __repr__(self):
        return  'State (%r, %r)' % self.run_state()

動かしてみる

Monad.call_state([1, 2, 3, 4])

State ([1, 2, 3, 4], [1, 2, 3, 4])

Monad.call_state([1, 2, 3, 4]) | pop()

State (1, [2, 3, 4])

Monad.call_state([1, 2, 3, 4]) | push(5)

State (None, [5, 1, 2, 3, 4])

Monad.call_state([1, 2, 3, 4]) | pop() | push(5)

State (None, [5, 2, 3, 4])

スタックの例

def stackExample(state):
    return state | push(1) \
                 | push(2) \
                 | push(3) \
                 | pop()   \
                 | push(4) \
                 | pop()

state = Monad.call_state([])
ret = stackExample(state)
print(state)
print(ret)

State ([], []) ← state
State (4, [2, 1]) ← ret

push と pop の処理を合成して元々の変数の値を変化させることなく、
状態更新を扱うことができた。

おわりに

以上のようにStateモナドを使うと

  • 状態付き計算の関数合成ができる

ようだ。

個人的にはこの例を通じてStateモナドを書いてみて、
モナドを理解するうえでは、"閉じている"っていうのが重要なんだと思った。
モノイドとの関連もそれでより合点がいった。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

PythonからExcelをwin32comで操作する

はじめに

Excel ファイルを自動で処理したい。そのための言語としていくつか手段があることが分かった。
どれを選ぶべきか。その中で結果として選んだ Python + win32com の操作例を書き残しておく。

なぜ Python + win32com で Excel を操作するのか

Excel VBA, PowerShell + COM, Python + xlwings, Python + openpyxl, Python + win32com を比較した結果、自分のニーズに近いのは win32com でした。

まず、Excelを自動で操作したいと考えたとき、初めに思いつくのはExcel VBAでした。ただ、これは向かない用途があるようでした。

Excelファイルの所有者にVBA入れて管理するにはすべてのユーザにマクロ付きのExcelファイルの利用を強制することになるのでOSからExcelのバージョンまで細かな動きをVBA側で考慮する必要があります。VBAを強制することによるセキュリティ上のリスク等も考え始めるとあまり選択肢としてよくない。

次に、データが詰まってるExcelファイルとVBAを管理するExcelファイルを分離することを考えたとしても、VBAのコード管理がめんどい。Gitなりで非テキストファイルを管理するのは簡単でなく、がんばってモジュールを節目でエクスポートして管理、というのは面倒そう。継続的にメンテする費用がなければ避けたい選択肢だった。

Excel VBA の次に Excel の自動化で思いついたのは、 PowerShell + COM でした。が、PowerShell って癖があって苦手意識がある。エスケープがバックスラッシュなの忘れるし、パス名にワイルドカード文字列が入るとエスケープ処理に翻弄される程度の、低レベル PowerShell コーダーには敷居が高すぎる。100行程度ならPowerShellで頑張る。それ以上はお勧めできない。

次に他の言語はさておき、 Python + win32com か、xlwings, openpyxl で悩んだ。

openpyxl は、xlsx を読めるが計算式の計算手段を持って無い気がするのに計算結果を得られそうなメソッドがあって不安がある。おそらくキャッシュしたデータを読めるのだと思うけれど値に不整合があるのではないか、仮に無かったとして書き込み処理を行った後に読んではいけないデータなのではと不安を感じる。誤差が許される読み取り処理や、読み取りせずExcelを出力するならopenpyxlは使い勝手よさそう。

xlwings は、素朴なExcelファイルなら採用したいくらい良くできてる。 dir(workbook) でメソッド一覧が出る、メソッド名が原則小文字で統一されている、など使い勝手が良い。ただし、すべてのExcelの機能に対応してないように見える。例えば、ざっと調べた限りでは保護されたExcelシートを解除したい場合は、VBAのコードをxlwings経由で実行する等のワークアラウンドが必要でデータ処理には向くが、かゆいところに手が届かない印象。また、Execel を導入済が前提となる。動作確認してないが macOS で動かしている事例があった。

残ったwin32comは上記の課題をすべてクリアできそう。ただし、制約として、Windows+Excel導入済みが前提になる。デスクトップでExcelを実行中にwin32com経由でExcelを実行すると挙動が変わる部分があるなどテスト面で環境の影響を受けやすい。 dir(workbook) でメソッド名の一覧が出なかったり、メソッド名の大文字小文字を誤ると期待通りに動かないといった動作もある。ドキュメントはあまりないので、Python書く際はVBAの知見なり、MS公式APIドキュメントなり、Stackoverflowなりを参照することになる。

いずれも一長一短ですね。Excelを最も細かく使えるのがwin32comで一択。開発者にやさしく今後期待できるのがxlwings。全部で1~2画面程度の短いコードならPowerShellでもまあいいか。Excelファイルを自分だけで使うなら版管理なしのVBAでもいいか、という印象です。

前提

  • Windows 10
  • Python3
  • Excel 2013

ディレクトリ、ファイル

. (directory)
|
+ data (directory)
  |
  +-- sample.xlsx

パッケージをインストールする

pip install pywin32

Excel を読む

Excel を起動・終了する

import win32com.client

# 起動する
app = win32com.client.Dispatch("Excel.Application")

# 終了する
app.Quit()

xlsxファイルからワークブックを読み取り専用で開く

from pathlib import Path

# 開く
abspath = str(Path(r"data/sample.xlsx").resolve())
workbook = app.Workbooks.Open(abspath, UpdateLinks=0, ReadOnly=True)

# 閉じる
workbook.Close()

ワークブックの一覧を得る

xxx[Index]xxx(Index) で要素番号の開始が異なるので注意する。

print(app.Workbooks.Count)
for i in range(0, app.Workbooks.Count):
    print(app.Workbooks[i].name)
print(app.Workbooks.Count)
for i in range(1, app.Workbooks.Count + 1):
    print(app.Workbooks(i).name)

ワークブックを有効にする

workbook.Activate()

シートの一覧を得る

print(workbook.Worksheets.Count)
for i in range(0, workbook.Worksheets.Count):
    print(workbook.Worksheets[i].name)
print(workbook.Worksheets.Count)
for i in range(1, workbook.Worksheets.Count + 1):
    print(workbook.Worksheets(i).name)

シート名からシートを得る

sheet = workbook.Worksheets("Sheet1")

シートからセルを得る

sheet.Cells.Item(1,1).Value

シートから範囲を得る

arg = "A1:B3"
r = sheet.Range(arg)

ret = list()
for row_index in range(1, r.Rows.Count + 1):
    row = []
    for col_index in range(1, r.Columns.Count + 1):
        row.append(r(row_index, col_index).Address)
    ret.append(row)

print(ret)
   # --> [['$A$1', '$B$1'], ['$A$2', '$B$2'], ['$A$3', '$B$3']]

セルの計算式を得る

cell.Formula = '=1+2'
cell.Formula
  # --> '=1+2'
cell.Value
  # --> 3.0

セルの値を得る

row_index, col_index = 1, 2
cell = sheet.Cells(row_index, col_index)
cell.ClearFormats()
cell.NumberFormatLocal
  # --> 'G/標準'

# 整数
cell.Value = 1
cell.Value
  # --> 1.0

# 文字列
cell.Value = 'あ'
cell.Value
  # --> 'あ'

# 日付に解釈されそうな文字列
cell.NumberFormatLocal
  # --> 'G/標準'
cell.Value = '2019-01-01'
cell.NumberFormatLocal
  # --> 'yyyy/m/d'
cell.Value
  # --> pywintypes.datetime(2019, 1, 1, 0, 0, tzinfo=TimeZoneInfo('GMT Standard Time', True))

import datetime
datetime.datetime.fromtimestamp(timestamp=pywindt.timestamp(), tz=pywindt.tzinfo)
  # --> datetime.datetime(2019, 1, 1, 0, 0, tzinfo=TimeZoneInfo('GMT Standard Time', True))

# 時刻に解釈されそうな文字列
cell.Value = '00:00:00'
cell.NumberFormatLocal
  # --> 'h:mm:ss'
cell.Value
  # --> '0.0'
cell.Value = '24:00:00'
cell.Value
  # --> '1.0'
cell.Value = '48:00:00'
cell.Value
  # --> '2.0'

セルのテキストを得る

cell.ClearFormats()
cell.NumberFormatLocal
  # --> 'G/標準'

cell.Value = '=1'
cell.Value
  # --> 1.0
cell.Text
  # --> '1'

Excel を書く

新規ワークブックを開く

workbook = app.Workbooks.Add()

ワークブックを保存する

workbook.Save()

その他

Excel を見えなくする

app.Visible = False

シートの保護を設定・解除する

sheet.Protect()
sheet.Unprotect()

警告を表示しないようにする

app.DisplayAlerts = False

セルのアドレスを得る

cell.Address
  # --> '$B$1'
cell.GetAddress()
  # --> '$B$1'
cell.GetAddress(RowAbsolute=False, ColumnAbsolute=False)
  # --> 'B1'
# NGな例
#cell.Address(RowAbsolute=False, ColumnAbsolute=False)

Tips

dirメソッドは使えない

import win32com.client
app = win32com.client.Dispatch("Excel.Application")
dir(app)
  # --> ['_ApplyTypes_', '_FlagAsMethod', '_LazyAddAttr_', '_NewEnum', '_Release_', '__AttrToID__', '__LazyMap__', '__bool__', '__call__', '__class__', '__delattr__', '__dict__', '__dir__', '__doc__', '__eq__', '__format__', '__ge__', '__getattr__', '__getattribute__', '__getitem__', '__gt__', '__hash__', '__init__', '__init_subclass__', '__int__', '__le__', '__len__', '__lt__', '__module__', '__ne__', '__new__', '__reduce__', '__reduce_ex__', '__repr__', '__setattr__', '__setitem__', '__sizeof__', '__str__', '__subclasshook__', '__weakref__', '_builtMethods_', '_enum_', '_find_dispatch_type_', '_get_good_object_', '_get_good_single_object_', '_lazydata_', '_make_method_', '_mapCachedItems_', '_oleobj_', '_olerepr_', '_print_details_', '_proc_', '_unicode_to_string_', '_username_', '_wrap_dispatch_']

参考資料

ライブラリ比較

PythonでExcelファイルを扱うライブラリの比較
https://note.nkmk.me/python-excel-library/

Microsoft

Workbook オブジェクト
https://docs.microsoft.com/ja-jp/office/vba/api/excel.workbook

Excel + VBA

VBAのソースを、Gitとかで管理する
https://qiita.com/ryotaro76/items/63cad3dfb891a9df7c88

PowerShell + Excel

PowerShell で Excel をどうのこうのすることに興味を持ってくれると嬉しい
https://qiita.com/miyamiya/items/161372111b68bad0744a

Python + openpyxl

openpyxl - A Python library to read/write Excel 2010 xlsx/xlsm files
https://openpyxl.readthedocs.io/en/stable/

Python + win32com

Python for Windows (pywin32) Extensions
https://github.com/mhammond/pywin32

TODO: Exccel + win32com でよくまとまった記事を見た記憶があるがリンクを見つけられない。見つけたら書く。

Python + xlwings

xlwings - Make Excel Fly!
https://docs.xlwings.org/en/stable/index.html

ExcelからPythonを使う
https://qiita.com/katzhide/items/60d0336b322105bf8fe9

PythonとxlwingsでExcelファイルをいじる
https://idontwannawork.github.io/posts/edit-excel-with-python-and-xlwings/

how to unprotect a sheet? #1032
https://github.com/xlwings/xlwings/issues/1032

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

pythonでインデントを不要にしてみた【php】

0. 動機

「pythonはc言語やjavaでいうブロックの代わりにインデントを使うんだよ」というのを そういうものだと受け入れたら、今までできたはずの言語で構文エラーしまくることになる事例を知った。
僕自身はpythonについて、ほとんどしらない。
唯一知ってるのは「pythonがインデントに意味を持たせたのは有害」ということだけ。
→pythonには「1行1ステートメント」という考え方があり、だからこそブロックの括弧やセミコロンは不要[1]というのは後から知った。

そこで、インデントの代わりにブロック{ }を使って記述したpythonコードを、正しいpythonコードに置き換えるプログラムを作ろうという気になった。
phpがインストールされているサーバからみんなが使えるように、phpで作る。

1.プログラムの構成

  1. テキストエリアから、インデントの代わりにブロックで書かれたpyファイルの中身を受け取る(元コンテンツと呼ぶことにする)
  2. 元ファイルのブロックはインデントに置き換え、元ファイルのインデントは捨てる
  3. 結果を表示

1-1.

次のようなblock2indent.htmlを作ってみた。

<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01 Strict//EN" "http://www.w3.org/TR/html4/strict.dtd">
<html>
    <head>
        <meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
        <title>インデントが不要でブロックが使えるpython</title>
    </head>
    <body>
        ①インデントの代わりにブロックを用いたpythonコードを入力<br>
        <form method="post" action="block2indent.php">
            <textarea name="content" cols="50" rows="5"></textarea>
            <br>
            <br>
            ②改行コードを選択<br>
            <select name="returnCode">
                <option value="rn"> &#92;r&#92;n </option>
                <option value="n"> &#92;n </option>
                <option value="r"> &#92;r </option>
            </select>
            <br>
            <br><input type="submit">
        </form>
    </body>
</html>

余談だが、Eclipseで「PHP 開発ツール (PDT)」をインストールし、エディタに「<」とだけ打ち込むと、入力候補としてという選択肢が表示される。これを無視してバックスペースキーを押して「<」を消すと、cssやhtmlなど様々なコードのテンプレートが選べるようになる。これでhtml 4.01 strictのコードなども容易にかける。

block2indent.htmlをクロームで開くと、図1.1.1のように表示され、「送信」を押すと、「ファイルが見つかりません」のエラーへ遷移した。
1.png

図1.1.1 block2indent.html

1-2.

block2indent.htmlを名前変更しblock2indent.phpとした。
そして、次のように追記した。

<?php
$samplePy=
"
class Man:{
    def __inin__(self, name):{
        self.name = name;
        print(\"インスタンス生成および初期化に成功\");
    }

    def hello(self):{
        print(\"Hello \" + self.name + \"!\")
    }
}

m = Man(\"David\");
m.hello();
";

/*/
if(isset($_POST['content']))
    print block2indent(killIndent($_POST['content']));
/*/
if(true)
    print block2indent(killIndent($samplePy));
//*/
else printHtml();


function printHtml()
{
    print
'<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01 Strict//EN" "http://www.w3.org/TR/html4/strict.dtd">
<html>
    <head>
        <meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
        <title>インデントが不要でブロックが使えるpython</title>
    </head>
    <body>
        ①インデントの代わりにブロックを用いたpythonコードを入力<br>
        <form method="post" action="block2indent.php">
            <textarea name="content" cols="50" rows="5"></textarea>
            <br>
            <br>
            ②改行コードを選択<br>
            <select name="returnCode">
                <option value="rn"> &#92;r&#92;n </option>
                <option value="n"> &#92;n </option>
                <option value="r"> &#92;r </option>
            </select>
            <br>
            <br>
            ③<input type="submit">
        </form>
    </body>
</html>';
}

    function killIndent($indentAlive)
    {
        $indentDying = preg_replace("/\A( |\t)+/i", "", $indentAlive);
        return preg_replace("/(\n|\r|(\r\n))( |\t)+/i", "$1", $indentDying);
    }

    function block2indent($blockAlive)
    {
        //行頭以外でブロックが開始していたら、ブロック開始直前に改行挿入
        //同様に、ブロックの終わりも改行を強制
        $blockDying = putNewLineBeforeBlockOpen($blockAlive);
        $blockDying = putNewLineAfterBlockClose($blockDying);

        //各行ごとの「深さ」を調べ、その分だけスペース4つを挿入
        $blockDying = createIndent($blockDying);

        //ブロックをなくす
        return killBlock($blockDying);
    }

        function putNewLineBeforeBlockOpen($blockOpenWhileARow)
        {
            return preg_replace("/(.+){/i", "$1\r\n{", $blockOpenWhileARow);
        }

        function putNewLineAfterBlockClose($blockCloseWhileARow)
        {
            return preg_replace("/}(.+)/i", "}\r\n$1", $blockCloseWhileARow);
        }

        function createIndent($noIndent)
        {
            //改行コードを統一
            $returnCode
            =
            strcmp($_POST['returnCode'], "r")===0 ? "\r":
            strcmp($_POST['returnCode'], "n")===0 ? "\n":
            "\r\n";

            $noIndent = preg_replace("/\r([^\n])/i", $returnCode."$1", $noIndent);
            $noIndent = preg_replace("/([^\r])\n/i", "$1".$returnCode, $noIndent);

            //改行区切りで配列に入れる
            $lines = explode($returnCode, $noIndent);

            //各行に対して、深さを調べる
            $i = 0;
            $depth = 0;
            $lineIdx2depth = array();
            foreach($lines as $line)
            {
                if(strpos($line, "{")===0)
                    $depth++;

                $lineIdx2depth[$i] = $depth;

                if(strpos($line, "}")!==false)
                    $depth--;

                $i++;
            }

            //書き込む
            $i = 0;
            $ownIndent = "";
            foreach($lines as $line)
            {
                for($j=0; $j < $lineIdx2depth[$i]*4; $j++)
                    $ownIndent .= " ";
                $ownIndent .= $line;
                $ownIndent .= $returnCode;
                $i++;
            }

            return $ownIndent;

        }


        function killBlock($blockAlive)
        {
            $returnCode
            =
            strcmp($_POST['returnCode'], "r")===0 ? "\r":
            strcmp($_POST['returnCode'], "n")===0 ? "\n":
            "\r\n";

            return preg_replace("/".$returnCode."( |\t)*{( |\t)*/i", "", preg_replace("/".$returnCode."( |\t)*}( |\t)*".$returnCode."/i", "", $blockAlive));

        }

printHtml関数が実行されると、1-1でみたblock2indent.htmlの内容がphpにより出力される。
しかし今回はif(true)に対するelseとして記述されているため、printHtmlは呼び出されない。その代わり、$samplePyに対してkillIndent関数やblock2indent関数が適用されたものが出力される。

$samplePyの内容は、次の通り、{ }を用いて書かれた「謝ったpythonコード」である。

class Man:{
    def __inin__(self, name):{
        self.name = name;
        print("インスタンス生成および初期化に成功");
    }

    def hello(self):{
        print("Hello " + self.name + "!")
    }
}

m = Man("David");
m.hello();

これにkillIndent関数が適用されると、次のようになる。

class Man:{
def __inin__(self, name):{
self.name = name;
print("インスタンス生成および初期化に成功");
}

def hello(self):{
print("Hello " + self.name + "!")
}
}

m = Man("David");
m.hello();

さらにblock2indent関数が適用されると、次のように正しいpythonコードが出力される。

class Man:
    def __inin__(self, name):
        self.name = name;
        print("インスタンス生成および初期化に成功");    

    def hello(self):
        print("Hello " + self.name + "!")    

m = Man("David");
m.hello();

実際には次の手順に従って実行されている。

  1. {の直前と}の直後に改行がなければ挿入
  2. 各行ごとに{ }の深さ$depthの取得
  3. 各行ごとに4×$depth個だけ空白を先頭に追加
  4. {}を行ごと削除

1-3

プログラム自体は完成したので、後はサーバにアップロードして、外部からhttp通信でアクセスできるようにすることを考えよう。

1-2で書いたblock2indent.phpに対し、

/*/
if(isset($_POST['content']))
    print block2indent(killIndent($_POST['content']));
/*/
if(true)
    print block2indent(killIndent($samplePy));
//*/
else printHtml();

の部分の先頭に/を追加し、

//*/
if(isset($_POST['content']))
    print block2indent(killIndent($_POST['content']));
/*/
if(true)
    print block2indent(killIndent($samplePy));
//*/
else printHtml();

として、サーバへアップロードするだけでよい。
こうすることで、サーバ上のblock2indent.phpは次のように振舞うことになる。

  1. 自身へ何もpost送信されていない場合(=最初にアクセスされたとき)
    1. 1-1で書いたblock2indent.htmlと同じものを出力
    2. 送信ボタンが押されると、 自分自身へテキストエリア内の文字列をpost送信
  2. 自身へテキストエリア内の文字列がpost送信された場合
    1. 1-2のアルゴリズム通り、pythonコードへ変換を行う
    2. 結果を出力する

2.png
図1.3.1 「誤ったpythonコード」を入力する例

実際、図1.3.1のように入力し、送信ボタンを押すと、次のように正しいコードが出力された。(但し、改行コードの選択肢に
は含めていないので、「ソースを表示」から見る必要がある。)

class Woman:
    def __init__(self, name):
        self.name = name;
        print("インスタンス生成および初期化に成功");    

    def hello(self):
        print("Hello " + self.name + "!")    

m = Man("Alice");
m.hello();

入力例のインデントを多少変えても、出力は同じになる。

2. このプログラムを利用したい方

つまらないものですが、http://rights-for.men/block2indent.php からどうぞ。

3 n.参考

[1]https://python.keicode.com/lang/control-basic-rule.php

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

pythonでも中括弧{ }でブロックを作れるようにしてみた【php】

0. 動機

「pythonのブロックはc言語やjavaみたいに中括弧({ })でやるんじゃなくて、インデントを使うんだよ」というのを そういうものだと受け入れたら、今までできたはずの言語でうっかり同じことをしてしまい、構文エラーしまくることになる事例を知った。
僕自身はpythonについて、ほとんどしらない。
唯一知ってるのは「pythonがインデントに意味を持たせたのは有害」ということだけ。
→pythonには「1行1ステートメント」という考え方があり、だからこそブロックの括弧やセミコロンは不要[1]というのは後から知った。

そこで、インデントの代わりに{ }を使ってブロックを記述したpythonコードを、正しいpythonコードに置き換えるプログラムを作ろうという気になった。
phpがインストールされているサーバからみんなが使えるように、phpで作る。

1.プログラムの構成

  1. テキストエリアから、インデントの代わりにブロックで書かれたpyファイルの中身を受け取る(元コンテンツと呼ぶことにする)
  2. 元ファイルのブロックはインデントに置き換え、元ファイルのインデントは捨てる
  3. 結果を表示

1-1.

次のようなblock2indent.htmlを作ってみた。

<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01 Strict//EN" "http://www.w3.org/TR/html4/strict.dtd">
<html>
    <head>
        <meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
        <title>インデントが不要でブロックが使えるpython</title>
    </head>
    <body>
        ①インデントの代わりにブロックを用いたpythonコードを入力<br>
        <form method="post" action="block2indent.php">
            <textarea name="content" cols="50" rows="5"></textarea>
            <br>
            <br>
            ②改行コードを選択<br>
            <select name="returnCode">
                <option value="rn"> &#92;r&#92;n </option>
                <option value="n"> &#92;n </option>
                <option value="r"> &#92;r </option>
            </select>
            <br>
            <br><input type="submit">
        </form>
    </body>
</html>

余談だが、Eclipseで「PHP 開発ツール (PDT)」をインストールし、エディタに「<」とだけ打ち込むと、入力候補としてという選択肢が表示される。これを無視してバックスペースキーを押して「<」を消すと、cssやhtmlなど様々なコードのテンプレートが選べるようになる。これでhtml 4.01 strictのコードなども容易にかける。

block2indent.htmlをクロームで開くと、図1.1.1のように表示され、「送信」を押すと、「ファイルが見つかりません」のエラーへ遷移した。
1.png

図1.1.1 block2indent.html

1-2.

block2indent.htmlを名前変更しblock2indent.phpとした。
そして、次のように追記した。

<?php
$samplePy=
"
class Man:{
    def __inin__(self, name):{
        self.name = name;
        print(\"インスタンス生成および初期化に成功\");
    }

    def hello(self):{
        print(\"Hello \" + self.name + \"!\")
    }
}

m = Man(\"David\");
m.hello();
";

/*/
if(isset($_POST['content']))
    print block2indent(killIndent($_POST['content']));
/*/
if(true)
    print block2indent(killIndent($samplePy));
//*/
else printHtml();


function printHtml()
{
    print
'<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01 Strict//EN" "http://www.w3.org/TR/html4/strict.dtd">
<html>
    <head>
        <meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
        <title>インデントが不要でブロックが使えるpython</title>
    </head>
    <body>
        ①インデントの代わりにブロックを用いたpythonコードを入力<br>
        <form method="post" action="block2indent.php">
            <textarea name="content" cols="50" rows="5"></textarea>
            <br>
            <br>
            ②改行コードを選択<br>
            <select name="returnCode">
                <option value="rn"> &#92;r&#92;n </option>
                <option value="n"> &#92;n </option>
                <option value="r"> &#92;r </option>
            </select>
            <br>
            <br>
            ③<input type="submit">
        </form>
    </body>
</html>';
}

    function killIndent($indentAlive)
    {
        $indentDying = preg_replace("/\A( |\t)+/i", "", $indentAlive);
        return preg_replace("/(\n|\r|(\r\n))( |\t)+/i", "$1", $indentDying);
    }

    function block2indent($blockAlive)
    {
        //行頭以外でブロックが開始していたら、ブロック開始直前に改行挿入
        //同様に、ブロックの終わりも改行を強制
        $blockDying = putNewLineBeforeBlockOpen($blockAlive);
        $blockDying = putNewLineAfterBlockClose($blockDying);

        //各行ごとの「深さ」を調べ、その分だけスペース4つを挿入
        $blockDying = createIndent($blockDying);

        //ブロックをなくす
        return killBlock($blockDying);
    }

        function putNewLineBeforeBlockOpen($blockOpenWhileARow)
        {
            return preg_replace("/(.+){/i", "$1\r\n{", $blockOpenWhileARow);
        }

        function putNewLineAfterBlockClose($blockCloseWhileARow)
        {
            return preg_replace("/}(.+)/i", "}\r\n$1", $blockCloseWhileARow);
        }

        function createIndent($noIndent)
        {
            //改行コードを統一
            $returnCode
            =
            strcmp($_POST['returnCode'], "r")===0 ? "\r":
            strcmp($_POST['returnCode'], "n")===0 ? "\n":
            "\r\n";

            $noIndent = preg_replace("/\r([^\n])/i", $returnCode."$1", $noIndent);
            $noIndent = preg_replace("/([^\r])\n/i", "$1".$returnCode, $noIndent);

            //改行区切りで配列に入れる
            $lines = explode($returnCode, $noIndent);

            //各行に対して、深さを調べる
            $i = 0;
            $depth = 0;
            $lineIdx2depth = array();
            foreach($lines as $line)
            {
                if(strpos($line, "{")===0)
                    $depth++;

                $lineIdx2depth[$i] = $depth;

                if(strpos($line, "}")!==false)
                    $depth--;

                $i++;
            }

            //書き込む
            $i = 0;
            $ownIndent = "";
            foreach($lines as $line)
            {
                for($j=0; $j < $lineIdx2depth[$i]*4; $j++)
                    $ownIndent .= " ";
                $ownIndent .= $line;
                $ownIndent .= $returnCode;
                $i++;
            }

            return $ownIndent;

        }


        function killBlock($blockAlive)
        {
            $returnCode
            =
            strcmp($_POST['returnCode'], "r")===0 ? "\r":
            strcmp($_POST['returnCode'], "n")===0 ? "\n":
            "\r\n";

            return preg_replace("/".$returnCode."( |\t)*{( |\t)*/i", "", preg_replace("/".$returnCode."( |\t)*}( |\t)*".$returnCode."/i", "", $blockAlive));

        }

printHtml関数が実行されると、1-1でみたblock2indent.htmlの内容がphpにより出力される。
しかし今回はif(true)に対するelseとして記述されているため、printHtmlは呼び出されない。その代わり、$samplePyに対してkillIndent関数やblock2indent関数が適用されたものが出力される。

$samplePyの内容は、次の通り、{ }を用いて書かれた「謝ったpythonコード」である。

class Man:{
    def __inin__(self, name):{
        self.name = name;
        print("インスタンス生成および初期化に成功");
    }

    def hello(self):{
        print("Hello " + self.name + "!")
    }
}

m = Man("David");
m.hello();

これにkillIndent関数が適用されると、次のようになる。

class Man:{
def __inin__(self, name):{
self.name = name;
print("インスタンス生成および初期化に成功");
}

def hello(self):{
print("Hello " + self.name + "!")
}
}

m = Man("David");
m.hello();

さらにblock2indent関数が適用されると、次のように正しいpythonコードが出力される。

class Man:
    def __inin__(self, name):
        self.name = name;
        print("インスタンス生成および初期化に成功");    

    def hello(self):
        print("Hello " + self.name + "!")    

m = Man("David");
m.hello();

実際には次の手順に従って実行されている。

  1. {の直前と}の直後に改行がなければ挿入
  2. 各行ごとに{ }の深さ$depthの取得
  3. 各行ごとに4×$depth個だけ空白を先頭に追加
  4. {}を行ごと削除

1-3

プログラム自体は完成したので、後はサーバにアップロードして、外部からhttp通信でアクセスできるようにすることを考えよう。

1-2で書いたblock2indent.phpに対し、

/*/
if(isset($_POST['content']))
    print block2indent(killIndent($_POST['content']));
/*/
if(true)
    print block2indent(killIndent($samplePy));
//*/
else printHtml();

の部分の先頭に/を追加し、

//*/
if(isset($_POST['content']))
    print block2indent(killIndent($_POST['content']));
/*/
if(true)
    print block2indent(killIndent($samplePy));
//*/
else printHtml();

として、サーバへアップロードするだけでよい。
こうすることで、サーバ上のblock2indent.phpは次のように振舞うことになる。

  1. 自身へ何もpost送信されていない場合(=最初にアクセスされたとき)
    1. 1-1で書いたblock2indent.htmlと同じものを出力
    2. 送信ボタンが押されると、 自分自身へテキストエリア内の文字列をpost送信
  2. 自身へテキストエリア内の文字列がpost送信された場合
    1. 1-2のアルゴリズム通り、pythonコードへ変換を行う
    2. 結果を出力する

2.png
図1.3.1 「誤ったpythonコード」を入力する例

実際、図1.3.1のように入力し、送信ボタンを押すと、次のように正しいコードが出力された。(但し、改行コードの選択肢に<br>は含めていないので、「ソースを表示」から見る必要がある。)

class Woman:
    def __init__(self, name):
        self.name = name;
        print("インスタンス生成および初期化に成功");    

    def hello(self):
        print("Hello " + self.name + "!")    

m = Man("Alice");
m.hello();

入力例のインデントを多少変えても、出力は同じになる。

2. このプログラムを利用したい方

つまらないものですが、http://rights-for.men/block2indent.php からどうぞ。

3.参考

[1]https://python.keicode.com/lang/control-basic-rule.php

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Pythonを使って擬似的にプログラミング言語を自作する

環境

macOS Mojave 10.14.4
python 3.5.4
g++ 9.2.0

やりたいこと

なるべく気軽にプログラミング言語を作って遊びたい
できれば実行ファイルを生成したい

どういう風にコンパイルするか

・コンパイラを作る

これはそこまで気軽でない&技術的に厳しい...ので別の方法を考える

・トランスパイルをしてから既存のコンパイラを使ってコンパイルする

コンパイラを作るのよりは気軽だし、個人的に楽しむだけなので強引な書き方をしてもいいのでこれにする

とりあえずトランスパイラを作ってみる

正しい作り方は知らないが別に今回は深く考えずに作ってみる
変換元の言語はcesという名前にし、返還後はc++のコードにすることにした

cesの仕様

・基本的にはBASICのような構文(というよりDckuino.jsのような感じかも)
・トランスパイルするとき、関数の引数は対応表の多重リスト内の番号で判別する

まずは単純にトランスパイルをしたソースコードを出力してみる

ces.py
# 変数の宣言
inp=""
tmp=0
out=""
INPUT=[]
OUTPUT=[]
k=0

# トランスパイルの対応表
ces_command=["imp","usi","out","box","#","retu","{","}","func"]
cpp_command=[["#include <",1,">"],["using namespace ",1,";"],["cout << ",1," << endl",";"],[1," ",2," = ",3,";"],["// ",1],["return ",1,";","\n}"],["{"],["}"],[1," ",2,"()","{"]]

# ENDと入力されるまで入力されたcesのコードをリストに追記
while inp != "END":
    inp=input()
    INPUT.append(list(map(str,inp.split())))

# 出力を見やすくするために一行空ける
print()

# 対応表を使ってトランスパイル
for i in INPUT:
    if i[0] in ces_command:
        tmp = ces_command.index(i[0])
        for u in cpp_command[tmp]:
            if type(u) is int:
                if u<=len(i)-1:
                    out += i[u]
            else:
                out += u
        OUTPUT.append([out])
        out = ""
# トランスパイルした結果を出力 
for i in OUTPUT:
    for j in i:
        print(j)

結果

---入力---
# いつものおまじない
imp bits/stdc++.h
imp string
usi std
# 変数の宣言・代入
box int i 1
box int j 10
box string s “HelloWorld!”
# main関数
func int main
# 出力
out s
out i+j
# 0を返す
retu 0
# 終了
END

---出力---
// いつものおまじない
#include <bits/stdc++.h>
#include <string>
using namespace std;
// 変数の宣言・代入
int i = 1;
int j = 10;
string s = “HelloWorld!”;
// main関数
int main(){
// 出力
cout << s << endl;
cout << i+j << endl;
// 0を返す
return 0;
}
// 終了

うまくいった(インデントは汚いけど最終的にコンパイルするので気にしないでおく)

エラーコードを出力してみる

今のままだと存在しない命令を呼び出したときや、関数の引数が足りない場合(引数が多い場合、余計な引数は無視される)、無視してそのままトランスパイルされてしまうので、その場合はエラーメッセージを出力してトランスパイルを失敗させたい。
今回は存在しない命令を呼び出したときと関数の引数が足りないときの2パターン分のエラーメッセージを用意すればいいので、

"^\nerror!\nThere are not enough arguments\nRequired amount : "n

" ^\nerror!\nThere is no ","code"," instruction"

Google先生の協力のもとエラーメッセージを作成した
これを先ほどのコードに混ぜると

ces.py
#変数の宣言
inp=""
tmp=0
out=""
INPUT=[]
OUTPUT=[]
ERROR=[]
k=0
ind=0
bo=False

# トランスパイルの対応表
ces_command=["imp","usi","out","box","#","retu","{","}","func"]
cpp_command=[["#include <",1,">"],["using namespace ",1,";"],["cout << ",1," << endl",";"],[1," ",2," = ",3,";"],["// ",1],["return ",1,";","\n}"],["{"],["}"],[1," ",2,"()","{"]]

# ENDと入力されるまで入力されたcesのコードをリストに追記
while inp != "END":
    inp=input()
    if inp[0]=="!":
        INPUT.append(["!",inp[1:len(inp)]])
    else:
        INPUT.append(list(map(str,inp.split())))

# 出力を見やすくするために一行空ける
print()

# 対応表を使ってトランスパイル・エラーの判定
for ind,i in enumerate(INPUT,1):
    if i[0] in ces_command:
        tmp = ces_command.index(i[0])
        for u in cpp_command[tmp]:
            if type(u) is int:
                if u<=len(i)-1:
                    out += i[u]
                else:
                    k=0RR
                    for q in cpp_command[tmp]:
                        if type(q) is int and q > k:
                            k=q
                    ERROR.append([str(i)," ^\nline : "+str(ind)+" error!\nThere are not enough arguments\nRequired amount : "+str(k)])
                    bo=True
                    break
            else:
                out += u
        OUTPUT.append([out])
        out = ""
    elif i != ["END"]:
        ERROR.append([i," ^\nline : "+str(ind)+" error!\nThere is no ",i," instruction"])
        bo=True

# エラーが出ていなければトランスパイルした結果を出力し、エラーが出ていればエラーを出力する
if bo == True:
    for i in ERROR:
        for j in i:
            print(j)
        print()
else:
    for i in OUTPUT:
        for j in i:
            print(j)

こうなり、

---入力---
imp bits/stdc++.h      
hello
world
niceday
hello
box int
out
END

---出力---
['hello']
 ^
line : 2 error!
There is no 
['hello']
 instruction

['world']
 ^
line : 3 error!
There is no 
['world']
 instruction

['niceday']
 ^
line : 4 error!
There is no 
['niceday']
 instruction

['hello']
 ^
line : 5 error!
There is no 
['hello']
 instruction

['box', 'int']
^
line : 6 error!
There are not enough arguments
Required amount : 3

['out']
^
line : 7 error!
There are not enough arguments
Required amount : 1

うまくいった!

ここまできたらあとはコンパイルをするだけ

コンパイルをどうやってするか

とりあえずコンパイラはg++を使うのは確定しているので、cesファイルを読み込んでトランスパイルをしたfile.cppを生成して、 g++ "file.cpp" -o "file.out" みたいなコマンドを実行するようにすれば良さそう

ces → c++ → 実行ファイルまでを実装したもの

ces.py
# コマンド実行・エラー発生時に途中終了するためのモジュールを読み込む
import sys
import os

# 変数の宣言
inp=""
tmp=0
out=""
INPUT=[]
OUTPUT=[]
ERROR=[]
k=0
com=""
bo=False

# トランスパイルの対応表
ces_command=["imp","usi","impio","impbit","out","box","!","#","retu","{","}"]
cpp_command=[["#include <",1,">"],["using namespace ",1,";"],["#include <iostream>\nusing namespace std;"],["#include <bits/stdc++.h>\nusing namespace std;"],["cout << ",1," << endl",";"],[1," ",2," = ",3,";"],[1],["// ",1],["return ",1,";"],["{"],["}"]]

# 引数の取得
fni=sys.argv
fn=fni[1]
cn=fni[2]

# 入力を受け付ける代わりに第1引数で指定されたファイルを読み込み、cesのコードをリストに追記していく
_file=open(fn,"r")
for line in _file:
    inp=line
    if inp[0]=="!":
        INPUT.append(["!",inp[1:len(inp)]])
    else:
        INPUT.append(list(map(str,inp.split())))

# ファイルを閉じる
_file.close()

# 対応表を使ってトランスパイル・エラーの判定
for i in S:
    if i[0] in ces_command:
        tmp = ces_command.index(i[0])
        for u in cpp_command[tmp]:
            if type(u) is int:
                if u<=len(i)-1:
                    out += i[u]
                else:
                    k=0
                    for q in cpp_command[tmp]:
                        if type(q) is int and q > k:
                            k=q
                    ERROR.append([str(i)," ^\nerror!\nThere are not enough arguments\nRequired amount : "+str(k)])
                    bo=True
                    break
            else:
                out += u
        OUTPUT.append([out])
        out = ""
    elif i != ["END"]:
        ERROR.append([i," ^\nerror!\nThere is no ",i," instruction"])
        bo=True

# エラーが出ていなければトランスパイルした結果を第2引数で指定されたファイル.cppに上書きし、トランスパイルの成功を通知する。エラーが出ていればエラーを出力する
if bo == True:

    # エラーの出力を見やすくするために一行空ける
    print()
    for i in E:
        for j in i:
            print(j)
        print()
else:
    print(":) < ces trance was successful!")
    _file = open(cn+".cpp","w")
    for i in O:
        for j in i:
            _file.write(j)
    # ファイルを閉じ、第2引数で指定されたファイル名の実行ファイルを作成する
    _file.close()
    com="g++ "+cn+".cpp -o "+cn
    os.system(com)
    # トランスパイルしてcppファイルは消しておく
    com="rm "+cn+".cpp"
    os.system(com)

試してみる

cesファイル

ces_test.ces
imp bits/stdc++.h
imp string
usi std
box int i 1
box int j 10
box string s "hello"
box string t "world"
func int main null
out i+j
out s
out t
out "niceday"
retu 0

コンパイル

user:~ user$ python ces.py ces_test.ces ces_test.out
:) < ces trance was successful!
user:~ user$ ./ces_test.out
11
hello
world
niceday
user:~ user$ 

コンパイルまでできてる!!!

すごく嬉しい:)

最後に

思ったよりも簡単にできたし、何より楽しいので、ぜひ一度擬似的にプログラミング言語を自作してみることをオヌヌメします。今回は個人的に楽しむようなのでまだ機能不足すぎますが、これからどんどん強化するのも楽しそうです。機能の拡張をやり次第追記していこうと思います。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

IBM Cloud functionsを触ってみた〜その1〜

IBM CloudはWatsonのAPIを使うためだけのクラウドサービスってイメージが強いですが、実際はAWSやGCPのようにDBやアプリを動かすためのサーバーも提供されており、IBM Cloudだけでwebサービスをつくるなんてことも可能です。そしてAWSのlambdaのようなサーバーレス実行環境も存在します。その名も「IBM Cloud functions」。というわけで今回はこのIBM Cloud Functionsを使っていろいろ遊んでみようと思います。

IBM Cloud Functionsって何?

IBM Cloudで提供されているFunction as a Service(FaaS)のことです。Functionを実行するためのイベント(トリガー)が発生してからサーバーが立ち上がって、コードが実行されてサーバーが止まるまでの時間が動作時間として計算される仕組みになっています。もともとはApache OpenWhiskとして提供されていたようですが、2017年8月にIBM Cloud Functionsに名称変更されたようですが大きな仕様変更は無さそうです。標準ではNode.js, python, Ruby, Swift, PHP, Goが対応しています。それ以外の言語はdockerで構築した環境をデプロイすることで動作できるそうです。ちなみにライトプランでも使えます。

とりあえず動かしてみた

まずはお約束のHello Worldをやってみます。ダッシュボードを開いたらメニュー(左上の三本線)を開き、そこからfunctionsを開きます。

スクリーンショット 2019-10-01 23.51.51.png

「作業の開始」をクリックし、そこからアクションを作成します。ここで言うアクションがFunctionのコードのことです。ということで「Create Action」をクリックします。
スクリーンショット 2019-10-01 23.56.28.png
すると、新しいアクションの設定画面に移りますので、Function名と言語を定義します。今回はpython3を選択しています。

スクリーンショット 2019-10-01 22.38.09.png

Createをクリックすると、以下のようにコードエディタのある画面が表示されます。
スクリーンショット 2019-10-02 00.04.04.png

中にはたった3行のコードが最初から書かれています。まずはシンプルにこのコードを実行してみます。Invokeをクリックしてみると右側に以下のような実行結果が出力されます。

スクリーンショット 2019-10-02 00.09.48.png

Logsには何も表示されていませんが、コードの中にあるprintの出力やエラーがここに表示されます。デバッグに使えますね。

REST APIで動かしてみる

では、今度はREST API的な使い方をやってみます。左側のメニューから「Endpoints」を選択してエンドポイントの情報を表示させます。その後、下の方にあるCURLの情報があるので、右端の書類のアイコンをクリックしてCurlコマンドをコピーします。ちなみにAPI-KEYの部分にはすでにAPIキーが入っているのでターミナル上でそのままコピーしたコマンドを実行できるようになっています。

スクリーンショット 2019-10-01 22.42.29.png

Curlを実行すると、以下のようなjsonが出力されます。

{
  "activationId": "698e86c70a5f40bb8e86c70a5f50bbfd",
  "annotations": [
    {
      "key": "path",
      "value": "your@mail_dev/kmiuraHelloWorld"
    },
    {
      "key": "waitTime",
      "value": 42
    },
    {
      "key": "kind",
      "value": "python:3.7"
    },
    {
      "key": "timeout",
      "value": false
    },
    {
      "key": "limits",
      "value": {
        "concurrency": 1,
        "logs": 10,
        "memory": 256,
        "timeout": 60000
      }
    }
  ],
  "duration": 2,
  "end": 1569937475019,
  "logs": [],
  "name": "kmiuraHelloWorld",
  "namespace": "YOUR_NAMESPACE",
  "publish": false,
  "response": {
    "result": {
      "message": "Hello world"
    },
    "status": "success",
    "success": true
  },
  "start": 1569937475017,
  "subject": "YOUR_MAIL",
  "version": "0.0.1"
}

なにか通知してみる

LINE Notifyのアクセストークンを発行する

せっかくなので何か通知をするアクションを作成してみます。そこで、LINE Notifyを使います。こちらからログインして、アカウント名→マイページを開きます。

スクリーンショット 2019-10-02 01.04.42.png

マイページの下にある「トークンを発行する」をクリックして、Notify用のトークン名を設定し、「1:1でLINE Notifyから通知を受け取る」を選択してトークンを発行してください。

※発行されたトークンは発行画面を閉じると2度と確認することが出来ないので忘れずにコピーしておきます。

スクリーンショット 2019-10-02 01.06.04.png
スクリーンショット 2019-10-02 01.14.39.png

IBM Cloud Functionsでの設定

もう1度Cloud Functionsに戻って先程コピーしたアクセストークンを記入します。コードに直接書く方法もありますが、今回は「Parameters」に変数を定義してみます。今回はParameter NameにはAPI_TOKENを、Parameter Valueには先程コピーしたアクセストークンを記入します。このとき、アクセストークンは必ずダブルクオーテーションで囲みます。

スクリーンショット 2019-10-02 01.30.36.png

ここで定義した値はコード上では、dict["ACCESS_TOKEN"]で呼び出すことが出来ます。以上を踏まえて、LINE Notifyに通知するコードを足したコードが以下に示すものになります。

#
#
# main() will be run when you invoke this action
#
# @param Cloud Functions actions accept a single parameter, which must be a JSON object.
#
# @return The output of this action, which must be a JSON object.
#
#
import sys
import json
import requests

def main(dict):
    # set Value
    access_token = dict["ACCESS_TOKEN"]
    url = 'https://notify-api.line.me/api/notify'
    headers = {
        'Authorization': 'Bearer {}'.format(access_token),
    }
    payload = {
        'message': 'Hello World',
    }

    # request Notify
    response = requests.post(url, headers=headers, params=payload)
    res = json.loads(response.text)
    print(res)

    return { 'message': res["message"] }

エディタのコードを上記に置き換えて、右上にあるSaveをクリックしてからInvokeをクリックして実行します。すると、以下のようにメッセージが表示されれば、成功です。
IMG_6824.png

IBM Cloud Functionsを使えば、サーバの保守を気にすることなく作りたい機能を実装出来てしまうので魅力的だと思いますが、動作が遅かったりデメリットもあったのでそこは次回また詳しく掘り下げた後にまとめようと思います。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

ナップザック問題

入力

N W
w1 v1
w2 v2
  ・
 ・
wN vN

商品の数N、ナップザックの容量W、各商品の重さと値段スペース区切り

出力

重さW以下での価値の総和の最大値

コード

dp.py
N,W = map(int,input().split())
v = [0] * (N)
w = [0] * (N)
for i in range(N):
    w_t,v_t = list(map(int,input().split()))
    v[i] = v_t
    w[i] = w_t

dp = [[0] * (W+1) for _ in range(N+1)]

for j in range(N):
    for k in range(1,W+1):
        if k >= w[j]:
            dp[j+1][k] = max(dp[j][k],dp[j][k-w[j]]+v[j])
        else:
            dp[j+1][k] = dp[j][k]

print(dp[N][W])

参考文献

この記事は以下の情報を参考にして執筆しました。

-アルゴリズムイントロダクション

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む