tag:crieit.net,2005:https://crieit.net/tags/Puppeteer/feed 「Puppeteer」の記事 - Crieit Crieitでタグ「Puppeteer」に投稿された最近の記事 2021-06-25T08:38:02+09:00 https://crieit.net/tags/Puppeteer/feed tag:crieit.net,2005:PublicArticle/17439 2021-06-25T03:57:06+09:00 2021-06-25T08:38:02+09:00 https://crieit.net/posts/puppeteer puppeteer でファイルをダウンロードするときに、任意のパスと名前で保存する <p><a target="_blank" rel="nofollow noopener" href="https://pptr.dev/">Puppeteer</a> を使ってファイルをダウンロードする際に、任意のパスと名前で保存したい。</p> <p>残念ながら、 現時点ではシンプルな方法は提供されていないようだ。<br /> 以下の Issue で何年にもわたって議論されているものの、 <strong>「コレ!」</strong> という解決方法は無さそう。<br /> <a target="_blank" rel="nofollow noopener" href="https://github.com/puppeteer/puppeteer/issues/299">Question: How do I get puppeteer to download a file? · Issue #299 · puppeteer/puppeteer</a></p> <p>しかし、 この Issue の <a target="_blank" rel="nofollow noopener" href="https://github.com/puppeteer/puppeteer/issues/299#issuecomment-668087154">#issuecomment-668087154</a> のコメントで、 なかなか泥臭い方法で実現するヒントが書かれていた。<br /> これを参考にして、任意のパスと名前でダウンロードファイルを保存してみよう。</p> <h2 id="実行方法"><a href="#%E5%AE%9F%E8%A1%8C%E6%96%B9%E6%B3%95">実行方法</a></h2> <p>あらかじめ、 <a target="_blank" rel="nofollow noopener" href="https://www.npmjs.com/package/puppeteer">puppeteer</a> の npm パッケージをローカルにインストールしておく。</p> <pre><code class="console">npm install puppeteer --save </code></pre> <p>その状態で、後述の .js ファイルを nodejs で実行すれば OK だ。</p> <pre><code class="console">node puppeteer-download-with-specify-name.js </code></pre> <h2 id="コードと解説"><a href="#%E3%82%B3%E3%83%BC%E3%83%89%E3%81%A8%E8%A7%A3%E8%AA%AC">コードと解説</a></h2> <gist src="https://gist.github.com/advanceboy/557a7690e1b584d11f38ff86434aef65.js"></gist> <p>何をしているのかというと、 GitHub 上の <a target="_blank" rel="nofollow noopener" href="https://www.npmjs.com/package/puppeteer">puppeteer</a> のソースコード ZIP ファイルをダウンロードする際に、 <a target="_blank" rel="nofollow noopener" href="https://chromedevtools.github.io/devtools-protocol/">Chrome DevTools Protocol</a> を直叩きして、 任意のパスとファイル名で保存している。</p> <p>具体的なポイントは、主に 以下の 2点。</p> <ul> <li><code>Page.setDownloadBehavior</code> メソッドで、 ファイルのダウンロードの許可とダウンロード先のディレクトリを指定</li> <li><code>Fetch.enable</code> メソッドと <code>Fetch.requestPaused</code> イベントで、 ファイルダウンロードのレスポンスに <code>Content-Disposition</code> HTTP ヘッダーを無理やりねじ込む</li> </ul> <p><a target="_blank" rel="nofollow noopener" href="https://developer.mozilla.org/ja/docs/Web/HTTP/Headers/Content-Disposition"><code>Content-Disposition</code> HTTP ヘッダー</a> のドキュメントに書かれている通り、 <code>attachment</code> と <code>filename</code> ディレクティブを指定することで、 ファイルが (ブラウザ内で表示されるのではなく)ダウンロードが必要であることと、 ダウンロード時のファイル名を指定することができる。</p> <p>但し、 <code>Page.setDownloadBehavior</code> メソッドは <a target="_blank" rel="nofollow noopener" href="https://chromedevtools.github.io/devtools-protocol/tot/Page/#method-setDownloadBehavior">実験的で且つ非推奨</a> なので、 将来にわたってサポートが続くかどうかはわからない点は、注意だ。<br /> 少なくとも、 Chromium 92.0.4512.0 (r884014) では問題なく動いている。</p> <p>ちなみに、実行する Chromium はヘッドレスモードでもヘッドフルモードでもどちらでも意図通り動くはず。</p> <hr /> <p>この方法は Chrome DevTools Protocol に思いっきり依存しているので、 Selenium など他のブラウザ自動化ツールでは同一の方法が難しく (※)、 Puppeteer ならではの方法と言える。<br /> ※: Selenium 4.x のプレリリース版を使えば、 Chrome DevTools Protocol にアクセスできるようだが、 イベントハンドラを書くのが難しそう? ドキュメントがそろってないのでまだなんとも…</p> <p>スクレイピング中にファイルをダウンロードする場合などでは、保存先のパスと名前を指定できたほうが良い気がするのだが……<br /> 今後の puppeteer や Chrome DevTools Protocol の更新でもっと簡単に実現できるようになることを期待しよう。</p> advanceboy tag:crieit.net,2005:PublicArticle/16378 2020-12-16T23:42:55+09:00 2020-12-20T00:38:39+09:00 https://crieit.net/posts/backstop-trial-and-error-20201215 BackstopJS で背景画像の高さを `vh` 単位で指定したページで画像やスクリーンショットが引き伸ばされる現象についてメモ (未解決) <p>メインビジュアル等で背景画像の高さを vh 単位で指定したページで、画像やスクリーンショットが引き伸ばされる現象に遭遇してしまいました。正常なスクリーンショットが撮影できないので困った……ということで、その現象について調べたメモです。</p> <h2 id="調査"><a href="#%E8%AA%BF%E6%9F%BB">調査</a></h2> <ul> <li><a target="_blank" rel="nofollow noopener" href="https://github.com/garris/BackstopJS/issues/785">Issues using vh units in CSS ・ Issue #785 ・ garris/BackstopJS</a></li> <li><a target="_blank" rel="nofollow noopener" href="https://github.com/garris/BackstopJS/issues/820">Take screenshot without resizing page ・ Issue #820 ・ garris/BackstopJS</a></li> </ul> <p>調べたらそれと思しき現象の Issues が上がっていました。ただ、どちらも現在進行形でオープンになっているので解決には至っていない模様……。</p> <h2 id="検証"><a href="#%E6%A4%9C%E8%A8%BC">検証</a></h2> <p>スレッドの中で提示された対処法:</p> <ol> <li><code>"mergeImgHack": true,</code> あるいは高さ決め打ちならば <code>"mergeImgHack": 1024,</code> のような値を <code>backstop.json</code> のルートに追記する</li> <li>beta版を使用する</li> </ol> <p>1.は効果がなかったので2.を試そうと思ったのですが、 Github のブランチ (<code>2.6.7</code>)と npm で指定した場合(<code>4.3.3</code>)で Beta のバージョンが異なる模様。</p> <p><code>package.json</code> の <code>dependencies</code> や <code>devDependencies</code> に <code>"backstopjs": "https://github.com/garris/BackstopJS#beta"</code> と指定すると <code>2.6.7</code>、<code>"backstopjs": "beta",</code> と指定すると <code>4.3.3</code> が落ちてきます。</p> <p>そこで各バージョンで何度か試行錯誤を繰り返しました。</p> <ul> <li><code>5.0.7</code>: <ul> <li>最新版</li> <li>背景画像が引き伸ばされる現象が発生する</li> <li>engine で chromy を指定しようとしたところ、 <code>createBitmaps | Chromy is no longer supported in version 5+. Please use version 4.x.x for chromy support.</code> とのこと。 <code>2.6.7</code> ではそこそこそれっぽい表示になったので <code>engine</code> を切り替えることができれば……と思ったのですが、ダメそうです</li> </ul></li> <li><code>4.3.3</code>: <ul> <li>背景画像が引き伸ばされる現象の解決には至らず</li> <li>いろいろ今どきに近くなっているので各種設定はしやすい</li> </ul></li> <li><code>2.6.7</code>: <ul> <li>昨日別の場所で試したときは動いたのですが動作再現できず</li> <li>内部的に PhantomJS を使用している模様。2018年に開発終了しているので今更使いたくはない</li> <li>設定を node script から渡すとき、オブジェクトではなくファイルパスを渡す模様。とすると、いったん <code>backstop.json</code> を書き出す処理を挟まないといけない</li> </ul></li> </ul> <p>よくよく先程のスレッドを見直してみると <code>npm install backstopjs@beta</code> とか <code>4.3.3</code> と言ったワードがあるので普通に npm 指定の方で良さそうですね。</p> <p>ひとまず <code>devDependencies</code> に <code>"backstopjs": "beta"</code> を指定してまた色々試してみたいと思います。</p> <h2 id="備考"><a href="#%E5%82%99%E8%80%83">備考</a></h2> <ul> <li><a target="_blank" rel="nofollow noopener" href="https://swet.dena.com/entry/2018/04/26/152326">Puppeteerによるフルページスクリーンショットを画像遅延読み込みに対応させる - DeNA Testing Blog</a></li> </ul> <p>スクロールに応じてコンテンツが表示される系のアニメーションもあるので、「下端までスクロールしてからスクリーンショットを撮影する」も対応したいですね……。</p> <h2 id="追記(2020/12/20)"><a href="#%E8%BF%BD%E8%A8%98%282020%2F12%2F20%29">追記(2020/12/20)</a></h2> <p>色々試行錯誤した結果のリポジトリを晒しておきます。</p> <ul> <li><a target="_blank" rel="nofollow noopener" href="https://github.com/arm-band/test_backstop_randa_back_stopper_2">arm-band/test_backstop_randa_back_stopper_2: BackstopJS のテストです。</a></li> </ul> <p>これは本当、どうしたものか……。</p> <h2 id="参考"><a href="#%E5%8F%82%E8%80%83">参考</a></h2> <ul> <li><a target="_blank" rel="nofollow noopener" href="https://github.com/garris/BackstopJS/issues/785">Issues using vh units in CSS ・ Issue #785 ・ garris/BackstopJS</a></li> <li><a target="_blank" rel="nofollow noopener" href="https://github.com/garris/BackstopJS/issues/820">Take screenshot without resizing page ・ Issue #820 ・ garris/BackstopJS</a></li> </ul> <h3 id="スクリプト"><a href="#%E3%82%B9%E3%82%AF%E3%83%AA%E3%83%97%E3%83%88">スクリプト</a></h3> <h4 id="複数枚のスクリーンショットを繋ぎ合わせる"><a href="#%E8%A4%87%E6%95%B0%E6%9E%9A%E3%81%AE%E3%82%B9%E3%82%AF%E3%83%AA%E3%83%BC%E3%83%B3%E3%82%B7%E3%83%A7%E3%83%83%E3%83%88%E3%82%92%E7%B9%8B%E3%81%8E%E5%90%88%E3%82%8F%E3%81%9B%E3%82%8B">複数枚のスクリーンショットを繋ぎ合わせる</a></h4> <ul> <li><a target="_blank" rel="nofollow noopener" href="https://github.com/garris/BackstopJS/issues/820#issuecomment-404963122">Take screenshot without resizing page ・ Issue #820 ・ garris/BackstopJS</a></li> </ul> <h4 id="下端までスクロールさせる"><a href="#%E4%B8%8B%E7%AB%AF%E3%81%BE%E3%81%A7%E3%82%B9%E3%82%AF%E3%83%AD%E3%83%BC%E3%83%AB%E3%81%95%E3%81%9B%E3%82%8B">下端までスクロールさせる</a></h4> <ul> <li><a target="_blank" rel="nofollow noopener" href="https://swet.dena.com/entry/2018/04/26/152326">Puppeteerによるフルページスクリーンショットを画像遅延読み込みに対応させる - DeNA Testing Blog</a></li> <li><a target="_blank" rel="nofollow noopener" href="https://gist.github.com/nnks1010/10abf3032933609d1b2bd5b99fc12586">絶対顔本スクレイピングするマン</a></li> </ul> <h4 id="Node.js での実行中スクリプトの絶対パス"><a href="#Node.js+%E3%81%A7%E3%81%AE%E5%AE%9F%E8%A1%8C%E4%B8%AD%E3%82%B9%E3%82%AF%E3%83%AA%E3%83%97%E3%83%88%E3%81%AE%E7%B5%B6%E5%AF%BE%E3%83%91%E3%82%B9">Node.js での実行中スクリプトの絶対パス</a></h4> <ul> <li><a target="_blank" rel="nofollow noopener" href="https://gist.github.com/uupaa/da42698d6b2d2cbb3cca">node.js で絶対パスや相対パスを取得する方法 npm __dirname</a></li> </ul> arm-band tag:crieit.net,2005:PublicArticle/16034 2020-08-18T23:18:26+09:00 2020-08-19T20:34:19+09:00 https://crieit.net/posts/Crieit-5f3be332191fb 【アップデート】Crieitバッジを作りました <h1 id="Crieitバッジ"><a href="#Crieit%E3%83%90%E3%83%83%E3%82%B8">Crieitバッジ</a></h1> <p>GitHubなどに貼るCrieitバッジを作りました。</p> <p>これが私(ckoshien)のバッジです。<br /> <a target="_blank" rel="nofollow noopener" href="http://crieit.net/users/ckoshien"><img src="https://ogp-vercel.vercel.app/crieit/ckoshien" alt="Crieit経験値" /></a></p> <p>ちなみにCrieit開発者だらさんはといえば....<br /> <a target="_blank" rel="nofollow noopener" href="http://crieit.net/users/dala00"><img src="https://ogp-vercel.vercel.app/crieit/dala00" alt="Crieit経験値" /></a></p> <p>....やっぱり強い強すぎる。</p> <h2 id="使い方"><a href="#%E4%BD%BF%E3%81%84%E6%96%B9">使い方</a></h2> <p>このコードの*****の部分を自分のユーザID(例:ckoshienとかdala00)に書き換えて貼るだけです。</p> <pre><code>[![Crieit経験値](https://ogp-vercel.vercel.app/crieit/*****)](http://crieit.net/users/*****) </code></pre> <h1 id="どうやって作ったか"><a href="#%E3%81%A9%E3%81%86%E3%82%84%E3%81%A3%E3%81%A6%E4%BD%9C%E3%81%A3%E3%81%9F%E3%81%8B">どうやって作ったか</a></h1> <h2 id="着想"><a href="#%E7%9D%80%E6%83%B3">着想</a></h2> <p><a target="_blank" rel="nofollow noopener" href="https://github.com/mikkame/mikkame">mikkame</a>さんの<a target="_blank" rel="nofollow noopener" href="https://qiita-badge.apiapi.app/">Qiita のスコアをGithub風バッジに変換するサービス</a>を見てCrieit版作ろう!と思いました。</p> <h2 id="技術的なこと"><a href="#%E6%8A%80%E8%A1%93%E7%9A%84%E3%81%AA%E3%81%93%E3%81%A8">技術的なこと</a></h2> <p>shield.io版からアップデートしました。<br /> 現在はvercel + NextJS + puppeteerでスクレイピングした情報で一旦画面を作ってスクリーンショットを撮っています。</p> <p>結果的にvercelを2台分(スクレイピング/スクリーンショット)使っています。</p> <h3 id="バックエンド"><a href="#%E3%83%90%E3%83%83%E3%82%AF%E3%82%A8%E3%83%B3%E3%83%89">バックエンド</a></h3> <ul> <li>vercel/NodeJS(lambda)</li> <li>puppeteer</li> </ul> <p>puppeteerでcrieitのユーザページをスクレイピングして経験値を取得しています。</p> <pre><code class="javascript">async function getCrieitBadge(user_id) { const browser = await puppeteer.launch({ args: chrome.args, executablePath: await chrome.executablePath, headless: chrome.headless, }); const page = await browser.newPage(); await page.goto("https://crieit.net/users/"+user_id); await page.setCacheEnabled(true); const itemSelector = "#app > #container > div.row > div > p.mb-2 > span:nth-child(1)"; const itemSelector2 = "#app > #container > div.row > div > p.mb-2 > span:nth-child(2)"; var item = await page.$(itemSelector); var item2 = await page.$(itemSelector2); var data = await (await item.getProperty('textContent')).jsonValue(); var data2 = await (await item2.getProperty('textContent')).jsonValue(); await page.close(); await browser.close(); return data; } </code></pre> <p>現在は画像を1日キャッシュするようにして返しています。<br /> ではよいCrieitライフを!!</p> ckoshien tag:crieit.net,2005:PublicArticle/15766 2020-03-14T22:03:11+09:00 2020-03-14T22:04:12+09:00 https://crieit.net/posts/1bbf25a7cba4095f0fd1afe817b553dd (改良版)パワプロ風画面ジェネレータを作ってみた <p><a href="https://crieit.now.sh/upload_images/102e92a702e89059033b1dff5b0f87c55e6cd23db0d46.jpg" target="_blank" rel="nofollow noopener"><img src="https://crieit.now.sh/upload_images/102e92a702e89059033b1dff5b0f87c55e6cd23db0d46.jpg?mw=700" alt="" /></a></p> <h1 id="作ったサービス"><a href="#%E4%BD%9C%E3%81%A3%E3%81%9F%E3%82%B5%E3%83%BC%E3%83%93%E3%82%B9">作ったサービス</a></h1> <p><a target="_blank" rel="nofollow noopener" href="https://pawapro-gen.netlify.com/">https://pawapro-gen.netlify.com/</a></p> <h1 id="一旦書いたボード"><a href="#%E4%B8%80%E6%97%A6%E6%9B%B8%E3%81%84%E3%81%9F%E3%83%9C%E3%83%BC%E3%83%89">一旦書いたボード</a></h1> <p><a href="https://crieit.net/boards/web1week-202003/6fbd2929361eb9caa53fc33d51d31f36">web1week - パワプロ風画面ジェネレータを作ってみた</a></p> <p>時間的に残りの機能間に合わないかと思っていたので、最低限動く機能で出したのですが、結論から言うと間に合いました。</p> <h1 id="インライン編集機能"><a href="#%E3%82%A4%E3%83%B3%E3%83%A9%E3%82%A4%E3%83%B3%E7%B7%A8%E9%9B%86%E6%A9%9F%E8%83%BD">インライン編集機能</a></h1> <p>当初はフォームとプレビュー機能を分けて作っていたのですが、<br /> ほとんど全てインラインで編集できるように変更しました。</p> <h1 id="twitterシェア機能"><a href="#twitter%E3%82%B7%E3%82%A7%E3%82%A2%E6%A9%9F%E8%83%BD">twitterシェア機能</a></h1> <h2 id="OGPサーバの構築"><a href="#OGP%E3%82%B5%E3%83%BC%E3%83%90%E3%81%AE%E6%A7%8B%E7%AF%89">OGPサーバの構築</a></h2> <p>以前<a href="https://crieit.net/posts/slack">slack流量計</a>を作った際に、web表彰のスクリーンショットを撮っていたので、OGP作成にはそれほど抵抗はありませんでした。</p> <p>あとはだらさんの作成されてる<a target="_blank" rel="nofollow noopener" href="https://github.com/dala00/puppeteer-ogp">OGPサーバ</a>を参考にさせていただきました。</p> <h3 id="OGPサーバの構築に使った技術"><a href="#OGP%E3%82%B5%E3%83%BC%E3%83%90%E3%81%AE%E6%A7%8B%E7%AF%89%E3%81%AB%E4%BD%BF%E3%81%A3%E3%81%9F%E6%8A%80%E8%A1%93">OGPサーバの構築に使った技術</a></h3> <ul> <li>NodeJS</li> <li>Express</li> <li>TypeScript</li> <li>puppeteer</li> <li>VPS</li> <li>docker-compose</li> </ul> <h2 id="ロジック"><a href="#%E3%83%AD%E3%82%B8%E3%83%83%E3%82%AF">ロジック</a></h2> <ul> <li>ジェネレータからDBにデータを登録する</li> <li>登録されたデータをもとにpuppeteerでスクリーンショットを撮る</li> <li>DBに画像を格納する</li> <li>OGPのサーバ問い合わせにはDBに保存してある画像を使用する <ul> <li>OGPサーバに直接アクセスさせると負荷が集中してサーバが落ちるため</li> </ul></li> </ul> <h3 id="DB登録処理は既存のAPサーバに追加"><a href="#DB%E7%99%BB%E9%8C%B2%E5%87%A6%E7%90%86%E3%81%AF%E6%97%A2%E5%AD%98%E3%81%AEAP%E3%82%B5%E3%83%BC%E3%83%90%E3%81%AB%E8%BF%BD%E5%8A%A0">DB登録処理は既存のAPサーバに追加</a></h3> <h3 id="OGPサーバは別コンテナ"><a href="#OGP%E3%82%B5%E3%83%BC%E3%83%90%E3%81%AF%E5%88%A5%E3%82%B3%E3%83%B3%E3%83%86%E3%83%8A">OGPサーバは別コンテナ</a></h3> <h2 id="twitter投稿"><a href="#twitter%E6%8A%95%E7%A8%BF">twitter投稿</a></h2> <p>アプリ起動時<code>componentDidMount</code>のタイミングでUUIDを生成するようにしました。<br /> <a target="_blank" rel="nofollow noopener" href="https://www.npmjs.com/package/react-share">react-share</a>というライブラリを使い、<br /> <code>beforeOnClick</code>メソッドでDBへの登録を行っています。<br /> ローディング画面も作りました(同時にバグ生成)。</p> <pre><code class="html"><TwitterShareButton url={'https://pawapro-gen.netlify.com/view/'+store.getState().uuid} hashtags={['パワプロ風画面ジェネレータで作ってみた']} beforeOnClick={async()=>{ store.dispatch(switchLoading(true)); let isSuccess = await postData(); store.dispatch(switchLoading(false)); if(!isSuccess){ alert('データの保存に失敗しました。時間を空けるか、リロードして再度作成してください。') return new Error(); }else{ return Promise.resolve(); } <span>}</span><span>}</span> > 保存してシェアする! </TwitterShareButton> </code></pre> <h1 id="苦労したこと"><a href="#%E8%8B%A6%E5%8A%B4%E3%81%97%E3%81%9F%E3%81%93%E3%81%A8">苦労したこと</a></h1> <h2 id="画像のキャッシュ"><a href="#%E7%94%BB%E5%83%8F%E3%81%AE%E3%82%AD%E3%83%A3%E3%83%83%E3%82%B7%E3%83%A5">画像のキャッシュ</a></h2> <p>ヘッダでキャッシュコントロールしたことがなかったので若干はまりました。</p> <h2 id="OGPのトリミング問題"><a href="#OGP%E3%81%AE%E3%83%88%E3%83%AA%E3%83%9F%E3%83%B3%E3%82%B0%E5%95%8F%E9%A1%8C">OGPのトリミング問題</a></h2> <p>twitterやFacebookでOGPの上下左右がトリミングされる問題があり、<br /> Jimpなどで余白の拡張をしようと思ったのですが、時間がなかったこともあり、<br /> React側でマージンをとってスクリーンショットを撮る際に余白ができるように修正しました。</p> <h1 id="できなかったこと"><a href="#%E3%81%A7%E3%81%8D%E3%81%AA%E3%81%8B%E3%81%A3%E3%81%9F%E3%81%93%E3%81%A8">できなかったこと</a></h1> <h2 id="チーム・選手画像のアップロード"><a href="#%E3%83%81%E3%83%BC%E3%83%A0%E3%83%BB%E9%81%B8%E6%89%8B%E7%94%BB%E5%83%8F%E3%81%AE%E3%82%A2%E3%83%83%E3%83%97%E3%83%AD%E3%83%BC%E3%83%89">チーム・選手画像のアップロード</a></h2> <p>OGPの生成で手一杯だったので、base64化してDBに格納するところまで考えられませんでした。後々追加する予定。</p> ckoshien tag:crieit.net,2005:PublicArticle/15686 2020-01-16T22:04:03+09:00 2020-01-16T22:04:03+09:00 https://crieit.net/posts/AV-AV スワイプで気になるAV女優を探すことができる「AVキャッチャー」というサービスをリリースしました。 <p>crieitでの初めての投稿になります。<br /> 今回、スワイプで気になるAV女優を探すことができる「AVキャッチャー」というサービスをリリースしたので、集客も含めて知見の共有ができればなと思い、記事を書いてみました。</p> <h1 id="どんなサービス?"><a href="#%E3%81%A9%E3%82%93%E3%81%AA%E3%82%B5%E3%83%BC%E3%83%93%E3%82%B9%EF%BC%9F">どんなサービス?</a></h1> <p>自分の好みを入力し、気になる女優をスワイプで探すことができるサービスです。<br /> スワイプすることによって、女優が出演する作品も簡単に探すことができるようになっています。<br /> <a href="https://crieit.now.sh/upload_images/38a082137c43e7a4c62bac88cc2324c75e205f3689bae.jpg" target="_blank" rel="nofollow noopener"><img src="https://crieit.now.sh/upload_images/38a082137c43e7a4c62bac88cc2324c75e205f3689bae.jpg?mw=700" alt="av_catcher_ogp.jpg" /></a><br /> <a target="_blank" rel="nofollow noopener" href="https://av-catcher.web.app/">https://av-catcher.web.app/</a></p> <p>登録不要なので、気軽に触ってみてください!</p> <h1 id="なんで作ったの?"><a href="#%E3%81%AA%E3%82%93%E3%81%A7%E4%BD%9C%E3%81%A3%E3%81%9F%E3%81%AE%EF%BC%9F">なんで作ったの?</a></h1> <p>最近のAV女優って、めちゃくちゃ可愛い人が多いです!<br /> もっとたくさんの人に知ってもらいたいと思い、作ってみました。</p> <h1 id="どんな構成?"><a href="#%E3%81%A9%E3%82%93%E3%81%AA%E6%A7%8B%E6%88%90%EF%BC%9F">どんな構成?</a></h1> <p>アプリ自体はReactで作成し、firebaseでhostingしています。<br /> また、データの永続化にはfirebaseのfirestoreを使用し、アプリからデータを取得する際はfirebaseのcloud functionsを通して取得するような構成になっています。</p> <h1 id="データはどのように取得しているの?"><a href="#%E3%83%87%E3%83%BC%E3%82%BF%E3%81%AF%E3%81%A9%E3%81%AE%E3%82%88%E3%81%86%E3%81%AB%E5%8F%96%E5%BE%97%E3%81%97%E3%81%A6%E3%81%84%E3%82%8B%E3%81%AE%EF%BC%9F">データはどのように取得しているの?</a></h1> <p>今回は、FANZAが公開している情報を元に、アプリを作成しています。<br /> FANZAでは、月間のDVD売り上げランキングを公開しているので、そちらのページから人気の女優名を取得し、FANZAが公開しているAPIを使って、詳細情報と商品情報を取得するようにしています。<br /> スクレイピングにはpuppeteerを使用し、firebaseのfunctionsを定期実行することによって、日時でデータを更新するようにしています。</p> <h1 id="作ってみた感想"><a href="#%E4%BD%9C%E3%81%A3%E3%81%A6%E3%81%BF%E3%81%9F%E6%84%9F%E6%83%B3">作ってみた感想</a></h1> <p>やはり自分の好きなことだと、夢中になって作れます。<br /> また、firebaseが非常に便利なため、簡単なアプリであれば短期間で作りきることができるようになりました。<br /> これも、近年の個人開発ブームを加速させている要因の一つになっているのではいかと思います。<br /> 自分は個人開発が大好きなので、この記事がどなたかの個人開発の背中を押せたら幸いです。</p> <p>リリースしてからのPVや売り上げ等は、今後の動きをみて随時更新していこうと思います。</p> AVキャッチャー tag:crieit.net,2005:PublicArticle/15445 2019-10-01T20:31:32+09:00 2019-10-01T20:31:32+09:00 https://crieit.net/posts/puppeteer-Twitter puppeteer初心者がTwitterブックマークをエクスポートするツールを作りながら、使い方をまとめてみた <p>ふと、puppeteerがおもしろそうだなと思い、前から欲しかった<br /> TwitterブックマークをJSONファイルにエクスポートするツールを題材に、<br /> いろいろ遊んでみた時に備忘録。</p> <p>puppeteerはサクッと使えるので、すてき(<em>´ω`</em>)</p> <h3 id="作ったもの"><a href="#%E4%BD%9C%E3%81%A3%E3%81%9F%E3%82%82%E3%81%AE">作ったもの</a></h3> <p>こんな感じで勝手に操作してエクスポートしてくれます(<em>´ω`</em>)</p> <blockquote class="twitter-tweet"><p lang="ja" dir="ltr">puppeteerで自動ログインして、ブクマをJOSNでエクスポートできるように(*´ω`*)わかりやすいように背景色を変えたりしてる(*´ω`*) <a target="_blank" rel="nofollow noopener" href="https://t.co/UJiGAiw5KN">pic.twitter.com/UJiGAiw5KN</a></p>— 積読ハウマッチ📚きらぷか (@kira_puka) <a target="_blank" rel="nofollow noopener" href="https://twitter.com/kira_puka/status/1178913910432256002?ref_src=twsrc%5Etfw">October 1, 2019</a></blockquote> <p>最終的なソースコードはGitHubで公開中。<br /> - <a target="_blank" rel="nofollow noopener" href="https://github.com/memory-lovers/export_twitter_bookmarks_puppeteer">memory-lovers/export_twitter_bookmarks_puppeteer: Twitter Bookmark Export Tool using Puppeteer</a></p> <p>ただ、注意事項がたくさんですが。。(-_-;)</p> <hr /> <h3 id="puppeteerの使い方"><a href="#puppeteer%E3%81%AE%E4%BD%BF%E3%81%84%E6%96%B9">puppeteerの使い方</a></h3> <h4 id="インストール"><a href="#%E3%82%A4%E3%83%B3%E3%82%B9%E3%83%88%E3%83%BC%E3%83%AB">インストール</a></h4> <pre><code class="console">$ npm install -S puppeteer </code></pre> <h4 id="基本的な雛形"><a href="#%E5%9F%BA%E6%9C%AC%E7%9A%84%E3%81%AA%E9%9B%9B%E5%BD%A2">基本的な雛形</a></h4> <p>基本的にはこんな感じ。</p> <ol> <li>ブラウザを起動</li> <li>ページを作成</li> <li>なんか処理する</li> <li>ブラウザの終了</li> </ol> <pre><code class="javascript">const puppeteer = require("puppeteer"); const fs = require("fs"); async function main() { let browser = null; try { // ブラウザの起動 browser = await puppeteer.launch(); // ページの作成 const page = await browser.newPage(); // 何らかの処理 } catch (error) { console.error(`Error: ${error}`, error); } finally { // ブラウザの終了 if (!!browser) await browser.close(); } } main().then(); </code></pre> <h4 id="puppeteerでできること"><a href="#puppeteer%E3%81%A7%E3%81%A7%E3%81%8D%E3%82%8B%E3%81%93%E3%81%A8">puppeteerでできること</a></h4> <h5 id="ブラウザの起動/停止"><a href="#%E3%83%96%E3%83%A9%E3%82%A6%E3%82%B6%E3%81%AE%E8%B5%B7%E5%8B%95%2F%E5%81%9C%E6%AD%A2">ブラウザの起動/停止</a></h5> <pre><code class="javascript">// ブラウザの起動: headlessで起動 const browser = await puppeteer.launch(); // ブラウザの起動: headlessじゃなく起動 const browser = await puppeteer.launch({ headless: false, slowMo: 10 }); // ブラウザの終了 await browser.close(); </code></pre> <p><code>headless: false</code>にすると、ブラウザが立ち上がって、動作確認画できる。<br /> <code>slowMo: 10</code>の値を大きくすると、スローモーションのように操作がゆっくりになる。</p> <h5 id="ページの開く/閉じる"><a href="#%E3%83%9A%E3%83%BC%E3%82%B8%E3%81%AE%E9%96%8B%E3%81%8F%2F%E9%96%89%E3%81%98%E3%82%8B">ページの開く/閉じる</a></h5> <pre><code class="javascript">// 新規ページの作成 const page = await browser.newPage(); // 画面サイズの設定 await page.setViewport({ width: 1280, height: 1200 }); // ページを閉じる await page.close(); </code></pre> <h5 id="指定したURLへ移動"><a href="#%E6%8C%87%E5%AE%9A%E3%81%97%E3%81%9FURL%E3%81%B8%E7%A7%BB%E5%8B%95">指定したURLへ移動</a></h5> <pre><code class="javascript">// 指定したURLへ移動 await page.goto("https://www.google.com", { waitUntil: "networkidle2" }); // 指定したURLへ移動: waitを設定 await page.goto("https://www.google.com", { waitUntil: "networkidle2" }); </code></pre> <p>オプションの<code>waitUntil</code>を指定すると、その条件が満たされるまでwaitする。<br /> 指定できるのは、以下の4つ。</p> <ul> <li><code>load</code>: <code>load</code>イベントが発火するまで</li> <li><code>domcontentloaded</code>: <code>DOMContentLoaded</code>イベントが発火するまで</li> <li><code>networkidle0</code>: ネットワーク接続が0個である状態が500ミリ秒続いたとき</li> <li><code>networkidle2</code>: ネットワーク接続が2個である状態が500ミリ秒続いたとき</li> </ul> <p>SPAとかの場合は、<code>networkidle2</code>とかまで待つと良さそう。</p> <p>参考: <a target="_blank" rel="nofollow noopener" href="https://codeday.me/jp/qa/20190513/814601.html">PuppeteerによるJavaScriptレンダリングされたHTMLの取得 - コードログ</a></p> <h5 id="要素の取得"><a href="#%E8%A6%81%E7%B4%A0%E3%81%AE%E5%8F%96%E5%BE%97">要素の取得</a></h5> <pre><code class="javascript">// 最初の`.button`の要素を取得 const button = await page.$('.button'); // すべての`.button`の要素を取得 const buttonList = await page.$$('.button'); </code></pre> <p>実際は<a target="_blank" rel="nofollow noopener" href="https://pptr.dev/#?product=Puppeteer&version=v1.20.0&show=api-class-elementhandle">ElementHandle</a>が返ってくる。</p> <p>1件取得と全件取得があるので注意。<br /> セレクタの書き方は<a target="_blank" rel="nofollow noopener" href="https://developer.mozilla.org/en-US/docs/Web/CSS/CSS_Selectors">CSS selectors</a>が使える。</p> <p><a target="_blank" rel="nofollow noopener" href="https://developer.mozilla.org/en-US/docs/Web/API/Document/evaluate">XPATH</a>で書ける<code>page.$x();</code>というのもある。</p> <h5 id="要素のクリック"><a href="#%E8%A6%81%E7%B4%A0%E3%81%AE%E3%82%AF%E3%83%AA%E3%83%83%E3%82%AF">要素のクリック</a></h5> <pre><code class="javascript">// クリック: ページからセレクタで指定 await page.click('.button'); // クリック: ElementHandlerからクリック const button = await page.$('.button'); await button.click(); // クリック: ページからElementHandlerを使ってevaluate const button = await page.$('.button'); await page.evaluate(v => v.click(), button) // クリック: ElementHandlerからevaluateでクリック const button = await page.$('.button'); await button.evaluate(v => v.click()) </code></pre> <p>クリックなど、JavaScriptを実行する方法はいくつかある。<br /> SPAなサイトだとうまく行かない場合があるが、<code>page.evaluaate</code>などを使うとうまくいく時がある。</p> <h5 id="入力する"><a href="#%E5%85%A5%E5%8A%9B%E3%81%99%E3%82%8B">入力する</a></h5> <pre><code class="javascript">// テキストを入力する: ページからセレクタで指定 await page.type('#text-input', "Hello"); // テキストを入力する: ElementHandlerで指定 const inputText = await page.$('#text-input'); await inputText.type("Hello"); </code></pre> <h5 id="待つ/waitする"><a href="#%E5%BE%85%E3%81%A4%2Fwait%E3%81%99%E3%82%8B">待つ/waitする</a></h5> <pre><code class="javascript">// 1000ms待つ await page.waitFor(1000); // 指定した要素が表示されるまで待つ await page.waitForSelector(`.foo`); // or await page.waitFor('.foo'); // 条件を満たすまで待つ await page.waitFor(() => !!document.querySelector('.foo')); // 移動するまで待つ await Promise.all([ page.waitForNavigation(), page.click('a.my-link'), ]); // or const navigationPromise = page.waitForNavigation(); await page.click('a.my-link'), await navigationPromise; </code></pre> <h5 id="その他もろもろ"><a href="#%E3%81%9D%E3%81%AE%E4%BB%96%E3%82%82%E3%82%8D%E3%82%82%E3%82%8D">その他もろもろ</a></h5> <p><code>evaluate</code>を使うとHTML要素に対して実行できるので、いろいろできる</p> <pre><code class="javascript"><br />// innerTextを取得 const innerText = await elm.evaluate(node => node.innerText); // textContentを取得 const textContent = await elm.evaluate(node => node.textContent); // href属性の取得 const href = await elm.evaluate(node => node.href); // 背景色変更 await elm.evaluate((v, color) => (v.style.backgroundColor = color), "gray"); // URLの取得 const url = await page.evaluate(_ => location.origin); // スクロール: 1画面分 await page.evaluate(_ => window.scrollBy(0, window.innerHeight)); // スクロール: 指定要素まで await page.evaluate(elm => window.scrollBy(0, elm.getBoundingClientRect().top), elm); </code></pre> <h5 id="スクリーンショットの取得"><a href="#%E3%82%B9%E3%82%AF%E3%83%AA%E3%83%BC%E3%83%B3%E3%82%B7%E3%83%A7%E3%83%83%E3%83%88%E3%81%AE%E5%8F%96%E5%BE%97">スクリーンショットの取得</a></h5> <pre><code class="javascript">// スクリーンショットの取得: 表示範囲のみ await page.screenshot({ path: "screenshot.png" }); // スクリーンショットの取得: フルページを指定 await page.screenshot({ path: "screenshot.png", fullPage: true }); // スクリーンショットの取得: 指定要素のみ const element = await page.$('h1'); await element.screenshot({path: 'screenshot_h1.png'}); </code></pre> <h5 id="描画されたHTMLの取得"><a href="#%E6%8F%8F%E7%94%BB%E3%81%95%E3%82%8C%E3%81%9FHTML%E3%81%AE%E5%8F%96%E5%BE%97">描画されたHTMLの取得</a></h5> <pre><code class="javascript">const fs = require("fs"); // HTMLの取得: ページ全体 const html = await page.content(); fs.writeFileSync("output.html", html); // HTMLの取得: 指定要素のみ const bodyHandle = await page.$('body'); const html_body = await page.evaluate(body => body.innerHTML, bodyHandle); fs.writeFileSync("output_body.html", html_body); </code></pre> <h3 id="エクスポートするツールを作ってみる"><a href="#%E3%82%A8%E3%82%AF%E3%82%B9%E3%83%9D%E3%83%BC%E3%83%88%E3%81%99%E3%82%8B%E3%83%84%E3%83%BC%E3%83%AB%E3%82%92%E4%BD%9C%E3%81%A3%E3%81%A6%E3%81%BF%E3%82%8B">エクスポートするツールを作ってみる</a></h3> <p>やりたいことは、こんな感じ。</p> <ol> <li>ブラウザ起動</li> <li>ログイン</li> <li>ブックマークページに移動</li> <li>以下繰り返し: 取得できる情報がなくなるまで <ul> <li>ブックマークの情報を取得</li> <li>ブックマークの削除</li> </ul></li> <li>取得した情報を.jsonファイルに書き出し</li> <li>ブラウザの停止</li> </ol> <h4 id="メインの処理はこんな感じ"><a href="#%E3%83%A1%E3%82%A4%E3%83%B3%E3%81%AE%E5%87%A6%E7%90%86%E3%81%AF%E3%81%93%E3%82%93%E3%81%AA%E6%84%9F%E3%81%98">メインの処理はこんな感じ</a></h4> <pre><code class="javascript">async function exportBookmarkMain() { let browser = null; try { // ブラウザの起動 browser = await puppeteer.launch({ headless: false, slowMo: 10 }); // ページの作成 const page = await browser.newPage(); await page.setViewport({ width: 1280, height: 1200 }); // ログイン: ログインページに移動&認証 await login(page); // ブックマークのエクスポート: ブックマークページに移動&ツイート上の取得 const bookmarks = await getTwitterBookmarks(browser, page); console.log(`bookmarks size is ${bookmarks.length}`); // 取得した情報の書き出し const timestamp = dayjs().format("YYYYMMDD_HHmmss"); const outputFile = `twitter_bookmarks_${timestamp}.json`; fs.writeFileSync(`output/${outputFile}`, JSON.stringify(bookmarks)); } catch (error) { console.error(`Error: ${error}`, error); } finally { // ブラウザの停止 if (!!browser) await browser.close(); } } </code></pre> <h4 id="ログイン処理"><a href="#%E3%83%AD%E3%82%B0%E3%82%A4%E3%83%B3%E5%87%A6%E7%90%86">ログイン処理</a></h4> <pre><code class="javascript">/** * ログイン処理 */ async function login(page) { // dotenvからアカウント情報の取得 const account = process.env.TWITTER_ACCOUNT; const password = process.env.TWITTER_PASSWORD; // 指定したURLへ移動: waitを設定 await page.goto("https://twitter.com/", { waitUntil: "networkidle2" }); await page.waitForSelector(`.LoginForm > .LoginForm-username > .text-input`); // アカウントとパスワード入力 await page.type(`.LoginForm > .LoginForm-username > .text-input`, account); await page.type(`.LoginForm > .LoginForm-password > .text-input`, password); // ログインボタンを押して、ページ遷移するまで待つ const navigationPromise = page.waitForNavigation(); await page.click(` .LoginForm > .EdgeButton`); await navigationPromise; } </code></pre> <h4 id="ブックマークのエクスポート処理"><a href="#%E3%83%96%E3%83%83%E3%82%AF%E3%83%9E%E3%83%BC%E3%82%AF%E3%81%AE%E3%82%A8%E3%82%AF%E3%82%B9%E3%83%9D%E3%83%BC%E3%83%88%E5%87%A6%E7%90%86">ブックマークのエクスポート処理</a></h4> <p>くり返す処理はこんな感じ。<br /> ツイートは<code><article></code>タグのようなので、それを起点に処理を進めていく。</p> <pre><code class="javascript">async function getTwitterBookmarks(browser, page) { const bookmarks = []; try { // ブックマークに移動 const bookmarksURL = "https://twitter.com/i/bookmarks"; await page.goto(bookmarksURL, { waitUntil: "networkidle2" }); // ブックマークしたツイートのHTML要素の取得 const articles = await page.$$("article"); for (let i = 0; i < articles.length; i++) { const article = articles[i]; // ツイートまでスクロール await page.evaluate(elm => window.scrollBy(0, elm.getBoundingClientRect().top), article); await page.waitFor(1000); // articleから情報を取得(別処理) const data = await toArticleData(browser, page, article); bookmarks.push(data); // ブックマークの削除(別処理) await deleteBookmark(browser, page, article); } } catch (error) { console.error(`** Error occuerred: ${error}`, error); } return bookmarks; } </code></pre> <p>無限ローディングを持つような場合、適宜スクロールしないと要素が表示されないので、<br /> ツイートごとにスクロールしている。</p> <h5 id="ブックマークしたツイートから情報を取得"><a href="#%E3%83%96%E3%83%83%E3%82%AF%E3%83%9E%E3%83%BC%E3%82%AF%E3%81%97%E3%81%9F%E3%83%84%E3%82%A4%E3%83%BC%E3%83%88%E3%81%8B%E3%82%89%E6%83%85%E5%A0%B1%E3%82%92%E5%8F%96%E5%BE%97">ブックマークしたツイートから情報を取得</a></h5> <p>かなりTwitterの仕様によっているけど</p> <ol> <li>取得したい要素を特定して、</li> <li>その要素を取得するセレクタを書き、</li> <li>innterTextやtextContentで文字を取得する</li> </ol> <p>といった、感じのことをしている。</p> <pre><code class="javascript">async function toArticleData(browser, page, article) => { // 初期化 const articleData = { accountName: "", accountId: "", accountURL: "", tweetText: "", tweetURL: "", links: [] }; // ツイートしたユーザのアカウント名とTwitterIdを取得 const account = "div > div:nth-of-type(2) > div:nth-of-type(2) > div:nth-of-type(1)"; const accountName = await article.$(`${account} a > div:nth-of-type(1) > div:nth-of-type(1)`); const accountId = await article.$(`${account} a > div:nth-of-type(1) > div:nth-of-type(2)`); articleData.accountName = await accountName.evaluate(node => node.innerText); articleData.accountId = await accountId.evaluate(node => node.innerText); // ツイートの内容を取得 const tweetData = "div > div:nth-of-type(2) > div:nth-of-type(2)"; const tweet = await article.$(`${tweetData} > div:nth-of-type(2)`); const tweetText = await tweet.evaluate(node => node.innerText); articleData.tweetText = tweetText; // ツイートに含まれるリンク(<a>)をすべて取得 const aTags = await article.$$(`${tweetData} a`); for (let i = 0; i < aTags.length; i++) { const aTag = aTags[i]; const text = await aTag.evaluate(node => node.textContent); const link = await aTag.evaluate(node => node.href); articleData.links.push({ link: link, text: text }); } // <a>の1つ目はユーザのURL articleData.accountURL = articleData.links[0].link; // <a>の2つ目はツイートのURL articleData.tweetURL = articleData.links[1].link; articleData.links.splice(0, 2); return articleData; }; </code></pre> <h5 id="ブックマークの削除"><a href="#%E3%83%96%E3%83%83%E3%82%AF%E3%83%9E%E3%83%BC%E3%82%AF%E3%81%AE%E5%89%8A%E9%99%A4">ブックマークの削除</a></h5> <pre><code class="javascript">async deleteBookmark(browser, page, article) { const waitTime = 1500; // 待ち時間 // 削除対象までスクロール await page.evaluate(elm => window.scrollBy(0, elm.getBoundingClientRect().top), article); await page.waitFor(1000); // 「ツイートを共有」ボタンをクリック const button = await article.$("div[aria-label='ツイートを共有']"); await page.evaluate(v => v.click(), button); // すこし待つ await page.waitFor(waitTime); // クリックするとメニューが出てくるので、取得 const menuItems = await page.$$("div[role='menuitem']"); // 非公開アカウントかどうかにより、メニューの数が変わるの処理を分ける if (menuItems.length === 3) { // 通常、メニューが3つあり、2つ目が削除ボタン await menuItems[1].click(); await page.waitFor(waitTime); } else if (menuItems.length === 1) { // 非公開の場合は、削除ボタンのみ表示 await menuItems[0].click(); await page.waitFor(waitTime); } }; </code></pre> <p>こんな感じで、「要素を探す→クリック→少し待つ」のくり返し。<br /> ただ、ブラウザで操作しているときでも、削除されないときがある。。</p> <h2 id="使ってみた感想"><a href="#%E4%BD%BF%E3%81%A3%E3%81%A6%E3%81%BF%E3%81%9F%E6%84%9F%E6%83%B3">使ってみた感想</a></h2> <p>スクレイピング自体始めてだったけど、puppeteer自体がすごくよく、簡単に使うことができた(<em>´ω`</em>)</p> <p>ただ、Twitterみたいなのを対象にするのは結構大変だった。。</p> <h5 id="1. どうセレクタを書けば、期待する要素をとってこれるのかを考えないといけない"><a href="#1.+%E3%81%A9%E3%81%86%E3%82%BB%E3%83%AC%E3%82%AF%E3%82%BF%E3%82%92%E6%9B%B8%E3%81%91%E3%81%B0%E3%80%81%E6%9C%9F%E5%BE%85%E3%81%99%E3%82%8B%E8%A6%81%E7%B4%A0%E3%82%92%E3%81%A8%E3%81%A3%E3%81%A6%E3%81%93%E3%82%8C%E3%82%8B%E3%81%AE%E3%81%8B%E3%82%92%E8%80%83%E3%81%88%E3%81%AA%E3%81%84%E3%81%A8%E3%81%84%E3%81%91%E3%81%AA%E3%81%84">1. どうセレクタを書けば、期待する要素をとってこれるのかを考えないといけない</a></h5> <p>特にscoped CSSを使っていて、class名がないdivばかりだとつらい</p> <h5 id="2. SPAなど動的に変わる部分が多いサイトだと、クリックなどがうまく動かないことがある"><a href="#2.+SPA%E3%81%AA%E3%81%A9%E5%8B%95%E7%9A%84%E3%81%AB%E5%A4%89%E3%82%8F%E3%82%8B%E9%83%A8%E5%88%86%E3%81%8C%E5%A4%9A%E3%81%84%E3%82%B5%E3%82%A4%E3%83%88%E3%81%A0%E3%81%A8%E3%80%81%E3%82%AF%E3%83%AA%E3%83%83%E3%82%AF%E3%81%AA%E3%81%A9%E3%81%8C%E3%81%86%E3%81%BE%E3%81%8F%E5%8B%95%E3%81%8B%E3%81%AA%E3%81%84%E3%81%93%E3%81%A8%E3%81%8C%E3%81%82%E3%82%8B">2. SPAなど動的に変わる部分が多いサイトだと、クリックなどがうまく動かないことがある</a></h5> <p>対象サイトのJavaScriptが正しく動作しない場合がある。。</p> <h5 id="3. 実行や動作確認に時間がかかるので、テストにかなり時間がかかる"><a href="#3.+%E5%AE%9F%E8%A1%8C%E3%82%84%E5%8B%95%E4%BD%9C%E7%A2%BA%E8%AA%8D%E3%81%AB%E6%99%82%E9%96%93%E3%81%8C%E3%81%8B%E3%81%8B%E3%82%8B%E3%81%AE%E3%81%A7%E3%80%81%E3%83%86%E3%82%B9%E3%83%88%E3%81%AB%E3%81%8B%E3%81%AA%E3%82%8A%E6%99%82%E9%96%93%E3%81%8C%E3%81%8B%E3%81%8B%E3%82%8B">3. 実行や動作確認に時間がかかるので、テストにかなり時間がかかる</a></h5> <p>あと、サイトのデザインが変わると追従対応しないといけない。。<br /> 便利だけど、かなり大変そうな感じ(<em>´ω`</em>)</p> <p>けど、ポイントを守ればかなり便利だなと、今更ながら体感(<em>´ω`</em>)</p> <h2 id="こんなのつくってます!!"><a href="#%E3%81%93%E3%82%93%E3%81%AA%E3%81%AE%E3%81%A4%E3%81%8F%E3%81%A3%E3%81%A6%E3%81%BE%E3%81%99%21%21">こんなのつくってます!!</a></h2> <p>積読用の読書管理アプリ 『積読ハウマッチ』をリリースしました!<br /> <a target="_blank" rel="nofollow noopener" href="https://tsundoku.site">積読ハウマッチ</a>は、Nuxt.js+Firebaseで開発してます!</p> <p><img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/478782/572d4947-f40b-e4dc-1c9c-bc584cd2a66c.png" width="200"/></p> <p>もしよかったら、遊んでみてくださいヽ(=´▽`=)ノ</p> <p>要望・感想・アドバイスなどあれば、<br /> 公式アカウント(<a target="_blank" rel="nofollow noopener" href="https://twitter.com/MemoryLoverz">@MemoryLoverz</a>)や開発者(<a target="_blank" rel="nofollow noopener" href="https://twitter.com/kira_puka">@kira_puka</a>)まで♪</p> <h1 id="参考にしたサイト様"><a href="#%E5%8F%82%E8%80%83%E3%81%AB%E3%81%97%E3%81%9F%E3%82%B5%E3%82%A4%E3%83%88%E6%A7%98">参考にしたサイト様</a></h1> <ul> <li><a target="_blank" rel="nofollow noopener" href="https://qiita.com/kanoe/items/9043a81d28a1b733b2e2">Puppeteerのセットアップから使い方まで〜ブラウザ操作の自動化〜 - Qiita</a></li> <li><a target="_blank" rel="nofollow noopener" href="https://pptr.dev/">Puppeteer v1.20.0</a></li> <li><a target="_blank" rel="nofollow noopener" href="https://codeday.me/jp/qa/20190513/814601.html">PuppeteerによるJavaScriptレンダリングされたHTMLの取得 - コードログ</a></li> <li><a target="_blank" rel="nofollow noopener" href="https://qiita.com/unhurried/items/56ea099c895fa437b56e#1-%E4%B8%80%E5%AE%9A%E6%99%82%E9%96%93%E3%83%8D%E3%83%83%E3%83%88%E3%83%AF%E3%83%BC%E3%82%AF%E9%80%9A%E4%BF%A1%E3%81%AE%E3%81%AA%E3%81%84%E3%81%93%E3%81%A8%E3%81%A7%E5%AE%8C%E4%BA%86%E3%82%92%E5%88%A4%E5%AE%9A%E3%81%99%E3%82%8B">puppeteerを使ったスクレイピング - Qiita</a></li> <li><a target="_blank" rel="nofollow noopener" href="https://gist.github.com/nnks1010/10abf3032933609d1b2bd5b99fc12586">絶対顔本スクレイピングするマン</a></li> <li><a target="_blank" rel="nofollow noopener" href="https://qiita.com/go_sagawa/items/85f97deab7ccfdce53ea">puppeteerでの要素の取得方法 - Qiita</a></li> <li><a target="_blank" rel="nofollow noopener" href="https://stocker.jp/diary/nth-child/">CSSのnth-childとnth-of-typeについて基本から学ぼう | Stocker.jp / diary</a></li> <li><a target="_blank" rel="nofollow noopener" href="https://qiita.com/tomi_linka/items/a68cf7840c3da002c6e0#%E3%83%AC%E3%83%B3%E3%83%80%E3%83%AA%E3%83%B3%E3%82%B0%E3%82%92%E5%BE%85%E3%81%A4">puppeteerでスクレイピング - Qiita</a></li> <li><a target="_blank" rel="nofollow noopener" href="https://qiita.com/rh_taro/items/32bb6851303cbc613124#%E9%81%B7%E7%A7%BB%E3%82%92%E5%BE%85%E3%81%A4-%E3%81%9D%E3%81%AE2">puppeteerでよく使うであろう処理の書き方 - Qiita</a></li> <li><a target="_blank" rel="nofollow noopener" href="https://qiita.com/AYA_iro/items/e5dd0956b3ba82f6bf31">puppeteerを体験してみた - Qiita</a></li> </ul> きらぷか@積読ハウマッチ/SSSAPIなど tag:crieit.net,2005:PublicArticle/14700 2018-12-28T19:11:14+09:00 2018-12-28T19:11:14+09:00 https://crieit.net/posts/Puppeteer-on-AWS-Lambda Puppeteer on AWS Lambda のスクリーンショットで文字が豆腐になるので、ウェブフォントと絵文字フォントをインストール <h2 id="状況"><a href="#%E7%8A%B6%E6%B3%81">状況</a></h2> <p>現在開発中の https://poiit.me で puppeteer のスクリーンショットを使って動的OGPをつくっています。<br /> CSSで動的な画像をデザインできるし、一つ lambda 関数を作っておけば、いろんなところで応用ができるのでいいですね。</p> <p>が、今回は絵文字が、豆腐になってしまったのでその対処法。</p> <p><img src="https://qiita-image-store.s3.amazonaws.com/0/6531/6a6ea738-cf5e-15cb-4eb3-acb2dc85e5d8.jpeg" alt="yahsan2-ogp.jpg" /></p> <h2 id="参考記事"><a href="#%E5%8F%82%E8%80%83%E8%A8%98%E4%BA%8B">参考記事</a></h2> <p>ベースはこちらの記事を参考にしました。<br /> <a target="_blank" rel="nofollow noopener" href="https://qiita.com/zyyx-matsushita/items/c33f79e33f242395019e">https://qiita.com/zyyx-matsushita/items/c33f79e33f242395019e</a></p> <p>基本は参考Qiita をベースに対応できましたが、絵文字フォントがどこにあるのか結構探したので、備忘録的にメモ。</p> <h2 id="絵文字フォントダウンロードと設置"><a href="#%E7%B5%B5%E6%96%87%E5%AD%97%E3%83%95%E3%82%A9%E3%83%B3%E3%83%88%E3%83%80%E3%82%A6%E3%83%B3%E3%83%AD%E3%83%BC%E3%83%89%E3%81%A8%E8%A8%AD%E7%BD%AE">絵文字フォントダウンロードと設置</a></h2> <p>Google の Noto Color Emoji はこちらから ダウンロードできました。<br /> <a target="_blank" rel="nofollow noopener" href="https://www.google.com/get/noto/#noto-emoji-zsye-color">https://www.google.com/get/noto/#noto-emoji-zsye-color</a></p> <p><strong>他のダウンロード可能な絵文字フォントを知っていたら、是非ダウンロード場所教えてください!</strong></p> <p>で、ダウンロードしたファイルを解答し <code>/.font/NotoColorEmoji.ttf</code> に設置</p> <h2 id="WEBフォント"><a href="#WEB%E3%83%95%E3%82%A9%E3%83%B3%E3%83%88">WEBフォント</a></h2> <p>こちらは絵文字以外のフォントについては、WEBフォントをつかっています。</p> <p>screenshot を撮る前に head に追加してやれば反映されるはず。</p> <pre><code class="js"> await page.evaluate(() => { var style = document.createElement('style') style.textContent = ` @import url('//fonts.googleapis.com/css?family=M+PLUS+Rounded+1c|Roboto:300,400,500,700|Material+Icons'); div, input, a, p{ font-family: "M PLUS Rounded 1c", sans-serif; };` document.head.appendChild(style); document.body.style.fontFamily = "'M PLUS Rounded 1c', sans-serif"; }) </code></pre> <p>その後、長めに wait させたないとフォントが反映されていないことがあるので注意</p> <pre><code> await page.waitFor(3000); </code></pre> <p>こちらの記事を参考にしています。<br /> <a target="_blank" rel="nofollow noopener" href="https://qiita.com/chimame/items/04c9b45d8467cf32892f">https://qiita.com/chimame/items/04c9b45d8467cf32892f</a></p> <h2 id="関数実行"><a href="#%E9%96%A2%E6%95%B0%E5%AE%9F%E8%A1%8C">関数実行</a></h2> <p>lambada で呼び出す関数で、フォントをサーバーにインストールする <code>fc-cache</code> コマンド実行してから、<br /> puppeteer の screenshot などを実行すれば、ちゃんと絵文字が反映されているはずです。</p> <p>アップロードすれば終了です。</p> <p><img src="https://qiita-image-store.s3.amazonaws.com/0/6531/54eae76b-6c6d-e567-d400-0c18bc462718.jpeg" alt="yahsan2-ogp-1.jpg" /></p> <p>めでたし!</p> <p>serverless を使ったベーシックな puppeteer on lambda は github に置いてあるので必要あれば。<br /> <a target="_blank" rel="nofollow noopener" href="https://github.com/yahsan2/puppeteer-example-on-serverless-lambda">https://github.com/yahsan2/puppeteer-example-on-serverless-lambda</a></p> <p>この記事のフォント反映バージョンも要望あればで公開しますので、コメントください〜!</p> <h3 id="poiit というサービスつくっています!"><a href="#poiit+%E3%81%A8%E3%81%84%E3%81%86%E3%82%B5%E3%83%BC%E3%83%93%E3%82%B9%E3%81%A4%E3%81%8F%E3%81%A3%E3%81%A6%E3%81%84%E3%81%BE%E3%81%99%EF%BC%81">poiit というサービスつくっています!</a></h3> <p>この記事がもし参考になれば、50円からサポートお願いします〜!<br /> <a target="_blank" rel="nofollow noopener" href="https://poiit.me/yahsan2/"><img src="http://res.cloudinary.com/dcsqopmuq/image/upload/v1545924831/poiit-production/yahsan2/yahsan2-ogp.jpg" alt="yahsan2-ogp-1.jpg" title="画像タイトル" /></a><br /> <a target="_blank" rel="nofollow noopener" href="https://poiit.me/yahsan2/">https://poiit.me/yahsan2/</a></p> yahsan2