調べ物した結果

現役SEが仕事と直接関係ないことを調べた結果とか感想とか

CookieClickerをPython Selenium自動的に進める試み②~スクリーンショットとアップグレード~

何をしたか

これの続き

couraeg.hatenablog.com

実行したら実行しっぱなしだったから時間指定できるようにした

最後の状態が知りたいので最終段階でスクショをとるようにした

画面右側のアップグレード品を全然買わないから買うようにした

バージョンアップごとにクラスわけるようにするために、割と無茶な継承のさせかたをした(listにメソッドぶっこんで順次実行するスタイル)

ソース

sample.py

from GetElepsed import GetElepsed

from selenium import webdriver
from selenium.webdriver.common.action_chains import ActionChains
from selenium.webdriver.common.keys import Keys
from pathlib import Path
import datetime

class Sample(object):
    
    # driverpath=各種Webドライバーexeのフルパス
    def __init__(self, driverpath):
        # ドライバーパスを指定 
        self.driver = webdriver.Chrome(driverpath)
        # URLに移動
        self.driver.get("http://natto0wtr.web.fc2.com/CookieClicker/")

    def isOverLimitTime(self, start ,limitTime) :
        try :
            return GetElepsed.getElepsedNow(start) > limitTime
        except Exception as e :            
            print(e)

    # フォルダ作ってスクショ
    def screenshot(self):
        try:
            p = Path('./images/' + self.__class__.__name__  + '/')
            p.mkdir(parents=True, exist_ok=True)
            
            now = datetime.datetime.now()
            imgpath = str(p.resolve()) + "\\" + now.strftime("%Y%m%d%H%M%S%f") + "_last.png"

            self.driver.get_screenshot_as_file(imgpath)
        except Exception as e :
            print(e)

    def clickBigCookie(self) :
        # cookieボタン見つけてクリックさせる
        try :
            element = self.driver.find_element_by_id("bigCookie")
            element.click()
        except Exception as e :
            print(e)
    def getBeforeExecFunctions(self) :
        self.beforeExecFunctions = []
    def getBeforeDoExecFunctions(self) :
        self.beforeDoExecFunctions = []
    def getDoExecFunctions(self):
        self.doExecFunctions = []
        self.doExecFunctions.append(self.clickBigCookie)        
    def getAfterDoExecFunctions(self) :
        self.afterDoExecFunctions = []
    def getAfterExecFunctions(self) :
        self.afterExecFunctions = []        

    # 各メソッド埋め込む処理
    # 開始終了以外は継承先で介入できるように適当に
    def init(self):
        self.getBeforeExecFunctions()
        self.getBeforeDoExecFunctions()
        self.getDoExecFunctions()
        self.getAfterDoExecFunctions()
        self.getAfterExecFunctions()

    def beforeExec(self):
        for func in self.beforeExecFunctions:
            try :
                func()
            except Exception as e :
                print(e)               

    def afterExec(self):
        for func in self.afterExecFunctions :
            try :
                func()
            except Exception as e :                    
                print(e)

    def beforeDoExec(self):
        for func in self.beforeDoExecFunctions :
            try :
                func()
            except Exception as e :                    
                print(e)

    def doExec(self) :
        for func in self.doExecFunctions:
            try :
                func()
            except Exception as e :                    
                print(e)
    def afterDoExec(self):
        for func in self.afterDoExecFunctions:
            try :
                func()
            except Exception as e :                    
                print(e)
            
    # limitTime:指定時間を超えたら止める.
    def exec(self, limitTime):
        self.init()

        #計測開始(開始時間も経過で処理する場合もあるから定義)
        self.start = datetime.datetime.now()

        # 前処理
        # ---ここから介入---
        self.beforeExec()
        while (not self.isOverLimitTime(self.start, limitTime)) :
            try :
                self.beforeDoExec()
                self.doExec()
                self.afterDoExec()
            except Exception as e :
                print(e)
        # 後処理
        self.afterExec()
        # ---ここまで介入---

        #スクショ取っておく
        self.screenshot()
        #終了
        self.driver.quit()

Sample.pyの解説

    # フォルダ作ってスクショ
    def screenshot(self):
        try:
            p = Path('./images/' + self.__class__.__name__  + '/')
            p.mkdir(parents=True, exist_ok=True)
            
            now = datetime.datetime.now()
            imgpath = str(p.resolve()) + "\\" + now.strftime("%Y%m%d%H%M%S%f") + "_last.png"

            self.driver.get_screenshot_as_file(imgpath)
        except Exception as e :
            print(e)

ここでスクショとってる。どうにもディレクトリ回りがC#とくらべて面倒だった

self.driver.get_screenshot_as_file(path)にパスを渡せばいいだけなんだけど。。。ディレクトリの指定ミスってても「吐き出さない」だけでエラーにもならないから面倒だった

   def getBeforeExecFunctions(self) :
        self.beforeExecFunctions = []
    def getBeforeDoExecFunctions(self) :
        self.beforeDoExecFunctions = []
    def getDoExecFunctions(self):
        self.doExecFunctions = []
        self.doExecFunctions.append(self.clickBigCookie)        
    def getAfterDoExecFunctions(self) :
        self.afterDoExecFunctions = []
    def getAfterExecFunctions(self) :
        self.afterExecFunctions = []        

    # 各メソッド埋め込む処理
    # 開始終了以外は継承先で介入できるように適当に
    def init(self):
        self.getBeforeExecFunctions()
        self.getBeforeDoExecFunctions()
        self.getDoExecFunctions()
        self.getAfterDoExecFunctions()
        self.getAfterExecFunctions()

    def beforeExec(self):
        for func in self.beforeExecFunctions:
            try :
                func()
            except Exception as e :
                print(e)               

    def afterExec(self):
        for func in self.afterExecFunctions :
            try :
                func()
            except Exception as e :                    
                print(e)

    def beforeDoExec(self):
        for func in self.beforeDoExecFunctions :
            try :
                func()
            except Exception as e :                    
                print(e)

    def doExec(self) :
        for func in self.doExecFunctions:
            try :
                func()
            except Exception as e :                    
                print(e)
    def afterDoExec(self):
        for func in self.afterDoExecFunctions:
            try :
                func()
            except Exception as e :                    
                print(e)

ド腐れゾーン。何が正解かわからないけど作ってるうちに楽しくなってしまった

~Functionsに関数をそのままぶち込んで、ループで各関数を読んでる。継承先もどしどしFunctionsにぶち込んで呼び出しは親に任せるというだいぶアレな作り。

継承先のSample2.pyでその威力がわかるのでそっち見たほうが早いかも

    # limitTime:指定時間を超えたら止める.
    def exec(self, limitTime):
        self.init()

        #計測開始(開始時間も経過で処理する場合もあるから定義)
        self.start = datetime.datetime.now()

        # 前処理
        # ---ここから介入---
        self.beforeExec()
        while (not self.isOverLimitTime(self.start, limitTime)) :
            try :
                self.beforeDoExec()
                self.doExec()
                self.afterDoExec()
            except Exception as e :
                print(e)
        # 後処理
        self.afterExec()
        # ---ここまで介入---

        #スクショ取っておく
        self.screenshot()
        #終了
        self.driver.quit()

ここがメインのロジック部分。各~Execの実行で~Functionsに埋め込まれたメソッドを実行する。からここはタイミングを定義してるだけのメソッド

Sample2.py

from Sample import Sample
from selenium import webdriver
from selenium.webdriver.common.action_chains import ActionChains
from selenium.webdriver.common.keys import Keys
import re

class Sample2(Sample):   
    #お店をクリックする(100回以下は無視)
    def upgradsClick(self):
        if self.loopCount < 100:
            return
        try:
            #<div onclick="Game.UpgradesById[56].click(event);" 
            # Game.tooltip.wobble();}" id="upgrade1" style="background-position:-528px -144px;"></div>
            for var in range(0,1000):
                element = self.driver.find_element_by_id("upgrade" + str(var))
                element.click()              
        except Exception as e :
            print("productClick:"+ str(e))  
    
    #施設をクリックする(100回以下は無視)
    def productClick(self) :
        if self.loopCount < 100:
            return
        for var in reversed(range(0,20)):           
            try:
                #要素の検証したらIDがproduct+連番だった。
                #<div class="product unlocked enabled" onmouseout="Game.tooltip.shouldHide=1;
                # " id="product0"><div class="icon off" id="productIconOff0" style="background-position:
                #要素の数は適当
                    element = self.driver.find_element_by_id("product" + str(var))
                    element.click()
            except Exception as e :
                print(e)

    # ループのカウンタを定義して初期化
    def initLoopCount(self):
        self.loopCount = 0

    # ループカウンタを加算。100を超えてる場合は戻す
    def addLoopCount(self):
        if (self.loopCount < 100) :
            self.loopCount = self.loopCount + 1
        else:
            self.loopCount = 0
    #メソッドリストにぶち込む
    def getBeforeExecFunctions(self):
        super().getBeforeExecFunctions()
        self.beforeExecFunctions.append(self.initLoopCount)

    def getDoExecFunctions(self):
        super().getDoExecFunctions()
        self.doExecFunctions.append(self.upgradsClick) 
        self.doExecFunctions.append(self.productClick)

    def getAfterDoExecFunctions(self):
        super().getAfterDoExecFunctions()
        self.afterDoExecFunctions.append(self.addLoopCount)

Sample2.pyの解説

    #メソッドリストにぶち込む
    def getBeforeExecFunctions(self):
        super().getBeforeExecFunctions()
        self.beforeExecFunctions.append(self.initLoopCount)

    def getDoExecFunctions(self):
        super().getDoExecFunctions()
        self.doExecFunctions.append(self.upgradsClick) 
        self.doExecFunctions.append(self.productClick)

    def getAfterDoExecFunctions(self):
        super().getAfterDoExecFunctions()
        self.afterDoExecFunctions.append(self.addLoopCount)

上で説明してたやつ。functionsのゲッターをオーバーライドして、任意のメソッドを突っ込むだけ

これであとは基底クラスの意図したタイミングでメソッドが呼ぼれるわけです。もしかしたらいけるか。ぐらいのつもりで作ったけど動いて感動した

実行前処理と、ループのメイン処理、ループの後処理にそれぞれメソッドを突っ込んでる

[f:id:couraeg:20190315004424p:plain]    #施設をクリックする(100回以下は無視)
    def productClick(self) :
        if self.loopCount < 100:
            return
        for var in reversed(range(0,20)):           
            try:
                #要素の検証したらIDがproduct+連番だった。
                #<div class="product unlocked enabled" onmouseout="Game.tooltip.shouldHide=1;
                # " id="product0"><div class="icon off" id="productIconOff0" style="background-position:
                #要素の数は適当
                    element = self.driver.find_element_by_id("product" + str(var))
                    element.click()
            except Exception as e :
                print(e)

メインの処理はここ

例のごとくfind_element_by_id("product" + str(var)で拾った

chromeでポチポチするだけで「product」というIdであることは容易にわかる

f:id:couraeg:20190315002243p:plain

ほしいところを右クリックして、検証を選ぶ

f:id:couraeg:20190315002246p:plain

ソースがでてくるからそれっぽいところを探す。(bigcookieもメインのJSじっくり見なくてもこれですぐ見つかった。

Js直接参照したりしない限り、この方法で大体IDつかめるのでいいとおもう

比較

f:id:couraeg:20190315004424p:plain


1時間ほど自動で走らせた結果。
左が何もアップデートしていないほう。右が自動でアップデートするほう。
Cps(くっきーぱーせこぉんず)も確実に伸びている。

100クリックごとに1回、各施設とアップデートができるか検査して、適当に購入しているが、
半日走らせたけど思ったよりCpsが上がってなかった。
調べたら「施設の価格が何を買っても全体的に増加する」せいで、
適当にチラ見しながら一番高い施設買ったほうがCpsの上りが大きいという悲しい結果だった。
もうちょいと「施設・アップデート」の購入アルゴリズムを見直さないと、放置してても大して成果があがらないので
その辺だけチューニングする。 

所感

・全然関係ないところ(継承回り、datetimeクラスの取り扱い、Mockの知識)にはまって楽しかった。
・開発者ツールすごい。検証ボタンやばい!
・スクショまでとれるなんてSelenium素敵!
python結構いい加減に書いても動くな
・Mockなにそれ?から「あーあれね。はいはい。あれね」ぐらいにはなった。

 GitHub

github.com