Selenium初探(一) -- GBF Casino Poker (crawler篇)

日期:2020/07/13 (一)

一、前言

  • 之前為了上VK網抓色色coser的照片,有寫過一個小爬蟲程式,但因為BeautifulSoup只能爬取靜態的資訊,不能與網頁互動,只能抓到縮圖。因此為了點開小圖以存取大圖,最後是使用Selenium來達成。
  • 後來這種類似按鍵精靈的功能,馬上就在我為了每個月賭場的玉剛跟月光晶苦惱時,現出一道曙光。嘿嘿,我決定要來用爬蟲對這個遊戲做壞壞的事。

二、Selenium的設置及使用

  • 大體上只要使用pip install selenium,並且確保WebDriver和瀏覽器版本相符即可。

    以Google Chrome為例,因為Chrome好像會自動更新,所以每隔一段時間要隨之更新一下WebDriver。

  • 大致架構如下,把driver設定好之後就可以對他進行操作了:

    from selenium import webdriver
    
    HOME_URL = "http://game.granbluefantasy.jp/#mypage"
    LOGIN_URL = "http://game.granbluefantasy.jp/#authentication"
    
    try:
        chrome_options = webdriver.ChromeOptions()
        chrome_options.add_argument("--incognito")		# anonymous mode
        #chrome_options.add_argument("--headless")		# run in background
        driver = webdriver.Chrome(options=chrome_options)
    
        # do something with driver:
        #	driver.get(LOGIN_URL)
        #	driver.find_element_by_css_selector(".blah-blah")
        #	..., etc.
    
    finally:
        driver.quit()
        logging("driver safely closed.")
    
  • 值得注意的是,每次結束程式要記得用quit()關掉driver,不然從工作管理員可以看到他還在背景執行。

  • 此時用finally優雅的處理就是個不錯的寫法,這樣即使出錯跳出也可以適當的handle。

三、功能函式

  • wait_for_element():WebDriver API find_element_by_css_selector()的強化版。
    • 因為常常driver操作得太快,網頁還沒將目標element載入時,driver就找不到,並且丟出NoSuchElementException跳出。
    • WebDriver API中有針對此問題的解法:Implicit WaitExplicit Wait,因為網路上有許多資料便不詳述了。
    • 但我自己不太喜歡的點是,這兩種方法都需要設定一段時間作為等待的期限,超過了一樣會丟出Exception。而我並不希望因為使用時的網速、快取等環境因素影響這個期限的值,而且無端的跳出程式也不是很穩定。
    • 因此我想,不如就把Exception接住,然後無限次的嘗試直到目標出現,簡單暴力。
      def wait_for_element(selector, root=driver, log=False):
          while True:
              try:
                  element = root.find_element_by_css_selector(selector)
                  logging("element [%s] found."%selector, enable=log)
                  return element	# if no exception, then break/return
              except NoSuchElementException:
                  error("Not able to find element [%s]."%selector, enable=log)
                  time.sleep(0.1)
      
  • click_element():最重要的功能,不能點擊一切就不能運轉。但也是我遇到最大、停滯最久的瓶頸。
    • 在Selenium中,大致有以下幾種對於物件操作的方法,多半Google到的會是前三種,各有他們適用的場合。但對於GBF的結構來說,他們也分別有其不可行的理由,而我最後主要採用的是方法(4)。

    • 方法(1):selenium對DOM物件操作的API,印象中內部其實就是方法(2)。

          # method 1: selenium click api
          target.click()
      
    • 方法(2):driver.execute_script()直接呼叫DOM內定義的各種動作函式。

          # method 2: execute javascript 
          driver.execute_script("arguments[0].click();", target)
          #driver.execute_script("arguments[0].mousedown;", target)
          #driver.execute_script("arguments[0].tap;", target)
      
    • 方法(3):selenium另外一個特別的動作API,ActionChain。可以在視窗上的絕對位置(或物件的相對位置)來進行滑鼠的點擊等各種動作。

          # method 3: ActionChains
          action = webdriver.ActionChains(driver)
          action.move_to_element(target)
          action.move_by_offset(target.size["width"]/2, target.size["height"]/2)
          action.click().perform()
      
      • 由於ActionChain是根據坐標定位並模擬滑鼠動作,因此不會對特定的物件發出動作event。也就是說,若有多層物件,接收到event的物件會是最外層或DOM內script定義在此處監聽event的物件,因為ActionChain只管在這個坐標做動作,不管對象是誰。
      • 但是由於GBF的網頁結構為SPA(Single Page Application),一個物件即使乍看之下是最外層,實際上卻有可能被利用SPA隱藏的物件所擋住。在這種情況下,ActionChain的點擊就會被隱藏的物件接到,可能沒作用而被吃掉,或是因為Not iteractable而丟出Exception

      ※關於GBF網頁結構的詳細資訊可以參考這篇: どうしてグラブルの「戻る」はリロードより高速なのか

      • 不過呢,ActionChain也有其適用的場合。例如,我們想要操作<canvas/>裡面的物件,但由於它們是繪製出來的,並沒辦法從DOM抓到。如果可以確保canvasdisplay且在最外層,那我們就可以用ActionChain移動到畫布中,用相對位置對其中的物件進行操作。
      • 實際上,在GBF賭場裡,撲克牌就是由canvas繪製而成,所以如果想要hold牌,用其他方法是行不通的,只能使用ActionChain來達成。
    • 方法(4):以driver.execute_script()使用jQuery的API。類似方法(2),但是是透過jQuery。

          # method 4: with jquery
          # selector: (id, "#"), (class_name, ".")
          #root.execute_script("$('%s').trigger('tap')"%(selector))
          root.execute_script("$(arguments[0]).trigger('tap')", target)
      
      • 因為前三個方法都行不通,遍尋Google之際,找到了這位中國朋友寫的TemperMonkey腳本: GBF auto poker
      • 實際爬過了GBF內handle點擊的script之後,發現其中有使用 jQuery Finger 的套件,而且對於各物件都有針對jQuery定義tap動作的function,或許這就是為什麼方法(1)(2)行不通,而方法(4)卻可以的原因吧(?)。
      • 不過說實在的,因為我也沒寫過jQuery,實際的原因我還真說不清楚==,總之就是可以用了。
    • 最終的function長這樣:

          def click_element(selector=None, root=driver, target=None, log=False):
              if target == None:
                  target = wait_for_element(selector, root=root, log=log)
              # method 4: with jquery
              root.execute_script("$(arguments[0]).trigger('tap')", target)
              logging("element [%s] clicked."%selector, enable=log)
      
  • wait_till_displayed()
    • 在整個Poker的流程裡,我用來判斷不同state的依據不是跳出來的提示文字(不然還要處理英文跟日文兩種),而是最底下操作的按鈕。
    • 由於SPA的關係,這些不同狀態的按鈕,在沒被用到的時候不是被刪掉了,而是被hide起來了。也就是說無論何時,我們都可以找到全部的按鈕。所以這時候不能使用上面的wait_for_element(),而是要檢查style裡面的display狀態。※is_displayed()是123456
    • 789
          def wait_till_displayed(element):
              if element == "start-or-double":
                  start_btn = wait_for_element(".prt-start")
                  double_btn = wait_for_element(".prt-yes")
                  while True:
                      if start_btn.is_displayed():
                          return "start"
                      if double_btn.is_displayed():
                          return "double"
                      time.sleep(0.1)
              else:
                  btn_name = {
                      "ok": ".prt-ok",
                      "start": ".prt-start",
                      "double": ".prt-double-select",
                  }
                  btn = wait_for_element(btn_name[element])
                  while not btn.is_displayed():
                      pass
                  return "done"
      
  • pick_cards():hold撲克牌的規則,但其實只是實現我自己玩牌時的邏輯而已。
    • 理論上不會在同場出現同樣的牌,所以用取後不放回(combination)計算。
    • 但是同樣的牌有時候在出現之後很快又再出現一次,所以推測每一場都是獨立亂數,算牌應該沒有用。
    • 簡單無腦的規則:
      • Joker必選
      • 數字一樣的必選
    • 需要多花力氣去看的規則,這時候電腦就比肉眼方便很多:
      • 同花和順子:沒有一樣數字的情況下,有三張以上就可以選。
      • 關於有鬼牌時的計算:
            4-flush: 11/50 = 22 %
            4-straight: space x 4/50 => 8 ~ 48 %
            Note: space examples:
                1 space:  5[]678 => 4/50 = 8 %
                2 spaces: []5678[] => 8/50 = 16 %
                3 spaces: []5[]67[] with 1 Joker => 12/50 = 24 %
                4 spaces: [][]567[][] with 1 Joker => 16/50 = 32 %
                5 spaces: [][]5[]6[][] with 2 Jokers => 20/50 = 40 %
                6 spaces: [][][]56[][][] with 2 Jokers => 24/50 = 48 %
        
    • 嗯。。。因為期望值是正的,所以差不多差不多就好,哈!

四、賭場Poker的流程圖

  • 後續大致上是按照這個流程實作。因為我主要是用按鈕來區分各個state,所以在流程圖內強調按下了哪個按鈕。
  • 這裡會畫出CAPTCHA是因為,若是因遊玩時間太長跳出來,他會固定在一局結束後跳出來,如果後續在GUI方面想要對此做特別的處理,就可以注意一下。
  • 另外註解一下,※continue?這個state主要是用一些衡量「保守程度」的參數來判斷是否要繼續賭Double Up,可供使用者調整,看是想要賭到底一次發財,還是慢慢累積。
    • card_range:遇到哪些牌面就不繼續賭,像是預設是6到10。
    • bet_threshold:賭注加倍到一定金額後就會開始看上述的牌面決定要不要繼續玩加倍。

五、成果及總結

  • (不知道為什麼這個片段一直輸xD)
  • 其實寫到後來想想,只以目前介紹的部分來說,確實可以只用TemperMonkey等JS腳本來寫就好。
  • 不過為了讓使用者可以自由訂定「保守程度」的參數,而且其實本來是想要公布給朋友們使用的,所以還是寫了個GUI。
  • 但我沒想到,寫GUI才是真正麻煩的開始。。。為了防呆好累啊。。。

後記:結果馬上在下一個古戰場前被Ban了,隔了幾個月後開新帳號捲土重來,千萬別以身試法和營運作對啊~