新人研修課題 クローラー作成記2

## はじめに
こんにちは、株式会社エーアイセキュリティラボ　新入社員の大桶です。
前回の記事では業務に必要な知識習得の一つのまとめとして、実際の製品とは異なる研修用の簡易的なクローラーの作成を始めたことについて書きましたが、先日無事に完成させることができました！
今回の記事では、作成中に行き当たった問題とその対処についてご紹介します。
前回いただいた[要件](https://qiita.com/AeyeScan/items/b442ca1aae70922de18b#%E8%A6%81%E4%BB%B6)のうち、特にformタグの巡回とログインの実行についての話がメインです。
はじめに比較的簡単だったaタグの巡回について述べ、次にaタグにはないformタグ巡回に独自の苦労とその対処について述べます。
次に、formタグ巡回の中でも処理が特殊なログインの実行について述べます。

## aタグの巡回
今回作成した簡易クローラーの動作を表すフローチャートのうち、aタグ巡回部分(赤いブロック)を拡大した図を以下に示します。
![aタグ.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2422105/1e3d295d-0123-255f-9e1b-836dc60a5c83.png)

aタグの巡回はおおむね以下のような手順で表せます。
前提: 巡回候補と巡回済みリストを保持する。初期状態では巡回候補は1つURLがあり(= 開始URL)、巡回済みリストは空。
1. 巡回候補から一つURLを取り出してGET リクエストを送る
2. リクエストを送ったURLを巡回済みリストに加える
3. 受け取ったレスポンスボディからaタグを探す
4. aタグからhref属性を取り出し、指定されたURLを巡回候補に加える。ここで巡回済みや巡回候補リストと重複するものは巡回候補に加えない。
5. 1に戻る。

さらに大まかに言えば「巡回候補URLにリクエストを送る」→「受け取ったリクエストから新しい巡回候補を見つける」の繰り返しです。

## formタグの巡回
今回作成した簡易クローラーの動作を表すフローチャートのうち、formタグ巡回部分(赤いブロック)を拡大した図を以下に示します。
![formタグ.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2422105/a41b3a7c-277e-fd69-9629-d640b74b4a23.png)

「巡回候補URLにリクエストを送る」→「受け取ったリクエストから新しい巡回候補を見つける」という流れはaタグと同じですが、formタグの巡回にはいくつかaタグにはない苦労が発生します。

### メソッドの変更やリクエストボディの作成
まず、aタグの巡回はhref属性で指定されたURLにGETリクエストを送るだけでできました。formタグもaction属性で指定されたURLにリクエストを送る点は同じです。しかし、formタグの場合はmethod属性によりリクエストのメソッドを変えて送信したり、inputやselectなどのフォームコントロールに応じて適切にフォームの入力内容を作成する必要があります。
今回実装したフォームコントロールに対する処理は具体的に以下のようになります。
| 名前 | 処理 | HTMLの例 | 結果の例 |
| --- | --- | --- | --- |
| inputタグ(type=text) | aaaを入力する | ```<input type="text" name=”ex1">``` | x1=aaa |
| inputタグ(type=password) | test1234を入力する | ``` <input type="password" name=”ex2"> ``` | ex2=test1234 |
| inputタグ(type=radio) | 一つ目のオプションを選択する | ```  <input type="radio" name="ex3" value="op3-1"> <input type="radio" name="ex3" value="op3-1"> ``` | ex3=op3-1 |
| inputタグ(type=submit) | valueの値をとる | ```<input type="submit" name=”ex4" value=”value4”>``` | ex4=value4 |
| selectタグ | 一つ目のオプションを選択する |  ``` <select name="ex5"> <option value="op5-1"> op1 </option> <option value="op5-2"> op2 </option> </select> ``` | ex5=op5-1 |

各フォームコントロールにこれらの処理を行なったうえで、得られた結果を&で結合したものがフォームの入力結果となります。
例えば上の表の例が全て同じフォームに含まれていた場合、フォーム入力結果は``` ex1=aaa&ex2=test1234&ex3=op3-1&ex4=value4&ex5=op5-1 ```となります。
これをGETメソッドの場合はクエリストリングとしてURLに付与し、POSTメソッドの場合はリクエストボディとして送信します。今回作成した簡易クローラーではPUTなどのその他のメソッドはサポートしていません。
また、私が今回作成した簡易クローラーは基本的にinputタグのtype属性のみで入力する値を決めているので、電話番号などの数字しか入力できないといった入力値のチェックがあるフォームに対応することはできません。
一方、弊社製品のAeyeScanはそのフォームが何を入力するものか(名前、電話番号、メールアドレス、など)を自動で判別して適切な形式の値を入力することができます。

### 属性が省略されている場合の対応
前節でformタグの巡回時にはformタグやその下位のフォームコントロールタグの属性を取得してリクエストを構成していることを述べました。しかし、formタグのaction、 method属性やinputタグのname、 type属性などは必須ではないため、省略されていることもあります。
そこで、取得しようとした属性が定義されていなかった場合の例外処理を実装する必要があります。formタグ解析について実装した例外処理を以下の表にまとめます。
| 例外 | 対応　|
| --- | --- |
| formタグにaction属性がない | action=(現在のURL)とする |
| formタグにmethod属性がない | method=GETとする |
formタグのmethodがGET, POST以外 | (サポート対象外)
inputタグにtype属性がない | type=textとする
inputタグのtype属性がtext, password, radio, submit以外 | (サポート対象外)
inputタグのname属性がない | 無視する
inputタグのvalue属性がない | value=””とする
selectタグにname属性がない | 無視する
selectタグの下位にoptionタグがない | そのselectタグを無視する
optionタグにvalue属性がない | value=(コンテンツ)とする。コンテンツもない場合はvalue=””とする。|

厳密に言えばaタグのhref属性も必須でないため同様の例外処理は必要なのですが、formタグのほうが特に必要な例外処理の数が多いためformタグ巡回に特有の苦労としました。

### 巡回候補、巡回済みリストの扱い
aタグの巡回手順を思い出すと、巡回候補と巡回済みリストを保持し、新しい巡回候補を見つけた時にはこれらとの重複をチェックする必要があります。
ここで、formタグの巡回を行うにはリンク先URLの他にメソッドとリクエストボディ(POSTの場合)が必要です。
よって、aタグの場合はURLのみを文字列リストとして保持すればよいのですが、formタグの場合はURL、メソッド、リクエストボディをオブジェクトのリストとして保持します。

前回の記事の[要件](https://qiita.com/AeyeScan/items/b442ca1aae70922de18b#%E8%A6%81%E4%BB%B6)により開発言語としてNode.jsを使用しますが、Node.jsではオブジェクト同士を===などで直接比較することはできません。
よって、重複チェックを行うときには各リストのオブジェクトを一度JSON.stringify()で文字列に変換したうえで比較を行います。
また、オブジェクト作成時の書き方によってはJSON.stringify()で文字列化したときに要素の順番が保証されないことに注意が必要です。

## ログイン
### ログインリクエストの作成
form入力の中でもログインフォームは特殊な処理を行う必要があります。まず、ログインフォームには適当な文字列ではなく、有効なIDやパスワードを入力する必要があります。
このため、今回作成した簡易クローラーではフォームの送信先があらかじめ登録されたログインチェック用のURLと一致する場合には、フォームの解析を行わず、あらかじめ登録されたログイン情報からリクエストを作成する仕様としました。
処理のフローチャートの中でこの部分に対応する分岐の拡大図がこちらです。
![login1.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2422105/f5caac7d-940f-2df6-6330-fd1de60aba9a.png)

### ログイン状態の維持
ログイン処理を実行するためには、ログインフォームにIDとパスワードを入力するだけでなく、そのレスポンスからセッションIDを受け取り、次のリクエストにセットする必要があります。
ログイン後の画面の巡回を続ける時はリクエストのたびに同じセッションIDをセットします。この時、レスポンスでセッションIDが更新されることがあるので、その場合は次のリクエストには更新後のセッションIDをセットします。

ログイン処理を維持する方法については概ね以上ですが、ここでまだ注意することがあります。ここまで特にログイン処理を行うタイミングについて指定がなかったため、ログイン処理を行なった時点でまだログイン前の画面へのリクエストが残っている可能性があります。また、前章で巡回時には巡回済みの画面との重複チェックを行うことを紹介しましたが、メソッド、URL、リクエストボディが同じでもログイン前後によって動作が変わる場合があるので、ログイン前とログイン後のリクエストは別のものとして扱いたいところです。この二つの問題への対処として、[巡回対象オブジェクト](#巡回候補巡回済みリストの扱い)にメソッド、URL、リクエストボディに加えてログイン後か否かをbooleanで記述します。これにより、リクエストを送るときはこのプロパティを参照してセッションIDをセットするかしないかを決めたり、ログイン前後のリクエストを区別することができます。

ここまでのログイン維持用の処理をまとめたものが以下のフローチャート(赤いブロック)になります。
![login2.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2422105/a4a852fa-0ce3-8609-fe2d-0627a3cea50e.png)

### 並行実行でログイン状態を維持するための工夫
前節でログインの維持について書きましたが、要件では並行実行で複数のページを同時に巡回することが指定されているため、並行実行数(以下、nとする)個のセッションIDを取得して管理する必要があります。
まず、セッションIDの取得についてです。通常は並行実行でそれぞれ異なるn個のリクエストを送りますが、そうするとログインを実行してもセッションIDが一つしか取得できず、ログイン後のページを複数同時に巡回することができません。
そこで、ログインリクエストを送る時は特別に同じリクエストをn個送ることとしました。これによりセッションIDもn個取得できるので、以降のログイン後の画面を複数同時に巡回できます。

次に、セッションIDの管理についてです。ログインリクエストのレスポンスから取得したn個のセッションIDは配列に格納して使います。ここで、複数のリクエストを同時に送る時にセットするセッションIDが重複しないようにする必要があります。この対策として、セッションIDをセットする時には各クライアントでまずセッションID配列からpopしてリクエストにセットし、レスポンスが返ってきてからセッションIDを配列にpushし直すようにしました。これによりセッションID配列には常に有効なセッションIDのみが入っていることになります。また、セッションIDの更新があった時はもとのIDの代わりに更新されたものをpushし直すことは前節で書いた通りです。

## おわりに
今回の記事では新人研修の課題として実際の製品とは異なる簡易クローラーを開発するうえで、苦労したこととしてformタグの巡回やログインの実行といった機能の実装について書きました。
熟練の方からすると当然のことばかりかもしれないですが、初心者としては一から実装したことで色々と学びがあったと思います。
次回の記事では完成した簡易クローラーを実際に動かしてみた様子をお見せします。