LoginSignup
42
32

More than 1 year has passed since last update.

Qiitaの指標をGASで集計してNotion Chartsで可視化する

Last updated at Posted at 2020-07-06

Qiitaの各指標(LGTM数, ストック数, はてブ数..)をGASを使って自動集計し、Notion Chartsで可視化してみました。指標が可視化されると記事執筆のモチベーションも上がるのでオススメです!
またQiitaの指標集計に興味がなくとも、Notion Chartsは本当に便利なので、「Notion Chartsで可視化する」の章だけでも読んでもらえると嬉しいです。

何を作るの?

こちらのNotionページのようにQiitaの各指標のダッシュボードを作ります。チャートの値は日時で更新されます。
今回集計する指標は以下の通りです。

  • Qiita 記事数
  • Qiita LGTM数
  • Qiita ストック数
  • Qiita フォロワー数
  • はてなブックマーク数

ezgif.com-optimize (1).gif

また、今回のclaspのプロジェクトは全て以下リポジトリにあります。もしサンプルコードが動かないなどあればこちらを参照してください。

GASで自動集計する

GAS(Google Apps Script)でQiita APIとはてなAPIを叩き、各指標をGoogleスプレッドシートに自動集計するまでを書きます。
GASをローカルで管理出来る様にするためCLIツールのClaspを利用します。

Claspプロジェクトの作成

まず、claspコマンドを使えるようにするために、claspをnpmでグローバルにインストールします。

$ npm i @google/clasp -g

次にclaspに自分のGoogleアカウントを紐づけます。

$ clasp login

loginコマンドでプラウザが開き、紐付けたいGoogleアカウントの選択と、権限の許可をしてください。
完了後コマンドラインにAuthorization successful.と表示されれば紐付け完了です。

最後にcreateコマンドでclaspプロジェクトを作成します。
--title qiita-kpiでプロジェクト名を設定し--type sheetsで、Google Spreadsheetが自動で作成されGASと紐づけています。また、--rootDirtで作業ディレクトリを指定しています。

# "qiita-kpi" の部分は任意のプロジェクト名を設定してください
$ clasp create --title qiita-kpi --type sheets --rootDir ./src

完了すると、.clasp.jsonとsrcディレクトリにappscript.jsonが生成されます。
.clasp.jsonにはGASスクリプトとの紐付きappscript.jsonにはプロジェクトの設定が保持されています。

createの段階だとGASのタイムゾーンがAmerica/New_Yorkとなっているので、Asia/Tokyoに修正しましょう。

src/appscript.json
{
+ "timeZone": "Asia/Tokyo",
- "timeZone": "America/New_York",
  "dependencies": {
  },
  "exceptionLogging": "STACKDRIVER",
  "runtimeVersion": "V8"
}

この状態でclasp openコマンドを実行すると、プラウザでGASのエディタ画面が開かれるはずです。
これでClaspプロジェクトの環境構築は完了です。

TypeScriptの環境構築と最初のスクリプト作成

TypeScriptで書きたいのでTypeScriptの環境を構築します。
Claspは標準でTypeScriptのトランスパイルに対応しているので、GASで使える関数の型定義をインストールするだけOKです。

$ npm init -y
$ npm install --save @types/google-apps-script

最初のスクリプトを書いてみます。
src配下にindex.tsを作成し、以下を記述してみましょう。

index.ts
function main() {
  const sheet = SpreadsheetApp.getActiveSheet();
  sheet.getRange(1,1).setValue("Hello Gas!");
}

これをGASにデプロイします。

# overrideを確認されるのでforce指定をしています。
$ clasp push -f

GASのエディタページを更新すると先ほどのコードが反映されているはずです。

スクリーンショット 2020-07-04 15.11.27.png

main関数を選択し、▶️ でスクリプトを実行してみましょう。
ここでスプレッドシートへのアクセス権限を聞かれた場合は、以下記事参考に権限を許可してください。
https://tonari-it.com/gas-script-approval/

実行が完了すると、紐づくスプレッドシートに文字列が入力されているはずです。

スプレッドシート一覧から新規に追加されているスプレッドシートを開き内容を確認してください。

スクリーンショット 2020-07-04 15.20.31.png

書き込みが完了しましたね🎉

Hello Claspの文字列は次項以降で不要なので削除し、代わりに以下のような表のヘッダー部分を作成してください。次項以降でA2以下に値を埋めていきます。

スクリーンショット 2020-07-04 21.21.49.png

Qiita API、はてなAPIから指標を集計

Qiita API、はてなAPIから指標を取得するスクリプトを書きます。
APIの型情報があると便利なので最初にダウンロードしておきます。こちらの記事で紹介している方法です。

# typesディレクトリの作成
$ mkdir src/types
# Qiita apiのschema.jsonからschema2typeを使って型情報を生成
$ curl https://qiita.com/api/v2/schema | npx json2ts > src/types/qiita-types.d.ts

そしてindex.tsを以下のように修正します。
コードの内容についてはコメントを記載しているので詳細な説明は割愛します。
ざっくりQiita API、はてなAPIから各指標を取得し集計、スプレッドシートの最終行に結果を挿入しています。

Qiita記事の総ブックマーク数の取得方法は、技術ブログの師匠@kakakakakkuさんに教えてもらいました。感謝 🙏

src/index.ts
import { User, Item } from "./types/qiita-types";

const QIITA_ACCESS_TOKEN = PropertiesService.getScriptProperties().getProperty(
  "qiitaAccessToken"
) as string;
const QIITA_USERNAME = PropertiesService.getScriptProperties().getProperty(
  "qiitaUsername"
) as string;

// -------------------------------------------------------------
// メイン処理 各指標のスプレッドシートへの書き込み
// -------------------------------------------------------------
function main() {
  const today = Utilities.formatDate(new Date(), "JST", "yyyy/MM/dd");
  // Qiitaの指標取得
  const qiitaKpi = new QiitaClient(
    QIITA_ACCESS_TOKEN,
    QIITA_USERNAME
  ).fetchKpi();
  // はてなの指標取得
  const hatenaKpi = new HatenaClient(QIITA_USERNAME).fetchKpi();

  const sheet = SpreadsheetApp.getActiveSpreadsheet().getActiveSheet();
  const insertLow = sheet.getLastRow() + 1;
  [
    today,
    qiitaKpi.postCount,
    qiitaKpi.lgtmCount,
    qiitaKpi.stockCount,
    qiitaKpi.followersCount,
    hatenaKpi.bookmarkCount
  ].forEach((data, i) => {
    sheet.getRange(insertLow, i + 1).setValue(data);
  });
}

// -------------------------------------------------------------
// Qiita API Client
// -------------------------------------------------------------
class QiitaClient {
  private readonly BASE_URL = "https://qiita.com/api/v2";
  private readonly PER_PAGE = 100;
  private readonly FETCH_OPTION = {
    headers: {
      Authorization: `Bearer ${this.accessToken}`,
    },
    method: "get" as const,
  };

  constructor(private accessToken: string, private username: string) {}

  // Qiita APIから指標取得
  fetchKpi() {
    const user = this.fetchUser();
    const items = this.fetchAllItems(user);
    const lgtmCount = this.tallyUpLgtmCount(items);
    const stockCount = this.tallyUpStockCount(items);

    return {
      lgtmCount,
      stockCount,
      followersCount: user.followers_count,
      postCount: user.items_count,
    };
  }

  // ユーザー情報の取得
  private fetchUser() {
    const response = UrlFetchApp.fetch(
      `${this.BASE_URL}/users/${this.username}`,
      this.FETCH_OPTION
    );
    return JSON.parse(response.getContentText()) as User;
  }

  // 全ての投稿記事の取得
  private fetchAllItems(user: User) {
    // 最大ページ数
    const maxPage = Math.ceil(user.items_count / this.PER_PAGE);
    // 投稿一覧の取得
    let allItems = [] as Item[];
    [...Array(maxPage)].forEach((_, i) => {
      const page = i + 1;
      const items = this.fetchItems(page, this.PER_PAGE);
      allItems = [...allItems, ...items];
    });
    return allItems;
  }

  // 投稿記事の取得
  private fetchItems(page: number, perPage: number) {
    const response = UrlFetchApp.fetch(
      `${this.BASE_URL}/authenticated_user/items?page=${page}&per_page=${perPage}`,
      this.FETCH_OPTION
    );
    return JSON.parse(response.getContentText()) as Item[];
  }

  // 記事をストックしたユーザーの取得
  private fetchStockers(itemId: string) {
    const response = UrlFetchApp.fetch(
      `${this.BASE_URL}/items/${itemId}/stockers`,
      this.FETCH_OPTION
    );
    return JSON.parse(response.getContentText()) as User[];
  }

  // LGTM数の集計
  private tallyUpLgtmCount(items: Item[]) {
    const lgtmCount = items.reduce(
      (result, item) => result + item.likes_count,
      0
    );
    return lgtmCount;
  }

  // ストック数の集計
  private tallyUpStockCount(items: Item[]) {
    const stockCount = items.reduce((result, item) => {
      const stockedUser = this.fetchStockers(item.id);
      return result + stockedUser.length;
    }, 0);
    return stockCount;
  }
}

// -------------------------------------------------------------
// Hatena API Client
// -------------------------------------------------------------
class HatenaClient {
  private readonly BASE_URL = "http://b.hatena.ne.jp";

  constructor(private qiitaUsername: string) {}

  // はてな APIから指標取得
  fetchKpi() {
    const bookmarkCount = this.fetchBookmarkCount();

    return {
      bookmarkCount,
    };
  }

  // ブックマークカウントの集計
  private fetchBookmarkCount() {
    const redirectUrl = this.getRedirectUrl(
      `${this.BASE_URL}/bc/https://qiita.com/${this.qiitaUsername}`
    );
    // `https://b.st-hatena.com/images/counter/default/00/00/0000653.gif` の形式で
    // ブクマ数が書かれたgif画像のURLを取得できるので、そこからブクマ数だけを抽出する
    const bookmarkCount = redirectUrl.match(
      /https:\/\/b.st-hatena\.com\/images\/counter\/default\/\d+\/\d+\/(\d+).gif/
    )![1];

    return Number(bookmarkCount);
  }

  // リダイレクトURLの取得
  private getRedirectUrl(url: string): string {
    const response = UrlFetchApp.fetch(url, {
      followRedirects: false,
      muteHttpExceptions: false,
    });
    const redirectUrl = (response.getHeaders() as any)["Location"] as string;
    if (redirectUrl) {
      const nextRedirectUrl = this.getRedirectUrl(redirectUrl);
      return nextRedirectUrl;
    } else {
      return url;
    }
  }
}

indext.tsを修正したら、内容をGASに反映します。

$ clasp push

次に、スクリプト上で参照している環境変数(スクリプトプロパティ)をGAS上で設定します。

上部メニューのファイル => プロジェクトのプロパティ => スクリプトのプロパティから以下2つの値を設定してください。

プロパティ名
qiitaAccessToken Qiita APIのアクセストークン。こちらを参考に取得してください。アクセストークンのスコープはread_qiitaだけで大丈夫です。
qiitaUsername Qiitaのアカウント名。アカウントページのプロフィール画像下部に表示されるIDの@を除いた部分です。

スクリーンショット 2020-07-04 17.27.42.png

設定できたらmain関数を選択し、▶️ でスクリプトを実行してみましょう。
※ ここで再度スプレッドシートへのアクセス権限を聞かれると思うので、再度許可してください。

スプレッドシートに各指標の値が出力されていれば成功です🎉

スクリーンショット 2020-07-04 21.21.20.png

定期実行の設定

これで集計処理の実装はできたので、あとはこのGASスクリプトを定期実行するようにします。
GASは標準で日時のトリガーを設定できるので、そちらを利用します。
まず、GASエディタ画面で実行ボタン横のタイマーマークを押します。

スクリーンショット 2020-07-04 20.31.34.png

GASのトリガー設定画面に遷移するので、新しいトリガーを作成します。
イベントのトリガーソースを「時間主導型」、時間ベースのトリガーのタイプを「日付ベースのトリガー」に設定し、あとは定期実行したい時刻を選択すれば完了です。

スクリーンショット 2020-07-04 20.32.55.png

この例だと、毎日午後11時〜12時にスクリプトが実行され、各指標がスプレッドシートに記録されます。

Notion Chartsで可視化する

次に、GASで集計した指標をNotion Chartで可視化する手順を書きます。

Notionとは?

まず最初にNotionの説明を簡単に。
Notionは、メモやタスク管理、カレンダー、スプレッドシート、Wikiなど、幅広い機能を備えているドキュメントアプリです。
簡単にページを外部公開できるのも特徴で、最近のノーコードの流れでますます注目を集めています。

スクリーンショット 2020-07-04 21.05.43.png

Notion Chartsとは?

Notion ChartsとはGoogleスレッドシートをデータソースに使い、Notionページに簡単にチャートを表示するツールです。

スクリーンショット 2020-07-04 21.09.08.png

こちらのデモページのように様々なチャートを表示できます。
https://www.notion.so/Digital-Footprint-d4295448c72b4056af9b06fa8ece36d4

指標の可視化

まずNotion ChartsからGoogleスプレッドシートの情報が読めるように、スプレッドシートの共有設定を一般公開に変更します。

スクリーンショット 2020-07-04 21.26.19.png

次に、Notion Chartsのトップページ下部のConnect your Google Sheetに値を入力します。
GOOGLE SHEETS DOCUMENT IDはGoogleスプレッドシートのIDです。シートURLの/d/から/editの間の文字列がIDとなります。
SHEET NAMEはそのままシート名です。
DATA RANGEはチャート化したい値の範囲です。シートの行追加でチャートデータも更新したいため行範囲は多めに指定しましょう。(例だとA1:B500

スクリーンショット 2020-07-04 21.27.20.png

他チャートの色や種類なども指定できますが、一旦デフォルトで大丈夫です。
下部のMAKE MAGICのボタンを押しましょう。
(どうでも良いのですが「MAKE MAGIC」って表現かっこいいですね)

スクリーンショット 2020-07-04 21.25.23.png

URLが表示されるので、ボタンクリックでコピーします。

スクリーンショット 2020-07-04 21.45.38.png

そしてNotionページを開き/embedを入力し、表示されたフォームにコピーした値を貼り付けます。

このようにチャートが表示されればOKです。

スクリーンショット 2020-07-04 21.44.58.png

あとは他の指標も同じ手順でチャート化しましょう。
そして最後にレイアウト整えれば完成です🎉

ezgif.com-optimize (1).gif

終わりに

以上「Qiitaの指標をGASで集計してNotion Chartsで可視化する」でした。
今回は、Qiita周りの指標とはてブ数のみでしたが、コードを追加すれば他にもGoogle Analyticsから週間PV、Twitter APIからフォロワー数なども集計することができます。以下リポジトリではそれらを集計しているので、もし興味あればコード見てみてください。
https://github.com/kawamataryo/blog-kpi

Notionは情報の一元化が簡単に出来てとても便利ですね。今後も使っていきたいです。

42
32
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
42
32