Heroku + Puppeteer + SendGridでスクレイピングした結果を自分のメアドに送る
ネタはなんでもいいのですが、日々更新されるサイトのキャプチャを取ってメールで送って欲しいことがあると思います。
例えば、
- 日経平均の業種別騰落率がヒートマップになっている画像を取得
- 騰落率のベスト・ワースト3をサマリーとして記載
- 自分のメールアドレスに送信
と、下記の画像のようなことを実現したいです。
1は 日経平均採用銘柄の株価一覧 :株式 :マーケット :日経電子版 のヒートマップ画像です。
方法としては
- Puppeteerで上記URLにアクセス。
- 画像部分の要素をスクショ
- 添付ファイルとする
- SendGrid経由で自分のメアドに送信
となります。
ローカルでサクサクっとメール受信までできたので、HerokuにDeployするのは楽勝だろうと思っていましたが、下記の点でハマりました。
HerokuにPuppeteerのBuildpackを充てる
$ heroku buildpacks:add buildpack_name
のコマンドでnodejs, puppeteer, 日本語対応用のパックのbuildpackをインストールします。
Puppeteer buildpackの作者(@jontewks)によるとこの順番が問題になるケースもあるようです。理由はよくわからないが私の環境ではこうなってます。
=== YOUR_APP_NAME Buildpack URLs 1. heroku/nodejs 2. jontewks/puppeteer 3. https://github.com/CoffeeAndCode/puppeteer-heroku-buildpack.git
Heroku Appの環境は以下の通りです。
$ heroku apps:info YOUR_APP_NAME === YOUR_APP_NAME Auto Cert Mgmt: false Dynos: web: 1 Git URL: https://git.heroku.com/YOUR_APP_NAME.git Owner: YOUR_APP_NAME@hogehoge.com Region: us Repo Size: 56 KB Slug Size: 231 MB Stack: heroku-18 Web URL: https://YOUR_APP_NAME.herokuapp.com/
この後、heroku run node your_puppeteer_app.js
するとエラーが….
Most likely you need to configure your SUID sandbox correctly SUIDってなんやねん….
2018-11-03T06:03:03.423619+00:00 app[web.1]: (node:4) UnhandledPromiseRejectionWarning: Error: Failed to launch chrome! 2018-11-03T06:03:03.423637+00:00 app[web.1]: 2018-11-03T06:03:03.423639+00:00 app[web.1]: (chrome:21): Gtk-WARNING **: 06:03:03.390: cannot open display: 2018-11-03T06:03:03.423641+00:00 app[web.1]: [1103/060303.407764:ERROR:nacl_helper_linux.cc(310)] NaCl helper process running without a sandbox! 2018-11-03T06:03:03.423643+00:00 app[web.1]: Most likely you need to configure your SUID sandbox correctly 2018-11-03T06:03:03.423645+00:00 app[web.1]: 2018-11-03T06:03:03.423646+00:00 app[web.1]: 2018-11-03T06:03:03.423648+00:00 app[web.1]: TROUBLESHOOTING: https://github.com/GoogleChrome/puppeteer/blob/master/docs/troubleshooting.md 2018-11-03T06:03:03.423649+00:00 app[web.1]: 2018-11-03T06:03:03.423651+00:00 app[web.1]: at onClose (/app/node_modules/puppeteer/lib/Launcher.js:342:14) 2018-11-03T06:03:03.423652+00:00 app[web.1]: at Interface.helper.addEventListener (/app/node_modules/puppeteer/lib/Launcher.js:331:50) 2018-11-03T06:03:03.423654+00:00 app[web.1]: at Interface.emit (events.js:187:15) 2018-11-03T06:03:03.423656+00:00 app[web.1]: at Interface.close (readline.js:379:8) 2018-11-03T06:03:03.423657+00:00 app[web.1]: at Socket.onend (readline.js:157:10) 2018-11-03T06:03:03.423659+00:00 app[web.1]: at Socket.emit (events.js:187:15) 2018-11-03T06:03:03.423660+00:00 app[web.1]: at endReadableNT (_stream_readable.js:1094:12) 2018-11-03T06:03:03.423662+00:00 app[web.1]: at process._tickCallback (internal/process/next_tick.js:63:19)
どうやらpuppeteer起動時の引数が問題なようです。
puppeteer.launch({args: ['--no-sandbox', '--disable-setuid-sandbox']});
この、'--disable-setuid-sandbox’を除いてみるとワークしました!
上記問題の解決に参考にしたリンク
Failed to launch chrome · Issue #807 · GoogleChrome/puppeteer · GitHub
SendGridを使って自分のメアドに送る
このブログが参考になりました。 やはり頼りになるのは公式ブログ。
肝となるのは、Puppeteerで取得した画像ファイルをbase64でエンコードしてattachmentsに渡す部分だと思います。 attachmentを複数形にしていなくて30分くらい公式のリファレンスとにらめっこしてました。
こんな感じで送信する内容を作ります。
const msg = { to: 'YOUR_EMAIL_ADDRESS', from: 'test@example.com', subject: `${png_file}`, html: `<html><body><pre>${emailBody}</pre><img src="cid:myimagecid"/></body></html>`, attachments: [ { filename: png_file, contenType: 'image/png', content: base64str, content_id: 'myimagecid' } ] };
ソースコード
package.json
{ "name": "nikkeiheatmap", "version": "0.0.1", "description": "", "main": "index.js", "scripts": { "test": "echo \"Error: no test specified\" && exit 1", "start" : "node run.js" }, "author": "", "license": "ISC", "dependencies": { "@sendgrid/mail": "^6.3.1", "moment": "^2.20.1", "puppeteer": "^1.4.0", "csv": "^3.1.0", "csv-writer": "^1.0.0", "dotenv": "^6.1.0" } }
.env
SENDGRID_APIKEY=YOUR_SENDGRID_APIKEY DYNO=false
your_puppeteer_app.js
const puppeteer = require('puppeteer'); const moment = require('moment'); const sgMail = require('@sendgrid/mail'); require('dotenv').config() const NikkeiData = require('./nikkeidata.js'); const fs = require("fs"); const FAKE_USERAGENT = 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_13_1) AppleWebKit/604.3.5 (KHTML, like Gecko) Version/11.0.1 Safari/604.3.5'; const NIKKEI_HEATMAP_URL = 'https://www.nikkei.com/markets/kabu/nidxprice/'; const HEATMAP_ELEM = 'div#CONTENTS_MARROW div.m-article'; const LAUNCH_OPTION = process.env.DYNO ? {args: ['--no-sandbox']} : {headless: false}; const TEMP_DIR = process.env.DYNO ? '/tmp/' : '' sgMail.setApiKey(process.env.SENDGRID_APIKEY); async function scrape_data_table(page) { const data = await page.evaluate(() => { const d = Array.from(document.querySelectorAll('div#CONTENTS_MARROW div.m-article div.highcharts-data-labels.highcharts-series-0 div span')).map((e) => { return e.innerText.split('\n') }); return d; },); return data; } function to_email_body(dx) { const data = dx.map((e) => { var d = new NikkeiData(e[0].split('(')[0], Number(e[1].replace('円', ''))) return d }).sort((a, b) => (a.delta < b.delta) ? 1 : ((b.delta < a.delta) ? -1 : 0)); // get top 3 elements const top3 = data.slice(0, 3) // get bottom 3 elements const bottom3 = data.slice(-3).reverse() return `[上昇]\n${top3.reduce((p, c) => { return p + '\n' + c })} \n\n [下落]\n${bottom3.reduce((p, c) => { return p + '\n' + c })}` } async function base64_encode(file) { var bitmap = fs.readFileSync(file); return new Buffer(bitmap).toString("base64"); } (async () => { const browser = await puppeteer.launch(LAUNCH_OPTION); const page = await browser.newPage(); await page.setUserAgent(FAKE_USERAGENT); await page.setViewport({width: 1000, height: 1000, deviceScaleFactor: 1}); await page.goto(NIKKEI_HEATMAP_URL, {waitUntil: 'networkidle2'}); await page.waitFor(1000); const element = await page.$(HEATMAP_ELEM); const png_file = await `nikkei_heatmap_${moment().format('YY-MM-DD hh:mm')}.png` const png_path = await `${TEMP_DIR}${png_file}` await element.screenshot({ path: png_path }); const data = await scrape_data_table(page); const emailBody = to_email_body(data); const base64str = await base64_encode(png_path); const msg = { to: 'YOUR_EMAIL_ADDRESS', from: 'test@example.com', subject: `${png_file}`, html: `<html><body><pre>${emailBody}</pre><img src="cid:myimagecid"/></body></html>`, attachments: [ { filename: png_file, contenType: 'image/png', content: base64str, content_id: 'myimagecid' } ] }; await sgMail.send(msg); await browser.close(); })();
この前新宿の本屋にいったらPuppeteer本が平積みされていました!
Puppeteer入門 スクレイピング+Web操作自動処理プログラミング
- 作者: ヴェネチア冒険團,美崎薫,小原亮一,酒井一成
- 出版社/メーカー: 秀和システム
- 発売日: 2018/09/19
- メディア: 単行本
- この商品を含むブログを見る