見出し画像

[GCP]CloudFunctionsでpuppeteerを使う

puppeteer(ぱぺてぃあ)を使った業務の自動化。
時々やってくるエラー通知。そのころにはpuppeteerのことを忘れていて調べるのに苦労することが多いので、今まで勉強したことをちゃんとまとめておこうと思う。
不定期追記予定

よくある対策

ページ遷移が遅くて30秒タイムアウトしてしまう時

・遷移後のページにしか存在しないDOM要素の出現を検知するまで待つ。

await page.waitForSelector('input[name="xxx"]');

・DOM要素が遷移後のページにしかないことの確認方法は、GoogleChromeのディベロッパーツールのConsoleタブで、$$('input[name="xxx"]')と入力し、遷移前のページでは空配列が返ってくること。

ダウンロードファイルをGCSにアップロードする

・ダウンロード先を /tmpフォルダに指定する
・/tmpフォルダからGCSへアップロードする

const {Storage} = require('@google-cloud/storage');
const downloadPath = '/tmp';
const projectId    = 'project_id';
const bucketName   = 'bucket1';
const upload_file  = 'filename_yyyymmdd.csv';

const storage = new Storage({"projectId": projectId});
const bucket = storage.bucket(bucketName);

exports.download_csv = async (req, res) => {
  const browser = await puppeteer.launch({--(省略)--});
  const page = await browser.newPage();
  const client = await page.target().createCDPSession();
  await client.send( //download先を指定
    'Page.setDownloadBehavior',
    {behavior : 'allow', downloadPath: downloadPath}
  );

  // 「ダウンロード」ボタンを押す
  await navigationPromise;
  const downloadLink = "//[contains(@href,'filename1.csv')]";
  await page.waitForXPath(downloadLink);
  const downloadLinkClick = await page.$x(downloadLink);
  await downloadLinkClick[0].click();
  await waitDownloadComplete(downloadPath)
    .catch((err) => console.error(err));
  console.log('-- download success');

  // GCSへアップロード
  await bucket.upload(upload_file)
    .then(res => {
      console.log(`-- ${upload_file} uploaded`);
    })
    .catch(err => {
      console.error('-- ERROR:', err);
    });

エラー内容調査用にスクリーンショットを取ってGCSにアップロード

  pngpath = downloadPath +'/screenshot_xx.png';
  await page.screenshot({ path: pngpath, fullPage:true});
  await bucket.upload(pngpath)
    .then(res => fs.unlinkSync(pngpath));

DOM要素検索の基礎

idで探す→#

await page.waitForSelector('#newbook');

classで探す→. (ドット)

await page.waitForSelector(.main-article');

ある要素の子要素を指定→スペースで区切る

//id="head_news"の要素の子要素で、かつ、class="main-article"
await page.waitForSelector('#head_news .main-article');

タグや属性名で探す

(例1) await page.waitForSelector('input');
(例2) await page.waitForSelector('input.gsc-input'); //class="gsc-input"を持つinputタグ
(例3) await page.waitForSelector('input[name="xxx"]');
(例4) await page.waitForSelector('[data-ship-id="12345"]');

自動化操作 page

URLからページ遷移 .goto

await page.goto(url);
await page.goto(url, {waitUntil: "domcontentloaded"});
await page.waitForSelector('div[class="xxx"]');

クリック .click

await page.click('input[class="xxx"]');
await page.waitForSelector('yyy');

テキスト入力 .type

await page.type('input[type="text"]', username);
await page.type('input[type="password"]', password);

コード例

package.json

{
  "name": "test_func",
  "version": "0.0.1",
  "dependencies": {
    "puppeteer": "^*"
    ,"delay": "^*"
    ,"fs": "^*"
    ,"@google-cloud/storage": "^*"
  }
}

index.js

// import module
const fs        = require('fs');
const puppeteer = require('puppeteer');
const {Storage} = require('@google-cloud/storage');

// variable
const url          = 'https://xxx';
const username     = 'tt';
const password     = 'pw';
const downloadPath = '/tmp';
const projectId    = 'project_id';
const bucketName   = 'bucket1';

// GCSのバケットを指定
const storage = new Storage({"projectId": projectId});
const bucket = storage.bucket(bucketName);

// main
exports.download_csv = async (req, res) => {
  // Puppeteer起動
  const browser = await puppeteer.launch({
    headless: true, 
    args: [
      '--disable-gpu',
      '--disable-dev-shm-usage',
      '--disable-setuid-sandbox',
      '--no-first-run',
      '--no-sandbox',
      '--no-zygote',
      '--single-process'
    ]
  });

  // 新しい空のページを開く
  const page = await browser.newPage();
  const client = await page.target().createCDPSession();
  await client.send( //download先を指定
    'Page.setDownloadBehavior',
    {behavior : 'allow', downloadPath: downloadPath}
    );

  // view portの設定.
  await page.setViewport({
    width: 1200,
    height: 800
  });

  // 待ち時間の設定
  const navigationPromise = page.waitForNavigation(); // 30秒でタイムアウトしてしまうので要注意
  
  // URLにアクセス
  await page.goto(url);
  await page.goto(url, {waitUntil: "domcontentloaded"});

  // ログイン情報を入力してログイン
  await navigationPromise;
  await page.type('input[type="text"]', username);
  await page.type('input[type="password"]', password);
  await page.click('button[class="xxx"]');
  await page.waitForSelector('div[class="xxx"]');
  console.log('-- login success'); 
  
  // ダウンロードページへ移動
  await navigationPromise;
  await page.goto(url_friends, {waitUntil: "domcontentloaded"});
  await page.waitForSelector('a[class="xxx"]');
  console.log('-- ダウンロードページへ');
  pngpath = downloadPath +'/screenshot_friend.png';
  await page.screenshot({ path: pngpath, fullPage:true});
  await bucket.upload(pngpath)
    .then(res => fs.unlinkSync(pngpath));
  
  // 「ダウンロード」ボタンを押す
  await navigationPromise;
  const downloadLink = "//a[contains(@href,'filename_ym.csv')]";
  await page.waitForXPath(downloadLink);
  const downloadLinkClick = await page.$x(downloadLink);
  await downloadLinkClick[0].click();
  await waitDownloadComplete(downloadPath)
    .catch((err) => console.error(err));
  console.log('-- download success');

  // GCSへアップロード
  await bucket.upload(upload_file)
    .then(res => {
      console.log(`-- ${upload_file} uploaded`);
    })
    .catch(err => {
      console.error('-- ERROR:', err);
    });
  
  //ブラウザを閉じる
  await page.waitForTimeout(5000);
  await browser.close();
  console.log('-- close browser');
};


const waitDownloadComplete = async (path, waitTimeSpanMs = 1000, timeoutMs = 60 * 1000) => {
  return new Promise((resolve, reject) => {

    const wait = (waitTimeSpanMs, totalWaitTimeMs) => setTimeout(
      () => isDownloadComplete(path).then(
        (completed) => {
          if (completed) {
            resolve();
          } else {

            const nextTotalTime = totalWaitTimeMs + waitTimeSpanMs;
            if (nextTotalTime >= timeoutMs) {
              reject('timeout');
            }

            const nextSpan = Math.min(
              waitTimeSpanMs,
              timeoutMs - nextTotalTime
            );
            wait(nextSpan, nextTotalTime);
          }
        }      
      ).catch(
        (err) => { reject(err); }
      ),
      waitTimeSpanMs
    );

    wait(waitTimeSpanMs, 0);
  });
};

const isDownloadComplete = async (path) => {
  return new Promise((resolve, reject) => {
    fs.readdir(path, (err, files) => {
      if (err) {
        reject(err);
      } else {
        if (files.length === 0) {
          resolve(false);
          return;
        }
        for(let file of files){

          // .crdownloadがあればダウンロード中のものがある
          if (/.*\.crdownload$/.test(file)) {
            resolve(false);
            return;
          }
        }
        resolve(true);
      }
    });
  });
};

exports.waitDownloadComplete = waitDownloadComplete;
exports.isDownloadComplete = isDownloadComplete;



この記事が気に入ったらサポートをしてみませんか?