Google Apps ScriptとChatGPT APIで技術トレンドをキャッチ

技術界隈は日進月歩で、新しいトレンドが次々と生まれています。しかし、その全てをキャッチアップするのはなかなか大変ですよね。

そこで、Google Apps ScriptとOpenAIのGPT-3.5を使って、はてなブックマークのホットエントリーから技術関連の人気記事をピックアップし、その要約をスプレッドシートに自動生成するコードを書いてみました。要約に目を通し、気になった記事だけを読みにいけば、情報収集の時間を短縮することができるはず！

![スプレッドシートに記録されたホットエントリーの情報](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/6760/73edd739-2803-ca4f-3942-3c23476d358d.png)

（Apps Scriptからのアクセスを制限しているサイトはsummaryが空になっています）

ちなみに、この記事は次のように要約してくれました：

> 記事は、Google Apps ScriptとOpenAIのGPT-3.5を使用して、はてなブックマークのホットエントリーから技術関連の人気記事をピックアップし、要約を自動生成する方法について説明しています。これにより、最新の技術トレンドを効率的にキャッチアップできます。具体的な手順としては、Googleスプレッドシートを作成し、Apps Scriptを使用してはてなブックマークのRSSフィードを解析し、エントリーの情報を取得します。その後、OpenAIのAPIを使用して記事の要約を作成し、スプレッドシートに記録します。要約を通じて気になる記事を選び、情報収集の時間を短縮することができます。また、このコードを拡張してチャットボットに応用することも可能であり、チームメンバーや仲間と技術の話題で交流することもできます。

# プロジェクトの目的

効率的に最新の技術トレンドをキャッチアップするために、人気の記事を抽出し、その要約を作るところまでを実現しました。コードを少し拡張してチャットボットに応用すれば、チームメンバーや仲間と共有するのも簡単です。技術の話をきっかけに雑談するのもいいですよね！

このプロジェクトのきっかけになったのはこちらの記事です。Google Apps Scriptでも作れそう！と思って取り組んでみたのですが、GPTのモデルに`gpt-3.5-turbo-16k`を使っていても、いい感じに要約してくれるので驚いています。

https://zenn.dev/sigmai_tech/articles/368533f22feb7f

# 開発環境の設定

次の3つの手順が必要です。

1. Googleスプレッドシートの新規作成
2. Apps Scriptのコードを2つ追加（`HotEntries.gs`、`ExtractContent.gs`）
3. OpenAIのAPIキーを取得して、スクリプトプロパティに追加（`OPEN_AI_API_KEY`）

## Googleスプレッドシートの新規作成

まず、Googleスプレッドシートを新規で作成し、シート名を`Posts`にします。また、一番上の行に、見出しとして`date`, `url`, `title`, `bookmarks`, `tags`, `imageUrl`, `description`, `summary`と書き込みます。

## Apps Scriptのコードを2つ追加

次に、スプレッドシートの上部メニューから、拡張機能 > Apps Script、を選択しスクリプトエディターを起動します。

そして、この後に掲載するソースコード`HotEntries.gs`と、次の記事に掲載している`ExtractContent.gs`もエディターにコピペして追加します。エディターを起動した時に表示されている`function myFunction() {}`は不要なので削除してください。

https://qiita.com/takatama/items/6f86d0d1e0de601506b5#%E3%82%BD%E3%83%BC%E3%82%B9%E3%82%B3%E3%83%BC%E3%83%89

## OpenAIのAPIキーを取得して、スクリプトプロパティに追加

記事を要約するために、OpenAIのAPIを利用します。OpenAIの公式サイトからAPIキーを取得してください。絶対に漏洩しないように気をつけてください。

そのAPIキーをスクリプトエディターのスクリプトプロパティ`OPEN_AI_API_KEY`として保存します。

# ソースコード`HotEntries.gs`

スクリプトエディターに追加するコードです。

```javascript:HotEntries.gs
// https://yatta47.hateblo.jp/entry/2019/06/18/210608 を参考に作りました。
function hotEntries_() {
  const feedUrl = "http://b.hatena.ne.jp/hotentry/it.rss";
  const xml = UrlFetchApp.fetch(feedUrl).getContentText();
  const document = XmlService.parse(xml);
  const root = document.getRootElement();
  const rss = XmlService.getNamespace('http://purl.org/rss/1.0/');
  const dc = XmlService.getNamespace('dc', 'http://purl.org/dc/elements/1.1/');
  const hatena = XmlService.getNamespace('hatena', 'http://www.hatena.ne.jp/info/xmlns#');
  const entries = [];

  for (let item of root.getChildren('item', rss)) {
    const title = item.getChild('title', rss).getText();
    const url = item.getChild('link', rss).getText();
    const description = item.getChild('description', rss).getText();
    const subjects = item.getChildren('subject', dc).map(subject => subject.getText());
    const date = item.getChild('date', dc).getText();
    const bookmarks = Number(item.getChild('bookmarkcount', hatena).getText());
    const imageUrl = item.getChild('imageurl', hatena)?.getText();
    entries.push({ title, url, description, subjects, date, bookmarks, imageUrl })
  }
  // ブックマーク数の大きい順に並び替え
  entries.sort((a, b) => b.bookmarks - a.bookmarks);
  return entries;
}

function queryGpt_(messages) {
  const url = 'https://api.openai.com/v1/chat/completions';
  const apiKey = PropertiesService.getScriptProperties().getProperty('OPEN_AI_API_KEY');
  if (!apiKey) throw new Error('OPEN_AI_API_KEYがスクリプトプロパティに設定されていません。');
  const optons = {
    method: "POST",
    muteHttpExceptions: true,
    headers: {
      'Content-Type': 'application/json',
      'Authorization': 'Bearer ' + apiKey,
    },
    payload: JSON.stringify({
      // model: 'gpt-4',
      // model: 'gpt-3.5-turbo-16k',
      model: 'gpt-3.5-turbo-1106',
      temperature: 0.7,
      max_tokens: 2048,
      messages
    }),
  };

  const json = JSON.parse(UrlFetchApp.fetch(url, optons).getContentText());
  console.log(json);
  if (!json.choices) throw new Error(JSON.stringify(json));
  return json.choices[0].message.content;
}

// https://zenn.dev/sigmai_tech/articles/368533f22feb7f を参考に作りました。
function summarizeText_(title, tags, text) {
  const messages = [
    {role: 'system', content: "You are a helpful assistant."},
    {role: 'assistant', content: `【Title】${title}\n【Tags】${tags ? tags.join('、'): ''}\n###\n${text}`},
    {role: 'user', content: `この記事の内容について、技術的な視点で要約をしてください。\nlang:ja`},  
  ];
  return queryGpt_(messages);
}

function summarizeContent_(url, title, subjects) {
  let html;
  try {
    html = UrlFetchApp.fetch(url).getContentText();
  } catch(e) {
    console.error(e);
    return '';
  }
  // 本文を多めに抽出したい場合のoptions
  // const opt = { threshold: 80, continuous_factor: 1.0 };
  const opt = {};
  const content = new ExtractContent().analyse(html, opt).asText();
  if (!title) title = content.title;
  const summary = summarizeText_(title, subjects, content.body);
  return summary;
}

const POSTS_SHEET_NAME = 'Posts';
const URL_COLUMN_INDEX = 1;
const MIN_BOOKMARKS = 200;

function summarizeHotEntries() {
  const postsSheet = SpreadsheetApp.getActive().getSheetByName(POSTS_SHEET_NAME);
  const postedUrls = postsSheet.getDataRange().getValues().slice(1).map(row => row[URL_COLUMN_INDEX]);
  hotEntries_().filter(entry => entry.bookmarks >= MIN_BOOKMARKS && !postedUrls.includes(entry.url)).forEach(entry => {
    // Apps Scriptからのアクセスを禁止しているurlはsummaryが空になる
    const summary = summarizeContent_(entry.url, entry.title, entry.subjects); 
    const row = [new Date(), entry.url, entry.title, entry.bookmarks, entry.subjects.join(', '), entry.imageUrl, entry.description, summary];
    postsSheet.appendRow(row);
    postedUrls.push(entry.url);
  });
}

function testS() {
  const url = 'https://qiita.com/takatama/items/bb90b889e892937550ee';
  console.log(summarizeContent_(url));
}
```

## `HotEntries.gs`の解説

ソースコード`HotEntries.gs`について簡単に解説しておきます。

### `hotEntries_()`

この関数は、はてなブックマークのRSSフィード（ホットエントリーのテクノロジーカテゴリー）から技術記事のエントリーを抽出します。参照するRSSフィードはこちらです。

https://b.hatena.ne.jp/hotentry/it.rss

RSSはXMLデータです。XmlServiceを使って解析し、各エントリーの情報（タイトル、URL、概要、タグ、日付、ブックマーク数、画像URL）を配列に格納します。そして、ブックマーク数が大きい順にソートして返します。

この関数を作るに当たって、次の記事を参考にしました。

https://yatta47.hateblo.jp/entry/2019/06/18/210608

### `queryGpt_()`

この関数は、OpenAI APIを使ってテキストを要約します。APIキーはスクリプトプロパティから取得し、UrlFetchAppを使ってOpenAIのエンドポイントにPOSTリクエストを送信します。要約を指示するプロンプトは、こちらの記事を参考にしました。

### `summarizeText_()`と`summarizeContent_()`

`summarizeText_()`は、記事のタイトル、タグ、テキストを受け取り、それをOpenAI APIに送信して要約を作成します。`summarizeContent_()`は、記事のURLからHTMLコンテンツをフェッチし、それを`summarizeText_()`に渡して要約を作成します。

### `summarizeHotEntries()`:

この関数は、`hotEntries_()`から得られたエントリーをループし、各エントリーを`summarizeContent_()`に渡して要約を作成します。そして、その要約と他のエントリー情報をスプレッドシートに記録します。

# 結果と展望

技術トレンドを効率よくキャッチアップし、それをスプレッドシートに記録することができました。このコードを拡張すれば、Google Chatや他のチャットプラットフォームと簡単に統合することができます。チームメンバーや仲間たちと技術トレンドについて語り合うきっかけになりそうですね。ぜひお楽しみください。

# 参考資料

https://b.hatena.ne.jp/hotentry/it

https://developer.hatena.ne.jp/ja/documents/bookmark/misc/feed

https://platform.openai.com/docs/guides/gpt/chat-completions-api

https://developers.google.com/apps-script