特定のサイトの更新情報をLINEに通知してくれるスクレイピングアプリを作ってみた！

# はじめに
特定のサイトのお知らせ欄が更新されたかな？と毎回見に行くことって結構あるかと思うのですが、時にはやはり忘れてしまうものです。
サイト自体に通知機能のようなものがあればそれを利用するのも一つの対策ですが、そういうものがない場合は毎日確認することをルーチン化する！...いや...やはりそういうわけにはいきません。

そこで今回は特定のサイトのお知らせ欄を1日に2回見にいき、更新されていたらその記事をLINEで通知してくれる、というアプリを作ってみました。

## 完成形
LINE Notifyから毎日7:00と17:00に以下のように通知が来るようになっています。

|記事が更新されていたとき|記事が更新されていないとき|
|---|---|
|<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/538249/5d68a0c9-265e-3ccf-e543-6f4d85cd5a12.png" width=230>|<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/538249/9fea656a-0720-b9cf-b4ec-fe7a801bd336.png" width=300>|




## アプリ概要と構成図
Amazon EventBridgeが特定の時間になったらLambda関数を実行します。
Lambda関数は実行後、特定のサイトからお知らせ内容をスクレイピングし、LINE Notifyを通して特定のグループLINEへ結果を通知する作りとなっています。
EventBridgeとLambda関数はserverless.templateで管理しています。

アプリ構成図は以下です。
![aws-scrapingapp.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/538249/89879b85-d3ea-b4be-7e40-67d9826cf5d2.png)

## 処理手順
簡潔ではありますがアプリの処理手順を以下に記載します。

### ①特定サイトのお知らせ欄をスクレイピング
時間になるとAWS Event Bridgeがアプリを起動します。
起動したアプリが特定サイトへ行き、お知らせ欄をガバっと取得（※）します。

※お知らせ欄は最新の記事10件程度しか掲載されていない作りになっていたので全取得するというロジックにしています。

### ②①で取得したお知らせ一覧から通知していないものを絞り込み & 保存

AWS S3にCSVファイル形式で1度通知したお知らせを保管しており、①で取得したお知らせ一覧の中でCSVファイルに保管されていないものを絞り込みます。
絞り込まれたお知らせたちが今回の通知対象となります（今回通知対象のものがある場合はそのデータを新規にCSVファイルに書き込みます）。

※保管方法についてはDBを利用したりその他色々あるかとは思いますが、DB使うと少し費用かかるかな？というのとこのアプリを作っている時期に業務にて**[CSV Helper](https://joshclose.github.io/CsvHelper/)**という.NETのライブラリを久々に利用する予定だったのでそれの扱いを思い出しておこうという理由からこのような保管形式としました。

### ③②で取得したお知らせをLineで通知
②で取得したお知らせをLineで通知します。

## それぞれの処理で使用したライブラリやサービスなど

### ①スクレイピング：AngleSharp

特定のサイトのお知らせ欄をスクレイピングする処理には`AngleSharp`という.NET向けのライブラリを利用しました。

https://anglesharp.github.io/

`QuerySelector`や`GetElementById`などJavaScriptでよく使うメソッドが利用できるので、特定のサイトの欲しい箇所を取得するのも結構楽にできました。

```cs
public async ValueTask<IHtmlCollection<IElement>> GetTopicDataAsync()
{
    using var stream = await _clientFactory.CreateClient().GetStreamAsync(new Uri(_scrapingTargetsConfig.Url));
    IHtmlDocument doc = await new HtmlParser().ParseDocumentAsync(stream);
    return doc.GetElementById("TopicId").QuerySelectorAll("table > tbody > tr");
}
```

### ②データ保存: AWSSDK.S3, AWSSDK.Extensions.NETCore.Setup, CSV Helper
当初はバッチ実行から1日前までの時間内のトピックのみ取得する、というロジックを組んでいたのですが、スクレイピング先のサイトのバグなのか、投稿日時が2日ほどずれており正常にトピックが取得できませんでした。
そこで、AWS S3へCSVファイルを保存し、そのCSVファイルに取得したものを保存し、そこにないもののみスクレイピングで取得していくというロジックへ変更しました。
CSVの操作については先ほど記載した`CSV Helper`を、ASP.NET CoreでS3の操作をする処理についても`AWSSDK.S3`, `AWSSDK.Extensions.NETCore.Setup`パッケージをNugetから取得することで比較的楽に作成することができました。

```cs
public async ValueTask<GetObjectResponse> GetCsvFile()
    => await _amazonS3Client.GetObjectAsync(_awsConfig.S3.BacketName, _awsConfig.S3.Key);

public async ValueTask UpdateCsvFile()
{
    var putRequest = new PutObjectRequest
    {
        BucketName = _awsConfig.S3.BacketName,
        Key = _awsConfig.S3.Key,
        FilePath = _localStorageConfig.CsvPath
    };
    await _amazonS3Client.PutObjectAsync(putRequest);
}
```

### ③LINEでの通知処理: LINE Notify
LINEへの送信については`LINE Notify`というサービスを利用することで作成しました。
以下の記事にまとめましたが、とても良いサービスなので今後も似た処理を作る際は重宝しそうです。

https://qiita.com/Ooooooomin_365/items/bd6f484266880c8def39

### （おまけ）インフラをコード化
こちらは以下の記事にまとめましたが、Visual StudioでServerlessプロジェクトを起動するとserverless.templateにてインフラ構成をJson形式で作成することができます。
EventBridgeなどもコード側で調整してサクッと反映させることができるのでとても良い感じでした。

https://qiita.com/Ooooooomin_365/items/cbc03000ea3f50b0dbdb

# おわりに
作った当初はLINEではなくメールで送信する感じだったり、AWS上にデプロイせずに自身のローカルPCのタスクスケジューラにセットしていたりと、今とは全然違う構成でした。
「こんな感じのほうが便利かな？」という感じでどんどん途中変更したり、前の現場の上司が（ありがたいことに）見てくださって「ここは〇〇したら面白そう！」みたいにアドバイスをくれたりという感じで実装が変わっていったのですが、それを通してたくさんの技術が学べてとても楽しかったです。

現在はこのLINE通知に任せっきりで、特に自分からサイトを確認しに行くことはなくなりました（笑）
ちょっとしたことでも日々の面倒なことを解決するアプリを作ると生活も楽になりますし、技術的にも新しいことが学べるので今後も続けていきたいと思います！