Puppeteerでログインページ内のPDFをダウンロードする

Puppeteerは非常に優れたChrome自動化のツールなのですが、ひとつ厄介なのが「PDFファイルのダウンロード」です。特に事務系の自動化を実現する場合、とあるサイトにログインして、請求日付を指定し、クリックをすると「新しいタブでPDFが表示される」といったパターンが結構遭遇します。

問題はこのURLを取得して、ページ内に要素を追加しクリックさせてダウンロード出来るのか?といったら出来ません(また、いわゆる名前を付けてリンク先を保存といったものも現在サポートされていません)。リンクをクリックすると、Chrome内でビューアとして開かれてしまい、ファイルのダウンロードが出来ないのです(サーバ側で.htaccessなどでダウンロードするように仕掛けている場合には別ですが)

そこで、今回このやっかいなPDFファイルのダウンロードを実現してみたいと思います。日経メディアマーケティングのサイトのPDFをダウンロードしてる事例になります。

今回使用するモジュール

今回、Chromeのダウンロード機能を使わずに、requestモジュールにてファイルダウンロードを行わせるようにしています。また、取得したデータは完成品フォルダの中に直接fs.writeFileSyncにて指定のファイル名で生成してしまいます。request-promiseのインストールは以下のコマンドにて。

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

今回対象としているサイトでは、以下のような問題点があるため、そのまま素直にボタンクリックでダウンロードができません。また、ログインした内部にあるファイルを取得するため、cookieを使用してrequestで叩かなければなりません。

  1. PDFファイル直リンクをクリックしても、Chrome内部のビューアで開かれてしまいダウンロードが出来ない
  2. bufferで取得してfs.writeSyncで書き出そうとしても、PDFではないものが取得されるのでこの手も使えません。
  3. requestモジュールを使ってHTTPリクエストで取得する場合も、Cookieを使ったログインセッションを使う必要があります。
  4. 対象のPDFのファイルのURLは毎回変化します。
  5. 請求書一覧には過去のものが列挙されていますが、指定の月のものだけが必要です。
  6. requestモジュールのオプションで必要なレスポンスデータは、setRequestInterceptionで横取りさせる必要があります。
  7. 入力する指定月のファイル名を生成してあげるのが要件の1つ

ソースコード

冒頭部分

冒頭の部分は、今自分が手掛けてる自動化ではもはやテンプレ化させているコードです。生成先フォルダやChromeパスの取得などなど。

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

プロンプト入力受付部分

  • promptsを使って、3つの質問を受け付けるようにします。
  • useridとpassword、指定の年度月の3つを質問し、それぞれの形式で受け付けます。passwordを指定しておくと****と隠した状態で表示されるようになります。
  • 取得した数値を引数にmain()を実行します。
  • この段階で完成品フォルダをmakeDirにて作成してしまいます。
  • promptsのtypeがnumberは数値入力なのですが、バグで5桁以上入れようとすると(特に0の連続)入力がクリアされてしまうので、今回はtextにしています。

Puppeteer部分

  • 今回はWindowsで実行してるので、executablePathはChromeがインストールされているパスを指定
  • Page.setDownloadBehaviorにてChromeに対してダウンロード先フォルダを指定しています。今回はデスクトップを指定。
  • PDFのページはポップアップで新しいタブで開かれる為、newPageにてそのurlを取得しておく。これをrequestで使用します。
  • fileNameはこの段階で作成しておきます。年度月のpdfとしました。

requestでファイルをダウンロード

一番やっかいな問題点はここです。

  • ポップアップが表示された後にsetRequestInterceptionをnewPageに対してtrueでセットする。これで移行のhttp通信を横取り
  • newPage.onイベントをまずは先に用意しておく。こうしておかないとreloadした時にページがフリーズしてしまいます。
  • newPage.reload()にて、ポップアップされたPDFのURLをリロードします。
  • newPage.onが発動すると、リロード時にリクエストされた内、必要なヘッダやリクエストボディの値をオプションに取得しておきます。
  • page.cookies()にてログインセッションのクッキーを取得し、リクエストオプションに追加します。
  • 最後にrequestモジュールでオプション付きでPDFの直リンクURLを叩く。ファイルがbodyで取得されるので、fs.writeSyncにて指定のフォルダに指定のファイル名で書き出します。
  • 完了したら、newPageのタブを閉じ、alertを出してあげて終了。

社内プロキシーを超える

社内業務用として作成した場合、requestを使ってるとプロキシーで阻まれることがあります。ChromeはOSのプロキシ設定を使うので必要ないのですが、Node.jsの部分であるrequestモジュールは対策が必要になります。以下にその部分だけをピックアップします。

  • 冒頭でrp.defaultにてプロキシーを使うように設定を追加。proxy変数に格納する
  • proxyでアクセスさせるので、proxy(options).then(function (body) {としてrequestモジュールを呼び出し実行
  • optionsにて、proxy: proxyuriを追加してもエラーになります。
  • Node.js部分はOSのプロキシ設定をデフォルトで使うようになっていないのでこのような指定が必要になります。

Chromeの設定を変更してダウンロード

Puppeteer自体に右クリックで名前を付けて保存は2022年現在も装備はされていないのですが、Chrome自体には以下の場所に、PDFドキュメントを内蔵ビューアで開くのではなく、直接ダウンロードするという設定が増えています。

  1. chrome://settings/content/pdfDocumentsを入力して実行
  2. 「PDF をダウンロードする」にチェックを入れる
  3. クリックすると内蔵ビューアじゃなく、直接ダウンロードされるようになる

PuppeteerにはCDPSessionというクラスにChromeの各種設定を変更する手段が用意されています。ダウンロード先を変更するといったようなケースでも利用しています。

StackOverFlowにこのCDPSessionを使って上記のセッティングを一時的に変更して、リンクをクリックしたらダウンロードという仕組みを使う事でrequestモジュールやCookieを使わず単純ダウンロードを実現する手段が掲載されていました。コードは以下のようなものになります。

  • 正確には上記の場合、PDFとXMLファイルを開くのではなくダウンロードするという設定が送られています。
  • 最後にFetch.disableにて設定を無効化して元に戻しています。

実際にテストサイトを開きリンクをクリックしただけで、何の設定もなくPDFをダウンロードできました。もちろん手動だと内蔵ビューアで開かれます。

図:この設定をコードから一時的に有効化する

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

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

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

関連リンク

コメントを残す

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

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