水無瀬のプログラミング日記

プログラムの勉強した時のメモ

Yahoo天気をスクレイピングして予報をSlackへ通知する

はじめに

ちょっと前にYahoo天気の指数情報に洗濯や熱中症などに混じって、
ビールが追加されて話題になってました。
数ある天気予報の中でビールの指数を出しているところは見たことがなかったので、
Yahoo天気の予報をSlackに通知して毎朝確認しよう!と思ったのが今回の話。
APIを提供してくれていれば楽だったんだけど、
無さそうだったのでスクレイピングして無理やり取ってくることにする。

今回はtypescriptでサクッと作ったものをAWSに乗せて使うことにする。

TL;DR.

コード

事前準備

コード側

今回使うライブラリなどをinstallしておく。

# 今回使うlibrart(httpリクエスト用、HTMLパーサ)
$ npm install --save request jsdom
# 型定義(好みで)
$ npm install --save-dev @types/request @types/jsdom
# その他
# TSとかeslintとかwebpackとかetc...

Slack側

ここからwebhook許可しとく

追加手順

プルダウンから通知を送りたいチャンネルを指定してAdd Incoming WebHooks integrationをクリック。
(もしかしたら日本語表記だと違うかも)

f:id:minase_mira:20190628215613p:plain

Setup InstructionsにあるWebhook URLをコピーしておく。 これでSlack側の設定は完了

f:id:minase_mira:20190628215746p:plain

実装

実践ドメイン駆動設計をレイヤードアーキテクチャの辺りまで読んだので、
レイヤードアーキテクチャ的に実装していくことにする。
+ typescriptでDIをやるためにinversifyを入れる。
必須ではないので必要であれば追加でインストール(npm install --save inversify)。

Interface

とりあえず叩かれる用。
受け取るパラメータも特にないので、ただapplication層を呼び出すだけ。

コード中の@injectable()@inject()inversify用の設定。
@injectable()はDIされる側の設定。interfaceの実装食らうに付けておく。
@inject()はDIする側の設定。これで、interfaceを呼び出せば対応する実装クラスを引っ張ってこれる。

Application

Domain使ってるところ。
Domデータ取ってくる→欲しいデータに整形する→Slackへ通知の流れ。

Domain

ScrapingService

サイトにGet飛ばしてhtml取ってくるところ。
取ってきたデータをjsdomを使ってパースして返す。

ConverterService

パースしたデータを使いやすいように整形するところ。
整形したり、必要なデータを取り出したりしている。

ゴリ押しに次ぐゴリ押しで実装してしまったので、 もうちょい綺麗にしたい。。。

InformSlackService

Slcakへ通知するところ。
送るデータを受け取って、Slackのwebhookで送れる形にしてあげる。

Infrastructure

HttpRequest

requestをラップしているだけ。
ここももう少し綺麗にしたいところ。
他のライブラリに変えても良い気がするし、promiseかまさなくても良いようにしたい。

inversify用設定

inversifyのGithubを参考にしながら設定を進める。

inversify.config.ts

interfaceと実装を紐づける設定。
DIコンテナ的な設定の認識。

const container = new Container();
container
  .bind<WeatherNewsService>(TYPES.WeatherNewsService)
  .to(WeatherNewsServiceImpl);
container
  .bind<ConverterService>(TYPES.ConverterService)
  .to(ConverterServiceImpl);
container
  .bind<InformSlackService>(TYPES.InformSlackService)
  .to(InformSlackServiceImpl);
container.bind<ScrapingService>(TYPES.ScrapingService).to(ScrapingServiceImpl);
container.bind<HttpRequest>(TYPES.HttpRequest).to(HttpRequestImpl);
container.bind<WeatherNews>(TYPES.WeatherNews).to(WeatherNews);

export { container };

inversify.type.ts

実行時に識別子が必要なので、宣言しておく。
公式にならってSymbolを使っているが、クラスでも文字列でもOKだそう。

const TYPES = {
  WeatherNewsService: Symbol.for('WeatherNewsService'),
  ConverterService: Symbol.for('ConverterService'),
  InformSlackService: Symbol.for('InformSlackService'),
  ScrapingService: Symbol.for('ScrapingService'),
  HttpRequest: Symbol.for('HttpRequest'),
  WeatherNews: Symbol.for('WeatherNews')
};

export { TYPES };

実行してみる

ここまでのコードを実行してみる。
実際に作ったものはAWSに乗せるように設定が変わっているけど、
概ね下記のようなwebpack.configを用意すればビルドできるはず。

// output.pathに絶対パスを指定する必要があるため、pathモジュールを読み込んでおく
const path = require('path');
const nodeExternals = require('webpack-node-externals');
const slsw = require('serverless-webpack');

module.exports = {
  mode: slsw.lib.webpack.isLocal ? 'development' : 'production',
  entry: slsw.lib.entries,
  devtool: 'source-map',
  target: 'node',
  module: {
    rules: [
      {
        test: /\.ts$/,
        use: [
          {
            loader: 'ts-loader'
          },
          {
            loader: 'eslint-loader'
          }
        ],
        exclude: /node_modules/
      }
    ]
  },
  resolve: {
    extensions: ['.js', '.json', '.ts'],
    modules: ['node_modules']
  },
  output: {
    libraryTarget: 'commonjs',
    path: path.join(__dirname, '.webpack'),
    filename: '[name].js',
  },
  externals: [nodeExternals()]
};

下記のようなメッセージがSlackに通知されればOK。

f:id:minase_mira:20190628215832p:plain

まとめ

実装して動かしてみるところまでやった。
思ったより長くなりそうなので、今回はここまでにする。
次回serverless frameworkを使ってAWSに上げるところまでやる。
それでは今回はこのへんで。

追記

次回やった

参考