electronでpuppeteerを使ってみることにした

すでに社内で、electronで作成した座席表アプリを配布済みです。この環境下でpuppeteer + nexeでバイナリ配布をするのも良いのですが、すでにある座席表アプリでpuppeteerを動かせないかな?と思い挑戦しました。

ただし、keytarも使わなければ行けない環境であるため、これまでのelectron 3.0.0 + keytar 4.2.1の環境では古すぎてpuppeteerが動きません(Error : Protocol errorが出て、getBrowserContexts wasn't foundと出ます)。そこで、今回electronを5.0にし、keytarを4.6.0でビルドし直した所うまく動いたので、これを元に進めたいと思います。

今回使用するモジュール等

ちなみに、あえてpuppeteer-in-electronを使わずとも、puppeteer-coreのみでインストール済みのChromeを起動させて、Chromeを操縦することは出来るので、puppeteer-in-electronを使わなくとも問題なく構築は可能です(実際自分は、puppeteer-coreのみでelectronからChromeを操縦させてます。

事前準備

Keytarのインストール

Node.jsはv10.15.1を利用しています。リビルド環境の構築はWindows向けで構築しています。electron-rebuildは最新版に入れ直しています。keytarのインストールは以下のような手順です。

npm i keytar@4.6.0
cd node_modules
cd keytar
node-gyp configure
cd ..¥..¥
electron-rebuild -w -keytar -p -f

Puppeteerをインストール

electron自体がChromiumなので、別途Chromiumは必要ありません。ということで、今回もpuppeteer-coreを利用します。また、electronのBrowserWindow内で使いたいので、puppeteer-in-electronもインストールします。

npm i puppeteer-core
npm i puppeteer-in-electron

electron v5.0.0はpuppeteerから操作が可能なので、今回のようなアプリを作る場合には、electronのバージョンは5以上推奨です(ただし、keytarを使うとなると簡単に最新版という事にはいかないのが残念)。

ソースコード

Puppeteer単体での場合

puppeteer + node.jsとは異なりpuppeteer-in-electronの場合冒頭の部分が少々異なります。その部分以降はこれまでのawaitから始まる命令文をつなげていけば良いので、そこだけ要注意です。また今回は、keytarとの併用ができるかの確認の為に、main()につなげて無駄にパスワードの保存と取り出しを記述しています。

//モジュールの読み込み
const {BrowserWindow, app} = require("electron");
const pie = require("puppeteer-in-electron")
const puppeteer = require("puppeteer-core");
const keytar = require('keytar');
 
//puppeteerで操作するメインの処理
const main = async () => {
  //puppeteer-in-electronに接続
  const browser = await pie.connect(app, puppeteer);
 
  //URLを指定してBrowserWindowに対してページの移動をさせる
  const window = new BrowserWindow();
  const url = "https://officeforest.org/wp/";
  await window.loadURL(url);
  
  //viewportのサイズの指定
  const viewportHeight = 800;
  const viewportWidth = 1280;
  
  //ページを表示する
  const page = await pie.getPage(browser, window);

  //ここから通常のpuppeteerと同じような記法になります。
  await page.setViewport({
    width: viewportWidth,
    height: viewportHeight
  });
  
  //スクリーンショットを撮って保存する
  await page.screenshot({ path: './result.png', fullPage: true})
  
  //BrowserWindowを閉じる
  await window.destroy();
};
 
//サービス名を構築
var servicename = "zaseki_sakura";

//puppeteerを動かす
main()
  .then(function(){
    //パスワードを保存する
    keytar.setPassword(servicename,"sakura","password");
    console.log("OK")
    return "sakura";
  })
  .then(function(data){
    const secret = keytar.getPassword(servicename,data);
                secret.then((result) => {
                    console.log(result);
                    return;
                });

  })
  • main関数がpuppeteerで自動操縦させる関数になります。
  • main関数冒頭のpuppeteerを起動する部分が異なります。pie.connect(app, puppeteer);にてbrowserを宣言
  • executablePathなどは不要です。BrowserWindowがそれを担います。
  • page.gotoは冒頭は不要です。BrowserWindowに対してloadUrlを指定でロードさせればOK
  • pie.getPage以降は通常のpuppeteerでの記法と同じになります。
  • Windowを閉じる場合には、electronなのでwindow.destroy()で閉じればOKです。
  • main関数につづけて、promiseにてkeytarによる資格情報マネージャへのパスワードの格納/取得を追加して動作確認しています。
  • electron 5.0.0 + keytar 4.6.0での動作確認が取れましたので、今後はこのバージョンを基準に開発していきたいと思います。

他のアプリ内に取り込んで使う場合

puppeteer-in-electronを使う上でちょっと厄介だったのが、既存のアプリに組み込んで使いたい場合。単体と違い、他のコードが存在し、それが原因でエラーが出たりします。特に厄介なのは、Must be called at startup before the electron app is ready.というエラー。app.onのreadyよりも前に、browserについて初期化しておいて、puppeteerに接続させておかなければなりません。よって、以下のような形で記述します。

//puppeteerモジュール読み込み
const pie = require("puppeteer-in-electron")
const puppeteer = require("puppeteer-core");
var browser = null;
var page = null;

//app.onより前にpuppeteerに接続させる
const main = async () => {
  browser = await pie.connect(app, puppeteer);
};
main();

//appの初期化
app.on('ready', () => {


});

//puppeteerで操作する
async function openform(tempid,frmname,frmmail){
  const window = new BrowserWindow({
    'width': 950,
    'height': 750,
    'autoHideMenuBar':true,
    webPreferences: {
      nodeIntegration: false,
      contextIsolation: false,
      webSecurity: false
    },
    'resizable':true,
    'fullscreenable':false,
    'fullscreen':false,
    'alwaysOnTop':false,
    'modal':true,
    'parent': mainWindow  //モーダル表示にしておく
  });

  const url = "コントロールするサイトのURL";
  await window.loadURL(url);

  page = await pie.getPage(browser, window);

  //viewportをフルサイズにする
  await page._client.send('Emulation.clearDeviceMetricsOverride');

  //自動入力
  await page.waitForSelector('.ui > .row:nth-child(1) > .column > .el-input > .el-input__inner')
  await page.click('.ui > .row:nth-child(1) > .column > .el-input > .el-input__inner')
  await page.type('.ui > .row:nth-child(1) > .column > .el-input > .el-input__inner', tempid)
  await page.waitForSelector('.ui > .ui > .ui > .row:nth-child(1) > .column')
  await page.click('.ui > .ui > .ui > .row:nth-child(1) > .column')
  await page.waitForSelector('.ui > .row:nth-child(2) > .column > .el-input > .el-input__inner')
  await page.click('.ui > .row:nth-child(2) > .column > .el-input > .el-input__inner')
  await page.type('.ui > .row:nth-child(2) > .column > .el-input > .el-input__inner', frmname)
  await page.type('.ui > .row:nth-child(3) > .column > .el-input > .el-input__inner', frmmail)
}
  • app.onのready前にbrowserとpageを宣言しておいて、browserに対してconnectさせて置かなければならない。
  • フォームに入力させる場合は、自動化よりも前に予め入力予定のワードはどこかで取得させておくとスムーズです。
  • kintoneのフォームブリッジは、ちょっと変な仕様で、直接機械的にinputに対して値をねじ込むと、入っていても送信時に「入っていない」と言われるので、このようにpuppeteerにて入力させ、keydownイベントを発火させる必要があります。
  • viewportを常にウィンドウサイズに合わせるには、await page._client.send('Emulation.clearDeviceMetricsOverride');にて実現可能です。
  • tempid, frmname, frmmailは予め自分の場合、MySQLから取得したものを格納してあります。
  • BrowserWindowが開かれるとPuppeteerが発動し、社員番号と名前、メールアドレスがフォームに自動入力されます。
  • webviewタグ表示の対象に対して、javascriptでキーコードを送っても反応しないので、Puppeteerでページ表示させて操作する方法がもっともベターです。

jQueryを使えるようにする

Puppeteerの非常に優れている点は、既存のフォームなどに対して後から、jQueryなどのモジュールを読み込ませて、機能を付け足すことが出来る点です。例えば、formブリッジのテキストボックスに対して、jQueryのAutocompleteを付け足すことで、フォームでの入力補助機能をアップさせたりなどなど。

今回、GoogleのCDNで配布されているライブラリを用いて、formブリッジのテキストボックスにautocompleteを設定してみました。

※Autocompleteは文字列でなければいけないので、xlsxモジュールなどでデータを生成する場合、String()で型変換しておくことが必要です。

・・・・前略・・・・
//外部ライブラリ挿入
await page.addScriptTag({url: 'https://ajax.googleapis.com/ajax/libs/jquery/1.12.4/jquery.min.js'});
await page.addScriptTag({url: 'https://ajax.googleapis.com/ajax/libs/jqueryui/1.12.1/jquery-ui.min.js'});
await page.addStyleTag({url: 'https://ajax.googleapis.com/ajax/libs/jqueryui/1.12.1/themes/smoothness/jquery-ui.css'});
await page.addStyleTag({path: 'css/autocomp.css'});

//autocomplate
const _tmp = await page.evaluate(() => {
  var data = [
    {label:'しいたけ', value:'2012'},
    {label:'しめじ', value:'2013'},
    {label:'まつたけ', value:'2014'},
    {label:'なめこ', value:'2015'},
    {label:'えのきだけ', value:'2016'},
    {label:'たもぎたけ', value:'2017'},
  ];

  //formに対してNAME属性を加える
  var forms = document.forms[0].setAttribute("name","kinoko");

  //4つ目の項目に対してid属性を加える
  document.kinoko.elements[3].setAttribute("id","salad");

  //autocomplateを加える
  $('#salad').autocomplete({
    source: data,
    autoFocus: true,
    delay: 500,
    minLength: 2
  });
});
  • addScriptTagにてURL指定でスクリプトライブラリを挿入可能です
  • addStyleTagにてURL指定でスタイルシートを挿入可能です。
  • page.evaluateにて、任意のスクリプトを対象のページに対して実行させることが可能です。
  • あとは、通常のHTML上で記述するようにJavaScriptを記述すればOK.
  • evaluate内に外側の変数を渡したい場合には、以下のように引数で渡すと利用すことが可能になります。
  • xlsxモジュールなどで、固定場所にあるexcelデータからautocomplete用のデータを生成しておき、引数でevaluateに渡せば最新のリストをもってautocompleteが反映可能です。
var data = [
    {label:'しいたけ', value:'2012'},
    {label:'しめじ', value:'2013'},
    {label:'まつたけ', value:'2014'},
    {label:'なめこ', value:'2015'},
    {label:'えのきだけ', value:'2016'},
    {label:'たもぎたけ', value:'2017'},
];

//data変数を引数として渡し、evaluate内で利用する
const _tmp = await page.evaluate((data) => {
  //formに対してNAME属性を加える
  var forms = document.forms[0].setAttribute("name","kinoko");

  //4つ目の項目に対してid属性を加える
  document.kinoko.elements[3].setAttribute("id","salad");

  //autocomplateを加える
  $('#salad').autocomplete({
    source: data,
    autoFocus: true,
    delay: 500,
    minLength: 2
  });
},data);

図:formブリッジに対してautocompleteが反映出来た

単一実行ファイルを作成する

Node.js 18よりSingle executable applicationsという機能が装備され、標準で単独実行ファイルが作成できるようになりました。結果pkgはプロジェクト終了となっています。よって、以下のエントリーの単一実行ファイルを作成するを参考に、Node18以降はexeファイルを作成することが可能です。

Puppeteerを使ってX(旧Twitter)へのポストを自動化する

関連リンク

コメントを残す

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

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