Puppeteerで特定要素が出てくるまで待機して値を取得する

業務用アプリにPuppeteerを組み込んで結構な数のウェブ操作自動化を実現していますが、「TeamSpiritの勤務表を開き、月次サマリーの中にあるフレックス清算時間を取得したい」という要望が出てきたので、これを今回はPuppeteerで取得してみたいと思います。

このデータの取得は意外と面倒な要素を含んでいました。

今回使用するライブラリ

今回のダウンロード上の問題点

今回のデータの取得先であるTeamSpiritの「月次サマリー」ですが、以下のような問題点を含んでいて、一筋縄では取得が出来ません。これらの問題点は前回特定要素の値が変わるまでウェイトの応用と、ポップアップウィンドウの操作が必要になります。

  1. 勤務表の月選択用セレクトボックスは、ページ表示後にJSにて構築されている為、ワンテンポ遅れてから表示される
  2. そのため、navigationPromiseでは対処できず、要素が表示されていないのに対象月の値をセットしようとして、Node is either not visible or not an HTMLElementのエラーが出てしまう
  3. また、月次サマリーはリンクではなく、これもポップアップウィンドウを表示するJSが入ってるボタンであるため、ポップアップウィンドウの操作が必要
  4. ポップアップウィンドウ側もデータが遅れて表示されるため、値を取得するためには、2.同様の問題が待っている

図:この部分の値が欲しい

コードと解説

ソースコード

async function puppetman(){
  const browser = await puppeteer.launch({
    headless: false,
    executablePath: chromepath,
    ignoreDefaultArgs: ["--guest",'--disable-extensions','--start-fullscreen','--incognito',],
    slowMo:100,
  });

  //pageを定義
  const page = await browser.newPage()
  const navigationPromise = page.waitForNavigation();

  //loginと選択関係
  var loginuid = "ここにログインID";
  var tspasswd = "ここにパスワード";
  var selemonth = "20211101";
  var repurl = "ここにログインURL"

  page.on('error', err=> {
    //エラー発生
    var options ={
      type:'info',
      title:"Puppeteerエラー",
      buttons: ['OK'],
      message:'エラーが発生しました。',
      detail:String(err)
    }
  
    //表示する
    var ret = dialog.showMessageBoxSync(null,options);
    console.log('error happen at the page: ', err);
  });

  //初期ページを開く
  await page.goto(repurl)
  await page.setViewport({ width: 1200, height: 900 })
  await navigationPromise

  //ログイン処理
  await page.waitForSelector('#theloginform > #login_form > #usernamegroup #username')
  await page.click('#theloginform > #login_form > #usernamegroup #username')
  await page.$eval('#theloginform > #login_form > #usernamegroup #username', (el, loginuid) => {
    return el.value = loginuid;
  }, loginuid);

  await page.waitForSelector('#wrapper > #content > #theloginform #password')
  await page.click('#wrapper > #content > #theloginform #password')
  await page.$eval('#wrapper > #content > #theloginform #password', (el, tspasswd) => {
    return el.value = tspasswd;
  }, tspasswd);

  await page.waitForSelector('#wrapper > #content > #theloginform #Login')
  await page.click('#wrapper > #content > #theloginform #Login')
  await navigationPromise
  await navigationPromise

  await page.waitForSelector('#tabContainer > nav > #tabBar > #\\30 1r2w000000szGE_Tab > a')
  await page.click('#tabContainer > nav > #tabBar > #\\30 1r2w000000szGE_Tab > a')

  //wait
  await navigationPromise
  await page.waitForNavigation({waitUntil:"networkidle2"});  //ネットワークがアイドル状態になるまで待機

  //セレクトボックスが出てくるまで待機する関数
  //特定要素が出てくるまでリトライ
  var selection = "";  //対象のセレクタ
  var targetpage = "";   //対象のページ要素

  var checkElement = async ({ interval, times }) => {
    //実行カウンタ
    var count = times;

    //selectorの存在をチェック
    const viewSuccess = await targetpage.$(selection).then(res => !!res);
    console.log(viewSuccess);
    
    //存在しているか確認
    if (viewSuccess) {
      //次の処理へ移行する
      await page.waitForTimeout(interval);
      console.log("要素あったよ")
      return;
    } else{
      if(times > 1 || times === -1) {
        //ウェイト実行
        await page.waitForTimeout(interval);
        console.log("ウェイト実行");

        //再帰的に要素確認を実行
        await checkElement({ interval, times: count - 1 });
      }
    } 
  };

  //セレクトボックスの存在確認チェック
  await page.waitForSelector('#yearMonthList')
  selection = '#yearMonthList';
  targetpage = page;
  await checkElement({interval: 3000, times: 10 });

  //セレクトチェンジ(指定の月に変更)
  await page.click('#yearMonthList');
  await page.select('#yearMonthList', selemonth);
  await page.waitForSelector('#yearMonthList')
  await page.click('#yearMonthList')
  await navigationPromise

  //月次サマリーをクリック
  await page.waitForSelector('.right > tbody > tr > td > .std-button2 > div')
  await page.click('.right > tbody > tr > td > .std-button2 > div')
  await navigationPromise

  //popupウィンドウを取得する
  const newPagePromise = new Promise(x => browser.once('targetcreated', target => x(target.page()))); 
  const popup = await newPagePromise;
  var pages = await browser.pages();
  var popwin = pages[pages.length - 1];

  //popupの特定要素を取得
  var itemSelector = "#summaryBottom > table > tbody > tr > td:nth-child(1) > table:nth-child(3) > tbody > tr:nth-child(4) > td.value > div"

  //popwinの特定要素の表示チェック
  await popwin.waitForSelector('#summaryBottom')
  selection = '#summaryBottom';
  targetpage = popwin;
  await checkElement({ interval: 3000, times: 10 });

  //特定要素の値を取得
  var data = await popwin.evaluate((selector) => {
    //要素を取得して返す
    return document.querySelector(selector).innerHTML;
  }, itemSelector);

  try{
    //ブラウザを閉じる
    await browser.close()
    browser.process().kill('SIGKILL');
  }catch(e){
    //エラー発生
    var options ={
      type:'info',
      title:"Puppeteerエラー",
      buttons: ['OK'],
      message:'不正に終了したので、アプリを再起動してやり直してください。',
      detail:e.message
    }
  
    //表示する
    var ret = dialog.showMessageBoxSync(null,options);
  }  
}

解説

  • 冒頭のログイン処理等はこれまでのPuppeteerでも装備してきた流れなので特にここで躓くことはないと思います。
  • 問題の勤務表の月選択用セレクトボックスはすぐに出てこないので、この要素が出てくるのをチェックするcheckElementという関数を別途用意します。
  • セレクトボックスのセレクタと、操作するページの2つを格納する変数を用意し、checkElementは引数にインターバル(ウェイト時間)と、リトライ回数(Times)を用意
  • まずは#yearMonthListの要素が出てくるまで、3秒間、10回のcheckElementを実行
  • 出てきたのを確認したら、セレクトボックスに対して値をpage.selectで送り込む
  • 次に、月次サマリーをクリック。ポップアップウィンドウをpopwinに取得させる
  • 対象のポップアップウィンドウ下部の特定の値のセレクタおよび、popwinをセレクタとページの変数に格納し、再度checkElementでウェイトを実行
  • popwinの中の要素をinnerHTMLで取得して返す
  • 最後にブラウザを終了させる

これで無事に対象の月の勤務表の月次サマリーの中にある特定要素の値を取得する事が出来ました。他の要素もまとめて取得もできるので、ポップアップウィンドウ内のデータを塊にして吐き出すようなコードを追加すると、二次利用がしやすくなるのではないかと思います。

照すべきエントリー

Puppeteerで特定要素の値が変わるまでウェイトを掛ける

Puppeteerでポップアップウィンドウを操作する

関連リンク

コメントを残す

メールアドレスが公開されることはありません。 が付いている欄は必須項目です

日本語が含まれない投稿は無視されますのでご注意ください。(スパム対策)