水曜日, 6月 25, 2025
水曜日, 6月 25, 2025
- Advertisment -
ホームニューステックニュース自社のorganizationに紐づくQiitaの記事を自動取得できるようにしてみた #GAS - Qiita

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



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

/**
 * 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:
    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) + '...');
  }
}





Source link

Views: 0

RELATED ARTICLES

返事を書く

あなたのコメントを入力してください。
ここにあなたの名前を入力してください

- Advertisment -