PuppeteerでChrome自動化で一番良く利用されるであろうシーンが「パラメータを設定して、ボタンクリックでCSVファイルをダウンロード」ではないだろうかと思います。実はこれ、RPAだと非常にシンプルなサイトならばともかく、結構複雑な仕組みのサイトの場合、とても難しい作業の1つです。

しかし、Puppeteerの場合、JavaScriptの実行やエレメント操作が可能であったり、Chromeに対して命令を送れるので、非常に難しいダウンロードであっても可能だったりします。今回はとあるサイトのCSVファイルをデスクトップにダウンロードしてみようと思います。

難易度:


今回使用するライブラリ等

今回、ユーザからの入力を受け付けるpromptsというモジュールを使っています。パスワードやIDをPuppeteerのスクリプト上に直書きするのはセキュリティ上マズイので、そういったケースで使います。今回は年度と月の指定だけで、パスワードは直書きです。注意が必要です。

また、今回pkgでパッケージングすると後述の問題が発生するので、nexeでパッケージを作ったところ、ファイルサイズも小さく、またきちんとevaluate出来たので、こちらでexeを作成しました。nexe自体はインストールは非常に簡単です。

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

今回対象にしているサイトは以下のような問題点を抱えています。そのまま素直にファイルを取りに行こうとすると、エラーを吐いて止まってしまいます。この問題点をクリアしつつ、指定のフォルダにダウンロードする為にいくつか細工を施します。

  1. CSVファイルの年度、月を指定するテキストボックスにはonChangeでJavaScriptが入ってしまっている。
  2. 1.のJavaScriptは実行されないと適切な期間指定が出来ないので、テキストボックスに値を入れればOKというわけにはいかない
  3. headlessの場合、名前をつけて保存のダイアログは出てこないので、Chromeに場所を指定する必要がある(デフォルトはWindowsならばダウンロードフォルダ)。
  4. サイト上のパラメータ指定画面等がiframe」の中に入ってしまっていて操作が面倒
  5. 年度と月を指定するテキストボックスははじめから値が入ってしまっている
  6. ダウンロードボタンのtarget属性が_blankになってしまっていて、新しいタブで開いてしまう。このままでは、page操作が出来ないでエラーが出てしまう。
  7. Chromeは管理者権限ではなくユーザ権限でインストールするので、いつもの場所ではない

特に面倒なのが、6.の問題。ボタンをクリックすると新しいタブで開かれダウンロードが始まるのですが、そうすると定義したpageからは外れてしまう為、2つ目のタブを操作しないとダウンロード完了と共にエラーを吐く。また、この時指定のフォルダではなくダウンロードフォルダにダウンロードされてしまいます。

この時のエラーは「Error: EPERM: operation not permitted」といったもので、chromeが不正終了した形になります。

ソースコード

冒頭部分

index.jsに全て記述します。冒頭の部分にはモジュールの読み込みと、デスクトップのパスを取得するコードを用意しておきます。

  • グローバル変数でデスクトップのパスを取得しておきます。
  • つづけて、getprompt()を実行してユーザの入力を受付待ちします。
  • chromeはいつもの「C:\\Program Files (x86)\\Google\\Chrome\\Application\\chrome.exe」ではなく、「C:\\Users\\ユーザー名\\AppData\\Local\\Google\\Chrome\\Application\\chrome.exe」となるため、ユーザ毎のパスを取得して、chromepathに格納する

プロンプト入力受付部分

  • promptsを使って、2つの質問を受け付けるようにします。
  • nendoとmonthの2つを質問し、数値で入力をしてもらいます。
  • 取得した数値を引数にmain()を実行します。

図:プロンプトの入力画面

Puppeteer部分

  • 今回はWindowsで実行してるので、executablePathはChromeがインストールされているパスを指定
  • Page.setDownloadBehaviorにてChromeに対してダウンロード先フォルダを指定しています。今回はデスクトップを指定。
  • ログインページでIDとPASSでログイン後、再度特定のページ(iframe内のURL)を直接表示するようにpage.gotoしています。
  • 年度と月はonChange属性が入ってる上に、maxlengthが指定されている。一度クリアしてからでないと入力が出来ない。
  • 上記のクリアをするために、page.$evalにてinputの中身を空にしています。
  • 続けてpage.keyboard.typeにて引数で受け取った値を入力
  • onChangeを働かせる為に適当なテキストボックスをfocusして、sleep実行。
  • 同様の作業を月の入力欄に対しても実行する
  • サイト上のオプションの選択、ドロップダウン項目の選択、検索のクリックをすると、条件に合致したデータが表示される。
  • 表示されたら、CSVダウンロードボタンをクリックするが、そのままでは_blankで開いてしまうので、対象のformのtarget属性をpage.$evalにて空に書き換えておく(これで、このタブの中でダウンロードが実行されるようになる)
  • ダウンロード中、拡張子が.crdownloadのファイルがある場合には、ループで10秒ウェイトを実行するようにしてある
  • 完了したら、browser.close()で処理を閉じる
  • 終了したよってメッセージがほしいならば、VBSのファイルでも用意して、呼び出して上げると良い。
  • Node.jsなので、特定の場所にフォルダの有無を確認してフォルダを作成して指定するであったり、ダウンロードしたファイルを特定のフォルダに移動するといった後処理を付け加えると尚、良いかもしれない。
  • 最後にawait page.addScriptTag({ content: script });を実行することで、サイトにJavaScriptを埋め込み実行。この時scriptにはwindow.alertをつけていますが、これで完了メッセージを出すようにすると、ユーザに通知が出来るのでGoodです。

ファイル名を変更して指定のフォルダへ移動

Puppeteerで指定のフォルダにそのままダウンロードは既に上記で示したコードで実現出来ています。しかし、実務の世界ではそれでオシマイではありません。所定の場所に例えば年度月でフォルダを作成し、その中へファイルを移動、ファイル名も年月日でわかりやすい名前をつけるといった作業が地味に時間を食います。ここら辺はNode.jsでの作業になります。

フォルダを作成

自分の場合、ファイルのダウンロード⇒リネーム⇒ファイルを移動をする場合、デスクトップに一時フォルダを作り、同時に所定の場所に年度月でフォルダを作成しています。しかし、フォルダ作成周りは、例えば既に同名のフォルダがあった場合であったり、対象のパスが存在しない(Box Driveなどを使っていて、Boxにログインしてなかった等)の場合のエラー処理等が非常に面倒です。

また、Box Driveの場合ファイルのパスにログインユーザ名が入るUsersディレクトリ以下にマウントされるので、人によってパスが異なるという問題もあったりします。これら面倒な問題を回避しつつ作成しなければなりません。以下に必要な部分だけのコードを掲示します。今回は、make-dirモジュールを使うことにします。

  • dir_desktop変数には、デスクトップに作成したtmpmanというフォルダを指定しています。これがダウンロード先の一時フォルダになります。
  • osモジュールからログインユーザ名を取得し、Box Driveの所定のフォルダをtargetPath変数に指定します。ここに年度月という名前でサブフォルダを作成することになります。
  • promptにて例えば開始日付を取得しておく。ユーザの入力した日付を元に年度月の値をsubstrにて取り出し、subfolder変数に格納する
  • make-dirにて一時フォルダを作成した後に、さらにmake-dirにてtargetPath指定の直下にsubfolderで指定したフォルダを作成する
  • プログラム終了時にfs.rmdirSync(dir_desktop);を実行すれば、一時フォルダを削除できます。非同期実行なので記述する場所に注意

ファイルの移動(名前の変更も込み)

Node.jsでのファイルの移動と名前変更は同じメソッドで出来るので、一発で実現が可能です。一時フォルダにダウンロードしてリネームと移動をすると、一時ファイルからファイルが消えてくれるので、次のダウンロードにもつなげることが出来ます。

  • fs.renameにて、引数に現在のファイルのフルパス、移動先のファイルのフルパス(ここで新しい名前を指定)で、リネームと移動が同時に出来ます。
  • 複数個のファイルの処理の場合はループの作り方とフルパスのファイル名の取得の仕方を変える必要がありますが、一個ずつであればこの方法がベターです。
  • 冗長気味ですがダウンロード中ファイルの移動がないように拡張子がcrdowloadのファイルの場合はスルーするようにしています。

Box Driveにファイルを移動する場合

Box Driveの所定のフォルダ内に対して、Node.js上でフォルダを作ることは可能なのですが、問題は上記のコードでダウンロードしたファイルを移動させようとすると、「npm ERR! EXDEV: cross-device link not permitted, rename」と出て、ファイルが移動出来ません。そこで、Box Driveの所定のフォルダに対して、上記と同じようなことを実現するには、リネームではなく、ファイルのコピーを行い、その後ダウンロードしたファイルを削除するという手順を踏むと実現が可能です。

また、Box Driveの場合、マウントフォルダが各ユーザフォルダであるため、osモジュールを使ってログインユーザ名を取得して、所定の格納先フォルダまでのパスを生成しておく必要があるので注意が必要です。

  • subfolderがBox Driveの所定の指定フォルダになります。ここに格納する
  • renameではなくcopyFileでファイルを指定の場所にコピー。その際にnewnameで指定したファイル名にする。
  • 完了後、ダウンロード元のファイルを削除しないと一時フォルダが削除できないので注意です。

exe化する上での問題

pkgの問題点

現在のコード、Node.js上で動かす分には問題なく動作します。しかし、pkgでパッケージ化すると、page.$evalの部分で「Passed function is not well-serializable」というエラーが出ます。どうやら、pkgでevaluateを利用するとこのような現象が起きるようで、キー入力の部分については、以下のように修正する事で入力が可能になりました。

問題は、後半の「await page.$eval(‘formタグのIDを指定する’, el => el.target = ”)」で、対象のelementのtarget属性を変更する部分。ここがどうしても引っかかってしまう。そこで、pkgではなくnexeにパッケージャを変更してexeを作ってみました。

nexeに変更した所、今回のようなElementの書き換えでevaluateしても問題が発生することなく、無事にcsvダウンロードが可能になりました。パッケージングのコマンドも簡単です。nexeのほうがpkgよりもファイルサイズが10MBほど小さいのも良いですね。無理してpkgにこだわる理由はありません。ただし、Keytarのようなネイティブモジュールは同梱されないようで、node_modulesフォルダがある場所にexeを置くと使えますが、exe単体だと参照できず、エラーになります。

nexeの問題点

Node.js v12.16.1nexe(v3.3.2)をインストールして使うと、以下のようなエラーが出てしまい、exeのビルドが出来ませんでした。バグですね。12.14.1の場合には問題なくビルドができましたので、最新版ではなくバージョンダウンして利用すると良いでしょう。

また、何かの拍子でnexeを実行してもそんなコマンド無いって言われる事があります。その場合、Windowsの場合は以下のパスを環境変数に追加すると良いです。

  1. タスクバーの検索窓より「システムの詳細設定」を検索実行
  2. 詳細設定タブの中にある環境変数をクリック
  3. ユーザ環境変数の中にあるPathを選択して編集をクリック
  4. 新規をクリックする
  5. 以下のパスを追加してあげてOKをする

関連リンク

共有してみる: