C#でSeleniumをやめてPlaywrightを使ってみた2

起動時タブが2つ開くのをなおす

前回、立ち上げ時に2つタブが開き、2つ目のほうに指示したURLのページが表示されるという現象が起きた。

枠内をクリックするとソースがクリップボードにコピーされます
            var ページ = await ブラウザコンテキスト.NewPageAsync();

理由は↑この行らしい。NewPageしているので、起動時にabout:blankのほかに、新しいページを開こうとしているということ。Chromeではかならず立ち上げ時に何らかのページはある。

枠内をクリックするとソースがクリップボードにコピーされます
            var ページ = ブラウザコンテキスト.Pages[0];

と、いうことで、単純にPagesの配列の[0]は保証されているんではないか。

Playwright Chromeのタブを開始時タブ1つに

はい、うまくいきました。

起動時複数タブが開く設定の時の動作

あとは、Chromeの設定によっては最初から複数タブが開かれるときがある。

Playwright Chromeで起動時の設定を変えると…

起動時の設定で、前回開いていたページを開くとか、特定のページセットを開くを設定している場合。上ではbingとか、yahooを開いたまま前回のページを開く設定で終了して、再立ち上げしてみる。

Playwright Chrome起動直後

Playwright Chromeのタブが予想外の更新?

最初は、閉じる前のページ+about:blankのタブが追加されて開き、about:blankにフォーカスが当たっている状態だった。yahoo.comの読み込み終了までしばらく待ったかと思ったら、yahoo.comのタブがgoogle.comに移動した。

これは…Pages[]って左のタブから順番ではない?

ページ.GotoAsync()のあとに以下を付け加えて同じ状況を再現してみる。

枠内をクリックするとソースがクリップボードにコピーされます
            Debug.WriteLine("ページ数? = " + ブラウザコンテキスト.Pages.Count.ToString());
            foreach(var タブ in ブラウザコンテキスト.Pages)
            {
                Debug.WriteLine("ページURL = " + タブ.Url);
            }

で、今回は左から2番目の「設定」のタブがgoogle.comに移動して、デバッグの出力結果はこんな↓。

PlaywrightでPagesの数が想定外

まてまてまてまて。タブ5個開いているぞ。GotoAsync()はawaitしているから、確実に5タブあるはずなのに。

調べてみたけどよくわからんかった。そういう時は、知らんぷり。起動時設定はabout:blankだけ開くように指定する。それ以上、何が必要であるものか…。(ものぐさ)


ブラウザを閉じたら、アプリも終了させるようにする

つづいて、ブラウザが終了してもウィンドウフォームが残ってしまってうっとおしいので、フォームを閉じる…というか、アプリケーションを終了させよう。

ちょっと面倒なので、ブラウザの操作の部分をクラス化して、Closeイベントを用意する。

枠内をクリックするとソースがクリップボードにコピーされます
        private class Chromeコントロール
        {
            public event EventHandler? Close;

            private IPlaywright? playwright;
            private IBrowserContext? ブラウザコンテキスト;
            private IPage? ページ;

            public async Task ブラウザ起動()
            {
                playwright = await Playwright.CreateAsync();

                ブラウザコンテキスト = await playwright.Chromium.LaunchPersistentContextAsync(
                    @"C:\ChromeProfile",
                    new BrowserTypeLaunchPersistentContextOptions
                    {
                        Headless = false,
                        Channel = "chrome"
                    }
                );

                ブラウザコンテキスト.Close += ブラウザコンテキスト_Close;

                ページ = ブラウザコンテキスト.Pages[0];
                await ページ.GotoAsync("https://www.google.com");
            }

            private void ブラウザコンテキスト_Close(object? sender, IBrowserContext e)
            {
                Close(this, EventArgs.Empty);
            }
        }

ブラウザコンテキストのCloseイベントはブラウザが落ちたとか、そんな感じのやつ。LaunchPersistentContextAsync()を使う場合は、Browserが使えないのでこうする。LaunchPersistentContextAsync()の説明にも、ブラウザコンテキストがCloseされたときはブラウザもCloseされると書いてあるし。

で親側は

枠内をクリックするとソースがクリップボードにコピーされます
        public Form1()
        {
            InitializeComponent();

            ブラウザ.Close += ブラウザ終了;
            Task.Run(() => ブラウザ.ブラウザ起動()).Wait();
        }

        Chromeコントロール ブラウザ = new Chromeコントロール();

        void ブラウザ終了(object? sender, EventArgs e)
        {
            Application.Exit();
        }

こんな感じにしとけば、ブラウザ終了時にフォームも終了する。


新しいタブを開く、閉じる

つづいては、タブの追加のしかた。これは簡単っぽい。最初のabout:blankのほかにタブが開いてしまうという現象で立証済み。

ブラウザコンテキストでNewPageAsync()をつかえばタブが追加されるので、あとはGotoAsync()なりなんなりすればいいし、複数タブを開きたいときはNewPageAsync()を好きなだけやればいい。

枠内をクリックするとソースがクリップボードにコピーされます
                var ページ1 = ブラウザコンテキスト.Pages[0];
                await ページ1.GotoAsync("https://www.google.com");

                var ページ2 = await ブラウザコンテキスト.NewPageAsync();
                await ページ2.GotoAsync("https://www.bing.com");

                var ページ3 = await ブラウザコンテキスト.NewPageAsync();
                await ページ3.GotoAsync("https://www.yahoo.com");


                // 開いたタブを閉じる
                await ページ2.CloseAsync();
                //await ブラウザコンテキスト.Pages[1].CloseAsync();

                await ページ3.CloseAsync();
                //await ブラウザコンテキスト.Pages[1].CloseAsync();

ページ2とページ3をわざわざ作っているが、ブラウザコンテキスト.Pages[]でもいい。Pages.Count = 3のときに、Pages[1].CloseAsync()すると、Pages[2]よりうしろは自動的にひとつ前に詰められる形になるので、もう一度Pages[1].CloseAsync()でページ2とページ3が閉じられる。

ページ2がプログラムなり、手動なりで既に閉じている場合はIsClosedプロパティがTrueかどうかでわかる。

枠内をクリックするとソースがクリップボードにコピーされます
                var ページ1 = ブラウザコンテキスト.Pages[0];
                await ページ1.GotoAsync("https://www.google.com");

                var ページ2 = await ブラウザコンテキスト.NewPageAsync();
                await ページ2.GotoAsync("https://www.bing.com");

                var ページ3 = await ブラウザコンテキスト.NewPageAsync();
                await ページ3.GotoAsync("https://www.yahoo.com");


                // 開いたタブを閉じる
                await ページ2.CloseAsync();
                //await ブラウザコンテキスト.Pages[1].CloseAsync();

                Debug.WriteLine("ページ2状態 = " + ページ2.IsClosed.ToString()); // True
                Debug.WriteLine("ページ3状態 = " + ページ3.IsClosed.ToString()); // False
                Debug.WriteLine("Pages[1]状態 = " + ブラウザコンテキスト.Pages[1].IsClosed.ToString()); //False

閉じられているページに対して処理をしようとすると例外が発生するので、このプロパティをつかうなり、try/catchするなりして逃げる。


指定したタブにフォーカスする

じゃ、次。新しいタブを開くと、そのタブにフォーカスがいくが、何らかのボタンをクリックしたいとかで古いタブにフォーカスしたいとき。いまどきあんまりない気がするけど。Seleniumとかだと、ボタンクリックするのに画面内にボタンが表示されていないといけないとかあったじゃない?Playwrightではそんなことがあるか知らんけど。

とりあえず、タブの切り替えはページに対してBringToFrontAsync()メソッドを行う。

枠内をクリックするとソースがクリップボードにコピーされます
                var ページ1 = ブラウザコンテキスト.Pages[0];
                await ページ1.GotoAsync("https://www.google.com");

                var ページ2 = await ブラウザコンテキスト.NewPageAsync();
                await ページ2.GotoAsync("https://www.bing.com");

                var ページ3 = await ブラウザコンテキスト.NewPageAsync();
                await ページ3.GotoAsync("https://www.yahoo.com");


                await ページ2.BringToFrontAsync();

これだけ。簡単だね。


ページのスクリーンショットを試す

ページ単位のスクリーンショット(フルページ)も簡単。

枠内をクリックするとソースがクリップボードにコピーされます
                await ページ2.ScreenshotAsync(
                    new PageScreenshotOptions {
                        FullPage = true ,
                        Path = @"C:\ChromeProfile\ScreenShot" 
                             + DateTime.Now.ToString("yyyyMMdd_HHmmss")+".png" 
                    }
                );

Pathを指定しない場合はファイルに保存されないで、byte[]で画像データが戻ってくるので、自前で保存するなりなんなり。FullPage = trueでも、スクロールしないと次のアーティクルが表示されないようなものは当然無理。スクロールさせる必要あり。


自動処理を作ってみる(想定するページの動き)

さて、ここまでできればhtml内の要素にアクセスして、直接値いじったりすればいい。

試しに、「https://www.infoseek.co.jp」でログインするのを自動で行ってみよう。

実際の画面の流れは以下の通り。

1.https://www.infoseek.jpへ飛び、「ログイン」ボタンを押す。


2.認証ページに飛んで行ってメールアドレスを入力し、「次へ」を押す。


3.パスワードを入力し「ログイン」を押す。


自動処理を作ってみる(トップページ)

まずは、1.の画面の処理。

F12を押してDevlToolsで「ログイン」を検索。ボタンは多分<p>のタグで「p.button.login」で行けるかな?

枠内をクリックするとソースがクリップボードにコピーされます
                ページ = ブラウザコンテキスト.Pages[0];
                try
                {
                    // 最初のページ
                    await ページ.GotoAsync("https://www.infoseek.co.jp");
                    await ページ.Locator("p.button.login").ClickAsync(
                        new LocatorClickOptions { Timeout = 10000 }
                    );
                } 
                catch (TimeoutException)
                {

                }

丁寧に、見つからなかった場合のTimeoutのtry/catchを入れたうえで、デフォルトの30秒→10秒にする。まずは、これで遷移できるか試してみると、ばっちり成功。


自動処理を作ってみる(ユーザーIDの入力)

まずはメールアドレス欄。「ユーザIDまたは」で検索してみたら、それらしき<input>タグがある。classの指定まで含めるのは怪しそうなので、「input#user_id」を検索欄に入れると1個しか見つからないので多分大丈夫だろう。valueのセットはFillAsync()を使う。

次は「次へ」のボタン。これは<div>タグがそれっぽい。長いが「div#cta」で検索してみると1つしか引っかからないので、これで行けるだろう。再度、試してみる。

枠内をクリックするとソースがクリップボードにコピーされます
                ページ = ブラウザコンテキスト.Pages[0];
                try
                {
                    // 最初のページ
                    await ページ.GotoAsync("https://www.infoseek.co.jp");
                    await ページ.Locator("p.button.login").ClickAsync(
                        new LocatorClickOptions { Timeout = 10000 }
                    );
                    // ユーザID入力して「次へ」
                    await ページ.Locator("input#user_id").FillAsync(
                        "hoge@hogehoge.com",
                        new LocatorFillOptions { Timeout = 10000 }
                    );
                    await ページ.Locator("div#cta").ClickAsync(
                        new LocatorClickOptions { Timeout = 10000 }
                    );
                }
                catch (TimeoutException)
                {

                }

よしよし。これもうまくいった。

自動処理を作ってみる(パスワードの入力)

次は、パスワード入力欄。<input>タグが特定できる。「input#password_current」で行ける。

次が、ログインボタン。「div#cta」だが、これだと2つ引っかかる。さらにクラスを指定しても同じdivのクラスをもつタグがある。メールアドレス入力時の「次へ」ボタンが見えない状態でタグだけあるようだ。

枠内をクリックするとソースがクリップボードにコピーされます
                ページ = ブラウザコンテキスト.Pages[0];
                try
                {
                    // 最初のページ
                    await ページ.GotoAsync("https://www.infoseek.co.jp");
                    await ページ.Locator("p.button.login").ClickAsync(
                        new LocatorClickOptions { Timeout = 10000 }
                    );
                    // ユーザID入力して「次へ」
                    await ページ.Locator("input#user_id").FillAsync(
                        "hoge@hogehoge.com",
                        new LocatorFillOptions { Timeout = 10000 }
                    );
                    await ページ.Locator("div#cta").ClickAsync(
                        new LocatorClickOptions { Timeout = 10000 }
                    );
                    // パスワード入力して「ログイン」
                    await ページ.Locator("input#password_current").FillAsync(
                        "hogehoge",
                        new LocatorFillOptions { Timeout = 10000 }
                    );
                    await ページ.Locator("div#cta").ClickAsync(
                        new LocatorClickOptions { Timeout = 10000 }
                    );
                }
                catch (TimeoutException)
                {

                }

これで実行すると…

こんな感じで特定できないためにエラーが発生する。ただ、ログインボタンは「ログイン」とかかれているので、LocatorのオプションのHasTextStringで特定してみる。

枠内をクリックするとソースがクリップボードにコピーされます
                ページ = ブラウザコンテキスト.Pages[0];
                try
                {
                    // 最初のページ
                    await ページ.GotoAsync("https://www.infoseek.co.jp");
                    await ページ.Locator("p.button.login").ClickAsync(
                        new LocatorClickOptions { Timeout = 10000 }
                    );
                    // ユーザID入力して「次へ」
                    await ページ.Locator("input#user_id").FillAsync(
                        "hoge@hogehoge.com",
                        new LocatorFillOptions { Timeout = 10000 }
                    );
                    await ページ.Locator("div#cta").ClickAsync(
                        new LocatorClickOptions { Timeout = 10000 }
                    );
                    // パスワード入力して「ログイン」
                    await ページ.Locator("input#password_current").FillAsync(
                        "hogehoge",
                        new LocatorFillOptions { Timeout = 10000 }
                    );
                    await ページ.Locator("div#cta",
                        new PageLocatorOptions { HasTextString = "ログイン" }
                    ).ClickAsync(
                        new LocatorClickOptions { Timeout = 10000 }
                    );
                }
                catch (TimeoutException)
                {

                }

これでどうだ!!

はい、パスワード入力が通って、ログイン後のページに戻りました。


自動処理を作ってみる(まとめ)

と、いうことで、作った部分全体のソースをもう一度。
枠内をクリックするとソースがクリップボードにコピーされます
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Data;
using System.Diagnostics;
using System.Drawing;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Windows.Forms;
using Microsoft.Playwright;

namespace Playwrightテスト
{
    public partial class Form1 : Form
    {
        public Form1()
        {
            InitializeComponent();

            ブラウザ.Close += ブラウザ終了;
            Task.Run(() => 自動処理());
        }

        Chromeコントロール ブラウザ = new Chromeコントロール();

        public async Task 自動処理()
        {
            await ブラウザ.ブラウザ起動();
            await ブラウザ.Infoseekログイン();
        }

        private void ブラウザ終了(object? sender, EventArgs e)
        {
            Application.Exit();
        }

        private class Chromeコントロール
        {
            public event EventHandler? Close;

            private IPlaywright? playwright;
            private IBrowserContext? ブラウザコンテキスト;
            private IPage? ページ;

            public async Task ブラウザ起動()
            {
                playwright = await Playwright.CreateAsync();

                ブラウザコンテキスト = await playwright.Chromium.LaunchPersistentContextAsync(
                    @"C:\ChromeProfile",
                    new BrowserTypeLaunchPersistentContextOptions
                    {
                        Headless = false,
                        Channel = "chrome"
                    }
                );

                ブラウザコンテキスト.Close += ブラウザコンテキスト_Close;
            }

            public async Task Infoseekログイン()
            {
                ページ = ブラウザコンテキスト.Pages[0];
                try
                {
                    // 最初のページ
                    await ページ.GotoAsync("https://www.infoseek.co.jp");
                    await ページ.Locator("p.button.login").ClickAsync(
                        new LocatorClickOptions { Timeout = 10000 }
                    );
                    // ユーザID入力して「次へ」
                    await ページ.Locator("input#user_id").FillAsync(
                        "hoge@hogehoge.com",
                        new LocatorFillOptions { Timeout = 10000 }
                    );
                    await ページ.Locator("div#cta").ClickAsync(
                        new LocatorClickOptions { Timeout = 10000 }
                    );
                    // パスワード入力して「ログイン」
                    await ページ.Locator("input#password_current").FillAsync(
                        "hogehoge",
                        new LocatorFillOptions { Timeout = 10000 }
                    );
                    await ページ.Locator("div#cta",
                        new PageLocatorOptions { HasTextString = "ログイン" }
                    ).ClickAsync(
                        new LocatorClickOptions { Timeout = 10000 }
                    );
                }
                catch (TimeoutException)
                {

                }
            }

            private void ブラウザコンテキスト_Close(object? sender, IBrowserContext e)
            {
                Close(this, EventArgs.Empty);
            }
        }
    }
}

こんな感じでどんどん記述していけば、自動処理できる。Seleniumでボタンが画面内にないとクリック失敗すると書いたけど、Playwrightでやってみたら、自動的に画面内に入るようにスクロールしてくれてました。

ん~、なかなかやりやすい感じ。これでChromeが変なところでおちたりしなければいいな。

いままでSeleniumで作ったものをPlaywrightに移行してみよう。


※2022/5/24 追記。

いま見ると、HasTextStringなんてなかなかレアなオプションを使ってますが、使い慣れた今なら単純にLocatorで「div#cta:has-text(\"ログイン\")」とするか「div#cta >> nth=1」とかつかったりして、簡潔に書くでしょう。それで十分です。初めて使った時の手探り感ですね。


C#でSeleniumをやめてPlaywrightを使ってみた3へ

コメント

このブログの人気の投稿

楽天ラッキーくじ 処理設定ページ

楽天ラッキーくじ スタートページ