Agouti(Go言語)でブラウザを自動操作する

ご挨拶

2023年の4月にAITPに入社しました壱岐と申します。

主に業務でちょっと役に立つようなTIPSや、性能改善に繋がりそうなことを投稿していくかと思いますのでよろしくお願いします!

はじめに

ブラウザを自動操作することでE2Eテスト的に使用することができたり、業務の一部を自動化することで業務効率の向上につながると思います。

各言語様々なライブラリがありますが、その中でも今回はGo言語のAgouti (ドキュメント: https://pkg.go.dev/github.com/sclevine/agouti ) というライブラリの入門編的使い方について紹介したいと思います。

前提条件

  • Go言語のバージョン: go1.20.4 windows/amd64
  • Chromeのバージョン: 113.0.5672.63
  • 使用OS: Windows 10

自動操作を行う対象として、以下の練習サイトをお借りして使用します。

https://hotel.testplanisphere.dev/ja/

前準備

使用しているChromeのバージョンに合わせた ChromeDriverを、

https://chromedriver.chromium.org/downloads からダウンロード後適当なディレクトリに移動してPathを通しておきます。

実装サンプル

とりあえずログインしてみる

※設定値をファイル化したり、エラー処理をしたりということをあまり意識せず、 とりあえずログイン処理を自動で行うところまで実装した例です。

コード

package main

import (
    "fmt"
    "log"

    "github.com/sclevine/agouti"
)

func main() {

    driver := agouti.ChromeDriver(
        agouti.ChromeOptions( // 起動オプション
            "args", []string{
                "--window-position=1280,0",
                "--window-size=640,1080",
            },
        ),
        agouti.ChromeOptions( // エラーの抑制
            "excludeSwitches", []string{
                "enable-logging",
            },
        ),
    )

    // 終了時にドライバも終了する
    defer driver.Stop()

    if err := driver.Start(); err != nil {
        log.Fatalf("Failed to start driver:%v", err)
        return
    }

    page, err := driver.NewPage()
    if err != nil {
        log.Printf("Failed to open page:%v", err)
        return
    }

    // ログインページに遷移
    if err := page.Navigate("<https://hotel.testplanisphere.dev/ja/login.html>"); err != nil {
        log.Printf("Failed to navigate:%v", err)
        return
    }

    // ログイン情報を入力
    page.FindByID("email").Fill("ichiro@example.com")
    page.FindByID("password").Fill("password")

    // ログインボタンをクリックする
    page.FindByID("login-button").Submit()

    // ログイン後のユーザ名を取得して表示
    username, _ := page.FindByID("username").Text()
    fmt.Println("ユーザ名: " + username)
}


E2Eテスト的な実装も加える

上記のコードに加えて表示される要素一つ一つを取得し、予測値と比較していくことで確認していきます。

// ログイン後の表示取得
email, _ := page.FindByID("email").Text()
username, _ := page.FindByID("username").Text()
rank, _ := page.FindByID("rank").Text()
address, _ := page.FindByID("address").Text()
tel, _ := page.FindByID("tel").Text()
gender, _ := page.FindByID("gender").Text()
birthday, _ := page.FindByID("birthday").Text()
notification, _ := page.FindByID("notification").Text()

if email != "ichiro@example.com" {
    log.Printf("メールアドレスが異なります")
}
if username != "山田一郎" {
    log.Printf("氏名が異なります")
}
if rank != "一般会員" {
    log.Printf("会員ランクが異なります") // プレミアム会員のためこのメッセージが表示される
}
if address != "東京都豊島区池袋" {
    log.Printf("住所が異なります")
}
if tel != "01234567891" {
    log.Printf("電話番号が異なります")
}
if gender != "男性" {
    log.Printf("性別が異なります")
}
if birthday != "未登録" {
    log.Printf("生年月日が異なります") 
}
if notification != "受け取る" {
    log.Printf("お知らせが異なります")
}

よく使いそうなメソッド

FindXXX系/AllByXXX系

上記のサンプルではIDを基準に要素を検索しましたが、name属性で検索するFindByNameやClass属性で検索するFindByClass、XPathで検索を行うFindByXPathなどがあります。

しかし、Class属性で下記のようにする場合、単一のものとして取得できないためambiguous findのエラーとなります。

listGroupItem := page.FindByClass("list-group-item")
fmt.Println(listGroupItem.Text())

// --->
// (実行時エラー)
// failed to select element from selection 'Class: list-group-item [single]': ambiguous find

複数の要素が対象となる検索する場合は、AllByXXX系のメソッドを使用します。

// クラス名で要素を取得し、取得できた全要素のテキストを表示する例
listGroupItem := page.AllByClass("list-group-item")
count, _ := listGroupItem.Count()
for i := 0; i < count; i++ {
    fmt.Println(listGroupItem.At(i).Text())
}

詳しくはドキュメントのFindXXXの箇所や、AllByXXXの箇所を参照してください。

JavaScriptを実行する

https://pkg.go.dev/github.com/sclevine/agouti#Page.RunScript

RunScriptで任意のJavaScriptを実行することができ、戻り値を受け取ることもできます。

var rtn interface{}
page.RunScript(
    "return new Date(ymd).getFullYear();",       // 実行するJavaScript
    map[string]interface{}{"ymd": "2023/01/23"}, // 引数をmapで渡す
    &rtn,                                        // 戻り値の受け取り
)

fmt.Println(rtn) // → 2023

スクリーンショットを撮る

https://pkg.go.dev/github.com/sclevine/agouti#Page.Screenshot

ファイル名を指定してScreenshotを呼ぶことで、画面のスクリーンショットを撮ることができます。

fileName := "screenshot.png"
page.Screenshot(fileName)

画面全体のスクリーンショットを撮る

参考: https://tamago-engineer.hateblo.jp/entry/2018/08/01/184053

JavaScript実行でbodyのサイズを取得し、ページのサイズを指定後にScreenshotを呼ぶことで画面全体のスクリーンショットを撮ることができます。

var width, height int
fileName := "screenshot.png"
page.RunScript("return document.body.scrollHeight;", nil, &height)
page.RunScript("return document.body.scrollWidth;", nil, &width)
page.Size(width, height)
page.Screenshot(fileName)

ただし、画面サイズは最初に指定するChromeDriverの起動オプション依存のため、フルHDサイズであれば"--window-size=1920,1080"のようにする必要があります。

その他注意事項等

自分で管理するサイト以外に使用する場合には、利用規約に目を通して規約違反とならないようにしましょう。

また、規約違反ではないサイトであったとしても、負荷がかかりすぎないように適度にスリープを入れる等の対処を行いましょう。

余談

https://github.com/sclevine/agouti は2021年の4月にReadMeに下記のようなコミットがあり、

NOTE: Agouti is no longer actively maintained. This was one of my earliest Go projects, and I am no longer happy with the API design. I recommend selecting an alternative WebDriver client for Go.

https://github.com/sclevine/agouti/blob/master/README.md

実際に最後のリリースが行われたのも2018年の3月とやや古めのライブラリですが、Go言語の書きやすさとGo言語でブラウザ操作(クリックさせたり、JavaScriptの実行をしたり)ができるライブラリが2023年5月現在では他に見当たらないため、私は現在でも使用しています。

Go言語にこだわらなければ https://www.selenium.dev/ja/documentation/ (JavaやPythonなど)であったり、Go言語でもブラウザ操作の必要がないスクレイピングを行う場合は https://github.com/gocolly/collyhttps://github.com/PuerkitoBio/goquery といったライブラリを使ってみるのも良いかもしれません。(機会があればこれらのツールも試してみて記事化してみようと思います。)

終わりに

以上、Agoutiの入門編的紹介となります。
うまく使って自動化ライフを~!

関連するタグ