note記事の月次振り返りで集計漏れを防ぐ|GASで分析データをdry-run監査

note記事の月次振り返りで集計漏れを防ぐ|GASで分析データをdry-run監査

ツールおじ|GoogleアカウントをAIエージェント化

ツールおじ|GoogleアカウントをAIエージェント化

月末に記事の数字をまとめるとき、先月の記事が混ざることがあります。閲覧数の空欄を0件として扱うと、伸びた記事を見誤りかねません。こうした集計ミスは、レポートを作ったあとより、元データを点検する段階で見つけたほうが直しやすくなります。

この記事ではnoteの記事運用表を例に、公開月・記事ID・状態・閲覧数・スキ数・コメント数を点検する無料GASを紹介します。シートは更新せず、noteへの接続や外部通信も行いません。ログに記事IDや実数値を出さず、問題がある行番号と分類だけを残します。

このコードは集計前の入力だけを点検します

できるのは、対象月のずれ、記事IDの空欄と重複、状態の表記、数値欄の空欄・負数・小数、集計対象フラグの不備を見つけることです。閲覧数やスキ数を合計したり、グラフや月次報告書を作ったりはしません。

記事の良し悪しや次に書くテーマ、売上や反応の増加も判断しません。「エラー0件」は、この記事で決めた入力形式に問題が見つからなかったという意味です。分析結果の正しさや成果を保証するものではありません。

最初は運用表のコピーと架空データで試してください。実在する読者名、コメント本文、メールアドレス、購入者情報は入力しないでください。

「記事分析」シートを準備します

シート名を「記事分析」にします。1行目のA列からG列へ、次の見出しを入力してください。

・公開日

・記事ID

・状態

・閲覧数

・スキ数

・コメント数

・集計対象

公開日は日付形式、閲覧数・スキ数・コメント数は0以上の整数にします。状態は「公開」「下書き」「非公開」のいずれか、集計対象は「対象」または「除外」と入力します。

テスト用の対象月はコード内の「2026-09」です。架空の記事IDとして「NOTE-TEST-001」などを使い、2〜4行から始めます。

コピーして使える最小コード

function auditNoteMonthlyAnalysisInput() {
  const config = {
    sheetName: '記事分析',
    targetMonth: '2026-09',
    maxRows: 500,
    dryRun: true
  };

  if (config.dryRun !== true) {
    throw new Error('安全のためdryRunはtrue固定です。');
  }
  if (!/^\d{4}-(0[1-9]|1[0-2])$/.test(config.targetMonth)) {
    throw new Error('targetMonthはYYYY-MM形式で指定してください。');
  }

  const sheet = SpreadsheetApp.getActiveSpreadsheet()
    .getSheetByName(config.sheetName);
  if (!sheet) {
    throw new Error('「記事分析」シートが見つかりません。');
  }

  const headers = [
    '公開日',
    '記事ID',
    '状態',
    '閲覧数',
    'スキ数',
    'コメント数',
    '集計対象'
  ];
  const actualHeaders = sheet.getRange(1, 1, 1, headers.length)
    .getDisplayValues()[0]
    .map(function(value) {
      return String(value).trim();
    });

  const headerMismatch = headers.some(function(header, index) {
    return actualHeaders[index] !== header;
  });
  if (headerMismatch) {
    throw new Error('A1:G1の見出しを手順どおりにしてください。');
  }

  const rowCount = Math.max(0, sheet.getLastRow() - 1);
  if (rowCount > config.maxRows) {
    throw new Error('確認対象が500行を超えています。範囲を分けてください。');
  }

  const rows = rowCount === 0
    ? []
    : sheet.getRange(2, 1, rowCount, headers.length).getValues();
  const allowedStatus = ['公開', '下書き', '非公開'];
  const allowedTarget = ['対象', '除外'];
  const seenArticleIds = {};
  const issues = [];

  function addIssue(row, code) {
    issues.push({ row: row, code: code });
  }

  function isValidDate(value) {
    return value instanceof Date && !Number.isNaN(value.getTime());
  }

  function isNonNegativeInteger(value) {
    return typeof value === 'number' &&
      Number.isInteger(value) &&
      value >= 0;
  }

  rows.forEach(function(row, index) {
    const rowNumber = index + 2;
    const publishedAt = row[0];
    const articleId = String(row[1]).trim();
    const status = String(row[2]).trim();
    const views = row[3];
    const likes = row[4];
    const comments = row[5];
    const target = String(row[6]).trim();

    if (row.every(function(value) { return value === ''; })) {
      return;
    }

    if (!isValidDate(publishedAt)) {
      addIssue(rowNumber, 'PUBLISHED_DATE_INVALID');
    }
    if (!articleId) {
      addIssue(rowNumber, 'ARTICLE_ID_EMPTY');
    } else if (seenArticleIds[articleId]) {
      addIssue(rowNumber, 'ARTICLE_ID_DUPLICATE');
    } else {
      seenArticleIds[articleId] = true;
    }
    if (allowedStatus.indexOf(status) === -1) {
      addIssue(rowNumber, 'STATUS_INVALID');
    }
    if (!isNonNegativeInteger(views)) {
      addIssue(rowNumber, 'VIEWS_INVALID');
    }
    if (!isNonNegativeInteger(likes)) {
      addIssue(rowNumber, 'LIKES_INVALID');
    }
    if (!isNonNegativeInteger(comments)) {
      addIssue(rowNumber, 'COMMENTS_INVALID');
    }
    if (allowedTarget.indexOf(target) === -1) {
      addIssue(rowNumber, 'TARGET_FLAG_INVALID');
    }

    if (target === '対象' && isValidDate(publishedAt)) {
      const month = Utilities.formatDate(
        publishedAt,
        Session.getScriptTimeZone(),
        'yyyy-MM'
      );
      if (month !== config.targetMonth) {
        addIssue(rowNumber, 'TARGET_MONTH_MISMATCH');
      }
    }
  });

  Logger.log('dry-run: true');
  Logger.log('確認行数: ' + rowCount);
  if (issues.length === 0) {
    Logger.log('入力上の不整合: 0件');
  } else {
    issues.forEach(function(issue) {
      Logger.log('行' + issue.row + ': ' + issue.code);
    });
  }
  Logger.log('記事IDや閲覧数などの実値はログへ出していません。');

  return {
    status: issues.length === 0 ? 'OK' : 'HOLD',
    checked: rowCount,
    issues: issues.length
  };
}

実行前に対象月とタイムゾーンを確認します

1. 元の運用表をコピーします。

2. コピー側に「記事分析」シートを作り、見出しと架空データを入力します。

3. コードのtargetMonthを集計したい月へ変更します。

4. Apps Scriptのプロジェクト設定で、タイムゾーンが運用表の日付基準と一致しているか確認します。

5. Apps Scriptへコードを貼り付け、auditNoteMonthlyAnalysisInputを実行します。

6. 実行ログに出た行番号とエラー分類を見て、シート側の入力を直します。

7. エラーが0件になるまで、コピー側でdry-runを繰り返します。

このコードが使うのは、開いているスプレッドシートの読み取りです。note API、Google Drive、Gmail、外部サービスには接続しません。セル更新、ファイル作成、投稿、削除、トリガー作成も行いません。

エラー分類から直す場所を絞ります

・PUBLISHED_DATE_INVALID:公開日が日付として入力されていません。

・ARTICLE_ID_EMPTY:記事IDが空欄です。

・ARTICLE_ID_DUPLICATE:同じ記事IDが前の行にもあります。

・STATUS_INVALID:状態が「公開」「下書き」「非公開」のいずれでもありません。

・VIEWS_INVALID:閲覧数が0以上の整数ではありません。

・LIKES_INVALID:スキ数が0以上の整数ではありません。

・COMMENTS_INVALID:コメント数が0以上の整数ではありません。

・TARGET_FLAG_INVALID:集計対象が「対象」「除外」のいずれでもありません。

・TARGET_MONTH_MISMATCH:「対象」の行がtargetMonthと異なる月です。

同じ行に複数の問題があれば、分類も複数表示されます。ログには値を出していないため、行番号を手がかりにコピーしたシートを確認してください。

0は有効、空欄はエラーとして扱います

公開直後の記事では、閲覧数・スキ数・コメント数が0件のことがあります。0は有効な数値です。一方、空欄は「まだ取得していない」のか「0件」なのか区別できないため、このコードではエラーにします。

集計対象が「除外」の行でも、数値欄と状態は点検します。除外理由があるからといって、元データの形式まで未確認にしないためです。不要な行は削除せず、「除外」として残せます。

記事IDは重複検出に使いますが、ログには出しません。記事タイトル、URL、コメント本文、読者情報も読み取り対象に含めていません。

集計を始める前のチェックリスト

・targetMonthを対象月へ変更した。

・Apps Scriptのタイムゾーンを確認した。

・公開日が日付形式になっている。

・記事IDに空欄と重複がない。

・状態が3つの許可値にそろっている。

・0件と空欄を区別した。

・実在する読者情報やコメント本文を入れていない。

・ログに記事IDや実数値が出ていない。

・元シートではなくコピー側で試した。

入力を整えてから月次の振り返りへ進みます

月次の振り返りは、集計式より前に元データの対象月と入力形式をそろえることが大切です。このコードなら、シートを変更せず、問題がある行と分類だけを確認できます。

エラーが0件になったら、閲覧数・スキ数・コメント数の集計やグラフ作成は別工程で行ってください。0件でも、記録漏れや数字の取得元までは判定できません。公開画面や管理画面の数値と見比べ、最終判断は人が行います。

noteの記事企画から公開後の数値まで、1つのシートで管理したい方へ

GasおじのTips商品一覧を見る

※学習・検証用の例です。実データへ適用する前にコピー環境で確認してください。Google公式・Google公認のツールではありません。note公式・note公認のツールでもありません。


あなたも記事の投稿・販売を
始めてみませんか?

Tipsなら簡単に記事を販売できます!
登録無料で始められます!

Tipsなら、無料ですぐに記事の販売をはじめることができます Tipsの詳細はこちら
 

この記事の販売者

ツールおじ|GoogleアカウントをAIエージェント化

おう、Gasおじだ。 AIツールとSaaSに散々突っ込んで遠回りした結果、最後に強かったのは「今あるGoogleアカウント」だった。 今ではAIエージェントであらゆるタスクを自動化して無双してるぞ。 GAS、スプレッドシート、フォーム、Gmail、AIエージェントを組み合わせれば、個人でも小さな会社でも、手作業だらけの業務はかなり自動化できる。 ここでは机上のAI論じゃなく、現場で動くテンプレ、コード、手順だけを置いていくぞ。 高いツールを増やす前に、まずお前さんのGoogleアカウントを仕事するエージェントに変えようじゃないか。

この販売者が書いた他の記事

  • Gmailの予約送信ミスを送信前に発見|GASで宛先・日時をdry-run監査

  • Gmailの定型連絡を「送信せず」まとめて下書き化する|Gmail下書きテンプレGAS Pro

    ¥2,980
    2 %獲得
    (59 円相当)
  • スプレッドシートの入力ミスを公開前に発見|GASでルール表をdry-run監査

関連のおすすめ記事

  • Gmailの定型連絡を「送信せず」まとめて下書き化する|Gmail下書きテンプレGAS Pro

    ¥2,980
    2 %獲得
    (59 円相当)
    ツールおじ|GoogleアカウントをAIエージェント化

    ツールおじ|GoogleアカウントをAIエージェント化

  • USBメモリで持ち運べるLinux Mint 22.1と永続的な書き込み領域の作成法【Rufus だけで作成する方法を初心者向けに徹底解説】

    ¥500
    2 %獲得
    (10 円相当)
    SmartSenior

    SmartSenior

  • 個人クリエイター公開準備チェックGAS Pro

    ¥2,980
    2 %獲得
    (59 円相当)
    ツールおじ|GoogleアカウントをAIエージェント化

    ツールおじ|GoogleアカウントをAIエージェント化