AWS LambdaによるスクレイピングでQiita Organizationの最新記事を取得しSlackに通知するBotを作った

[Organizationページ](https://qiita.com/organizations/acall)の「最新の投稿」をAWS Lambda(node.js)でスクレイピングし、新しい投稿の記事のタイトルとURLをSlackに投稿するBotを作ってみました

コードはここに置いてます(大分汚いです)
https://github.com/uji/qiita-organization-scraping/blob/master/app.js

# AWS Lambdaの処理の流れ
1. 「最新の投稿」にある記事をスクレイピングで取得
2. AWS S3にあるtxtファイルを確認し、Slack未投稿の記事の有無を確認
3. 未投稿の記事をSlackに投稿
4. 最新記事のタイトルをtxtファイルでAWS S3に保存

投稿済みの記事を永続化する必要があるのですが、DB使うのは大げさな気がしたのでS3にtxtで保存します

# スクレイピング
AWS Lambda 用のchromium [chrome-aws-lambda](https://github.com/alixaxel/chrome-aws-lambda) を使ってスクレイピングしました
Google Cloud Functionsでも使えるっぽいです
`puppeteer`と使用感はほぼ同じです

`.of-ItemLink_header-title` classを取ってくると「最新の投稿」の要素を取ってこれます

```js
const chromium = require("chrome-aws-lambda");
exports.handler = async (event, context) => {
  let browser = null;
  let elems = [];

  try {
    browser = await chromium.puppeteer.launch({
      args: chromium.args,
      defaultViewport: chromium.defaultViewport,
      executablePath: await chromium.executablePath,
      headless: chromium.headless,
    });

    let page = await browser.newPage();

    await page.goto("http://qiita.com/organizations/" + process.env.ORGANIZATION_NAME);
    const selector = ".of-ItemLink_header-title";
    elems = await page.$$eval(selector, es => es.map(e => [e.textContent, e.href]));
  } catch (error) {
    return context.fail(error);
  } finally {
    if (browser !== null) {
      await browser.close();
    }
  }

  return context.succeed(elems);
};
```

# Slackに通知する
`@slack/bolt`または`@slack/web-api`でSlack APIを叩き、取得した記事をSlackに投げます
Slack Botでメッセージを送るには、`singning secret` 、`bot token`、 `channel id` が必要になります

```js:boltの場合

const { App, ExpressReceiver } = require("@slack/bolt");

const receiver = new ExpressReceiver({
  signingSecret: process.env.SLACK_SIGNING_SECRET
});

const app = new App({
  token: process.env.SLACK_BOT_TOKEN,
  receiver: receiver
});

app.client.chat.postMessage({
  channel: process.env.SLACK_CHANNEL,
  text: "message",
  token: process.env.SLACK_BOT_TOKEN
});
```

# S3のテキストファイル読み込み、上書き
投稿済みの記事のタイトルを永続化します
`aws-sdk` を使うと結構簡単にS3にアクセスできます

```js:読み込み
const AWS = require("aws-sdk");
const s3 = new AWS.S3({ region: "ap-northeast-1" });
let s3Params = {
  Bucket: process.env.BACKET_NAME,
  Key: process.env.FILE_NAME
};

s3.getObject(s3Params, (err, data) => {
  if (err) return context.fail(err);
  else latest = data.Body.toString();
});
```

```js:上書き
const AWS = require("aws-sdk");
const s3 = new AWS.S3({ region: "ap-northeast-1" });
let s3Params = {
  Bucket: process.env.BACKET_NAME,
  Key: process.env.FILE_NAME,
  Body: "タイトル"
};

s3.putObject(s3Params, (err) => {
  if (err) return context.fail(err);
});
```

# AWS Lambdaの設定
ランタイムにnode.jsを指定してLambdaを作成します

まあまあ時間のかかる処理なので、基本設定からメモリ、タイムアウト時間を変更しておきます
メモリは512MB、タイムアウト時間は1分にしました

node_modulesはzipにまとめてLayerに登録します
[AWS Lambda レイヤー](https://docs.aws.amazon.com/ja_jp/lambda/latest/dg/configuration-layers.html)


# AWS Lambdaで動かす

コードをLambdaに書いてテストを実行してみます

<img width="939" alt="スクリーンショット 2020-02-24 0.53.23.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/402968/9d7efc1b-3028-cbc1-9eba-02c9b1179c26.png">

<img width="762" alt="スクリーンショット 2020-02-23 0.20.28.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/402968/58da46e9-1511-9263-d0cb-4ce97bfc1a38.png">

Slackに通知されました

# AWS Cloud Watch Eventsで定期実行
AWS LambdaのトリガーにAWS Cloud Watch Eventsを追加して定期実行されるようにします

<img width="851" alt="スクリーンショット 2020-02-24 1.38.12.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/402968/ec48efc7-9187-86e0-db94-8139271a919b.png">

毎朝9時に実行される設定にしました

# まとめ
今回はスクレイピングで取得しましたがQiita APIを使ってユーザーごとの最新記事をとる方法もあります
そっちの方が良さそう