knowledgeの前月リストを取得し、投稿日を可視化する

## はじめに
みなさん、knowledgeはご存知ですか？
Qiitaのような情報共有サイトを簡易的に構築できるオープンソースです。
[knowledge/jp](https://information-knowledge.support-project.org/ja/)

弊社も社内で技術情報を共有し、スキルアップを図るために利用しています。

そんなknowledgeさんですが、単体ではレポーティング機能はない（はず）です。
DBに`H2database`または`PostgreSQL`を利用できるようなのでSQLで直接データは取れるのですが、私にそんな権限はないです。
WebAPIもないです。

そうだ。スクレイピングしよう

## 成果物
```
8月のレポート
◇◆－－－－－－－－－－－－－－－－－－－－－－－－－－－－－◆◇
#100 knowledgeの前月リストを取得し、投稿日を可視化する ayumu_terahara
https://xxxxxxxx.co.jp/knowledge/open.knowledge/view/100
◇◆－－－－－－－－－－－－－－－－－－－－－－－－－－－－－◆◇
contributions in the last month
      □ □ □ ■
Mon   ■ □ □ □
      □ □ □ ■
Wed   □ □ ■ ■
    □ □ □ □ □
Fri □ □ □ □ □
    ■ □ ■ ■ □
◇◆－－－－－－－－－－－－－－－－－－－－－－－－－－－－－◆◇
```
投稿内容の一覧と投稿が何日に行われたのかを可視化します（データはダミー）
ちなみに可視化部分はGithubのContributionsのあれのオマージュです。
\# 可視化部分だけ記事にした方が映えそう :thinking: 

## ソースコード
https://github.com/a-terarara/fetch-knowledge-list-amonth

## 使ったもの
 - Node.js v10
 - [chromeless](https://github.com/prisma-archive/chromeless)

chromelessは現在更新が止まっており、[Puppeteer](https://github.com/GoogleChrome/puppeteer)の利用が推奨されていますが、そんなに運用考えていないのでとりあえずchromelessで。

## 処理概要

### 前月リストの取得
```js
const scraping = async () => {
  const chromeless = new Chromeless();

  const list = await chromeless
    .goto(`https://${process.env.DOMAIN}/knowledge/open.knowledge/list`)
    .type(process.env.KNOWLEDGEUSER, 'input[name="username"]')
    .type(process.env.KNOWLEDGEPASS, 'input[name="password"]')
    .click("#content_top > div > form > div:nth-child(4) > div > button")
    .press(13)
    .wait("#knowledgeList")
    .evaluate(() => {
      return [].map.call(
        document.querySelectorAll(
          "#knowledgeList > div.col-sm-12.col-md-8.knowledge_list > div"
        ),
        div => {
          const title = div.childNodes[1].childNodes[1];
          const writer =
            div.childNodes[1].childNodes[3].childNodes[3].innerText;
          const postDate = div.childNodes[1].childNodes[3].childNodes[4].data.substring(
            8,
            13
          );
          return {
            title: title.innerText,
            href: title.href.split("?")[0],
            writer,
            postDate
          };
        }
      );
    });

  chromeless.end();

  return list;
};
```

ログインしてからTOPページの投稿一覧を取得するだけの簡単なお仕事です。
TOPページに表示される投稿一覧は50だけ表示されています。
**もし、ページ跨ぎでチェックしたい場合は50番目の投稿日をチェックしてから再帰判定してください。**

### 前月リストの取得
```js
const contributes = (list, beforeMonth) => {
  const targetList = list.filter(
    contribution => contribution.postDate.substring(0, 2) === beforeMonth
  );
  return targetList.map(l => `${l.title} ${l.writer}\n${l.href}`).join("\n");
};
```
投稿リストの整形です。
下記をリスト数分並べるだけの簡単なお仕事です。

```
#100 knowledgeの前月リストを取得し、投稿日を可視化する ayumu_terahara
https://xxxxxxxx.co.jp/knowledge/open.knowledge/view/100
```

### 投稿日の可視化

```js
const contributionsGraphGenerator = (list, month) => {
  const beginDate = moment();
  beginDate.month(month - 1);
  beginDate.date(1);
  const lastDate = moment();
  lastDate.date(1);
  lastDate.month(month);
  lastDate.date(0);

  const beginDay = beginDate.weekday();

  const dates = Array(lastDate.date()).fill(" ");
  const contributionsPoint = dates.map((val, index) => {
    const mmdd = `${month}/${("0" + (index + 1)).slice(-2)}`;
    return list.find(l => l.postDate === mmdd) ? "■" : "□";
  });
  const emptyPoints = Array(beginDay).fill(" ");
  const flatFullContributionsPoints = emptyPoints.concat(contributionsPoint);

  const daysInWeek = 7;
  const matrixFullContributions = Array(
    flatFullContributionsPoints.length / daysInWeek
  )
    .fill()
    .map((val, index) =>
      flatFullContributionsPoints.slice(
        index * daysInWeek,
        index * daysInWeek + daysInWeek
      )
    );

  const transposeMatrix = a => a[0].map((_, c) => a.map(r => r[c]).join(" "));
  const t = transposeMatrix(matrixFullContributions).map((val, index) => {
    const empty = "    ";
    switch (index) {
      case 0:
        return empty + val;
      case 1:
        return "Mon " + val;
      case 2:
        return empty + val;
      case 3:
        return "Wed " + val;
      case 4:
        return empty + val;
      case 5:
        return "Fri " + val;
      case 6:
        return empty + val;
      default:
        return " ";
    }
  });

  const graph = `contributions in the last month\n\`\`\`
${t.join("\n")}\n\`\`\``;
  return graph;
};
```

長くなってしまったので切り出します。。。

```js
  const beginDate = moment();
  beginDate.month(month - 1);
  beginDate.date(1);
  const lastDate = moment();
  lastDate.date(1);
  lastDate.month(month);
  lastDate.date(0);

  const beginDay = beginDate.weekday();

  const dates = Array(lastDate.date()).fill(" ");
  const contributionsPoint = dates.map((val, index) => {
    const mmdd = `${month}/${("0" + (index + 1)).slice(-2)}`;
    return list.find(l => l.postDate === mmdd) ? "■" : "□";
  });
  const emptyPoints = Array(beginDay).fill(" ");
  const flatFullContributionsPoints = emptyPoints.concat(contributionsPoint);
```
`contributionsPoint`: 月初日、月末日を取り、配列を決めます。 
`emptyPoints`: 当該月にかからない部分は半角スペース埋めで、表示しないようにします。

```js
  const daysInWeek = 7;
  const matrixFullContributions = Array(
    flatFullContributionsPoints.length / daysInWeek
  )
    .fill()
    .map((val, index) =>
      flatFullContributionsPoints.slice(
        index * daysInWeek,
        index * daysInWeek + daysInWeek
      )
    );
```

週の7日分ずつ切り出して二次元配列化します。
ただし、この時点では縦横が出力したい状態と逆になっています。

```js
  const transposeMatrix = a => a[0].map((_, c) => a.map(r => r[c]).join(" "));
  const t = transposeMatrix(matrixFullContributions).map((val, index) => {
    const empty = "    ";
    switch (index) {
      case 0:
        return empty + val;
      case 1:
        return "Mon " + val;
      case 2:
        return empty + val;
      case 3:
        return "Wed " + val;
      case 4:
        return empty + val;
      case 5:
        return "Fri " + val;
      case 6:
        return empty + val;
      default:
        return " ";
    }
  });
```

縦横をtransposeします。
ついでに列ヘッダに曜日列を追加しています。

## おわりに
可視化部分は投稿有・無だけで判別していますが、本当は例のあれと同様に濃さで表現したかったです。
例のあれのアスキーアートどこかに転がってないですかね？ :eyes: