PuppeteerでBasic認証を通過しページ数を取得する
最近久しぶりに、ちょっとした仕事で「Basic認証の掛かってるページ」にログインして、検索をし、出てきたページからファイルをダウンロードしてほしいという依頼があり、実装をしてみました。
ページ数は検索結果により変動し、ページネーションの数が異なるので、最大ページ数を取得して後は連続でファイルを順次ダウンロードを実行させるのですが、記録の為に残しておこうと思います。
目次
今回使用するライブラリ
今回のダウンロード上の問題点
今回対象にしているサイトは以下のような問題点を抱えています。検索ページで検索を行った結果表示されるページは全データが掲載されてるわけではなく、最大200件ずつのページネーションがされており、総件数を200で割った分だけページが存在していて、全部のデータを必要とする場合には、工夫が必要です。
- 通常のログインページではなく、Basic認証でログインが必要
- 検索ページでは様々な検索オプションが用意されている(今回はオプション指定しない)
- 検索後のページでは、デフォルト50件ずつのレコードが表示されている(最大200件)
- 件数に応じてページネーションの最大数が変わる為、全件取得したい場合は、ページネーションの最大数を取得する必要がある
- ページネーション数は最期のページへのリンクにjavascriptで移動する関数内に指定がある(例:paging(900);といった感じ。900ページある事がここからわかる)
- 全件チェックを入れて、ダウンロードをクリックする必要がある
今回は全件ダウンロードの一歩手前の最初のページだけダウンロードを実行させてみます。全件必要な場合は、さらに次のページへの移動とループでページネーションの最大数分だけ、ダウンロード処理が必要です。
コードと解説
今回のコードは冒頭部分とプロンプト受付部分については、これまでのエントリーとほぼ同じなので省略しています。必要な方はそちらを参照してください。
ソースコード
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 |
//Puppeteerメイン処理 async function main(userid,pw) { const browser = await puppeteer.launch({ headless: false, executablePath: chromepath, ignoreDefaultArgs: ["--guest",'--disable-extensions','--start-fullscreen','--incognito',], slowMo:100, }); //ブラウザのダウンロード先をすべて統一する await browser.on('targetcreated', async () =>{ const pageList = await browser.pages(); pageList.forEach((page) => { page._client.send('Page.setDownloadBehavior', { behavior: 'allow', downloadPath: deskpath, }); }); }); //pageを定義 const page = await browser.newPage() const navigationPromise = page.waitForNavigation() //Basic認証を突破してログイン await page.authenticate({username: userid, password: pw}); await page.goto(url, {waitUntil: "domcontentloaded"}); await page.setViewport({ width: 1300, height: 900 }) await navigationPromise //検索ボタンをクリック await page.waitForSelector('.search #search_button') await page.click('.search #search_button') await navigationPromise //200件表示に変更 await page.waitForSelector('#contents > #new_service_medicine_code_search_form > #search_result #limit') await page.click('#contents > #new_service_medicine_code_search_form > #search_result #limit') await page.select('#contents > #new_service_medicine_code_search_form > #search_result #limit', '200') await navigationPromise await sleep(5000); //最後のページの数値を取得 var cnt = 1; await page.waitForSelector('.clear-fix:nth-child(6) > .float-left > .pagination > li:nth-child(10) > a') const lastnum = await page.evaluate(() => { var node = document.querySelector('.clear-fix:nth-child(6) > .float-left > .pagination > li:nth-child(10) > a').getAttribute('href'); //数値だけ取り出す return node.replace(/[^0-9]/g, ''); }); //ダウンロード実行 await page.waitForSelector('#new_service_medicine_code_search_form > #search_result #all_check') await page.click('#new_service_medicine_code_search_form > #search_result #all_check') await page.waitForSelector('#new_service_medicine_code_search_form > #search_result > .clearfix > .float-left > .download_excel') await page.click('#new_service_medicine_code_search_form > #search_result > .clearfix > .float-left > .download_excel') //終了メッセージを表示 const script = `window.alert('処理が完了しました')`; await page.addScriptTag({ content: script }); //ブラウザを閉じる //await browser.close() } //スリープ用関数 function sleep(milliSeconds) { return new Promise((resolve, reject) => { setTimeout(resolve, milliSeconds); }); } |
解説
今回のコードの最初の壁はBasic認証。しかしこれは簡単で、await page.authenticate({username: userid, password: pw}); これだけです。useridがユーザID、pwがパスワードで今回のアプリはpromptsにてユーザから入力してもらっています。簡単にログインが可能です。
次の壁が検索結果は200件表示にし、ページネーションの数を取得する事。200件表示に変更後にsleepを5秒間入れてるのは、変更後にページネーションの関数の数値が入れ替わるのですが、変わる前に次に進んでしまうため。これを待つ為にsleepさせています。
これは、page.evaluateを利用し、document.querySelectorにて、最後のページに飛ぶリンク要素内にあるhrefの値を取り出し、またそこから正規表現で数値だけを抜き出してページネーション数としています。正規表現はnode.replace(/[^0-9]/g, '');で指定して取り出しています。
あとは全件チェックを入れてダウンロードボタンをクリックするだけ。実際にあと全件取得をするには
- 次のページのリンクをクリックする(このページにある、pagingという関数に数値を入れればそのページに移動できるので、これを移動するのも良いかも)
- 全件チェックを入れる
- ダウンロードを実行
- ダウロードが完了するまで処理を待機
- これをページネーションの最大値分だけ、ループで処理を行う
この処理が必要になります。また、このページの検索はPOSTで送信されている為、URLに検索オプションは無いので、送信処理はクリックなどで自前で実装が必要です。
図:ここのhrefの中身を取得し、正規表現で数値のみ取り出す
Page.setDownloadBehaviorがDeprecatedな件
コードの中の「ブラウザのダウンロード先をすべて統一する」の部分、ここでは、これまでpage._client.sendを利用してPage.setDownloadBehaviorを送り込む事で、ダウンロード先を指定の場所に変更していました。
しかしこの機能が2020年7月頃、Deprecatedになり、代わりに以下のコードでダウンロード先を変更する必要有りとGithubのIssueに掲載されていました。今回のコードで言えばPageを定義後に
1 2 3 4 5 6 7 |
//pageを定義 const page = await browser.newPage() const navigationPromise = page.waitForNavigation() //ブラウザのダウンロード先をすべて統一する const cdpsession = await page.target().createCDPSession(); cdpsession.send ("Browser.setDownloadBehavior", {behavior:"allow", downloadPath: dir_desktop }); |
といったように、createCDPSessionを定義して、Browser.setDownloadBehaviorとして送り込むように変更すべしとのこと。実際にコードを置き換えて実行してみたら問題なくダウンロード出来ました。これまではPage単位だったものをBrowser単位になっている為、Pageのリストを回してセットする必要がなくなっています。
関連リンク
- JavaScriptでアンカータグのhrefを取得する方法を現役エンジニアが解説【初心者向け】
- cheerio - cheerio / jqueryセレクター:タグaのテキストを取得する方法?
- Puppeteer: add basic auth header for main page domain only, not for 3rd party requests
- Page.setDownloadBehaviour deprecated
- page._client.send('Page.setDownloadBehavior', {behavior: 'allow', downloadPath: './tmp/order'}) causing error
- TypeScript + Seleniumのヘッドレスモードで実行したら指定したディレクトリにファイルがダウンロードされない場合の対処法