初めてのシステム案件で詰まったお話(Heroku + puppeteer)

# はじめに

こんにちは！ばーんです

今回は自分が初めて納品したシステム開発のお仕事で得たものを整理していきます。

今回書いていく内容は

- システム開発全般
- puppeteer(スクレイピング)に関すること
- Heroku(PaaS)に関すること

について書いていきます。これからシステム開発していきたい！
と考えている方は、見ていただけると気づきがあるのかなと思います。

## 結論

ざっくり大切だなと感じたことが3点。

**①デバッグ / コードレビューはお金を支払ってでも現役エンジニアにお願いする**
(デバッグは詰まりに詰まったか、単純に自分がしているところを見てもらう)
見ている観点がそもそも違うのでとても勉強になります。

**②基礎を学ぶことが遠回りのように見えて近道**
結局上辺だけの解決方法では解決しないことが殆どでした。
今回で言えば公式ドキュメントなどの一次情報を見る。linuxについて理解を深めるなど。

**③簡単なことはGASで解決できる**
システム開発これから携わりたい！という方はまずGAS触ればいいと思います。
クライアントの要望の多くは「スプレッドシート + GAS」で解決できました。
GASはできないことが明確なので、見積もりをたてる一つの基準になるかなと思いますし。

また、会社員の方であれば社内のルーチンなどは大抵GAS使えば効率化できます。
信頼を得るまでは時間外で学習としてやってみて完成させる。使ってもらって感触よければ信頼がついて業務時間中にできるようになります。

### 取り掛かる時の自分の状況 / 案件の概要 / 技術選定 / 結果

#### 取り掛かる時の自分の状況

- エンジニアリングの学習歴5ヶ月ほど
- 静的なWebサイト制作数件。JSはjQueryをコピってぺぐらいのレベル(アルゴリズムちょっとやったぐらい)
- サーバー？なんか大きそうな機械？？
- SQL…オレ…シッテル(知らない)

#### 案件の概要

- 別々のECサイトからデータを持ってきて比較したい
- 1日1回持ってくればいい
- データはn万件想定
- 知り合いからの紹介。作ってみて良さそうであれば買い取りたい(どちらかというと買い取りたい)
- 技術選定や大枠のフォローはあり

#### 技術選定

- Heroku
- PostgreSQL
- Node.js
- puppeteer

フレームワークは無し

#### 結果

- 6月中旬から開始で8月頭に納品
- 納品直後にエラー頻発して2週間ぐらいその対応に追われる
- 現在は安定運用

### システム開発について

実際にコードを書いて指摘を受けた部分、アドバイスされたこと、自分で感じたことを書いてます。

#### エラーを読む

これができているようで出来ていなかったです。一番良いのは現役のエンジニアにデバッグしてもらうことかと思います。
できればその様子を横で見させてもらうこと(もしくは自分のデバッグ処理を横で見ててもらう)。

思考プロセスが全く違うなと感じました。

僕は起きた事象から「何が起きたか？」を推測して進めようとしてました。
(経験の浅い人はこうする傾向があると思います)
現役の方はエラーが出ているところから順番に確認していきました。

#### ログを見る

特にシステム開発であれば本番環境で動かすと思うのですが、ログが出力されるようになっていると思います。
(むしろなっていなければ、出すようにした方がいい)

これもエラーと全く同じです。まず、ログを見る。とても大事。

#### 詰まるところが●ぬほど多い。というか●ぬ

調べていったり要件を細かく分けていくと「あれ？これ意外といけんじゃね？」感がでます。
ネットに落ちてるコードちょっと変えれば…と思っていたらめっちゃ詰まります。

(例)Webページに遷移した後スクレイピングする処理
1. ローカル環境で試す
2. Webページにはすぐに遷移できる
3. データの取得がうまくいかない
4. データを取得した時にtextで取得しているので、DOMのツリー構造を保持していない
5. ツリー構造ごと取得する
6. 動作を確認できたので本番環境で試すことにする
7. 本番にデプロイできない
8. デプロイなんとかできても動かない(環境構築ができていない)
9. 環境構築できてもローカルで出ないエラーが発生する
…etc

毎日こんな感じでしたToT

#### デバッグ処理は変数をダンプすることが多々

詰まったエラーで一番多かったのは、変数にデータを想定通りに渡せていないことでした。
それに気付かずロジックを書き換えたり…非常に無駄な時間を過ごしました。

`console.log()`で泥臭く1つ1つの変数を確認していくとエラーの原因に行き当たることが多かったです。

#### 変数名 / 関数名は分かり易く

基準は第三者が見た時に何をするのか分かるかどうか？

いろんな人にたくさん言われましたが中々習得できず…
他人のコードを積極的に見たり、コードレビューを重ねて受けるのがいいと思います。

#### 結局一番見た記事は一次情報(公式)

記事を探すときは、大枠で

- こういうことできるかな？を探す
- エラーを直接打って対応方法を調べる

が多いと思うのですが、残念ながら記事に便利なものは余り多くはありませんでした。
なぜならシステム開発は応用的なものを作り上げていくので(基礎 + 基礎 = 応用)。
微妙に基礎から変更するにはそのメソッドが「何に対してどう動くのか」という仕組み部分を知る必要がある為に、公式にいってメソッドの処理を細かく見ないといけません。

> こういうことできるかな？を探す

これはQiitaなどで書かれていることもありました。
ただ数回の検索ででてこない場合はロジックを考えてメソッドを組み合わせないとできないことが殆どでした。

> エラーを直接打って対応方法を調べる

多くの人がぶつかったエラーでない場合は大抵でてきません。そして、自身とは関係ないことが殆ど。
エラーを読んで各メソッドがどういう処理をしているか把握する。変数に何が入っているか確認する。

基礎を把握することが一番の近道でした。

#### 処理の流れを先に書く

今回処理ごとに進めていって最後の段階でロジックが破綻していることに気付いたことがあります…
そんなことがあったり、メンターからのフィードバックも受け現在は処理の流れを先に書くようにしています。

```js
//(例)オセロゲームの場合

// mainの処理
// イベント(クリック)をリッスンしている
// マスをクリックする(ユーザー側の操作)
// そのマスが押せるか判定する
// 押せる場合は石を置く
// それによって裏返る石がないか確認する
// 次のターンに移る

function(turn){
    ターン数を変更する処理
    return turn
}

function(turn){
    番手を表記する処理
    return 番手
}

function put (turn){
    石(マス)の状態を変更する処理(設置)
    配列の数値を変更する処理
    turn(先手後手)によって配列の変更パターンが変わる
    return なし
}

function turnOver (put){
    putした後に発火させる
    石(マス)の状態を変更する処理(裏返り)
    配列の数値を変更する処理
    return なし
}

function(turnOver){
    turnOverした後に発火させる
    設置するところを明示する処理
    ※マス目の配列を3にする
}
```

これは現在書いているオセロゲームの処理ですが、このように日本語で記載していくことが大切だなーと学びました。
結果として時間が短縮できますし、今後必要な実装なども見えてくるので。
(上記のオセロゲームは書きかけのものです)

この記事なんかは見易かったのでおすすめです＾＾
http://labs.timedia.co.jp/2013/07/write-othello-in-javascript-with-functional-style.html

#### 都度メモをとる

エラーとずっと対峙していると「あれ…？このエラーさっきも出会わなかったっけ？」
みたいな迷子状態が発生します。特に頭も動かしてるのでゴチャゴチャになってます。

都度メモをとること。何でもいいです。

```js
// hogehogeは想定通りfugafugaの値が挿入されていた
// HOGEが動いていない可能性がある→確認したが実行はされていた
```

こんな感じで大丈夫なので自分の思考をメモにとっておいて、自分のキャッシュを軽くすることが大切です。

#### デバッグ用の一時的なコードなら、その旨をコメントなどで示す

「後で消そう〜」みたいな処理もその旨を記載する。
未来の自分が覚えているとは限らない。

#### コードコメントは消す

便利なので「とりあえず今の処理はコピーしておいておいて…」は大抵使わない。
積もっていって大抵は醜いコードの塊ができるのでGitを使ってコメントはなるべく使わない。

また、コードの説明もできる限り避ける。
コメント書かなくても読めるようなコードを目指す。

#### DBの基礎は知っておく

簡単な成績表みたいなもので良いので、SQLでかけるようになっておく。
CRUD処理はできるようになっておく。

### puppeteer(スクレイピング)に関すること

puppeteerの処理を実装していて詰まった内容を記載していきます。

#### スクレイピングはサーバー攻撃になり得る

これはpuppeteerに限らずですが、スクレイピングはサーバー攻撃になり得ます。
スクレイピングではないものの、過去学生の方が悪意なく書いたコードが無限に繰り返される処理で事件になったこともあります。

また、スクレイピングを利用規約にNGと明示しているサイトもあります。
(というか基本的に快くは受け入れられていないはず)

実装前は利用規約を確認すること。
スリープ処理を入れること。

#### headless: falseについて

ローカルで実装しているときは`headless: false`をオプションにつけて動かすととても見易いですが、本番環境(Heroku上)では使えません(ディスプレイがないので当然)
しかも、最悪なことに`headless: false`の状態だと動かないメソッドもあり、本番環境で苦労したことを覚えています…

早い段階で`headless: ture`で動かすことをお勧めします。

実例で言うと

- click
- goto

この2つは`headless: false`で動きが変わるので注意が必要です。

#### goto処理について

おそらくgotoメソッドは不具合を抱えています。
https://stackoverflow.com/questions/62618052/how-to-get-puppeteer-to-simply-load-a-web-page

このStackOverflowにあるGitHubのIssuesも見ましたが、答えが出でてない。

症状としては、

1. httpリクエストが飛ぶ
2. レスポンスが返ってこない
3. タイムアウトエラーになる(この後何度繰り返しても同一URLに弾かれる)

議論の中では、CSS / JSのロードが終わっていないことも書かれていてそれが原因？
と書かれていましたが、CSS / JSのロードを無効にしても同じ症状。

また、質問者は解決したと書いていますが、そのURLへのgoto処理諦めているだけなので根本的には解決していない。gotoできなくなるURLに共通の特徴もない。

自分の場合は1,000件につき10件程度の処理落ちだったので、そのまま無視して実装しています。
(この辺はシステム要件によって対応が変わってきますが、自分の場合は無視しても問題なかったので)

自分は似たような案件がくれば次はseleniumで対応すると思います。

#### click処理について

`page.click`処理は`headless: false`では機能しません。
公式の中でChromiumのバグだ！って以前見たのですが見つけられずorz

ページを次のページに移動させる処理はURLのpage部分に変数を入れてforで回して解決しました。

#### userAgentについて

こちらも意外な落とし穴だったのですが、`headless: false`の状態はuserAgentに「headlessで動いてるで」っていう記載がされています。
そして、サイトによってはheadlessはアクセスを弾くサイトもあるのでご注意を。

その場合はuserAgentを変更するメソッドがあるのでそちらを使用してください。

#### 要素があるかを判断する

```js
const getProduct = await page.$("className")
.then((res) => !!res);
if (getProduct) {
    // 要素があった時の処理
}
```

これはとても使えました＾＾
(元記事があったのですが、見つからなかったので断念してます)

#### 要素が複数ある場合の処理

要素の取得は単数形と複数形があります。
取得するメソッドに応じて取得してくる内容も`ElementHandle `なのか`NodeList`なのか変わってきます。
こういった細かい違いは探さないと出てこずハマりやすいのでご注意ください。

ちなみにpuppeteerで開発する際はこの2記事がとてもオススメです！
ここでわからなければ大抵公式にしか解答がなかったように思います。

https://qiita.com/taminif/items/1ba7f68aedd68bae5e09
https://qiita.com/go_sagawa/items/85f97deab7ccfdce53ea

#### DOMのツリー構造を保持したい場合

記事に書いている要素の取得は、DOM取得→パース処理を流れで書いています。
DOMのツリー構造をそのまま保持して兄弟要素などを判定したい場合にそのまま流用してるとハマるのでご注意ください。

自分は一旦空の配列に入れて対応しました。

#### new Browser

今回自分はn千件ほどの処理を同一ブラウザで処理していたのですが、puppeteerの仕様としてブラウザのキャッシュはずっと保持し続けるようです(というより解放されない)。
ブラウザをnewしたタイミングで作り直されるとのこと。

で、処理落ちを繰り返したので数件ほどでブラウザを立ち上げ直す処理を入れたら改善されました。

#### 並列処理について

map + Promise.allで擬似的に並列処理を実装できます。

```js
async () => {
    await Promise.all(
        sampleArr[i].map(async (product) => {
            // mapで繰り返したい処理
        })
    );
}
```

件数が多い処理を実装するときはおすすめです＾＾

### Heroku(PaaS)に関すること

Herokuで実行する時に詰まった部分です。

#### サーバーとは何かを理解する

これが理解できていなかったので無茶苦茶詰まりました…
一番おすすめなのは`heroku run bash`でbashを使用することができるので、
使用した後`ls`や`pwd`コマンドを打つと普段ターミナルで操作しているような感覚で操作できます。
後、デプロイされている内容を視覚的に確認できます。

自身は今回から「サーバーとは何か？」をもっと知っていかないとまずいと考えてlinuxの学習を始めました。
AWS / GCP共に無料でインスタンス生成までできるのでおすすめです＾＾
(両方公式ドキュメントも豊富ですし、記事も沢山あるので1人でもアプリをデプロイするぐらいまではいけると思います)

#### 無料枠でできること / できないこと

これは最初に明確にしていった方が良かったです…
今回の自分の件であればPostgreSQLは無料枠だと1万レコードが限界でした。

また、メモリの容量や稼働時間などクライアントが知りたがる範囲は事前に把握しておくべきでした。

#### まず、本番環境で小さい単位で動かしてみる

当然ローカルとは環境が違うので、ビルドする時の障害があったり、ローカルでは動くけど本番で動かないメソッドなどもあります。
Heroku + puppeteerに関して言うと専用のビルドパックが必要です。
これは、過去不具合があったものを修正するために後から出されたのですが、タイミングによっては自分がひっかかることもあり得ます。

なので全部完成させてからではなく、ポイントポイントで確認すると手戻りも少なくなるのかなと思います。

#### ローカルと同じ環境を構築する

納品後に**「ローカルでは動くけど、本番環境で想定通りの振る舞いをしない」**という自体が発生ToT

しかも、問題が何個か重なっているので改修にとても時間がかかりました(どうしようもなくなったので、最後は現役エンジニアの方にデバッグ手伝ってもらいました)

この時にDockerを使うと「環境が違うことが原因」or「そもそもコードに問題がある」という問題を切り分けられたので、楽に対応することができます。

#### (番外編)クレジットカード登録

herokuのような海外のサイトを使うとそこそこの頻度でクレジットカードの登録を求められます。
登録しておくとお金使わなくても使用できるものが変わるとか、そもそも登録しないとサービス使わせませんみたいなサービスもあるかと。

自分はそんなときはバンドルカードを使用しています。
https://vandle.jp/

クレジットカードのように使えますが、実態はプリペイドカードとして運用できるのでチャージ金額以上の支払いは無くせます。
(運用やプランによってはクレジットカードと同じ状態になるのでご注意ください)

年会費、登録費無料で身分証不要＆即発行(アプリ)
便利なのでおすすめです＾＾

## さいごに

さいごまで見ていただいてありがとうございました！

今回で学んだことが非常に多くあったので、今後はそれらを消化しながら進んでいきたいと思います。

**①デバッグ / コードレビューはお金を支払ってでも現役エンジニアにお願いする
②基礎を学ぶことが遠回りのように見えて近道
③簡単なことはGASで解決できる**

一応結論をもう一回載せて締めにさせていただきます。ありがとうございましたm_ _m