Qiitaの自分の記事閲覧回数一覧をGoogleAppsScriptで自動収集してみた

# 📌 Google スプレッドシート + GAS を活用！ Qiita 記事の閲覧数推移を可視化するツールを作成しました 🚀

Qiitaに投稿した記事の**閲覧数推移**を定期的に記録し、  
**日々の増加数をグラフ化できるWebアプリ**をGoogle スプレッドシート + GASで作成しました！🎉

## 📝 背景
Qiitaでは各記事の閲覧数をAPIで取得できますが、  
**「過去のデータを記録し、推移を分析する機能」は提供されていないようです**。  
そのため、自作ツールを作成し、**日々のデータを蓄積・可視化できるようにしました**。

## 🧭 フロー
```mermaid
graph TD
  A[利用者] -->|1.スクリプトコピペ| B[GASスクリプト]
  A -->|2.プロパティ設定| C[GAS プロパティストア]
  A -->|3.トリガー設定| D[GAS トリガー]
  
  C -->|アクセストークン取得| B
  D -->|定期実行| B
  
  B -->|Qiita APIへリクエスト| E[Qiita API]
  E -->|JSONレスポンス受信| B
  
  B -->|データ蓄積、グラフ化、集計| F[スプレッドシート]
```


---

## 🎯 本アプリの特徴
✅ **Qiita APIを利用して、記事の閲覧数を自動取得**
✅ **Google スプレッドシートにデータを蓄積**（記事 ID / タイトル / 閲覧数 / いいね数 / ストック数 など）  
✅ **GAS（Google Apps Script）を使って定期実行（トリガー設定）**  
✅ **取得したデータをもとに、記事ごとの「閲覧数増加数」を計算**  
✅ **Google スプレッドシートの `FILTER()`などを活用し、記事ごとのデータを自動抽出**  
✅ **記事タイトルをプルダウンで選択すると、その記事の閲覧数増加数をグラフ化！** 📊  
✅ **日々の閲覧数増加数top3を一覧化！**   

---

## 🛠 仕組み
本アプリは **Google スプレッドシート + GAS（Google Apps Script）** を活用して動作します。

1. **📡 Qiita API から記事データを取得**
2. **📊 Google スプレッドシートにデータを保存**
3. **🔄 GAS による定期実行（トリガー設定）**
4. **🔎 `FILTER()` を使い、記事タイトルを選択すると閲覧数増加数を自動表示**
5. **📈 LINE グラフで日々の増加数を可視化**
5. **💮 日々の増加数top3を集計**

---

## 🖥 画面イメージ
### 1. データ蓄積用の `QiitaData` シート
| 取得日 | 記事ID | タイトル | 投稿日 | 更新日 | 閲覧数 | いいね数 | ストック数 | 閲覧数増加数 |
|------|------|------|------|------|------|------|------|------|
| 2025-03-10 | 0123456789abcdefghij | 良い感じのタイトル | 2025-03-07 | 2025-03-08 | 1 | 0 | 0 | 0 |
| 2025-03-11 | 0123456789abcdefghij | 良い感じのタイトル | 2025-03-07 | 2025-03-08 | 10 | 0 | 0 | 9 |
| 2025-03-12 | 0123456789abcdefghij | 良い感じのタイトル | 2025-03-07 | 2025-03-08 | 20 | 1 | 1 | 10 |

---

### 2. 記事ごとのデータを可視化する `ViewIncreasesChart` シート
- **記事タイトルをプルダウンで選択**
- **該当記事の「取得日」「閲覧数増加数」を自動表示**
- **スプレッドシートのグラフ機能で、閲覧数増加数を可視化 📊**

| 記事タイトルを選択（B1） | 記事ID（E1） | 取得日（A列） | 閲覧数増加数（B列） |
|----------------|----------------|---------|---------|
| `良い感じのタイトル` | `0123456789abcdefghij` | 2025-03-10 | 0 |
|  |  | 2025-03-11 | 9 |
|  |  | 2025-03-12 | 10 |

✅ **記事を切り替えるだけで、自動でグラフが更新される！**

---

### 3. 取得日ごとのtop3を集計する `ViewIncreasesTop3` シート
| 取得日 | 2025-03-10 | 2025-03-11 | 2025-03-12 |
|------|------|------|------|
| top1記事名 | 良い感じのタイトル | 良さげなタイトル | 目を引くタイトル |
| top1閲覧数増加数 | 0 | 10 | 15 |
| top1記事名 | 良さげなタイトル | 気になるタイトル | 二度見するタイトル |
| top1閲覧数増加数 | 0 | 6 | 10 |
| top1記事名 | 目を引くタイトル | 硬派なタイトル | 気になるタイトル |
| top1閲覧数増加数 | 0 | 4 | 7 |
| 閲覧数増加数合計 | 0 | 50 | 70 |

---

## ⏳ 設定手順
### 🔹 1. スプレッドシートを作成
Google スプレッドシートを作成し、ファイル名を任意に設定します。「qiita_pv_chk_tool」など。

### 🔹 2. GAS（Google Apps Script）を設定
GAS エディタを開き、プロジェクト名を任意に設定します。「qiita_pv_chk_tool」など。
その後、以下のスクリプトを貼り付け、保存します。

<details><summary>script</summary>

```
/**
 * スクリプトプロパティからアクセストークンを取得
 * @return {string} Qiita API のアクセストークン
 */
function getAccessToken() {
  return PropertiesService.getScriptProperties().getProperty("QIITA_ACCESS_TOKEN");
}

/**
 * メイン処理。Qiita APIから全記事を取得（ページネーション対応）
 * @return {Object[]} 取得した記事のデータリスト（JSON）
 */
function fetchAllQiitaArticles() {
  var accessToken = getAccessToken();
  var urlBase = "https://qiita.com/api/v2/authenticated_user/items?page=";
  var perPage = 100;
  var allArticles = [];
  
  for (var page = 1; page <= 10; page++) {  // 例: 最大10ページ（1000記事分）
    var url = urlBase + page + "&per_page=" + perPage;
    
    var options = {
      "method": "get",
      "headers": {
        "Authorization": "Bearer " + accessToken
      }
    };

    var response = UrlFetchApp.fetch(url, options);
    var articles = JSON.parse(response.getContentText());
    
    if (articles.length === 0) {
      break;  // 取得件数が0になったら終了
    }

    allArticles = allArticles.concat(articles);
  }

  return allArticles;
}

/**
 * Qiitaデータをスプレッドシートに保存
 * @return {void} なし
 */
function saveQiitaDataToSheet() {
  Logger.log("**** start saveQiitaDataToSheet ****");
  var masterSheet = SpreadsheetApp.getActiveSpreadsheet().getSheetByName("QiitaData");
  if (!masterSheet) {
    masterSheet = SpreadsheetApp.getActiveSpreadsheet().insertSheet("QiitaData");
  }
  
  var articles = fetchAllQiitaArticles();
  var now = new Date().toISOString().substring(0, 10);  // 取得日 YYYY-MM-DD 形式
  
  // 🔹 ヘッダー行の作成（初回実行時のみ）
  if (masterSheet.getLastRow() == 0) {
    masterSheet.appendRow(["取得日", "記事ID", "タイトル", "投稿日", "更新日", "閲覧数", "いいね数", "ストック数", "閲覧数増加数"]);
  }

  articles.forEach(function(article) {
    var createdDate = article.created_at.substring(0, 10);  // YYYY-MM-DD
    var updatedDate = article.updated_at.substring(0, 10);  // YYYY-MM-DD
    
    if (!isDataExists(masterSheet, article.id, now)) {
      masterSheet.appendRow([
        now,
        article.id, 
        article.title, 
        createdDate, 
        updatedDate, 
        article.page_views_count || 0, 
        article.likes_count || 0, 
        article.stocks_count || 0,
        "" // 閲覧数増加数（後で計算）
      ]);
    }
  });

  sortSheet(masterSheet);
  calculateViewIncreases(masterSheet);
  createOrUpdateChartSheet();
  createOrUpdateTop3Sheet()
}

/**
 * データの重複を防ぐ（記事ID + 取得日でチェック）
 * @param {GoogleAppsScript.Spreadsheet.Sheet} sheet データを確認するシート
 * @param {string} articleId 記事のID
 * @param {string} date 取得日
 * @return {boolean} データが既に存在する場合は true、存在しない場合は false
 */
function isDataExists(sheet, articleId, date) {
  var data = sheet.getDataRange().getValues();
  for (var i = 1; i < data.length; i++) {
    if (data[i][1] == articleId && data[i][0] == date) {
      return true;
    }
  }
  return false;
}

/**
 * データを記事ID＆取得日でソート（ヘッダーを除外）
 * @param {GoogleAppsScript.Spreadsheet.Sheet} sheet ソート対象のシート
 * @return {void} なし
 */
function sortSheet(sheet) {
  var lastRow = sheet.getLastRow();
  if (lastRow > 1) { // データがある場合のみソート
    var range = sheet.getRange("A2:I" + lastRow); // A2 以降をソート
    range.sort([{ column: 2, ascending: true }, { column: 1, ascending: true }]); // 記事ID（B列）、取得日（A列）
  }
}

/**
 * 閲覧数増加数を計算
 * @param {GoogleAppsScript.Spreadsheet.Sheet} sheet 計算対象のシート
 * @return {void} なし
 */
function calculateViewIncreases(sheet) {
  var data = sheet.getDataRange().getValues();
  var lastRow = sheet.getLastRow();
  
  var articleViews = {}; // 記事ごとの過去の閲覧数を保存

  for (var i = 1; i < lastRow; i++) { // ヘッダーをスキップ
    var dateStr = data[i][0]; // 取得日（A列）
    
    // 🔹 `Date` オブジェクトの場合、 `YYYY-MM-DD` に変換
    if (dateStr instanceof Date) {
      dateStr = dateStr.toISOString().split("T")[0];
    }
    
    var articleId = data[i][1]; // 記事ID（B列）
    var views = parseInt(data[i][5], 10); // 閲覧数（F列）

    if (!dateStr || isNaN(views)) {
      Logger.log("Skipping row " + (i + 1) + " due to invalid data.");
      continue; // 無効なデータがある場合スキップ
    }

    var prevDate = getPreviousDate(dateStr);
    var key = articleId + "_" + dateStr; // 記事ID + 取得日
    var prevKey = articleId + "_" + prevDate;

    Logger.log("views : " + views);
    Logger.log("Processing : " + key);
    Logger.log("Previous Date : " + prevDate);
    Logger.log("articleViews[" + prevKey + "] : " + articleViews[prevKey]);

    if (articleViews[prevKey] !== undefined) {
      var increase = views - articleViews[prevKey];
      Logger.log("Increase calculated: " + increase);
      sheet.getRange(i + 1, 9).setValue(increase); // 閲覧数増加数（I列）
    } else {
      Logger.log("No previous data found for " + prevKey + ", setting increase to 0");
      sheet.getRange(i + 1, 9).setValue(0); // 初日は増加なし
    }
    
    articleViews[key] = views; // 現在の閲覧数を保存
  }
}

/**
 * 記事単位の閲覧数推移グラフ用シートを作成・更新
 * @return {void} なし
 */
function createOrUpdateChartSheet() {
  var ss = SpreadsheetApp.getActiveSpreadsheet();

  // 📊 記事ごとの閲覧数推移を可視化するシート
  var chartSheet = ss.getSheetByName("ViewIncreasesChart");
  if (!chartSheet) {
    chartSheet = ss.insertSheet("ViewIncreasesChart");
  } else {
    chartSheet.clear(); // 既存データをクリア
  }

  var masterSheet = ss.getSheetByName("QiitaData");
  var lastRow = masterSheet.getLastRow();
  if (lastRow < 2) return; // データがない場合は処理を中止

  // 🔹 記事タイトル選択セルを作成
  chartSheet.getRange("A1").setValue("記事タイトルを選択:");
  chartSheet.getRange("B1").setFormula("=INDEX(SORT(UNIQUE('QiitaData'!C2:C)), 1, 1)"); // デフォルトのタイトル

  var titleListRange = masterSheet.getRange("C2:C" + lastRow);
  var rule = SpreadsheetApp.newDataValidation()
    .requireValueInRange(titleListRange, true)
    .setAllowInvalid(false)
    .build();
  chartSheet.getRange("B1").setDataValidation(rule); // 記事タイトルのプルダウン

  // 🔹 選択された記事タイトルに対応する記事IDを取得
  chartSheet.getRange("D1").setValue("対応する記事ID:");
  chartSheet.getRange("E1").setFormula("=INDEX('QiitaData'!B:B, MATCH($B$1, 'QiitaData'!C:C, 0))");

  // 🔹 グラフ用のデータ取得欄
  chartSheet.getRange("A3").setValue("取得日");
  chartSheet.getRange("B3").setValue("閲覧数増加数");

  // **既存データが競合しないようにクリア**
  chartSheet.getRange("A4:B").clearContent();

  // **取得日 (`A4` のセルに FILTER() を設定)**
  chartSheet.getRange("A4").setFormula("=FILTER('QiitaData'!A:A, 'QiitaData'!B:B=$E$1)");

  // **閲覧数増加数 (`B4` のセルに FILTER() を設定)**
  chartSheet.getRange("B4").setFormula("=FILTER('QiitaData'!I:I, 'QiitaData'!B:B=$E$1)");

  // 🔹 グラフ作成
  var chart = chartSheet.newChart()
    .setChartType(Charts.ChartType.LINE)
    .addRange(chartSheet.getRange("A3:B"))
    .setPosition(5, 3, 0, 0)
    .setOption("title", "選択した記事の閲覧数推移")
    .build();
  chartSheet.insertChart(chart);
}

/**
 * 日別閲覧数増加ランキング（TOP3）用シートを作成・更新
 * @return {void} なし
 */
function createOrUpdateTop3Sheet() {
  var ss = SpreadsheetApp.getActiveSpreadsheet();
  var top3Sheet = ss.getSheetByName("ViewIncreasesTop3");
  if (!top3Sheet) {
    top3Sheet = ss.insertSheet("ViewIncreasesTop3");
  } else {
    top3Sheet.clear(); // 既存データをクリア
  }

  var masterSheet = ss.getSheetByName("QiitaData");
  var lastRow = masterSheet.getLastRow();
  if (lastRow < 2) return; // データがない場合は処理を中止

  // 🔹 タイトル行（A1から設定）
  var rowHeaders = [
    ["取得日"],
    ["top1記事名"],
    ["top1閲覧数増加数"],
    ["top2記事名"],
    ["top2閲覧数増加数"],
    ["top3記事名"],
    ["top3閲覧数増加数"],
    ["閲覧数増加数合計"]
  ];
  top3Sheet.getRange("A1:A8").setValues(rowHeaders);

  // 🔹 取得日を横方向に展開（B1から設定）
  top3Sheet.getRange("B1").setFormula("=TRANSPOSE(SORT(UNIQUE('QiitaData'!A2:A), 1, TRUE))");

  // 🔹 取得日のユニーク数を A10 に移動
  top3Sheet.getRange("A10").setValue("取得日総数");
  top3Sheet.getRange("A11").setFormula("=COUNTA(UNIQUE('QiitaData'!A2:A))");

  // 🔹 取得日数を取得して動的に `columnLetters` を設定
  SpreadsheetApp.flush(); // 計算結果を確実に反映させるため
  var uniqueDatesCount = top3Sheet.getRange("A11").getValue(); // 取得日数
  var columnLetters = getColumnLetters(uniqueDatesCount); // 無制限の列拡張対応

  // 🔹 B2 以降の式を適用（取得日数分だけ処理）
  columnLetters.forEach(function (col) {
    // **top1**
    top3Sheet.getRange(col + "2").setFormula(`=IFERROR(INDEX(SORT(FILTER('QiitaData'!$C$2:$C, 'QiitaData'!$A$2:$A=${col}$1), FILTER('QiitaData'!$I$2:$I, 'QiitaData'!$A$2:$A=${col}$1), FALSE), 1, 1), "")`);
    top3Sheet.getRange(col + "3").setFormula(`=IFERROR(INDEX(SORT(FILTER('QiitaData'!$I$2:$I, 'QiitaData'!$A$2:$A=${col}$1), FILTER('QiitaData'!$I$2:$I, 'QiitaData'!$A$2:$A=${col}$1), FALSE), 1, 1), "")`);

    // **top2**
    top3Sheet.getRange(col + "4").setFormula(`=IFERROR(INDEX(SORT(FILTER('QiitaData'!$C$2:$C, 'QiitaData'!$A$2:$A=${col}$1), FILTER('QiitaData'!$I$2:$I, 'QiitaData'!$A$2:$A=${col}$1), FALSE), 2, 1), "")`);
    top3Sheet.getRange(col + "5").setFormula(`=IFERROR(INDEX(SORT(FILTER('QiitaData'!$I$2:$I, 'QiitaData'!$A$2:$A=${col}$1), FILTER('QiitaData'!$I$2:$I, 'QiitaData'!$A$2:$A=${col}$1), FALSE), 2, 1), "")`);

    // **top3**
    top3Sheet.getRange(col + "6").setFormula(`=IFERROR(INDEX(SORT(FILTER('QiitaData'!$C$2:$C, 'QiitaData'!$A$2:$A=${col}$1), FILTER('QiitaData'!$I$2:$I, 'QiitaData'!$A$2:$A=${col}$1), FALSE), 3, 1), "")`);
    top3Sheet.getRange(col + "7").setFormula(`=IFERROR(INDEX(SORT(FILTER('QiitaData'!$I$2:$I, 'QiitaData'!$A$2:$A=${col}$1), FILTER('QiitaData'!$I$2:$I, 'QiitaData'!$A$2:$A=${col}$1), FALSE), 3, 1), "")`);

    // **閲覧数増加数合計**
    top3Sheet.getRange(col + "8").setFormula(`=IFERROR(SUM(FILTER('QiitaData'!$I$2:$I, 'QiitaData'!$A$2:$A=${col}$1)), 0)`);
  });
}

/**
 * Googleスプレッドシートの列名（A, B, ..., Z, AA, AB, ..., ZZ, AAA...）を動的に生成
 * @param {number} count 必要な列数
 * @return {string[]} 列名のリスト
 */
function getColumnLetters(count) {
  var columnLetters = [];
  for (var i = 0; i < count; i++) {
    columnLetters.push(columnIndexToLetter(i + 2)); // B列からスタート
  }
  return columnLetters;
}

/**
 * インデックス（1から始まる数値）をGoogleスプレッドシートの列名に変換
 * @param {number} index 列のインデックス（1=A, 2=B, ..., 27=AA）
 * @return {string} 列名
 */
function columnIndexToLetter(index) {
  var letter = "";
  while (index > 0) {
    index--;
    letter = String.fromCharCode(65 + (index % 26)) + letter;
    index = Math.floor(index / 26);
  }
  return letter;
}

/**
 * 取得日の1日前を取得
 * @param {string} dateString 日付（YYYY-MM-DD 形式）
 * @return {string} 1日前の日付（YYYY-MM-DD 形式）
 */
function getPreviousDate(dateString) {
  if (!dateString) return "";
  Logger.log("dateString : " + dateString);

  var date;
  
  // 🔹 `dateString` が `Date` 型だった場合、 `YYYY-MM-DD` の文字列に変換
  if (dateString instanceof Date) {
    date = dateString;
    Logger.log("dateString instanceof Date");
  } else if (typeof dateString === "string") {
    if (dateString.includes("GMT")) {
      // 🔹 `toString()` の形式になっている場合、 `Date` オブジェクトに変換
      Logger.log("dateString.includes GMT");
      date = new Date(dateString);
    } else {
      // 🔹 "YYYY-MM-DD" 形式なら、 `Date` に変換
      var dateParts = dateString.split("-");
      if (dateParts.length !== 3) {
        Logger.log("Invalid date format: " + dateString);
        return "";
      }
      var year = parseInt(dateParts[0], 10);
      var month = parseInt(dateParts[1], 10) - 1;
      var day = parseInt(dateParts[2], 10);
      date = new Date(year, month, day);
      Logger.log("year, month, day");
    }
  } else {
    Logger.log("Unexpected date format: " + dateString);
    return "";
  }
  
  date.setDate(date.getDate());
  
  var prevDate = date.toISOString().split("T")[0]; // YYYY-MM-DD 形式
  Logger.log("Corrected Previous Date: " + prevDate);
  return prevDate;
}

```

</details>

### 🔹 3. Qiita API のアクセストークンを設定
Qiita APIで自分の記事の情報を取得するためには アクセストークン（APIキー） が必要です。
GASではスクリプトプロパティ（Script Properties）を使うことで、スクリプトに直書きせず、安全に管理できます。

:::note info
アクセストークンがまだない場合は、Qiitaにログイン後、
「設定＞アプリケーション＞個人用アクセストークン＞read_qiita」から、
新しくトークンを発行してください。
:::

- スクリプトプロパティの設定手順
  - GASエディタを開く（Googleスプレッドシート → 拡張機能 → Apps Script）
  - 上部メニューの「プロジェクトの設定」を開く
  - 「スクリプト プロパティ」を追加
    - キー : QIITA_ACCESS_TOKEN
    - 値 : {あなたのQiitaアクセストークン}

### 🔹 4. GASのトリガーを設定
GASには「トリガー（Trigger）」という機能があり、スクリプトを**定期的に自動実行**できます。

- トリガーの設定手順
  - GASエディタの左メニュー「トリガー」→「トリガーを追加」
    - 実行する関数を選択 → saveQiitaDataToSheet
    - 実行するデプロイを選択 → Head
    - イベントのソースを選択 → 時間主導型 → 「日付ベースのタイマー」→「1日1回」＃時間は任意です。
    - その他はデフォルトで保存

### 🔹 5. アクセス許可、動作確認
「saveQiitaDataToSheet」ファンクションを手動実行してみましょう。
機能の利用内容に応じて、任意のプロジェクト名（qiita_pv_chk_toolなど）について、Googleアカウントへのアクセス権が必要となります。
初回は許可を求められるため、以下の許可を与えてください。
- qiita_pv_chk_tool に付与するアクセス権:
  - Google スプレッドシートのすべてのスプレッドシートの参照、編集、作成、削除
  - 外部サービスへの接続

✅ これで 完全自動でデータ収集 & 可視化ができるようになります！

## 🔗 まとめ
Qiita APIを活用し、記事の**閲覧数推移を記録・可視化するツール**を作成しました！
Googleスプレッドシート + GASのみで**無料で実装可能**💰✨
記事タイトルを選択するだけで、閲覧数増加数の推移がグラフ化されます！
トリガーを設定することで完全自動化が可能！ 💡
🚀 これでQiita記事の閲覧数推移を、簡単に分析できるようになりました！
ぜひ試してみてください！😊

## 🎯 こんな方にオススメ
✅ Qiitaの記事のパフォーマンスを定期的にチェックしたい方
✅ どんな記事の閲覧数が増えているか、可視化したい方
✅ Googleスプレッドシート + GASを活用してデータ収集を自動化したい方

---

テンション高めに紹介を書きましたが、
サーバレスでのwebAPI利用、自動収集の手法として、
今回はGASを利用してみました。
これは便利ですね。
目の前で表が埋められていく、グラフが書かれるのは見ていて単純に面白いというのも良かったです。

手に入れた一覧や数字に一喜一憂するため、というよりは、せっかく手に入る情報なら蓄積してみたい、
分析してみたい、自動化してみたい、無料でできることをやってみたい、というモチベーションで作成しました。

:::note info
手軽にそのタイミングの閲覧数などが一覧で見たい、という場合は以下をご利用ください。

https://qiita.com/kubo4ka/items/8fec35160d9f56347ea4

:::