11
3

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

自社のorganizationに紐づくQiitaの記事を自動取得できるようにしてみた

Posted at

はじめに

皆さん、こんにちは!Govtech事業本部開発部の八巻です!

そろそろ本格的にネタが尽きてきており、元上司が手を緩めてくれることをかなり期待しながら、今日も元気に記事を書いております。

さて今回はQiitaで特定の組織の記事を自動で集計し、スプレッドシートにまとめる方法をご紹介します。
今回は人事の方が手動で貼り付けて頂いており、エンジニアとしてなんとかできないかと思い奮闘してみました。

今後はコピペで使えるようにしたので、少しでもqiitaの社内organizaitonの記事を管理したい方に刺されば幸いです。笑

1. この記事でできること

この設定を行うと、GoogleスプレッドシートにQiitaの情報を自動で集計できます。

  • 指定した組織の、指定した期間内に投稿された記事の情報を取得
  • 各記事の以下の情報をスプレッドシートに出力
    • 日付(YYYY/MM/DD(曜日)形式)
    • ユーザー名
    • タイトル
    • いいね数
    • ストック数
    • 記事URL
  • ユーザーごとの総いいね数を自動集計し、多い順に表示
  • ユーザーごとの記事投稿数を自動集計し、多い順に表示
  • 全記事の中からいいね数トップ10の記事を抽出して表示

定期的に自動実行するように設定すれば、常に最新のデータを手に入れることも可能です。

2. Googleスプレッドシートの準備

まずは、データを出力するためのスプレッドシートを用意しましょう。

  1. Googleドライブで新しいGoogleスプレッドシートを作成します。
  2. スプレッドシートの名前をわかりやすいものに変更します(例: Qiita記事集計レポート)。
  3. シート名が「シート1」になっていることを確認します。もし別の名前になっている場合は、「シート1」に変更してください。

3. Google Apps Script(GAS)の設定

次に、実際にデータを取得・集計するスクリプトを設定します。

  1. 用意したGoogleスプレッドシートを開きます。

  2. メニューバーから「拡張機能」>「Apps Script」を選択します。新しいタブでスクリプトエディタが開きます。

  3. スクリプトエディタにすでに記述されているコード(例: function myFunction() {})があれば、すべて削除します。

  4. 以下のGASコードをすべてコピーし、スクリプトエディタに貼り付けます。

    /**
     * Qiita APIから記事を複数ページに渡って取得し、スプレッドシートに出力する関数。
     * 組織名と日付をコード内に直接指定し、指定された形式で出力する。
     * ユーザー単位でいいね数と記事投稿数を集計し、それぞれ多い順に表示する。
     * さらに、いいね数トップ10の記事情報をN〜P列に出力する。
     */
    function fetchAndOutputQiitaArticlesWithTopLikes() {
      const spreadsheet = SpreadsheetApp.getActiveSpreadsheet();
      const sheet = spreadsheet.getSheetByName('シート1'); // ☆ココをあなたのシート名に修正!
      // 例: const sheet = spreadsheet.getSheetByName('シート1');
    
      // エラーチェック: シートが存在しない場合
      if (!sheet) {
        Logger.log('指定されたシートが見つかりません。シート名を確認してください。');
        SpreadsheetApp.getUi().alert('エラー', '指定されたシートが見つかりません。スクリプト内のシート名を確認してください。', SpreadsheetApp.getUi().ButtonSet.OK);
        return;
      }
    
      // 既存のデータをクリア(ヘッダー行は残す)
      // J列からP列までの集計データ範囲もクリア
      sheet.getRange('J:P').clearContent();
      const lastRow = sheet.getLastRow();
      if (lastRow > 1) { // ヘッダー行のみの場合はクリアしない
        sheet.getRange(2, 1, lastRow - 1, sheet.getLastColumn()).clearContent();
      }
    
      // --- ヘッダー行を設定 (列順) ---
      const articleHeaders = ['日付', 'ユーザー名', 'タイトル', 'いいね数', 'ストック数', '記事URL'];
      sheet.getRange(1, 1, 1, articleHeaders.length).setValues([articleHeaders]).setFontWeight('bold');
    
      // --- 検索条件をコード内に直接指定 ---
      // ☆ここをあなたの組織名と日付に修正!
      const organization = 'uluru'; // Qiitaの組織ID(URLのorganizations/の次の部分)
      const startDate = '2025-01-01'; // 検索開始日 (YYYY-MM-DD 形式)
      // 実行日を終了日とする。必要であれば特定の固定日付 '2025-06-30' などに設定も可能
      const endDate = Utilities.formatDate(new Date(), SpreadsheetApp.getActiveSpreadsheet().getSpreadsheetTimeZone(), 'yyyy-MM-dd'); 
      
      const apiToken = ''; // ★オプション:ここにQiita APIの個人用アクセストークンを記載(推奨)
                           // 例: const apiToken = 'your_qiita_api_token_here';
                           // トークン設定方法は「補足:APIトークンの設定(推奨)」を参照
    
      const perPage = 100; // 1ページあたりの取得件数(Qiita APIの最大は100件)
    
      // Qiita APIのリクエストヘッダー
      const requestHeaders = {
        'Accept': 'application/json', // JSON形式でレスポンスを受け取ることを指定
      };
      if (apiToken) {
        requestHeaders['Authorization'] = `Bearer ${apiToken}`; // トークンがあれば認証ヘッダーに追加
      }
    
      let allArticles = []; // 取得した全記事を格納する配列
      let page = 1;         // 取得するページ番号
      const maxPages = 10;  // 取得する最大ページ数(GASの実行時間制限やAPI制限を考慮し、上限を設定)
    
      try {
        // クエリ文字列を構築 (例: "org:uluru created:>=2025-01-01 created:<=2025-06-24")
        const query = `org:${organization} created:>=${startDate} created:<=${endDate}`;
        const baseUrl = `https://qiita.com/api/v2/items?query=${encodeURIComponent(query)}&per_page=${perPage}`;
    
        // 複数ページに渡って記事を取得するループ
        while (page <= maxPages) {
          const urlWithPage = `${baseUrl}&page=${page}`; // ページ番号を追加したURL
          Logger.log(`Fetching: ${urlWithPage}`); // デバッグ用にログに出力
    
          const options = {
            'headers': requestHeaders,
            'muteHttpExceptions': true // エラー時も例外を投げずにレスポンスを取得し、自分でエラーコードをチェック
          };
          const response = UrlFetchApp.fetch(urlWithPage, options); // APIリクエストを実行
          const statusCode = response.getResponseCode(); // HTTPステータスコードを取得
          const jsonText = response.getContentText(); // レスポンス本文をテキストで取得
    
          // APIからの応答が成功(200 OK)以外の場合
          if (statusCode !== 200) {
            let errorMessage = `APIからエラー応答: ステータスコード ${statusCode}`;
            try {
              const errorData = JSON.parse(jsonText); // エラーレスポンスがJSON形式か試す
              if (errorData.message) {
                errorMessage += ` - ${errorData.message}`; // エラーメッセージがあれば追加
              }
              if (errorData.type) {
                errorMessage += ` (Type: ${errorData.type})`; // エラータイプがあれば追加
              }
            } catch (e) {
              errorMessage += ` - レスポンス: ${jsonText.substring(0, 200)}...`; // JSONでない場合はレスポンスの一部を表示
            }
            throw new Error(errorMessage); // エラーを投げてcatchブロックで処理
          }
    
          const pageArticles = JSON.parse(jsonText); // レスポンスをJSONとしてパース
    
          if (pageArticles.length === 0) {
            // 取得できる記事がなくなったらループを終了
            break;
          }
    
          allArticles = allArticles.concat(pageArticles); // 取得した記事を全記事リストに追加
    
          // 1ページあたりの記事数がperPage(100件)未満であれば、それが最後のページと判断しループ終了
          if (pageArticles.length < perPage) {
            break;
          }
    
          page++; // 次のページへ
        }
    
        // 記事が1件も見つからなかった場合
        if (allArticles.length === 0) {
          sheet.getRange('A2').setValue('該当する記事が見つかりませんでした。検索条件(組織名、日付)を確認してください。');
          Logger.log('No articles found for the given criteria.');
          return;
        }
    
        // --- 記事を日付の古い順にソート ---
        allArticles.sort((a, b) => new Date(a.created_at).getTime() - new Date(b.created_at).getTime());
    
        // 曜日を日本語で表現するための配列
        const weekdays = ['', '', '', '', '', '', ''];
    
        // 取得した記事データを整形して配列に格納 (記事ごとの出力データ)
        const outputArticleData = allArticles.map(article => {
          const createdDate = new Date(article.created_at);
          const year = createdDate.getFullYear();
          const month = (createdDate.getMonth() + 1).toString().padStart(2, '0'); // 0埋め
          const day = createdDate.getDate().toString().padStart(2, '0');         // 0埋め
          const weekday = weekdays[createdDate.getDay()]; // 曜日を取得
    
          // 日付を「YYYY/MM/DD(曜日)」形式にフォーマット
          const formattedDate = `${year}/${month}/${day}(${weekday})`;
    
          return [
            formattedDate,                                // A列: 日付
            article.user ? article.user.id : 'N/A',       // B列: ユーザー名
            article.title,                                // C列: タイトル
            article.likes_count,                          // D列: いいね数
            article.stocks_count,                         // E列: ストック数
            article.url                                   // F列: 記事URL
          ];
        });
    
        // スプレッドシートのA列からF列に記事ごとに出力
        sheet.getRange(2, 1, outputArticleData.length, outputArticleData[0].length).setValues(outputArticleData);
    
    
        // --- ユーザー単位でいいね数と記事投稿数を集計 ---
        const userLikes = {};       // ユーザーごとの総いいね数を格納
        const userArticleCounts = {}; // ユーザーごとの記事投稿数を格納
    
        allArticles.forEach(article => {
          const userId = article.user ? article.user.id : 'N/A'; // ユーザーIDを取得
          
          // いいね数の集計
          const likes = article.likes_count || 0; // いいね数がnullの場合は0として扱う
          if (userLikes[userId]) {
            userLikes[userId] += likes;
          } else {
            userLikes[userId] = likes;
          }
    
          // 記事投稿数の集計
          if (userArticleCounts[userId]) {
            userArticleCounts[userId]++;
          } else {
            userArticleCounts[userId] = 1;
          }
        });
    
        // いいね数の集計データを配列に変換し、いいね数が多い順にソート
        const sortedUserLikes = Object.entries(userLikes)
          .map(([userId, totalLikes]) => [userId, totalLikes]) // [キー, 値]の配列に変換
          .sort((a, b) => b[1] - a[1]); // いいね数で降順ソート
    
        // 記事投稿数の集計データを配列に変換し、投稿数が多い順にソート
        const sortedUserArticleCounts = Object.entries(userArticleCounts)
          .map(([userId, count]) => [userId, count]) // [キー, 値]の配列に変換
          .sort((a, b) => b[1] - a[1]); // 投稿数で降順ソート
    
        // J列とK列のヘッダーを設定(ユーザーごとの総いいね数)
        sheet.getRange('J1').setValue('ユーザー名 (総いいね)').setFontWeight('bold');
        sheet.getRange('K1').setValue('総いいね数').setFontWeight('bold');
    
        // J列とK列に集計結果を出力
        if (sortedUserLikes.length > 0) {
          sheet.getRange(2, 10, sortedUserLikes.length, sortedUserLikes[0].length).setValues(sortedUserLikes);
        } else {
          sheet.getRange('J2').setValue('ユーザーごとのいいね集計データがありません');
        }
    
        // L列とM列のヘッダーを設定(ユーザーごとの記事投稿数)
        sheet.getRange('L1').setValue('ユーザー名 (投稿数)').setFontWeight('bold');
        sheet.getRange('M1').setValue('記事投稿数').setFontWeight('bold');
    
        // L列とM列に集計結果を出力
        if (sortedUserArticleCounts.length > 0) {
          sheet.getRange(2, 12, sortedUserArticleCounts.length, sortedUserArticleCounts[0].length).setValues(sortedUserArticleCounts);
        } else {
          sheet.getRange('L2').setValue('ユーザーごとの投稿数集計データがありません');
        }
    
        // --- いいね数トップ10の記事情報をN, O, P列に出力 ---
        // 全記事をいいね数で降順にソート
        const sortedArticlesByLikes = [...allArticles].sort((a, b) => b.likes_count - a.likes_count);
        
        // トップ10(または全記事が10未満なら全記事)を抽出
        const top10Articles = sortedArticlesByLikes.slice(0, 10);
    
        // N, O, P列のヘッダーを設定
        sheet.getRange('N1').setValue('ユーザー名 (トップ記事)').setFontWeight('bold');
        sheet.getRange('O1').setValue('記事名 (トップ記事)').setFontWeight('bold');
        sheet.getRange('P1').setValue('いいね数 (トップ記事)').setFontWeight('bold');
    
        // N, O, P列に出力するデータを作成
        if (top10Articles.length > 0) {
          const outputTop10Data = top10Articles.map(article => [
            article.user ? article.user.id : 'N/A', // N列: ユーザー名
            article.title,                           // O列: 記事名
            article.likes_count                      // P列: いいね数
          ]);
          sheet.getRange(2, 14, outputTop10Data.length, outputTop10Data[0].length).setValues(outputTop10Data);
        } else {
          sheet.getRange('N2').setValue('いいね数トップ記事がありません');
        }
    
        // 処理完了のログとアラート
        Logger.log(`記事を ${allArticles.length} 件取得し、スプレッドシートに出力しました。`);
        Logger.log(`ユーザーごとのいいね数を ${sortedUserLikes.length} 件集計し、J列とK列に出力しました。`);
        Logger.log(`ユーザーごとの投稿数を ${sortedUserArticleCounts.length} 件集計し、L列とM列に出力しました。`);
        Logger.log(`いいね数トップ10の記事を ${top10Articles.length} 件集計し、N列からP列に出力しました。`);
    
      } catch (e) {
        // エラー発生時のログとアラート
        Logger.log('処理中にエラーが発生しました: ' + e.message);
        sheet.getRange('A2').setValue('エラー: ' + e.message.substring(0, 100) + '...');
      }
    }
    
  5. コード内の以下の部分を、あなたの設定に合わせて必ず修正してください。

    • const sheet = spreadsheet.getSheetByName('2025年_カレンダー_自動化');
      • '2025年_カレンダー_自動化' の部分を、実際に使っているシート名に修正します。例: 'シート1'
    • const organization = 'uluru';
      • 'uluru' の部分を、集計したいQiitaの組織IDに修正します。組織IDは、Qiitaの組織ページのURL(https://qiita.com/organizations/[組織ID])で確認できます。
    • const startDate = '2025-01-01';
      • 集計を開始したい日付YYYY-MM-DD 形式で入力します。
    • const apiToken = '';
      • Qiita APIの個人用アクセストークンをここに貼り付けます。詳細は後述の「補足:APIトークンの設定(推奨)」を参照してください。推奨しますが、設定しなくても基本的な記事取得は可能です(ただしレート制限が厳しくなります)。
  6. スクリプトエディタ上部のフロッピーディスクアイコン(保存ボタン)をクリックして保存します。

4. スクリプトの実行

スクリプトを実行して、データがスプレッドシートに出力されるか確認しましょう。

  1. スクリプトエディタで、関数名のドロップダウンが fetchAndOutputQiitaArticlesWithTopLikes になっていることを確認します。
  2. 再生ボタン(▶)をクリックします。
  3. 初回実行時のみ、認証を求められます。
    • 「許可を確認」ボタンをクリックします。
    • Googleアカウントを選択します。
    • 「このアプリはGoogleによって確認されていません」という警告が出ても、「詳細を表示」>「(プロジェクト名)に移動」をクリックして続行します。
    • 「Googleアカウントへのアクセスをリクエストしています」という画面で、スクリプトがスプレッドシートへのアクセスと外部サービス(Qiita API)への接続を許可するように求められるので、「許可」をクリックします。
  4. 認証が完了するとスクリプトが実行されます。しばらく待つと、スプレッドシートにデータが自動で入力されます。

5. スクリプトの自動実行設定(定期実行)

このスクリプトを毎日自動で実行するように設定することもできます。

  1. スクリプトエディタの左側メニューから時計のアイコン(「トリガー」)をクリックします。
  2. 画面右下の「トリガーを追加」ボタンをクリックします。
  3. 以下の設定を行います。
    • 「実行する関数を選択」: fetchAndOutputQiitaArticlesWithTopLikes
    • 「イベントの種類を選択」: 「時間主導型
    • 「時間ベースのトリガーのタイプを選択」: 「日単位のタイマー
    • 「時刻を選択」: 実行したい時間帯を選択します(例: 午前1時~2時)。
  4. 「保存」をクリックします。

これで、毎日指定した時間にスクリプトが自動で実行され、スプレッドシートのデータが更新されるようになります。

おわりに

今回の記事制作はだいぶLLMにおんぶに抱っこでしたが、素敵なスクリプトを作成したことに免じて許して頂きたいなと思います。笑
Qiitaのイベントが行われる度に「〇〇さんが以前作成したスクリプト使いましょう!!」と社内で名前が上がりドヤれると思うので、皆さんもぜひ使ってみて下さい!!

11
3
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
11
3

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?